feat: wire lobby task and VIP prefab flows and sync UI assets

This commit is contained in:
2026-09-06 18:46:37 +08:00
parent 842c68ef55
commit 9c4510697c
184 changed files with 8348 additions and 19133 deletions
@@ -0,0 +1,74 @@
/** Run inside the Creator Game View through MCP. Synthetic state; no login or business packets. */
export async function verifyLobbyMenuPreview() {
const engine=await globalThis.System.import('cc');
const modules=Array.from(globalThis.System.entries()).map(([,module])=>module);
const exported=name=>{const module=modules.find(m=>m[name]);if(!module)throw Error(`Missing preview export ${name}`);return module[name];};
const LobbyPanel=exported('LobbyPanel'),createRemoteConfig=exported('createRemoteConfig');
const resolveLobbyPolicy=exported('resolveLobbyPolicy'),resolveLobbyMenuConfig=exported('resolveLobbyMenuConfig');
const service=engine.director.getScene().getChildByName('PlatformServices');
const owner=service.getComponent('PlatformStartup'),assets=service.getComponent('LobbyAssets');
owner.unsubscribe?.();owner.runtime?.stop();owner.panel?.dispose();owner.panel=null;
await new Promise((resolve,reject)=>engine.director.loadScene('PlatformLobby',error=>error?reject(error):resolve()));
const canvas=engine.director.getScene().getChildByName('Canvas');
const popup=new engine.Node('MenuVerificationPopup');popup.parent=canvas;popup.layer=canvas.layer;
popup.addComponent(engine.UITransform).setContentSize(1600,720);
const player={playerid:7,nickname:'菜单接线验证',avatar:'',sex:0,bean:123456789,roomcard:1234,taskstate:1,advanced:0,score:9,sign:'验证账号',ip:'127.0.0.1'};
const state={app:{phase:'logged-in'},room:{kind:'outside'},players:{selfPlayerId:7,entities:{7:player}}};
const actions=[],tips=[],checks=[];
const assert=(condition,message)=>{if(!condition)throw Error(message);checks.push(message);};
const config=owner.config;
const make=(data,context={versionState:0,launchMode:0})=>{
const remote=createRemoteConfig({gameNoticeShow:-1,...data},config.identity);
const panel=new LobbyPanel(canvas,popup,assets,config,resolveLobbyPolicy(remote),state,text=>tips.push(text),
()=>actions.push('logout'),()=>({errorCode:1}),()=>actions.push('url'),resolveLobbyMenuConfig(remote),context,action=>actions.push(action),()=>{},(name,data)=>owner.callNative(name,data),{request:()=>actions.push('vip-query'),join:()=>actions.push('vip-join'),describe:()=> '测试玩法'});
popup.setSiblingIndex(canvas.children.length-1);
return panel;
};
const click=(panel,id)=>panel.menuNodes.get(id).emit(engine.Button.EventType.CLICK);
let panel=make({rankList:1,cdKey:2,hideTaskBtn:1,miniPro:1,hideRoomCard:2});
assert(!panel.menuNodes.get(17).activeInHierarchy&&!panel.taskTip.activeInHierarchy,'hidden task also hides child red dot');
assert(!panel.menuNodes.get(14).activeInHierarchy,'miniPro hides share');
assert(!panel.menuNodes.get(10).active&& !panel.menuNodes.get(3161).active,'hideRoomCard hides purchase and number');
assert(panel.menuNodes.get(3264).active&&panel.menuNodes.get(3233).active,'cdKey and rank are visible');
assert(!panel.menuNodes.get(3256).active,'phone binding stays hidden');
click(panel,17);click(panel,14);click(panel,10);
assert(actions.length===0,'hidden buttons cannot dispatch events');
click(panel,3264);assert(actions.at(-1)==='cdkey','cdkey dispatches named business event');
click(panel,7);assert(panel.modalKind==='settings','settings button opens existing prefab');
const before=actions.length;click(panel,3264);assert(actions.length===before,'modal blocks underlying menu');
panel.close();click(panel,9);assert(panel.modalKind==='profile','avatar opens existing profile prefab');
panel.close();click(panel,12);assert(panel.modalKind==='join','join button opens existing join prefab');panel.close();
click(panel,3120);assert(panel.noticeMenu.getChildByPath('group-95').active,'more button opens tray');
assert(panel.menuNodes.get(604).active&&!panel.menuNodes.get(235).active,'tray applies rank and miniPro policy');
const trayBackground=panel.menuNodes.get(3121),trayUI=trayBackground.getComponent(engine.UITransform);
assert(trayBackground.position.x-trayUI.width*trayUI.anchorX===200-640,'tray resize preserves source left edge');
click(panel,5);assert(panel.modalKind==='notice','tray notice opens existing notice prefab');panel.close();
panel.render({...state,app:{phase:'reconnecting'}});const count=actions.length;click(panel,3264);
assert(actions.length===count,'reconnecting blocks menu action');
panel.dispose();
panel=make({agentmode:1,androidPay:1,rankList:1,cdKey:2});
assert(panel.menuNodes.get(10).active&&panel.menuNodes.get(509).active,'androidPay restores both purchase buttons');
assert(panel.taskTip.activeInHierarchy&&panel.taskTip===panel.menuNodes.get(17).getChildByName('Tip'),'red dot is the authored Tip child');
const badge=panel.taskTip;
panel.render({...state,players:{...state.players,entities:{7:{...player,taskstate:0}}}});
assert(!badge.active,'taskstate clears badge');panel.render(state);
assert(panel.taskTip===badge&&badge.active,'rerender reuses the authored Tip sprite');
for(const id of [10,286,287,3161]){click(panel,id);assert(actions.at(-1)==='buy-roomcard',`roomcard hit region ${id}`);}
for(const id of [508,509,496,3162]){click(panel,id);assert(actions.at(-1)==='buy-bean',`bean hit region ${id}`);}
assert(tips.length===0,`existing actions run without errors: ${tips.join(';')}`);
panel.dispose();
panel=make({cdKey:2},{versionState:1,launchMode:0});
assert(!panel.menuNodes.get(3233).active&&!panel.menuNodes.get(14).active,'review mode hides rank and share');
assert(panel.menuNodes.get(3264).active,'cdKey applies after review branch');panel.dispose();
panel=make({agentmode:1,androidPay:1,rankList:1,cdKey:2});
click(panel,14);assert(panel.modalKind==='share','share UI opens without a native host');
panel.modal.getChildByPath('group-11/Spirit72').emit(engine.Button.EventType.CLICK);
assert(panel.modal===null,'share close restores lobby');
click(panel,679);assert(panel.modalKind==='vip-rooms'&&!panel.menu.active,'VIP entry shows its page and hides main menu');
panel.modal.getChildByPath('group-103/Spirit3182').emit(engine.Button.EventType.CLICK);
assert(panel.modal===null&&panel.menu.active,'VIP close restores main menu');
globalThis.__lobbyMenuVerification={panel,checks,actions,tips,synthetic:true};
await new Promise(resolve=>setTimeout(resolve,200));
return {synthetic:true,scene:engine.director.getScene().name,checks:checks.length,failures:tips,
canvas:canvas.getComponent(engine.UITransform).contentSize,actions};
}
@@ -0,0 +1,40 @@
/** Execute in an isolated Creator browser preview, never a live player session. */
export async function verifyTaskListPreview() {
const e=await System.import('cc');
const service=e.director.getScene().getChildByName('PlatformServices');
const owner=service?.getComponent('PlatformStartup');
if(owner){owner.unsubscribe?.();owner.runtime?.stop();owner.panel?.dispose();owner.panel=null;}
await new Promise((resolve,reject)=>e.director.loadScene('PlatformLobby',err=>err?reject(err):resolve()));
const prefab=await new Promise((resolve,reject)=>e.assetManager.loadAny('4b529d70-7a96-479e-b8bc-0a92a5dc33c2',(err,a)=>err?reject(err):resolve(a)));
const root=e.instantiate(prefab);root.parent=e.director.getScene().getChildByName('Canvas');
const list=root.getComponent('TaskListView'),checks=[];
const assert=(condition,message)=>{if(!condition)throw Error(message);checks.push(message);};
const data=Array.from({length:6},(_,i)=>({taskid:i+1,title:'完成今日首次对局任务',award:i+2,finish:1,total:4,state:i===1?1:i===2?2:0}));
list.render(data,'房卡');
const content=list.scrollView.content,items=content.children.map(n=>n.getComponent('TaskItemView'));
assert(items.length===6,'six prefab instances');
assert(content.getComponent(e.UITransform).width===1776,'content measures all six items');
assert(list.scrollView.getMaxScrollOffset().x===592,'two additional items scroll horizontally');
assert(list.scrollView.horizontal&&!list.scrollView.vertical,'horizontal only');
assert(content.parent.getComponent(e.Mask)?.enabled,'viewport mask enabled');
assert(root.getChildByPath('group-10/Spirit176').position.x===0,'viewport centered');
assert(items[0].progressBar.progress===0.25,'progress uses finish divided by total');
assert(items[0].progressBar.barSprite.node.getComponent(e.UITransform).width===50.5,'progress scales bar from left');
assert(items[0].doButton.node.active&&!items[3].doButton.node.active,'only first unfinished entry shows do button');
assert(items[1].claimButton.node.active&&!items[1].progressBar.node.active,'claimable entry replaces progress');
assert(items[2].claimedMark.active&&!items[2].claimButton.node.active,'claimed entry replaces claim button');
const events=[];root.on('task-claim',id=>events.push(['claim',id]));root.on('task-do',id=>events.push(['do',id]));
items[0].doButton.node.emit(e.Button.EventType.CLICK);items[1].claimButton.node.emit(e.Button.EventType.CLICK);
items[2].claimButton.node.emit(e.Button.EventType.CLICK);items[3].doButton.node.emit(e.Button.EventType.CLICK);
assert(JSON.stringify(events)===JSON.stringify([['do',1],['claim',2]]),'only visible actions emit correct task ids');
const old=content.children[0];list.render(data.slice(0,1),'房卡');
assert(content.children.length===1&&old.parent===null,'refresh replaces previous entries');
list.render([],'房卡');assert(content.children.length===0&&content.getComponent(e.UITransform).width===0,'empty list clears extent');
list.render(data,'房卡');
// Destruction is deferred until the end of the frame; include that boundary.
await new Promise(resolve=>e.director.once(e.Director.EVENT_AFTER_DRAW,resolve));
await new Promise(resolve=>e.director.once(e.Director.EVENT_AFTER_DRAW,resolve));
assert(!document.body.innerText.includes('TypeError:'),'refresh finishes without deferred destruction errors');
globalThis.__taskQA={e,root,list,data,events,checks};
return {checks};
}
@@ -0,0 +1,54 @@
/** Isolated preview integration: real Runtime/Router/Store/UI, recording wire port. */
export async function verifyTaskNetworkPreview(){
const e=await System.import('cc'),modules=Array.from(System.entries()).map(([,m])=>m);
const exported=name=>{const m=modules.find(m=>m[name]);if(!m)throw Error(`Missing ${name}`);return m[name];};
const source=e.director.getScene().getChildByName('PlatformServices').getComponent('PlatformStartup');
const config=source.config,assets=source.getComponent('LobbyAssets');
source.unsubscribe?.();source.runtime?.stop();source.panel?.dispose();source.panel=null;
await new Promise((resolve,reject)=>e.director.loadScene('PlatformLobby',err=>err?reject(err):resolve()));
const sent=[],tips=[],checks=[];let receive,panel;
const assert=(ok,message)=>{if(!ok)throw Error(message);checks.push(message);};
const runtime=new (exported('PlatformRuntime'))({
gameEntry:exported('createLoginStageGameEntry')(config.identity.gameid,()=>{}),
resolveRuntimeConfig:async()=>config,
createWireClient:()=>({subscribe:cb=>{receive=cb;return ()=>{};},start:()=>{},stop:()=>{},send:packet=>sent.push(packet),switchServer:()=>{},reconnectCurrent:()=>{}}),
scene:{showLoading:()=>{},showLogin:()=>{},showLobby:()=>{},showRoom:()=>{},showReconnect:()=>{},showKicked:()=>{},showServerDenial:()=>{},showFatal:err=>{throw err;},
showTips:(msg,time,kind)=>{if(kind==='show_message')panel?.cancelTaskRequest();tips.push(msg);}},
loadResources:async()=>{},waitForMinimumDisplay:async()=>{},getLoginDeviceSnapshot:()=>{throw Error('Test uses inbound login fixture');},
});
await runtime.start();receive({type:'open',server:config.servers[0]});
const packet=(rpc,data)=>receive({type:'message',message:{route:'agent',rpc,data}});
packet('player_login',{state:0,playerid:7,nickname:'任务接线验证',avatar:'',sex:0,bean:0,roomcard:3,taskstate:1,score:0,sign:null,ip:'127.0.0.1'});
const canvas=e.director.getScene().getChildByName('Canvas'),popup=new e.Node('TaskNetworkTestPopup');popup.parent=canvas;popup.layer=canvas.layer;popup.addComponent(e.UITransform).setContentSize(1600,720);
const remote=exported('createRemoteConfig')({gameNoticeShow:-1,agentmode:1,hideTaskBtn:0},config.identity);
panel=new (exported('LobbyPanel'))(canvas,popup,assets,config,exported('resolveLobbyPolicy')(remote),runtime.state,msg=>tips.push(msg),()=>{},()=>null,()=>{},
exported('resolveLobbyMenuConfig')(remote),{versionState:0,launchMode:0},action=>{if(action==='tasks')runtime.getPlayerTasks();else tips.push(action);},id=>runtime.claimTask(id));
popup.setSiblingIndex(canvas.children.length-1);
const off=runtime.subscribeState(state=>panel.render(state),err=>{throw err;});
const clickTask=()=>panel.menuNodes.get(17).emit(e.Button.EventType.CLICK);
clickTask();clickTask();
assert(sent.length===1&&sent[0].rpc==='get_player_task','task button sends one request while pending');
assert(JSON.stringify(sent[0].data)===JSON.stringify({agentid:config.identity.agentid,playerid:7,gameid:config.identity.gameid}),'request identity comes from config and authenticated store');
packet('update_roomcard',{roomcard:4,text:'余额更新'});
assert(panel.taskPending==='list','balance notification preserves pending list');
packet('get_player_task',{tasks:[{taskid:9,title:'今日登录奖励',award:2,finish:1,total:1,state:1}]});
assert(panel.modalKind==='tasks','list response opens authored task prefab');
const view=panel.modal.getComponent('TaskListView'),claim=view.scrollView.content.children[0].getComponent('TaskItemView').claimButton;
claim.node.emit(e.Button.EventType.CLICK);claim.node.emit(e.Button.EventType.CLICK);
assert(sent.length===2&&sent[1].rpc==='get_task_award','claim sends one award request');
assert(JSON.stringify(sent[1].data)===JSON.stringify({agentid:config.identity.agentid,playerid:7,taskid:9}),'award packet preserves legacy fields');
packet('update_roomcard',{roomcard:6,text:'奖励到账'});
assert(panel.taskPending==='award','balance notification preserves duplicate claim guard');
packet('get_task_award',{taskid:9,taskstate:0});
assert(!panel.taskTip.active,'award response clears red dot');
assert(view.scrollView.content.children[0].getComponent('TaskItemView').claimedMark.active,'award response renders claimed mark');
panel.modal.getChildByPath('group-10/Spirit115').emit(e.Button.EventType.CLICK);
assert(panel.modal===null,'close button closes task list');
clickTask();packet('show_message',{msg:'任务暂不可用',time:0});assert(panel.taskPending===null,'server message releases request guard');
clickTask();receive({type:'slow'});assert(panel.taskPending===null,'reconnect cancels pending request');
const count=sent.length;clickTask();assert(sent.length===count,'reconnect blocks task sends');
await new Promise(resolve=>e.director.once(e.Director.EVENT_AFTER_DRAW,resolve));
assert(!document.body.innerText.includes('TypeError:'),'no deferred UI lifecycle errors');
off();panel.dispose();runtime.stop();popup.destroy();
return {checks,sent:sent.map(({rpc,data})=>({rpc,fields:Object.keys(data)}))};
}
@@ -0,0 +1,33 @@
export async function verifyVipRoomsPreview(){
const e=await System.import('cc');e.game.resume();
const modules=Array.from(System.entries()).map(([,m])=>m),get=n=>{const m=modules.find(m=>m[n]);if(!m)throw Error(n);return m[n];};
const service=e.director.getScene().getChildByName('PlatformServices');
const owner=service.getComponent('PlatformStartup');owner.unsubscribe?.();owner.runtime?.stop();owner.panel?.dispose();owner.panel=null;
await new Promise((ok,fail)=>e.director.loadScene('PlatformLobby',err=>err?fail(err):ok()));e.game.resume();
const canvas=e.director.getScene().getChildByName('Canvas');
const prefab=await new Promise((ok,fail)=>e.assetManager.loadAny('ddf22b83-c329-4cb2-ac80-7b4999472522',(err,a)=>err?fail(err):ok(a)));
const root=e.instantiate(prefab);root.parent=canvas;root.setPosition(0,0,0);root.getChildByName('group-3').active=false;
const view=root.getComponent('VipRoomListView'),checks=[],joins=[];
const assert=(ok,msg)=>{if(!ok)throw Error(msg);checks.push(msg);};root.on('vip-join',c=>joins.push(c));
view.configure(()=> '测试玩法\n四人桌');
const raw=Array.from({length:4},(_,i)=>({shortCode:'10000'+i,owner:'房主'+i,destwo:'测试 VIP '+i,playerList:Array.from({length:9},()=>[0,123,null,456]),roomcodes:{roomtype:[1],roomlist:Array.from({length:9},(_,j)=>900000+i*10+j)},seatCount:Array(9).fill(4),deskState:Array.from({length:9},(_,j)=>j%2),forbidSelect:i===0}));
const rooms=get('parseVipRooms')({roomInfo:raw}),codes=raw.map(r=>r.shortCode).reverse();view.render(rooms,codes);
const content=view.roomsScroll.content;
assert(content.children.length===4,'four room prefab instances');
assert(content.children[0].getChildByName('Spirit3178').getComponent(e.Label).string==='100003','saved order wins over server order');
assert(content.getComponent(e.UITransform).height===796,'room scroll extent uses 199 spacing');
content.children[0].getChildByName('Spirit3192').emit(e.Button.EventType.CLICK);
assert(joins[0]==='100003','enter emits VIP short code');
content.children[0].emit(e.Button.EventType.CLICK);
assert(view.selectedCode==='100003'&&view.desksScroll.node.active&&!view.roomsScroll.node.active,'row opens selected desk page');
const desks=view.desksScroll.content;
assert(desks.children.length===9&&desks.getComponent(e.UITransform).height===725,'nine desk prefabs in scroll content');
assert(desks.children[1].position.x-desks.children[0].position.x===285,'two column spacing matches source');
assert(desks.children[0].getChildByName('Spirit3188').getComponent(e.Label).string==='人数:2/4','desk occupancy');
assert(desks.children[1].getChildByName('Spirit3186').getComponent(e.Sprite).spriteFrame===view.deskFrames[1],'in battle desk frame');
desks.children[1].emit(e.Button.EventType.CLICK);assert(joins[1]===900031,'desk click emits actual room code');
view.render(rooms,codes);assert(view.selectedCode==='100003','refresh retains selection');
view.render(rooms.slice(0,3),codes);assert(view.selectedCode===null&&!view.desksScroll.node.active,'removed selected room returns to page one');
view.render([],codes);assert(content.children.length===0,'empty response clears all rooms');
view.render(rooms,codes);globalThis.__vipQA={root,view,checks};return {checks};
}