feat: wire lobby task and VIP prefab flows and sync UI assets
This commit is contained in:
@@ -10,7 +10,8 @@ export class LobbyAssets extends Component {
|
||||
@property(Prefab) profile:Prefab|null=null;
|
||||
@property(Prefab) settings:Prefab|null=null;
|
||||
@property(Prefab) join:Prefab|null=null;
|
||||
@property(Prefab) tasks:Prefab|null=null;
|
||||
@property(Prefab) share:Prefab|null=null;
|
||||
@property([SpriteFrame]) digits:SpriteFrame[]=[];
|
||||
@property([SpriteFrame]) switches:SpriteFrame[]=[];
|
||||
@property(SpriteFrame) taskTip:SpriteFrame|null=null;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.
|
||||
import { resolveLaunchWaitTime } from '../../framework/config/remote-config.ts';
|
||||
import { resolveLoginPolicy, type LoginPolicy } from '../../framework/presentation/login-policy.ts';
|
||||
import { LoginAccounts, type LoginNativeSettings } from '../../framework/adapters/native/login-accounts.ts';
|
||||
import {VipRoomHistory} from '../../framework/adapters/native/vip-room-history.ts';
|
||||
import {vipCode} from '../../framework/protocol/contracts/vip-room.ts';
|
||||
import { WebViewJavascriptBridgeAdapter, type WebViewJavascriptBridgeEnvironment } from '../../framework/adapters/native/webview-javascript-bridge.ts';
|
||||
import { PlatformRuntime } from '../../framework/platform/runtime.ts';
|
||||
import { WireClient } from '../../framework/net/wire-client.ts';
|
||||
@@ -18,8 +20,11 @@ import { SceneFlow } from './scene-flow.ts';
|
||||
import { LoginPanel } from './login-panel.ts';
|
||||
import { LobbyAssets } from './LobbyAssets.ts';
|
||||
import { LobbyPanel } from './lobby-panel.ts';
|
||||
import { logNativeCall } from '../../framework/core/native-call-log.ts';
|
||||
import type { NativeCallName } from '../../framework/adapters/native/native-contracts.ts';
|
||||
import { resolveLobbyPolicy } from '../../framework/presentation/lobby-model.ts';
|
||||
import { openLobbyUrl } from '../../framework/adapters/native/lobby-links.ts';
|
||||
import { openLobbyUrl, resolveLobbyLaunchMode } from '../../framework/adapters/native/lobby-links.ts';
|
||||
import { resolveLobbyMenuConfig, LOBBY_UNAVAILABLE, type LobbyContext } from '../../framework/presentation/lobby-menu.ts';
|
||||
import { FeedbackPanels } from './feedback-panels.ts';
|
||||
import { isLoginResponse } from '../../framework/protocol/first-slice-routes.ts';
|
||||
|
||||
@@ -49,6 +54,7 @@ export class PlatformStartup extends Component {
|
||||
private accounts:LoginAccounts|null=null;
|
||||
private panel:LoginPanel|null=null;
|
||||
private lobby:LobbyPanel|null=null;
|
||||
private lobbyContext:LobbyContext|null=null;
|
||||
private openExternal:((url:string)=>void)|null=null;
|
||||
private bridge:WebViewJavascriptBridgeAdapter|null=null;
|
||||
private restoreShareLogin:(()=>void)|null=null;
|
||||
@@ -101,6 +107,8 @@ export class PlatformStartup extends Component {
|
||||
this.accounts=new LoginAccounts({storage:window.localStorage,settings:native,random:Math.random,now:Date.now,
|
||||
gameid:this.config.identity.gameid,agentid:this.config.identity.agentid,policy:this.policy,
|
||||
submit:(account,mode)=>this.login(account,mode),tip:text=>this.tip(text)});
|
||||
this.lobbyContext={versionState:this.accounts.versionState,
|
||||
launchMode:resolveLobbyLaunchMode(window.location.search,bridgeHost,(window as any).app_Launchtype)};
|
||||
const previous=(window as any).sharelogin;
|
||||
const callback=(openid:unknown,headimgurl:unknown,nickname:unknown,sex:unknown,city:unknown,Province:unknown,unionid:unknown)=>{
|
||||
if(!this.terminal)this.acceptNative({openid,headimgurl,nickname,sex,city,Province,unionid},'global');
|
||||
@@ -123,7 +131,9 @@ export class PlatformStartup extends Component {
|
||||
this.bridge.register('sharelogin',data=>this.acceptNative(data,'bridge'));
|
||||
this.bridge.register('getlocationinfo',locationCallback);
|
||||
this.startLocation=()=>{if(bridgeHost)required(this.bridge,'native bridge').call('startlocation',2,()=>{});
|
||||
else required(injected.settings,'settings').startlocation(2);};
|
||||
else {logNativeCall('settings','startlocation',[2],true);required(injected.settings,'settings').startlocation(2);}};
|
||||
}else{
|
||||
this.startLocation=()=>logNativeCall('settings','startlocation',[2],false);
|
||||
}
|
||||
required(this.countdown,'countdown').start(resolveLaunchWaitTime(this.config.remoteConfig));
|
||||
await this.startSession(true);
|
||||
@@ -136,6 +146,12 @@ export class PlatformStartup extends Component {
|
||||
createWireClient:config=>{
|
||||
const wire=new WireClient({servers:config.servers,transportFactory:()=>new CocosWebSocketTransport()});
|
||||
wire.subscribe(event=>{this.events.push({kind:'wire',data:event.type});
|
||||
if(event.type==='message'&&(isLoginResponse(event.message)||(event.message.route==='agent'&&event.message.rpc==='self_join_room'))){
|
||||
const data=event.message.data as Record<string,unknown>;
|
||||
if(data.state===0&&data.shortcode){const id=config.identity;
|
||||
new VipRoomHistory(window.localStorage,id.gameid,id.agentid).add(vipCode(data.shortcode,'$.data.shortcode'));
|
||||
}
|
||||
}
|
||||
if(event.type==='open'&&this.reconnecting){
|
||||
this.reconnecting=false;this.feedback?.reconnect(false);
|
||||
this.waitingReply=this.busy||this.runtime!.state.players.selfPlayerId!==null;
|
||||
@@ -152,7 +168,7 @@ export class PlatformStartup extends Component {
|
||||
showLobby:()=>{void this.showLobby().catch(error=>this.fail(error));},
|
||||
showRoom:()=>{this.saveAcceptedLogin();this.fail(Error('当前入口尚未接入子游戏场景'));},
|
||||
showReconnect:()=>{this.waitingReply=false;this.feedback?.loading(false);this.reconnecting=true;this.feedback?.reconnect(true);},
|
||||
showTips:(message,time)=>this.tip(message,time),
|
||||
showTips:(message,time,source)=>{if(source==='show_message')this.lobby?.cancelTaskRequest();this.tip(message,time);},
|
||||
showKicked:data=>{
|
||||
const packet=data as {msg?:string;fromOther?:unknown;gameid?:unknown};
|
||||
const text=packet.msg||`网络出现异常!${packet.fromOther?(packet.gameid==='G2hw0ubng0zcoI0r4mx3H2yr4GejidwO'?2:1):0}#(请重启游戏)`;
|
||||
@@ -255,13 +271,35 @@ export class PlatformStartup extends Component {
|
||||
popup.addComponent(UITransform).setContentSize(1600,720);
|
||||
this.lobby=new LobbyPanel(this.canvas(),popup,required(this.getComponent(LobbyAssets),'LobbyAssets'),
|
||||
required(this.config,'config'),resolveLobbyPolicy(required(this.config,'config').remoteConfig),
|
||||
required(this.runtime,'runtime').state,message=>this.tip(message),()=>this.logout(),()=>this.location,url=>required(this.openExternal,'browser host')(url));
|
||||
required(this.runtime,'runtime').state,message=>this.tip(message),()=>this.logout(),()=>this.location,url=>required(this.openExternal,'browser host')(url),
|
||||
resolveLobbyMenuConfig(required(this.config,'config').remoteConfig),required(this.lobbyContext,'lobby context'),action=>{
|
||||
this.events.push({kind:'lobby-action',data:action});
|
||||
if(action==='tasks'){required(this.runtime,'runtime').getPlayerTasks();return;}
|
||||
this.tip(required(LOBBY_UNAVAILABLE[action],`lobby action handler ${action}`));
|
||||
},taskid=>required(this.runtime,'runtime').claimTask(taskid),(name,data)=>this.callNative(name,data),{
|
||||
request:codes=>required(this.runtime,'runtime').getVipRooms(codes),
|
||||
join:code=>required(this.runtime,'runtime').joinVipRoom(code,this.location),
|
||||
describe:()=> '子游戏规则描述尚未接入',
|
||||
});
|
||||
popup.setSiblingIndex(this.canvas().children.length-1);
|
||||
required(this.status,'status').node.setSiblingIndex(this.canvas().children.length-1);
|
||||
this.events.push({kind:'scene',data:'Lobby'});
|
||||
});
|
||||
this.tip('');
|
||||
}
|
||||
private callNative(name:string,data:Record<string,unknown>):void {
|
||||
const win=window as unknown as {settings?:Record<string,unknown>;app_version?:number|string};
|
||||
if(win.app_version!==undefined&&Number(win.app_version)!==0){
|
||||
const bridge=this.bridge;
|
||||
if(bridge?.isReady()){bridge.call(name as NativeCallName,data,()=>{});return;}
|
||||
logNativeCall('WVJB',name,[data],false);return;
|
||||
}
|
||||
const args=name==='friendsSharetypeUrlToptitleDescript'?
|
||||
[data.sharefriend,data.type,data.webpageUrl,data.title,data.description]:[data];
|
||||
const method=win.settings?.[name];
|
||||
logNativeCall('settings',name,args,typeof method==='function');
|
||||
if(typeof method==='function')Reflect.apply(method,win.settings,args);
|
||||
}
|
||||
private logout():void{
|
||||
if(this.returningToLogin||this.terminal)return;
|
||||
this.returningToLogin=true;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { _decorator, Button, Component, Label, Node, ProgressBar } from 'cc';
|
||||
import { taskItemModel, type TaskData } from '../../framework/presentation/task-item-model.ts';
|
||||
const {ccclass,property}=_decorator;
|
||||
function required<T>(value:T|null,name:string):T{if(value===null)throw Error(`TaskItem missing ${name}`);return value;}
|
||||
|
||||
@ccclass('TaskItemView')
|
||||
export class TaskItemView extends Component {
|
||||
@property(Label) titleLabel:Label|null=null;
|
||||
@property(Label) rewardLabel:Label|null=null;
|
||||
@property(Label) progressLabel:Label|null=null;
|
||||
@property(ProgressBar) progressBar:ProgressBar|null=null;
|
||||
@property(Button) doButton:Button|null=null;
|
||||
@property(Button) claimButton:Button|null=null;
|
||||
@property(Node) claimedMark:Node|null=null;
|
||||
private model:ReturnType<typeof taskItemModel>|null=null;
|
||||
private readonly claim=()=>{if(this.model?.showClaim)this.node.emit('task-claim',this.model.taskid);};
|
||||
private readonly perform=()=>{if(this.model?.showDo)this.node.emit('task-do',this.model.taskid);};
|
||||
onLoad():void{
|
||||
required(this.claimButton,'claimButton').node.on(Button.EventType.CLICK,this.claim);
|
||||
required(this.doButton,'doButton').node.on(Button.EventType.CLICK,this.perform);
|
||||
}
|
||||
render(task:TaskData,index:number,roomCardName:string):void{
|
||||
const model=taskItemModel(task,index,roomCardName);this.model=model;
|
||||
required(this.titleLabel,'titleLabel').string=model.title;
|
||||
required(this.rewardLabel,'rewardLabel').string=model.reward;
|
||||
const progress=required(this.progressBar,'progressBar');progress.progress=model.progress;progress.node.active=model.showProgress;
|
||||
const progressLabel=required(this.progressLabel,'progressLabel');progressLabel.string=model.progressText;progressLabel.node.active=model.showProgress;
|
||||
required(this.doButton,'doButton').node.active=model.showDo;
|
||||
required(this.claimButton,'claimButton').node.active=model.showClaim;
|
||||
required(this.claimedMark,'claimedMark').active=model.showClaimed;
|
||||
}
|
||||
onDestroy():void{
|
||||
// Children may already be destroyed when this parent component is released.
|
||||
this.claimButton?.node?.off(Button.EventType.CLICK,this.claim);
|
||||
this.doButton?.node?.off(Button.EventType.CLICK,this.perform);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "bb88d61b-9847-46bc-9c71-4ad6d196d501",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { _decorator, Component, instantiate, Layout, Prefab, ScrollView, UITransform } from 'cc';
|
||||
import { taskItemModel, type TaskData } from '../../framework/presentation/task-item-model.ts';
|
||||
import { TaskItemView } from './TaskItemView.ts';
|
||||
const {ccclass,property}=_decorator;
|
||||
|
||||
/** Task response consumers call render; business requests are supplied via task-claim/task-do events. */
|
||||
@ccclass('TaskListView')
|
||||
export class TaskListView extends Component {
|
||||
@property(Prefab) itemPrefab:Prefab|null=null;
|
||||
@property(ScrollView) scrollView:ScrollView|null=null;
|
||||
render(tasks:readonly TaskData[],roomCardName:string):void{
|
||||
if(!Array.isArray(tasks))throw Error('TaskList requires tasks array');
|
||||
tasks.forEach((task,index)=>taskItemModel(task,index,roomCardName));
|
||||
if(!this.itemPrefab||!this.scrollView?.content)throw Error('TaskList missing itemPrefab or scrollView.content');
|
||||
const itemPrefab=this.itemPrefab;
|
||||
const content=this.scrollView.content;
|
||||
const layout=content.getComponent(Layout);
|
||||
if(!layout)throw Error('TaskList content missing Layout');
|
||||
this.scrollView.stopAutoScroll();
|
||||
for(const child of [...content.children]){child.removeFromParent();child.destroy();}
|
||||
tasks.forEach((task,index)=>{
|
||||
const item=instantiate(itemPrefab);item.parent=content;
|
||||
const view=item.getComponent(TaskItemView);if(!view)throw Error('TaskItem prefab missing TaskItemView');
|
||||
view.render(task,index,roomCardName);
|
||||
item.on('task-claim',(taskid:number|string)=>this.node.emit('task-claim',taskid));
|
||||
item.on('task-do',(taskid:number|string)=>this.node.emit('task-do',taskid));
|
||||
});
|
||||
layout.updateLayout();
|
||||
// Empty list still has a defined extent; Layout has no children to measure in that case.
|
||||
if(tasks.length===0){const ui=content.getComponent(UITransform);if(!ui)throw Error('TaskList content missing UITransform');ui.width=0;}
|
||||
this.scrollView.scrollToLeft(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "a5fd73e7-5651-48e5-a4c6-7fa8d570ef3e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {_decorator,Button,Component,instantiate,Label,Node,Prefab,ScrollView,Sprite,SpriteFrame,UITransform} from 'cc';
|
||||
import type {VipCode,VipRoom} from '../../framework/core/types/vip-room.ts';
|
||||
import {sameVipCode} from '../../framework/core/types/vip-room.ts';
|
||||
import {occupiedSeats,orderedVipRooms} from '../../framework/presentation/vip-room-model.ts';
|
||||
import {requireNode} from './node-bindings.ts';
|
||||
const {ccclass,property}=_decorator;
|
||||
function need<T>(v:T|null|undefined,name:string):T {if(v==null)throw Error(`VipRoomList missing ${name}`);return v;}
|
||||
@ccclass('VipRoomListView')
|
||||
export class VipRoomListView extends Component {
|
||||
@property(Prefab) itemPrefab1:Prefab|null=null;
|
||||
@property(Prefab) itemPrefab2:Prefab|null=null;
|
||||
@property(ScrollView) roomsScroll:ScrollView|null=null;
|
||||
@property(ScrollView) desksScroll:ScrollView|null=null;
|
||||
@property([SpriteFrame]) titleFrames:SpriteFrame[]=[];
|
||||
@property([SpriteFrame]) deskFrames:SpriteFrame[]=[];
|
||||
private rooms:readonly VipRoom[]=[];
|
||||
private selected:VipCode|null=null;
|
||||
private describe:(roomtype:readonly unknown[])=>string=()=>{throw Error('VIP room description provider is not configured');};
|
||||
configure(describe:(roomtype:readonly unknown[])=>string):void {this.describe=describe;this.showRooms();}
|
||||
get selectedCode():VipCode|null {return this.selected;}
|
||||
render(rooms:readonly VipRoom[],codes:readonly VipCode[]):void {
|
||||
this.rooms=orderedVipRooms(rooms,codes);
|
||||
this.drawRooms();
|
||||
if(this.selected!==null&&!this.rooms.some(r=>sameVipCode(r.shortCode,need(this.selected,'selected code'))))this.showRooms();
|
||||
else if(this.selected!==null)this.drawDesks();
|
||||
}
|
||||
showRooms():void {this.selected=null;this.page(false);}
|
||||
private page(second:boolean):void {
|
||||
need(this.roomsScroll,'roomsScroll').node.active=!second;need(this.desksScroll,'desksScroll').node.active=second;
|
||||
for(const id of [3175,3199])requireNode(this.node,`group-103/Spirit${id}`).active=second;
|
||||
requireNode(this.node,'group-103/Spirit3259').active=false;
|
||||
if(this.titleFrames.length!==2)throw Error('VIP title requires two frames');
|
||||
need(requireNode(this.node,'group-103/Spirit3174').getComponent(Sprite),'title Sprite').spriteFrame=this.titleFrames[second?1:0];
|
||||
}
|
||||
private text(root:Node,id:number,text:string):void {need(requireNode(root,`Spirit${id}`).getComponent(Label),`Spirit${id} Label`).string=text;}
|
||||
private click(node:Node,fn:()=>void):void {const b=node.getComponent(Button)||node.addComponent(Button);b.transition=Button.Transition.NONE;node.on(Button.EventType.CLICK,fn);}
|
||||
private clear(scroll:ScrollView):Node {
|
||||
const content=need(scroll.content,'content');
|
||||
for(const n of [...content.children]){n.removeFromParent();n.destroy();}return content;
|
||||
}
|
||||
private drawRooms():void {
|
||||
const scroll=need(this.roomsScroll,'roomsScroll'),offset=scroll.getScrollOffset(),content=this.clear(scroll);
|
||||
need(content.getComponent(UITransform),'content UITransform').height=Math.max(460,this.rooms.length*199);
|
||||
for(const [i,room] of this.rooms.entries()){
|
||||
const row=instantiate(need(this.itemPrefab1,'itemPrefab1'));row.parent=content;row.setPosition(315.5,-97-i*199,0);
|
||||
this.text(row,3177,room.title);this.text(row,3178,String(room.shortCode));this.text(row,3179,room.owner);
|
||||
this.text(row,3180,`${room.playerList.reduce((n,p)=>n+occupiedSeats(p),0)} (人)`);
|
||||
this.text(row,3189,room.roomcodes?this.describe(room.roomcodes.roomtype):'房主还未创建房间!');
|
||||
this.click(row,()=>{this.selected=room.shortCode;this.page(true);this.drawDesks();need(this.desksScroll,'desksScroll').scrollToTop(0);});
|
||||
this.click(requireNode(row,'Spirit3192'),()=>this.node.emit('vip-join',room.shortCode));
|
||||
}
|
||||
scroll.scrollToOffset(offset,0);
|
||||
}
|
||||
private drawDesks():void {
|
||||
const room=need(this.rooms.find(r=>sameVipCode(r.shortCode,need(this.selected,'selected code'))),'selected room');
|
||||
const scroll=need(this.desksScroll,'desksScroll'),offset=scroll.getScrollOffset(),content=this.clear(scroll);
|
||||
need(content.getComponent(UITransform),'content UITransform').height=Math.max(460,Math.ceil(room.playerList.length/2)*145);
|
||||
const notice=requireNode(this.node,'group-103/Spirit3259');notice.active=room.roomcodes!==null;
|
||||
if(!room.roomcodes)return;
|
||||
need(notice.getComponent(Label),'mode Label').string=room.forbidSelect?' 比赛模式\n禁止自由选桌':' 普通模式\n允许自由选桌';
|
||||
if(this.deskFrames.length!==2)throw Error('VIP desk requires two frames');
|
||||
room.roomcodes.roomlist.forEach((code,i)=>{
|
||||
const item=instantiate(need(this.itemPrefab2,'itemPrefab2'));item.parent=content;item.setPosition(125+(i%2)*285,-65.5-Math.floor(i/2)*145,0);
|
||||
need(requireNode(item,'Spirit3186').getComponent(Sprite),'desk Sprite').spriteFrame=this.deskFrames[room.deskState[i]?1:0];
|
||||
this.text(item,3187,String(i+1));this.text(item,3188,`人数:${occupiedSeats(room.playerList[i])}/${room.seatCount[i]}`);
|
||||
// Legacy only displays forbidSelect; the server decides whether entry is allowed.
|
||||
this.click(item,()=>this.node.emit('vip-join',code));
|
||||
});scroll.scrollToOffset(offset,0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "72bf923b-71ec-400c-a761-c0d83a5c6c8d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
import { assetManager, BlockInputEvents, ImageAsset, Label, Mask, Node, Prefab, ScrollView, Sprite, SpriteFrame, Texture2D, UITransform, Widget, instantiate, isValid } from 'cc';
|
||||
import { assetManager, BlockInputEvents, Button, ImageAsset, Label, Mask, Node, Prefab, ScrollView, Sprite, SpriteFrame, Texture2D, UITransform, Widget, instantiate, isValid } from 'cc';
|
||||
import type { RuntimeConfig } from '../../framework/config/runtime-config.ts';
|
||||
import { selectLobbyModel, shouldShowNotice, type LobbyModel, type LobbyPolicy } from '../../framework/presentation/lobby-model.ts';
|
||||
import type { PlatformState } from '../../framework/platform/stores/platform-types.ts';
|
||||
import { LobbyAssets } from './LobbyAssets.ts';
|
||||
import { TaskListView } from './TaskListView.ts';
|
||||
import {VipRoomListView} from './VipRoomListView.ts';
|
||||
import {VipRoomHistory} from '../../framework/adapters/native/vip-room-history.ts';
|
||||
import type {VipRoom,VipCode} from '../../framework/core/types/vip-room.ts';
|
||||
import { resolveSharePolicy,sharePayload } from '../../framework/presentation/share-model.ts';
|
||||
import type { TaskData } from '../../framework/core/types/task.ts';
|
||||
import { requireNode } from './node-bindings.ts';
|
||||
import { LOBBY_MENU_ACTIONS, lobbyMenuState, lobbyTrayState, type LobbyAction, type LobbyContext, type LobbyMenuConfig, type LobbyMenuState } from '../../framework/presentation/lobby-menu.ts';
|
||||
|
||||
function need<T>(value:T|null|undefined,name:string):T {if(value==null)throw Error(`Lobby missing ${name}`);return value;}
|
||||
const label=(root:Node,path:string)=>need(requireNode(root,path).getComponent(Label),`${path} Label`);
|
||||
@@ -25,15 +32,34 @@ export class LobbyPanel {
|
||||
private avatarGeneration=0;
|
||||
private cardValue:number|null=null;
|
||||
private beanValue:number|null=null;
|
||||
private readonly menuNodes=new Map<number,Node>();
|
||||
private readonly taskTip:Node;
|
||||
private taskData:readonly TaskData[]|undefined;
|
||||
private taskPending:'list'|'award'|null=null;
|
||||
private taskTimer:ReturnType<typeof setTimeout>|null=null;
|
||||
private vipData:readonly VipRoom[]|undefined;
|
||||
constructor(private readonly canvas:Node,private readonly popup:Node,private readonly assets:LobbyAssets,
|
||||
private readonly config:RuntimeConfig,private readonly policy:LobbyPolicy,state:PlatformState,
|
||||
private readonly tip:(text:string)=>void,private readonly logout:()=>void,
|
||||
private readonly location:()=>unknown,private readonly openUrl:(url:string)=>void){
|
||||
private readonly location:()=>unknown,private readonly openUrl:(url:string)=>void,
|
||||
private readonly menuConfig:LobbyMenuConfig,private readonly context:LobbyContext,
|
||||
private readonly onAction:(action:LobbyAction)=>void,
|
||||
private readonly claimTask:(taskid:number|string)=>void,
|
||||
private readonly nativeCall:(name:string,data:Record<string,unknown>)=>void,
|
||||
private readonly vip?:{request:(codes:readonly VipCode[])=>void;join:(code:VipCode)=>void;describe:(roomtype:readonly unknown[])=>string}){
|
||||
this.model=selectLobbyModel(state);
|
||||
if(assets.switches.length!==2)throw Error('LobbyAssets requires 2 switch frames');
|
||||
this.menu=this.add(assets.menu,canvas);this.header=this.add(assets.header,canvas);this.noticeMenu=this.add(assets.noticeMenu,canvas);
|
||||
requireNode(this.header,'group-103').active=false;
|
||||
requireNode(this.noticeMenu,'group-95').active=false;
|
||||
requireNode(this.noticeMenu,'group-95').active=menuConfig.template.isGameHall;
|
||||
for(const root of [this.menu,this.header,this.noticeMenu]){
|
||||
for(const node of root.getComponentsInChildren(UITransform).map(ui=>ui.node)){
|
||||
const match=/^Spirit(\d+)$/.exec(node.name);
|
||||
if(match){const id=Number(match[1]);if(this.menuNodes.has(id))throw Error(`Duplicate lobby node ${id}`);this.menuNodes.set(id,node);}
|
||||
}
|
||||
}
|
||||
this.menuNodes.set(10,requireNode(this.header,'group-3/购买房卡'));
|
||||
this.menuNodes.set(9,requireNode(this.header,'group-3/PlayerInfo/头像框'));
|
||||
// Original JumpMenuScene hides these transient/obsolete fields.
|
||||
for(const id of [219,220,157,497])requireNode(this.menu,`group-3/Spirit${id}`).active=false;
|
||||
// updateNoticeBoard clears all old contact samples before rendering source values.
|
||||
@@ -44,30 +70,67 @@ export class LobbyPanel {
|
||||
const [title,value]=pairs[index];const titleLabel=label(this.menu,`group-3/Spirit${title}`),valueLabel=label(this.menu,`group-3/Spirit${value}`);
|
||||
titleLabel.string=contact.label;valueLabel.string=contact.value;titleLabel.node.active=true;valueLabel.node.active=true;
|
||||
});
|
||||
requireNode(this.menu,'group-3/Spirit17').active=!policy.hideTask;
|
||||
requireNode(this.menu,'group-3/Spirit14').active=!policy.hideShare;
|
||||
label(this.noticeMenu,'group-3/Spirit145').string=policy.scroll;
|
||||
this.bind(requireNode(this.menu,'group-3/Spirit7'),()=>this.openSettings());
|
||||
this.bind(requireNode(this.header,'group-3/PlayerInfo/头像框'),()=>this.openProfile());
|
||||
this.bind(requireNode(this.menu,'group-3/Spirit12'),()=>this.openJoin());
|
||||
this.bind(requireNode(this.menu,'group-3/Spirit3120'),()=>{const tray=requireNode(this.noticeMenu,'group-95');tray.active=!tray.active;});
|
||||
this.bind(requireNode(this.noticeMenu,'group-95/Spirit5'),()=>this.openNotice());
|
||||
// Unconnected game/service entries give explicit feedback instead of issuing guessed packets.
|
||||
const remaining:Record<number,string>={11:'创建房间需要接入目标子游戏规则',13:'战绩功能尚未接入',14:'分享功能尚未接入',17:'任务列表尚未接入',
|
||||
483:'比赛场尚未接入',568:'星星场尚未接入',3016:'商城尚未接入',3018:'福袋尚未接入',99:'房间列表尚未接入',76:'二维码尚未接入',3139:'试玩房间尚未接入',234:'更多游戏尚未接入'};
|
||||
for(const [id,message] of Object.entries(remaining))this.bind(requireNode(this.menu,`group-3/Spirit${id}`),()=>this.tip(message));
|
||||
for(const name of ['购买房卡','Spirit509','Spirit3233','Spirit3232','Spirit3256','Spirit3264'])this.bind(requireNode(this.header,`group-3/${name}`),()=>this.tip('此功能尚未接入'));
|
||||
for(const id of [3017,235,604,3057,15])this.bind(requireNode(this.noticeMenu,`group-95/Spirit${id}`),()=>this.tip('此功能尚未接入'));
|
||||
for(const [id,action] of Object.entries(LOBBY_MENU_ACTIONS))this.bind(need(this.menuNodes.get(Number(id)),`menu ${id}`),()=>this.dispatch(action));
|
||||
this.bind(requireNode(this.noticeMenu,'group-95/Spirit3122'),()=>{requireNode(this.noticeMenu,'group-95').active=false;});
|
||||
this.applyMenuState(lobbyMenuState(menuConfig,context));
|
||||
if(policy.contacts.length>0)requireNode(this.menu,'group-3/Spirit16').active=false;
|
||||
this.taskTip=requireNode(this.menu,'group-3/Spirit17/Tip');
|
||||
need(this.taskTip.getComponent(Sprite),'Spirit17/Tip Sprite');
|
||||
this.render(state);
|
||||
const key=`tsgame_gameNoteTime_${config.identity.gameid}${config.identity.agentid}`;
|
||||
const previous=window.localStorage.getItem(key),time=previous===null?null:Number(previous);
|
||||
if(time!==null&&!Number.isFinite(time))throw Error(`Invalid ${key}`);
|
||||
const now=Date.now();
|
||||
if(shouldShowNotice(policy.noticeHours,time,now,false)){
|
||||
if(context.versionState===0&&shouldShowNotice(policy.noticeHours,time,now,false)){
|
||||
if(policy.noticeHours>0)window.localStorage.setItem(key,String(now));
|
||||
this.openNotice();
|
||||
}
|
||||
}
|
||||
private dispatch(action:LobbyAction):void{
|
||||
switch(action){
|
||||
case 'share':this.openShare();return;
|
||||
case 'vip-rooms':this.openVipRooms();return;
|
||||
case 'tasks':this.requestTask('list',()=>this.onAction('tasks'));return;
|
||||
case 'settings':this.openSettings();return;
|
||||
case 'profile':this.openProfile();return;
|
||||
case 'join-room':this.openJoin();return;
|
||||
case 'notice':this.openNotice();return;
|
||||
case 'tray':{
|
||||
const tray=requireNode(this.noticeMenu,'group-95');
|
||||
if(tray.active){tray.active=false;return;}
|
||||
if(this.context.versionState===1)return;
|
||||
const state=lobbyTrayState(this.menuConfig,this.context);
|
||||
const bg=requireNode(tray,'Spirit3121');bg.active=true;
|
||||
need(bg.getComponent(UITransform),'tray background').width=150*Object.values(state.visible).filter(Boolean).length+30;
|
||||
this.applyMenuState(state);
|
||||
tray.active=true;return;
|
||||
}
|
||||
default:this.onAction(action);
|
||||
}
|
||||
}
|
||||
private applyMenuState(state:LobbyMenuState):void{
|
||||
// A projection includes nodes from the adjacent notice layer. Missing IDs belonging to other layers are intentional.
|
||||
for(const [id,active] of Object.entries(state.visible)){const node=this.menuNodes.get(Number(id));if(node)node.active=active;}
|
||||
for(const id of new Set([...Object.keys(state.left),...Object.keys(state.top)])){
|
||||
const node=this.menuNodes.get(Number(id));if(!node)continue;
|
||||
const ui=need(node.getComponent(UITransform),`menu ${id} transform`);
|
||||
const widget=node.getComponent(Widget);
|
||||
// Original coordinates are top-left in a centered 1280x720 layout; retain the 1600x720 Cocos canvas.
|
||||
const left=state.left[Number(id)],top=state.top[Number(id)];
|
||||
if(left!==undefined){
|
||||
const x=left-640+ui.width*ui.anchorX;
|
||||
if(widget){widget.isAlignLeft=false;widget.isAlignRight=false;widget.isAlignHorizontalCenter=true;widget.horizontalCenter=x;}
|
||||
node.setPosition(x,node.position.y,0);
|
||||
}
|
||||
if(top!==undefined){
|
||||
const y=360-top-ui.height*(1-ui.anchorY);
|
||||
if(widget){widget.isAlignTop=false;widget.isAlignBottom=false;widget.isAlignVerticalCenter=true;widget.verticalCenter=y;}
|
||||
node.setPosition(node.position.x,y,0);
|
||||
}
|
||||
if(widget)widget.updateAlignment();
|
||||
}
|
||||
}
|
||||
private add(prefab:Prefab|null,parent:Node):Node {
|
||||
const root=instantiate(need(prefab,'prefab'));root.parent=parent;root.setPosition(0,0,0);root.active=true;
|
||||
const widget=root.getComponent(Widget);
|
||||
@@ -76,10 +139,12 @@ export class LobbyPanel {
|
||||
return root;
|
||||
}
|
||||
private bind(node:Node,action:()=>void,modal=false):void {
|
||||
const listener=()=>{if(this.disposed||(!modal&&this.modal!==null)||!this.model.canAct)return;
|
||||
const button=node.getComponent(Button)??node.addComponent(Button);
|
||||
button.transition=Button.Transition.NONE;
|
||||
const listener=()=>{if(this.disposed||!node.activeInHierarchy||!button.interactable||(!modal&&this.modal!==null)||!this.model.canAct)return;
|
||||
try{action();}catch(error){this.tip(error instanceof Error?error.message:String(error));}};
|
||||
node.on(Node.EventType.TOUCH_END,listener);
|
||||
(modal?this.modalReleases:this.releases).push(()=>{if(isValid(node))node.off(Node.EventType.TOUCH_END,listener);});
|
||||
node.on(Button.EventType.CLICK,listener);
|
||||
(modal?this.modalReleases:this.releases).push(()=>{if(isValid(node))node.off(Button.EventType.CLICK,listener);});
|
||||
}
|
||||
render(state:PlatformState):void {
|
||||
if(this.disposed)return;this.model=selectLobbyModel(state);
|
||||
@@ -88,12 +153,16 @@ export class LobbyPanel {
|
||||
if(this.cardValue!==this.model.roomcard){this.numbers('Spirit3161','Spirit287',this.model.roomcard);this.cardValue=this.model.roomcard;}
|
||||
if(this.beanValue!==this.model.bean){this.numbers('Spirit3162','Spirit496',this.model.bean);this.beanValue=this.model.bean;}
|
||||
// Task tip stays attached to its button and inherits its visibility.
|
||||
const task=requireNode(this.menu,'group-3/Spirit17');
|
||||
let tip=task.getChildByName('TaskTip');
|
||||
if(!tip){tip=new Node('TaskTip');tip.parent=task;tip.layer=task.layer;tip.setPosition(45,25,0);
|
||||
tip.addComponent(UITransform).setContentSize(63,63);const dot=tip.addComponent(Sprite);dot.sizeMode=Sprite.SizeMode.CUSTOM;dot.spriteFrame=need(this.assets.taskTip,'task tip frame');}
|
||||
tip.active=this.model.taskstate!==0;
|
||||
this.taskTip.active=this.model.taskstate!==0;
|
||||
if(!this.model.canAct)this.cancelTaskRequest();
|
||||
if(state.tasks!==this.taskData){
|
||||
this.taskData=state.tasks;
|
||||
const show=this.taskPending==='list'||this.modalKind==='tasks';
|
||||
this.cancelTaskRequest();
|
||||
if(show&&state.tasks&&this.model.canAct){this.tip('');this.openTasks(state.tasks);}
|
||||
}
|
||||
this.updateAvatar();
|
||||
if(state.vipRooms!==this.vipData){this.vipData=state.vipRooms;if(this.modalKind==='vip-rooms'&&state.vipRooms)this.renderVipRooms(state.vipRooms);}
|
||||
if(this.modalKind==='profile')this.renderProfile();
|
||||
}
|
||||
private numbers(name:string,background:string,value:number):void {
|
||||
@@ -153,7 +222,7 @@ export class LobbyPanel {
|
||||
const group=requireNode(modal,'group-6');
|
||||
for(const name of ['申请解散','邀请好友'])requireNode(group,name).active=false;
|
||||
this.bind(requireNode(group,'设置遮罩'),()=>this.close(),true);
|
||||
this.bind(requireNode(group,'退出登录'),()=>this.logout(),true);
|
||||
this.bind(requireNode(group,'退出登录'),()=>{if(this.context.launchMode===1)this.tip('请返回游戏大厅退出登录');else this.logout();},true);
|
||||
for(const [name,key] of [['音乐底',`tsgame_bgm_${this.config.identity.gameid}`],['音效底',`tsgame_sound_${this.config.identity.gameid}${this.config.identity.agentid}`]] as const){
|
||||
const sprite=need(requireNode(group,name).getComponent(Sprite),name);
|
||||
const read=()=>{const raw=window.localStorage.getItem(key);if(raw===null){window.localStorage.setItem(key,'1');return true;}
|
||||
@@ -220,9 +289,93 @@ export class LobbyPanel {
|
||||
for(const id of [27,59])this.bind(requireNode(modal,`group-5/Spirit${id}`),()=>this.close(),true);
|
||||
// Room entry needs the real game host before a room request can be sent safely.
|
||||
for(const id of [28,29,30,31,32,33,34,35,36,37,38,39,40])this.bind(requireNode(modal,`group-5/Spirit${id}`),()=>this.tip('加入房间将在目标子游戏接入后启用'),true);
|
||||
for(let id=236;id<=241;id++)label(modal,`group-5/Spirit${id}`).string='';
|
||||
for(let id=236;id<=241;id++)requireNode(modal,`group-5/Spirit${id}`).active=false;
|
||||
}
|
||||
openShare():void {
|
||||
const modal=this.open(this.assets.share,'share','group-11'),policy=resolveSharePolicy(this.config);
|
||||
this.bind(requireNode(modal,'group-11/Spirit72'),()=>this.close(),true);
|
||||
for(const [id,friend] of [[74,1],[75,2]] as const)this.bind(requireNode(modal,`group-11/Spirit${id}`),()=>{
|
||||
const data=sharePayload(policy,friend);if(data)this.nativeCall('friendsSharetypeUrlToptitleDescript',data);
|
||||
this.close();
|
||||
},true);
|
||||
if(policy.background){
|
||||
const node=requireNode(modal,'group-11/Spirit345'),sprite=need(node.getComponent(Sprite),'share background');
|
||||
assetManager.loadRemote<ImageAsset>(policy.background,{ext:'.png'},(error,image)=>{
|
||||
if(this.disposed||this.modal!==modal||!isValid(node))return;
|
||||
if(error){this.tip(`分享背景加载失败:${error.message}`);return;}
|
||||
const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame;
|
||||
this.modalReleases.push(()=>{sprite.spriteFrame=null;frame.destroy();texture.destroy();});
|
||||
});
|
||||
}
|
||||
}
|
||||
private openVipRooms():void {
|
||||
const modal=this.open(this.assets.header,'vip-rooms','group-103'),group=requireNode(modal,'group-103');
|
||||
this.menu.active=false;this.noticeMenu.active=false;
|
||||
this.bind(requireNode(group,'Spirit3182'),()=>this.close(),true);
|
||||
for(const id of [3190,3191])requireNode(group,`Spirit${id}`).active=false;
|
||||
const advanced=need(this.model.advanced,'players.self.advanced');
|
||||
for(const id of advanced?[3170,3171]:[3183,3184,3194,3195,3196,3197,3198,3214,3215,3235])requireNode(group,`Spirit${id}`).active=false;
|
||||
const view=need(modal.getComponent(VipRoomListView),'VipRoomListView'),port=need(this.vip,'VIP command port');
|
||||
view.configure(port.describe);
|
||||
this.bind(requireNode(group,'Spirit3175'),()=>view.showRooms(),true);
|
||||
this.bind(requireNode(group,'Spirit3184'),()=>{this.close();this.onAction('create-room');},true);
|
||||
this.bind(requireNode(group,'Spirit3240'),()=>this.openJoin(),true);
|
||||
this.bind(requireNode(group,'Spirit3231'),()=>this.onAction('vip-rank'),true);
|
||||
this.bind(requireNode(group,'Spirit3199'),()=>this.tip(`房间 ${view.selectedCode}:短房号排行模块尚未接入`),true);
|
||||
for(const id of [3194,3195,3214])this.bind(requireNode(group,`Spirit${id}`),()=>this.tip('黑名单管理模块尚未接入'),true);
|
||||
for(const id of [3196,3197,3215])this.bind(requireNode(group,`Spirit${id}`),()=>this.tip('统一魅力值设置模块尚未接入'),true);
|
||||
this.bind(requireNode(group,'Spirit3235'),()=>this.tip('好友管理模块尚未接入'),true);
|
||||
if(advanced){
|
||||
label(group,'Spirit3198').string='(黑名单内玩家临时禁入房间,请尽快到后台处理好友关系)';
|
||||
requireNode(group,'Spirit3235').active=!!this.config.remoteConfig.getValue('managerUrl');
|
||||
}
|
||||
const join=(code:VipCode)=>{if(this.model.canAct&&this.modal===modal){try{port.join(code);}catch(e){this.tip(e instanceof Error?e.message:String(e));}}};
|
||||
modal.on('vip-join',join);this.modalReleases.push(()=>modal.off('vip-join',join));
|
||||
const refresh=()=>{if(this.model.canAct&&this.modal===modal)port.request(this.vipHistory().read());};
|
||||
const raw=this.config.remoteConfig.getValue('vipFlushTime');
|
||||
// GameData.sysConfig defines the optional refresh setting as 10000 ms.
|
||||
const delay=raw==null?10000:Number(raw);if(!Number.isFinite(delay)||delay<=0)throw Error('Invalid vipFlushTime');
|
||||
const timer=setInterval(()=>{try{if(this.vipHistory().read().length>0)refresh();}catch(e){this.tip(e instanceof Error?e.message:String(e));}},delay);
|
||||
this.modalReleases.push(()=>clearInterval(timer));
|
||||
if(this.vipData)this.renderVipRooms(this.vipData);
|
||||
refresh();
|
||||
if(!advanced){const wx=this.config.remoteConfig.getValue('vipWx');label(group,'Spirit3171').string=wx==null?'':String(wx);}
|
||||
}
|
||||
private vipHistory():VipRoomHistory {return new VipRoomHistory(window.localStorage,this.config.identity.gameid,this.config.identity.agentid);}
|
||||
private renderVipRooms(rooms:readonly VipRoom[]):void {need(need(this.modal,'VIP modal').getComponent(VipRoomListView),'VipRoomListView').render(rooms,this.vipHistory().read());}
|
||||
private requestTask(kind:'list'|'award',send:()=>void):void {
|
||||
if(this.taskPending||!this.model.canAct)return;
|
||||
this.taskPending=kind;
|
||||
this.setTaskButtons(false);
|
||||
this.taskTimer=setTimeout(()=>{this.cancelTaskRequest();this.tip('任务请求超时,请重试');},10000);
|
||||
try{send();}catch(error){this.cancelTaskRequest();throw error;}
|
||||
}
|
||||
/** Server show_message and connection transitions release the local request guard. */
|
||||
cancelTaskRequest():void {
|
||||
if(this.taskTimer!==null)clearTimeout(this.taskTimer);
|
||||
this.taskTimer=null;this.taskPending=null;this.setTaskButtons(true);
|
||||
}
|
||||
private setTaskButtons(enabled:boolean):void {
|
||||
if(this.modalKind==='tasks'&&this.modal)for(const button of this.modal.getComponentsInChildren(Button)){
|
||||
if(button.node.name==='Spirit124'||button.node.name==='Spirit125')button.interactable=enabled;
|
||||
}
|
||||
}
|
||||
private openTasks(tasks:readonly TaskData[]):void {
|
||||
const modal=this.modalKind==='tasks'?need(this.modal,'tasks'):this.open(this.assets.tasks,'tasks','group-10');
|
||||
if(this.modalReleases.length===0){
|
||||
for(const id of [112,115])this.bind(requireNode(modal,`group-10/Spirit${id}`),()=>this.close(),true);
|
||||
const claim=(taskid:number|string)=>{if(this.model.canAct&&!this.disposed&&this.modal===modal){
|
||||
try{this.requestTask('award',()=>this.claimTask(taskid));}catch(error){this.tip(error instanceof Error?error.message:String(error));}
|
||||
}};
|
||||
const perform=()=>{if(this.model.canAct&&!this.taskPending&&this.modal===modal){this.close();this.openShare();}};
|
||||
modal.on('task-claim',claim);modal.on('task-do',perform);
|
||||
this.modalReleases.push(()=>{modal.off('task-claim',claim);modal.off('task-do',perform);});
|
||||
}
|
||||
need(modal.getComponent(TaskListView),'TaskListView').render(tasks,this.policy.roomCardName);
|
||||
}
|
||||
close():void {
|
||||
this.cancelTaskRequest();
|
||||
if(this.modalKind==='vip-rooms'){this.menu.active=true;this.noticeMenu.active=true;}
|
||||
for(const release of this.modalReleases.splice(0))release();
|
||||
this.modal?.destroy();this.modal=null;this.modalKind='';
|
||||
}
|
||||
@@ -230,5 +383,6 @@ export class LobbyPanel {
|
||||
if(this.disposed)return;this.disposed=true;this.avatarGeneration++;this.close();
|
||||
for(const release of this.releases.splice(0))release();
|
||||
this.avatarFrame?.destroy();this.avatarTexture?.destroy();
|
||||
for(const root of [this.menu,this.header,this.noticeMenu])if(isValid(root))root.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user