feat: integrate login flow, lobby UI and bitmap font resources
This commit is contained in:
@@ -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": {}
|
||||
}
|
||||
+113
-18
@@ -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
|
||||
}
|
||||
]
|
||||
+13
@@ -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&¬ice.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&¬ice.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": {}
|
||||
}
|
||||
+5
-1
@@ -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;}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user