feat: integrate login flow, lobby UI and bitmap font resources

This commit is contained in:
2026-09-06 13:46:57 +08:00
parent e1bcfd91ec
commit 842c68ef55
141 changed files with 11279 additions and 1717 deletions
@@ -0,0 +1,16 @@
import { _decorator, Component, Prefab, SpriteFrame } from 'cc';
const {ccclass,property}=_decorator;
/** Editor-owned asset references; no runtime UUID/path guessing. */
@ccclass('LobbyAssets')
export class LobbyAssets extends Component {
@property(Prefab) menu:Prefab|null=null;
@property(Prefab) header:Prefab|null=null;
@property(Prefab) noticeMenu:Prefab|null=null;
@property(Prefab) notices:Prefab|null=null;
@property(Prefab) profile:Prefab|null=null;
@property(Prefab) settings:Prefab|null=null;
@property(Prefab) join:Prefab|null=null;
@property([SpriteFrame]) digits:SpriteFrame[]=[];
@property([SpriteFrame]) switches:SpriteFrame[]=[];
@property(SpriteFrame) taskTip:SpriteFrame|null=null;
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "37208ac6-fe57-4e31-9993-68a6299966cd",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -16,6 +16,12 @@ import { CocosLogoCountdownView, createLogoCountdown } from './logo-countdown.ts
import { requireNode } from './node-bindings.ts';
import { SceneFlow } from './scene-flow.ts';
import { LoginPanel } from './login-panel.ts';
import { LobbyAssets } from './LobbyAssets.ts';
import { LobbyPanel } from './lobby-panel.ts';
import { resolveLobbyPolicy } from '../../framework/presentation/lobby-model.ts';
import { openLobbyUrl } from '../../framework/adapters/native/lobby-links.ts';
import { FeedbackPanels } from './feedback-panels.ts';
import { isLoginResponse } from '../../framework/protocol/first-slice-routes.ts';
const {ccclass,property}=_decorator;
function required<T>(value:T|null|undefined,name:string):T{if(value==null)throw Error(`PlatformStartup missing ${name}`);return value;}
@@ -25,15 +31,25 @@ function required<T>(value:T|null|undefined,name:string):T{if(value==null)throw
export class PlatformStartup extends Component {
@property(SceneAsset) loadingScene:SceneAsset|null=null;
@property(SceneAsset) loginScene:SceneAsset|null=null;
@property(SceneAsset) lobbyScene:SceneAsset|null=null;
@property(Prefab) loadingPrefab:Prefab|null=null;
@property(Prefab) loginPrefab:Prefab|null=null;
@property(Prefab) protocolPrefab:Prefab|null=null;
@property(Prefab) phonePrefab:Prefab|null=null;
@property(Prefab) tipsPrefab:Prefab|null=null;
@property(Prefab) waitPrefab:Prefab|null=null;
@property(Prefab) reconnectPrefab:Prefab|null=null;
@property(Prefab) kickPrefab:Prefab|null=null;
private feedback:FeedbackPanels|null=null;
private waitingReply=false;
private reconnecting=false;
private runtime:PlatformRuntime|null=null;
private config:RuntimeConfig|null=null;
private policy:LoginPolicy|null=null;
private accounts:LoginAccounts|null=null;
private panel:LoginPanel|null=null;
private lobby:LobbyPanel|null=null;
private openExternal:((url:string)=>void)|null=null;
private bridge:WebViewJavascriptBridgeAdapter|null=null;
private restoreShareLogin:(()=>void)|null=null;
private restoreLocation:(()=>void)|null=null;
@@ -48,6 +64,7 @@ export class PlatformStartup extends Component {
private device:LoginDeviceSnapshot|null=null;
private busy=false;
private terminal=false;
private returningToLogin=false;
private readonly events:Array<{kind:string;data:unknown}>=[];
private readonly flow=new SceneFlow(name=>new Promise<void>((resolve,reject)=>{
if(!director.loadScene(name,error=>error?reject(error):resolve()))reject(Error(`Cannot load scene ${name}`));
@@ -57,6 +74,7 @@ export class PlatformStartup extends Component {
async start():Promise<void>{
try{
required(this.loadingScene,'loadingScene');required(this.loginScene,'loginScene');
required(this.lobbyScene,'lobbyScene');required(this.getComponent(LobbyAssets),'LobbyAssets');
required(this.loadingPrefab,'loadingPrefab');required(this.loginPrefab,'loginPrefab');
required(this.protocolPrefab,'protocolPrefab');required(this.phonePrefab,'phonePrefab');
director.addPersistRootNode(this.node);
@@ -73,6 +91,9 @@ export class PlatformStartup extends Component {
search:window.location.search,fetcher:new HttpConfigFetcher(),localFetcher:new CocosLocalConfigFetcher()});
if(this.terminal)return;
this.policy=resolveLoginPolicy(this.config.remoteConfig);
this.openExternal=url=>openLobbyUrl(url,bridgeHost?{kind:'bridge',call:(name,data)=>required(this.bridge,'native bridge').call(name,data,()=>{})}:
injected.settings?{kind:'settings',browser:target=>(injected.settings as unknown as {browser(url:string):void}).browser(target)}:
{kind:'h5',open:target=>{window.open(target,'_blank','noopener');}},Date.now);
const loginConfig={source:this.config.source,gameserver:this.config.gameserver,identity:this.config.identity,
showVisitor:this.policy.showVisitor,deviceLogin:this.policy.deviceLogin,qqLogin:this.policy.qqLogin};
this.events.push({kind:'login-config',data:loginConfig});
@@ -105,35 +126,68 @@ export class PlatformStartup extends Component {
else required(injected.settings,'settings').startlocation(2);};
}
required(this.countdown,'countdown').start(resolveLaunchWaitTime(this.config.remoteConfig));
this.runtime=new PlatformRuntime({gameEntry:createLoginStageGameEntry(this.config.identity.gameid,event=>this.events.push(event)),
await this.startSession(true);
}catch(error){this.fail(error);}
}
/** 退出只重建账号会话;配置、原生桥和已加载资源由常驻组件保留。 */
private async startSession(firstStart:boolean):Promise<void>{
this.runtime=new PlatformRuntime({gameEntry:createLoginStageGameEntry(required(this.config,'config').identity.gameid,event=>this.events.push(event)),
resolveRuntimeConfig:async()=>required(this.config,'config'),
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'&&event.message.route==='agent'&&event.message.rpc==='player_login')this.loginResponse=event.message.data;
if(event.type==='open'&&this.reconnecting){
this.reconnecting=false;this.feedback?.reconnect(false);
this.waitingReply=this.busy||this.runtime!.state.players.selfPlayerId!==null;
this.feedback?.loading(this.waitingReply);
}
if(event.type==='message'&&isLoginResponse(event.message)){
this.loginResponse=event.message.data;
this.waitingReply=false;this.reconnecting=false;
this.feedback?.loading(false);this.feedback?.reconnect(false);
}
});return wire;
},
scene:{showLoading:()=>{},showLogin:()=>{void this.showLogin().catch(error=>this.fail(error));},
showLobby:()=>{try{required(this.accounts,'accounts').loginSucceeded(required(this.loginResponse,'login response'),required(this.device,'prepared login device').deviceLogin);
this.busy=false;this.panel?.setBusy(false);this.panel?.close();this.tip('登录成功');}catch(error){this.fail(error);}},
showRoom:()=>this.fail(Error('当前入口尚未接入子游戏场景')),
showReconnect:()=>this.tip('连接中断,正在重连…'),
showKicked:data=>this.fail(Error(`账号退出:${JSON.stringify(data)}`)),
showServerDenial:outcome=>{this.busy=false;this.panel?.setBusy(false);this.tip(JSON.stringify(outcome.data));},
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),
showKicked:data=>{
const packet=data as {msg?:string;fromOther?:unknown;gameid?:unknown};
const text=packet.msg||`网络出现异常!${packet.fromOther?(packet.gameid==='G2hw0ubng0zcoI0r4mx3H2yr4GejidwO'?2:1):0}#(请重启游戏)`;
this.fail(Error(text));
},
showServerDenial:outcome=>{
this.busy=false;this.panel?.setBusy(false);
// Desk.login explicitly defines an empty/missing msg as 登录失败.
if(outcome.rpc==='player_login'){
this.loginResponse=null;
const message=outcome.data.msg;
if(message!=null&&typeof message!=='string')throw Error('player_login.msg must be a string');
this.tip(message||'登录失败');
}else this.tip(JSON.stringify(outcome.data));
},
showFatal:error=>this.fail(error)},
loadResources:()=>this.loadLoginResources(),
waitForMinimumDisplay:()=>required(this.countdown,'countdown').promise,
loadResources:()=>firstStart?this.loadLoginResources():Promise.resolve(),
waitForMinimumDisplay:()=>firstStart?required(this.countdown,'countdown').promise:Promise.resolve(),
getLoginDeviceSnapshot:()=>required(this.device,'prepared login device'),
});
this.unsubscribe=this.runtime.subscribeState(state=>this.countdown?.socketReady(state.app.phase==='connected'||state.app.phase==='logged-in'),error=>this.fail(error));
this.unsubscribe=this.runtime.subscribeState(state=>{
if(firstStart)this.countdown?.socketReady(state.app.phase==='connected'||state.app.phase==='logged-in');
this.lobby?.render(state);
},error=>this.fail(error));
await this.runtime.start();
}catch(error){this.fail(error);}
}
private canvas():Node{return required(director.getScene(),'scene').getChildByName('Canvas') ?? (()=>{throw Error('Scene requires Canvas');})();}
private createStatus():void{
const canvas=this.canvas();const node=new Node('Status');node.parent=canvas;node.layer=canvas.layer;
node.setPosition(0,315,0);node.addComponent(UITransform).setContentSize(1200,60);
this.status=node.addComponent(Label);this.status.string='';this.status.fontSize=24;this.status.lineHeight=28;
this.feedback?.dispose();
this.feedback=new FeedbackPanels(canvas,{tips:required(this.tipsPrefab,'tipsPrefab'),loading:required(this.waitPrefab,'waitPrefab'),
reconnect:required(this.reconnectPrefab,'reconnectPrefab'),kick:required(this.kickPrefab,'kickPrefab')});
this.feedback.loading(this.waitingReply);this.feedback.reconnect(this.reconnecting);
}
private async bindLoading():Promise<void>{
const logo=instantiate(required(this.loadingPrefab,'loadingPrefab'));logo.parent=this.canvas();logo.setPosition(0,0,0);
@@ -184,6 +238,42 @@ export class PlatformStartup extends Component {
if(this.terminal)return;
try{required(this.accounts,'accounts').acceptNative(data,channel);}catch(error){this.tip(error instanceof Error?error.message:String(error));}
}
private saveAcceptedLogin():void{
if(this.loginResponse!==null){
required(this.accounts,'accounts').loginSucceeded(required(this.loginResponse,'login response'),required(this.device,'prepared login device').deviceLogin);
this.loginResponse=null;
this.busy=false;
}
}
private async showLobby():Promise<void>{
if(this.terminal)return;
this.saveAcceptedLogin();
this.panel?.dispose();this.panel=null;
await this.flow.enterOnce(required(this.lobbyScene,'lobbyScene').name,()=>{
this.createStatus();
const popup=new Node('PopupRoot');popup.parent=this.canvas();popup.layer=this.canvas().layer;
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));
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 logout():void{
if(this.returningToLogin||this.terminal)return;
this.returningToLogin=true;
required(this.accounts,'accounts').logout();
this.unsubscribe?.();this.unsubscribe=null;
this.lobby?.dispose();this.lobby=null;
this.panel?.dispose();this.panel=null;
this.runtime?.stop();this.runtime=null;
this.waitingReply=false;this.reconnecting=false;this.feedback?.loading(false);this.feedback?.reconnect(false);this.feedback?.closeTips();
this.busy=false;this.device=null;this.loginResponse=null;
void this.startSession(false).catch(error=>this.fail(error)).finally(()=>{this.returningToLogin=false;});
}
/** 所有登录最终收口:再次校验协议与就绪状态,保存本次模式供登录响应使用。 */
private login(account:LoginAccountIdentity,mode:DeviceLoginMode):void{
const runtime=required(this.runtime,'runtime');
@@ -198,20 +288,25 @@ export class PlatformStartup extends Component {
if(city!==undefined&&(!city||typeof city.ip!=='string'||typeof city.province!=='string'||typeof city.city!=='string'))throw Error('Invalid returnCitySN');
this.device={location:this.location,machineid,machineroom:'',deviceLogin:mode,loginPlayerId:{enabled:false},...(city?{ip:city.ip}:{})};
this.busy=true;this.panel?.setBusy(true);
this.waitingReply=true;this.feedback?.loading(true);
try{runtime.login(city?{...account,province:city.province,city:city.city}:account);}
catch(error){this.busy=false;this.panel?.setBusy(false);throw error;}
catch(error){this.busy=false;this.waitingReply=false;this.feedback?.loading(false);this.panel?.setBusy(false);throw error;}
}
private tip(text:string,time=3000):void{
if(text)this.feedback?.showTips(text,time);
if(this.status&&isValid(this.status))this.status.string='';
}
private tip(text:string):void{if(this.status&&isValid(this.status))this.status.string=text;}
private fail(error:unknown):void{
this.tip(error instanceof Error?error.message:String(error));console.error('[PlatformStartup]',error);
this.release();
console.error('[PlatformStartup]',error);
try{this.feedback?.kick(error instanceof Error?error.message:String(error));}
finally{this.release();}
}
/** 幂等释放计时器、订阅和宿主回调,阻止销毁后的迟到消息重新激活流程。 */
private release():void{
if(this.terminal)return;this.terminal=true;this.flow.dispose();this.panel?.dispose();
if(this.terminal)return;this.terminal=true;this.flow.dispose();this.panel?.dispose();this.lobby?.dispose();
this.countdown?.cancel();this.countdownView?.dispose();this.unsubscribe?.();this.bridge?.dispose();
this.restoreShareLogin?.();this.restoreLocation?.();this.runtime?.stop();
}
onDestroy():void{this.release();}
onDestroy():void{this.release();this.feedback?.dispose();}
getDiagnostics(){return {events:[...this.events],ready:this.runtime?.ready,busy:this.busy,terminal:this.terminal};}
}
@@ -0,0 +1,111 @@
[
{
"__type__": "cc.AnimationClip",
"_name": "ReconnectFrames",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"sample": 10,
"speed": 1,
"wrapMode": 2,
"enableTrsBlending": false,
"_duration": 0.8,
"_hash": 0,
"_tracks": [
{
"__id__": 1
}
],
"_exoticAnimation": null,
"_events": [],
"_embeddedPlayers": [],
"_additiveSettings": {
"__id__": 6
},
"_auxiliaryCurveEntries": []
},
{
"__type__": "cc.animation.ObjectTrack",
"_binding": {
"__type__": "cc.animation.TrackBinding",
"path": {
"__id__": 2
},
"proxy": null
},
"_channel": {
"__id__": 4
}
},
{
"__type__": "cc.animation.TrackPath",
"_paths": [
{
"__id__": 3
},
"spriteFrame"
]
},
{
"__type__": "cc.animation.ComponentPath",
"component": "cc.Sprite"
},
{
"__type__": "cc.animation.Channel",
"_curve": {
"__id__": 5
}
},
{
"__type__": "cc.ObjectCurve",
"_times": [
0,
0.1,
0.2,
0.30000000000000004,
0.4,
0.5,
0.6000000000000001,
0.7000000000000001
],
"_values": [
{
"__uuid__": "5690de47-2417-4e4c-b948-b772697cd010@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "64f9e5ff-6381-4051-9be8-0e678a056519@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "1fa00bd2-79b8-49e9-bdf3-c0f48dd0ff0e@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "fdf821b1-90eb-433d-ac8d-7826a943d482@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "d8abacb4-7e61-4cd1-87fa-68de231eb7cd@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "13972a6c-5bca-44f4-b8c7-52f7f2e1ab58@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "fc639c96-4683-4f02-8769-df540f1ee86c@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "133c04b5-5ec3-4bab-b362-2049efe926fe@f9941",
"__expectedType__": "cc.SpriteFrame"
}
]
},
{
"__type__": "cc.AnimationClipAdditiveSettings",
"enabled": false,
"refClip": null
}
]
@@ -0,0 +1,13 @@
{
"ver": "2.0.4",
"importer": "animation-clip",
"imported": true,
"uuid": "6df11aed-a35b-4009-8bb9-a3f42fb20cf7",
"files": [
".bin"
],
"subMetas": {},
"userData": {
"name": "ReconnectFrames"
}
}
@@ -0,0 +1,66 @@
import {Animation, BlockInputEvents, Label, Node, Prefab, Tween, UITransform, Widget, instantiate, isValid, tween} from 'cc';
import {requireNode} from './node-bindings.ts';
/** Original layers 612/614/615/616. Scene-independent state is owned by PlatformStartup. */
export class FeedbackPanels {
private timer:ReturnType<typeof setTimeout>|null=null;
private nodes:Node[]=[];
private tipNode:Node;
private loadingNode:Node;
private reconnectNode:Node;
private kickNode:Node;
private locked=false;
constructor(canvas:Node,assets:{tips:Prefab;loading:Prefab;reconnect:Prefab;kick:Prefab}){
const mount=(asset:Prefab)=>{
const n=instantiate(asset);n.parent=canvas;n.setPosition(0,0,0);
const w=n.getComponent(Widget);if(w){w.target=canvas;w.isAlignLeft=w.isAlignRight=w.isAlignTop=w.isAlignBottom=true;w.left=w.right=w.top=w.bottom=0;w.updateAlignment();}
if(!n.getComponent(BlockInputEvents))n.addComponent(BlockInputEvents);
n.active=false;this.nodes.push(n);return n;
};
this.loadingNode=mount(assets.loading);this.reconnectNode=mount(assets.reconnect);
this.tipNode=mount(assets.tips);this.kickNode=mount(assets.kick);
requireNode(this.tipNode,'group-89').active=false;
this.tipNode.on(Node.EventType.TOUCH_END,()=>this.closeTips());
}
private front(n:Node):void{n.setSiblingIndex(n.parent!.children.length-1);if(this.locked)this.kickNode.setSiblingIndex(this.kickNode.parent!.children.length-1);}
private text(root:Node,path:string,bgPath:string,message:string):void{
const n=requireNode(root,path),label=n.getComponent(Label);if(!label)throw Error(`${path} requires Label`);
n.getComponent(Widget)!.enabled=false;
label.string=message.replace(/#/g,'\n');label.enableWrapText=false;label.overflow=Label.Overflow.NONE;
label.horizontalAlign=Label.HorizontalAlign.CENTER;label.verticalAlign=Label.VerticalAlign.CENTER;
label.updateRenderData(true);
const size=n.getComponent(UITransform)!;
if(size.width>1000){label.enableWrapText=true;label.overflow=Label.Overflow.RESIZE_HEIGHT;size.width=1000;label.updateRenderData(true);}
const bg=requireNode(root,bgPath);const w=bg.getComponent(Widget);if(w)w.enabled=false;
const height=Math.max(105,size.height+60);
bg.getComponent(UITransform)!.setContentSize(Math.max(570,size.width+100),height);
bg.setPosition(0,18,0);n.setPosition(0,18,0);
}
showTips(message:string,time=3000):void{
if(this.locked||!message)return;this.closeTips();
this.text(this.tipNode,'group-32/提示文字','group-32/提示背景',message);
this.tipNode.active=true;this.front(this.tipNode);
if(time>0)this.timer=setTimeout(()=>this.closeTips(),time);
}
closeTips():void{if(this.timer!==null)clearTimeout(this.timer);this.timer=null;if(isValid(this.tipNode))this.tipNode.active=false;}
private spin(root:Node,path:string,visible:boolean):void{
const n=requireNode(root,path);Tween.stopAllByTarget(n);root.active=visible&&!this.locked;
if(root.active){this.front(root);tween(n).by(1,{angle:-360}).repeatForever().start();}
}
loading(show:boolean):void{this.spin(this.loadingNode,'group-40/载入动画',show);}
reconnect(show:boolean):void{
const animation=requireNode(this.reconnectNode,'group-35/重连动画').getComponent(Animation);
if(!animation||!animation.defaultClip)throw Error('Reconnect requires serialized AnimationClip');
this.reconnectNode.active=show&&!this.locked;
if(this.reconnectNode.active){this.front(this.reconnectNode);animation.play();}else animation.stop();
}
kick(message:string):void{
this.loading(false);this.reconnect(false);this.closeTips();this.locked=true;
this.text(this.kickNode,'group-33/强制退出提示文字','group-33/强制下线提示底',message);
this.kickNode.active=true;this.front(this.kickNode);
}
dispose():void{
this.closeTips();for(const n of this.nodes){if(!isValid(n))continue;for(const c of n.getComponentsInChildren(UITransform))Tween.stopAllByTarget(c.node);n.destroy();}
this.nodes=[];
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "000c909d-064e-4734-a837-168fdab1062b",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,234 @@
import { assetManager, BlockInputEvents, 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 { requireNode } from './node-bindings.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`);
/** Scene-local view. All identity/assets come from PlatformStore; configuration is resolved once. */
export class LobbyPanel {
private releases:Array<()=>void>=[];
private modalReleases:Array<()=>void>=[];
private modal:Node|null=null;
private modalKind='';
private disposed=false;
private model:LobbyModel;
private menu:Node;
private header:Node;
private noticeMenu:Node;
private avatarUrl:string|null=null;
private avatarFrame:SpriteFrame|null=null;
private avatarTexture:Texture2D|null=null;
private avatarGeneration=0;
private cardValue:number|null=null;
private beanValue:number|null=null;
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){
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;
// 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.
for(const id of [180,193,194,682,178,179,340]){label(this.menu,`group-3/Spirit${id}`).string='';requireNode(this.menu,`group-3/Spirit${id}`).active=false;}
label(this.menu,'group-3/Spirit681').string='客服信息';
const pairs=[[180,682],[193,178],[194,179]];
policy.contacts.forEach((contact,index)=>{
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('此功能尚未接入'));
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(policy.noticeHours>0)window.localStorage.setItem(key,String(now));
this.openNotice();
}
}
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);
if(widget){widget.target=parent;widget.isAlignLeft=true;widget.isAlignRight=true;widget.isAlignTop=true;widget.isAlignBottom=true;
widget.left=0;widget.right=0;widget.top=0;widget.bottom=0;widget.updateAlignment();}
return root;
}
private bind(node:Node,action:()=>void,modal=false):void {
const listener=()=>{if(this.disposed||(!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);});
}
render(state:PlatformState):void {
if(this.disposed)return;this.model=selectLobbyModel(state);
label(this.header,'group-3/PlayerInfo/昵称').string=this.model.nickname;
label(this.header,'group-3/PlayerInfo/ID').string=`ID:${this.model.playerid}`;
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.updateAvatar();
if(this.modalKind==='profile')this.renderProfile();
}
private numbers(name:string,background:string,value:number):void {
const root=requireNode(this.header,`group-3/${name}`),ui=need(root.getComponent(UITransform),name);
const numberLabel=label(root,'NumberLabel');
numberLabel.string=String(value);
numberLabel.updateRenderData(true);
const width=need(numberLabel.node.getComponent(UITransform),'number label transform').width;
ui.setContentSize(width,28);
const bg=requireNode(this.header,`group-3/${background}`),bgUI=need(bg.getComponent(UITransform),background);
bgUI.width=width>119?width+55:155;
const widget=root.getComponent(Widget);if(widget)widget.enabled=false;
root.setPosition(bg.position.x+8,root.position.y,0);
}
private updateAvatar():void {
if(this.avatarUrl===this.model.avatar)return;
this.avatarUrl=this.model.avatar;const generation=++this.avatarGeneration;
const sprite=need(requireNode(this.header,'group-3/PlayerInfo/头像框/头像').getComponent(Sprite),'avatar');
sprite.sizeMode=Sprite.SizeMode.CUSTOM;
need(sprite.node.getComponent(UITransform),'avatar transform').setContentSize(63,65);
sprite.spriteFrame=null;
this.avatarFrame?.destroy();this.avatarTexture?.destroy();this.avatarFrame=null;this.avatarTexture=null;
if(this.model.avatar==='')return;
assetManager.loadRemote<ImageAsset>(this.model.avatar,{ext:'.png'},(error,image)=>{
if(this.disposed||generation!==this.avatarGeneration)return;
if(error){this.tip(`头像加载失败:${error.message}`);return;}
const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;
this.avatarTexture=texture;this.avatarFrame=frame;sprite.spriteFrame=frame;
if(this.modalKind==='profile')this.renderProfile();
});
}
private open(prefab:Prefab|null,kind:string,group:string):Node {
this.close();requireNode(this.noticeMenu,'group-95').active=false;
const modal=this.add(prefab,this.popup);this.modal=modal;this.modalKind=kind;
for(const child of modal.children)child.active=child.name===group;
const shield=new Node('ModalInputShield');shield.parent=modal;shield.layer=modal.layer;shield.setSiblingIndex(0);
shield.addComponent(UITransform).setContentSize(1600,720);shield.addComponent(BlockInputEvents);
return modal;
}
openProfile():void {
const modal=this.open(this.assets.profile,'profile','group-18');
this.bind(requireNode(modal,'group-18/Spirit116'),()=>this.close(),true);this.renderProfile();
this.bind(requireNode(modal,'group-18/Spirit484'),()=>this.tip('实名认证尚未接入'),true);
}
private renderProfile():void {
const modal=need(this.modal,'profile');
const values:Record<number,string>={196:this.model.nickname,197:`ID:${this.model.playerid}`,198:`IP:${need(this.model.ip,'player ip')}`,
199:`积分:${this.model.score}`,510:`${this.policy.starName}:${this.model.bean}`,3152:'个性签名:',3157:this.model.sign};
const location=this.location() as {errorCode?:number;province?:string;city?:string}|null;
values[200]=location?.errorCode===0?`${need(location.province,'location province')}-${need(location.city,'location city')}`:'获取定位失败!';
for(const [id,text] of Object.entries(values))label(modal,`group-18/Spirit${id}`).string=text;
const avatar=need(requireNode(modal,'group-18/Spirit118').getComponent(Sprite),'profile avatar');
avatar.sizeMode=Sprite.SizeMode.CUSTOM;avatar.spriteFrame=this.avatarFrame;
}
openSettings():void {
const modal=this.open(this.assets.settings,'settings','group-6');
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);
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;}
if(raw!=='0'&&raw!=='1')throw Error(`Invalid ${key}`);return raw==='1';};
const render=()=>{sprite.spriteFrame=this.assets.switches[read()?0:1];};render();
this.bind(sprite.node,()=>{window.localStorage.setItem(key,read()?'0':'1');render();},true);
}
for(const name of ['Spirit357','Spirit388'])this.bind(requireNode(group,name),()=>this.tip('原生震动与语音开关尚未接入'),true);
}
openNotice():void {
const modal=this.open(this.assets.notices,'notice','group-12'),group=requireNode(modal,'group-12');
for(const name of ['通知遮罩','通知关闭'])this.bind(requireNode(group,name),()=>this.close(),true);
const close=requireNode(group,'通知关闭');
const closeWidget=close.getComponent(Widget);if(closeWidget)closeWidget.enabled=false;
close.setPosition(475,264,0);close.active=true;
// Replace the old ifast clipping surface with a native masked ScrollView.
const viewport=new Node('NoticeViewport');viewport.parent=group;viewport.layer=group.layer;viewport.setPosition(125,-30,0);
viewport.addComponent(UITransform).setContentSize(725,450);viewport.addComponent(Mask);
const scroll=viewport.addComponent(ScrollView);scroll.horizontal=false;
const contentRoot=new Node('NoticeContent');contentRoot.parent=viewport;contentRoot.layer=group.layer;
const contentUI=contentRoot.addComponent(UITransform);contentUI.setAnchorPoint(0.5,1);contentUI.setContentSize(725,450);contentRoot.setPosition(0,225,0);scroll.content=contentRoot;
const contentLabel=label(group,'通知内容');const textWidget=contentLabel.node.getComponent(Widget);if(textWidget)textWidget.enabled=false;
contentLabel.node.parent=contentRoot;contentLabel.node.setPosition(-345,0,0);
const textUI=need(contentLabel.node.getComponent(UITransform),'notice text');textUI.setAnchorPoint(0,1);textUI.setContentSize(690,450);
contentLabel.overflow=Label.Overflow.RESIZE_HEIGHT;contentLabel.enableWrapText=true;contentLabel.horizontalAlign=Label.HorizontalAlign.LEFT;contentLabel.verticalAlign=Label.VerticalAlign.TOP;
const imageNode=requireNode(group,'Spirit3102');const imageWidget=imageNode.getComponent(Widget);if(imageWidget)imageWidget.enabled=false;
imageNode.parent=contentRoot;imageNode.setPosition(0,0,0);need(imageNode.getComponent(UITransform),'notice image transform').setAnchorPoint(0.5,1);
let pageGeneration=0;
let selectedPage=0;
let releaseImage=()=>{};
this.modalReleases.push(()=>releaseImage());
const link=requireNode(group,'Spirit576');
this.bind(link,()=>this.openUrl(this.policy.notices[selectedPage].url),true);
this.bind(imageNode,()=>this.openUrl(this.policy.notices[selectedPage].url),true);
const show=(index:number)=>{
selectedPage=index;releaseImage();releaseImage=()=>{};
const generation=++pageGeneration;
const notice=this.policy.notices[index];
for(const id of [404,3105,3106,576])requireNode(group,`Spirit${id}`).active=false;
link.active=notice.type===0&&notice.url!=='';
imageNode.active=false;
const content=contentLabel;content.string=notice.text;content.node.active=notice.type===0;
content.updateRenderData(true);contentUI.height=Math.max(450,textUI.height);scroll.scrollToTop(0);
if(notice.type===1&&notice.image){
imageNode.active=true;
const sprite=need(imageNode.getComponent(Sprite),'notice image');sprite.spriteFrame=null;
assetManager.loadRemote<ImageAsset>(notice.image,{ext:'.png'},(error,image)=>{
if(this.disposed||this.modal!==modal||generation!==pageGeneration||!isValid(imageNode))return;
if(error){this.tip(`公告图片加载失败:${error.message}`);return;}
const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.sizeMode=Sprite.SizeMode.CUSTOM;sprite.spriteFrame=frame;
need(imageNode.getComponent(UITransform),'notice image transform').setContentSize(725,notice.height);contentUI.height=Math.max(450,notice.height);scroll.scrollToTop(0);
releaseImage=()=>{sprite.spriteFrame=null;frame.destroy();texture.destroy();};
});
}
};
for(let index=0;index<3;index++){
const notice=this.policy.notices[index],visible=index===0||notice.text!==''||notice.image!=='';
for(const id of [570+index,573+index]){const button=requireNode(group,`Spirit${id}`);button.active=visible;this.bind(button,()=>show(index),true);}
}
show(0);
}
openJoin():void {
const modal=this.open(this.assets.join,'join','group-5');
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='';
}
close():void {
for(const release of this.modalReleases.splice(0))release();
this.modal?.destroy();this.modal=null;this.modalKind='';
}
dispose():void {
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();
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "59042cb2-3da1-41c9-a4aa-e37e5d655e37",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,5 +1,6 @@
import { resources, TextAsset } from 'cc';
import { DEBUG } from 'cc/env';
import { STARTUP_CONFIG } from '../../framework/config/profiles.ts';
import { ConfigFetchError, ConfigParseError, type ConfigFetcher } from '../../framework/config/remote-config.ts';
/**
@@ -18,8 +19,11 @@ export class CocosLocalConfigFetcher implements ConfigFetcher {
});
});
try {
return JSON.parse(asset.text) as unknown;
const data: unknown = JSON.parse(asset.text);
if (STARTUP_CONFIG.debugIO) console.log('[Config LOCAL]', `${resourcePath}.txt`, data);
return data;
} catch {
if (STARTUP_CONFIG.debugIO) console.log('[Config LOCAL]', `${resourcePath}.txt`, { parseError: 'Invalid JSON', raw: asset.text });
throw new ConfigParseError(`本地配置 JSON 解析失败: ${resourcePath}.txt`);
} finally {
// 内容已转为普通数据;下次启动重新加载,避免沿用旧 TextAsset 缓存。
@@ -32,13 +32,9 @@ export class LoginPanel {
if(policy.deviceLogin)left('手机登录',490);
if(accounts.versionState===1){requireNode(group,'渠道logo').active=false;left('微信登录',113,320,98);left('游客登录',488);}
else left('微信登录',415);
// Both switches may be enabled by the remote hierarchy. Keep both actions
// visible, with the user-requested side-by-side layout instead of overlap.
if(state.visitor&&state.phone){
for(const [name,x] of [['游客登录',-210],['手机登录',210]] as const){
const node=requireNode(group,name),widget=node.getComponent(Widget);if(widget)widget.enabled=false;
node.setPosition(x,node.position.y,node.position.z);
}
if(state.visitor){
const node=requireNode(group,'游客登录'),widget=node.getComponent(Widget);if(widget)widget.enabled=false;
node.setPosition(0,node.position.y,node.position.z);
}
// 登录中展示节点统一位于 prefab 的 Logining 分组,路径必须与资源一致。
const logining=requireNode(group,'Logining');
@@ -93,33 +89,16 @@ export class LoginPanel {
const title=requireNode(group,'Spirit3252').getComponent(Sprite);if(!title)throw Error('Spirit3252 requires Sprite');title.spriteFrame=frames.loginTitle;
const saved=this.accounts.phonePanelState();
const input=(name:string,textName:string,placeholderName:string)=>{
const node=requireNode(group,name),labelNode=requireNode(group,textName);
const node=requireNode(group,`${name}/InputEditBox`);
const edit=node.getComponent(EditBox);
if(!edit||!edit.textLabel||!edit.placeholderLabel)throw Error(`${name}/InputEditBox requires serialized EditBox label references`);
const placeholder=requireNode(group,placeholderName);
const glyph=labelNode.getComponent(UITransform);if(!glyph)throw Error(`${textName} requires UITransform`);
// Existing art remains the input background; native EditBox owns text entry.
const ui=node.getComponent(UITransform);if(!ui)throw Error(`${name} requires UITransform`);
const text=new Node('InputText');text.parent=node;text.layer=node.layer;
const textUi=text.addComponent(UITransform);textUi.setAnchorPoint(0,0.5);textUi.setContentSize(ui.width-85,ui.height);
text.setPosition(-ui.width/2+65,0,0);
const inputLabel=text.addComponent(Label);inputLabel.string='';inputLabel.fontSize=glyph.height;
inputLabel.horizontalAlign=Label.HorizontalAlign.LEFT;inputLabel.overflow=Label.Overflow.CLAMP;inputLabel.color=Color.BLACK;
const hint=new Node('Placeholder');hint.parent=node;hint.layer=node.layer;
hint.addComponent(UITransform).setContentSize(ui.width-20,ui.height);
const hintLabel=hint.addComponent(Label);hintLabel.string='';hintLabel.fontSize=glyph.height;
// Configure before onLoad so EditBox does not create orphan default labels.
labelNode.active=false;node.active=false;
const edit=node.addComponent(EditBox);edit.textLabel=inputLabel;edit.placeholderLabel=hintLabel;
edit.string='';edit.placeholder='';
edit.inputMode=EditBox.InputMode.NUMERIC;edit.maxLength=18;
requireNode(group,textName).active=false;
edit.string='';
const changed=()=>{edit.string=edit.string.replace(/\D/g,'');placeholder.active=edit.string.length===0;};
node.on(EditBox.EventType.TEXT_CHANGED,changed);this.modalReleases.push(()=>node.off(EditBox.EventType.TEXT_CHANGED,changed));
changed();node.active=true;
const alignText=()=>{
textUi.setAnchorPoint(0,1);textUi.setContentSize(ui.width-85,ui.height);
text.setPosition(-ui.width/2+65,ui.height/2,0);inputLabel.verticalAlign=Label.VerticalAlign.CENTER;
};
alignText();node.on(Node.EventType.SIZE_CHANGED,alignText);
this.modalReleases.push(()=>node.off(Node.EventType.SIZE_CHANGED,alignText));return edit;
node.on(EditBox.EventType.TEXT_CHANGED,changed);
this.modalReleases.push(()=>node.off(EditBox.EventType.TEXT_CHANGED,changed));
changed();return edit;
};
const phone=input('Spirit3243','Spirit3247','Spirit3249');
const code=input('Spirit3244','Spirit3248','Spirit3250');
@@ -127,28 +106,26 @@ export class LoginPanel {
const codeButton=requireNode(group,'Spirit3245'),codeSprite=codeButton.getComponent(Sprite);
if(!codeSprite||!codeSprite.spriteFrame)throw Error('Spirit3245 requires SpriteFrame');
const idleFrame=codeSprite.spriteFrame;
const timerNode=requireNode(group,'Spirit3246'),timerSprite=timerNode.getComponent(Sprite);
if(!timerSprite)throw Error('Spirit3246 requires Sprite');timerSprite.enabled=false;
const timerWidget=timerNode.getComponent(Widget);if(timerWidget)timerWidget.enabled=false;
timerNode.setPosition(codeButton.position);timerNode.getComponent(UITransform)!.setContentSize(148,28);
const timerText=new Node('CountdownText');timerText.parent=timerNode;timerText.layer=timerNode.layer;
timerText.addComponent(UITransform).setContentSize(148,28);
const timerLabel=timerText.addComponent(Label);timerLabel.fontSize=28;timerLabel.color=Color.BLACK;
const timerNode=requireNode(group,'Spirit3246');
const timerLabel=requireNode(timerNode,'CountdownText').getComponent(Label);
if(!timerLabel||!timerLabel.font)throw Error('Spirit3246/CountdownText requires serialized bitmap Label');
let restTime=saved===null?0:saved.restTime;
let waiting=restTime>0;
const renderTimer=()=>{timerNode.active=waiting;timerLabel.string=`${restTime}秒`;codeSprite.spriteFrame=waiting?frames.codeWaiting:idleFrame;};
const renderTimer=()=>{timerNode.active=waiting;timerLabel.string=`${restTime}s`;codeSprite.spriteFrame=waiting?frames.codeWaiting:idleFrame;};
renderTimer();
if(waiting){const timer=window.setInterval(()=>{restTime--;if(restTime<0){waiting=false;window.clearInterval(timer);}renderTimer();},1000);
this.modalReleases.push(()=>window.clearInterval(timer));}
this.bind(codeButton,()=>{if(restTime<=0&&phone.string)this.tip('请通过相关渠道获取');},true);
this.bind(requireNode(group,'Spirit3253'),()=>{if(!this.accounts.requireAgreement())return;
if(!phone.string||!code.string){this.tip('手机号与验证码均不能为空!');return;}
this.accounts.phone(phone.string,code.string);this.close();},true);
// Keep the entered values and modal until successful navigation disposes it.
this.accounts.phone(phone.string,code.string);},true);
for(const name of ['Spirit3251','Spirit3241'])this.bind(requireNode(group,name),()=>this.close(),true);
const background=requireNode(group,'Spirit3242');if(!background.getComponent(BlockInputEvents))background.addComponent(BlockInputEvents);
}
/** 帧切换由 prefab 的 AnimationClip 驱动;登录状态只负责显隐及播放生命周期。 */
setBusy(busy:boolean):void{
busy=busy&&this.modal===null;
const group=requireNode(this.root,'group-2/Logining');
group.active=busy;
for(const name of ['登录中遮罩','登录头像背景框','登录中头像','登录中昵称','登录中动画','登录中头像框'])requireNode(group,name).active=busy;
@@ -2,9 +2,11 @@
export class SceneFlow {
private pending:Promise<void>=Promise.resolve();
private disposed=false;
private destination:string|null=null;
constructor(private readonly load:(name:string)=>Promise<void>){}
/** 串行加载后绑定新场景;等待期间已销毁则禁止继续绑定。 */
enter(name:string,bind:()=>void|Promise<void>):Promise<void>{
this.destination=name;
const next=this.pending.then(async()=>{
if(this.disposed)throw Error('SceneFlow disposed');
await this.load(name);
@@ -14,5 +16,10 @@ export class SceneFlow {
this.pending=next;
return next;
}
enterOnce(name:string,bind:()=>void|Promise<void>):Promise<void>{
if(this.disposed)return Promise.reject(Error('SceneFlow disposed'));
if(this.destination===name)return this.pending;
return this.enter(name,bind);
}
dispose():void{this.disposed=true;}
}