feat: integrate platform room entry, UI migration and erqiwang documentation

This commit is contained in:
2026-09-07 21:01:08 +08:00
parent 9c4510697c
commit 30a1b4114c
259 changed files with 84995 additions and 30201 deletions
@@ -0,0 +1,50 @@
import { _decorator, Component, Label, Mask, Node, UITransform, Widget } from 'cc';
import { announcementMotion } from '../../framework/presentation/lobby-model.ts';
import { requireNode } from './node-bindings.ts';
const {ccclass}=_decorator;
function required<T>(value:T|null,name:string):T {if(value===null)throw Error(`Announcement missing ${name}`);return value;}
/** Native clipping and movement of the authored Announcement children. */
@ccclass('AnnouncementView')
export class AnnouncementView extends Component {
private background:Node|null=null;
private textNode:Node|null=null;
private viewport:Node|null=null;
private message='';
private elapsed=0;
private textY=0;
show(message:string):void {
if(!this.viewport){
const bg=requireNode(this.node,'Spirit219'),text=requireNode(this.node,'Spirit220');
const bgUI=required(bg.getComponent(UITransform),'Spirit219 UITransform');
// The text is animated; an authored Widget must not reset its X every frame.
const widget=text.getComponent(Widget);if(widget)widget.enabled=false;
const viewport=new Node('AnnouncementClip');viewport.layer=this.node.layer;viewport.parent=bg;
const clipUI=viewport.addComponent(UITransform);clipUI.setAnchorPoint(0,0);
clipUI.setContentSize(bgUI.contentSize);
viewport.setPosition(-bgUI.width*bgUI.anchorX,-bgUI.height*bgUI.anchorY,0);
viewport.addComponent(Mask);
this.textY=text.position.y-bg.position.y+bgUI.height*bgUI.anchorY;
text.parent=viewport;
const textUI=required(text.getComponent(UITransform),'Spirit220 UITransform');
textUI.setAnchorPoint(0,textUI.anchorY);
const label=required(text.getComponent(Label),'Spirit220 Label');
label.enableWrapText=false;label.overflow=Label.Overflow.NONE;label.horizontalAlign=Label.HorizontalAlign.LEFT;
this.background=bg;this.textNode=text;this.viewport=viewport;
}
this.message=message;this.elapsed=0;
required(required(this.textNode,'text').getComponent(Label),'label').string=message;
this.draw();
}
update(dt:number):void {if(this.viewport){this.elapsed+=dt;this.draw();}}
private draw():void {
const text=required(this.textNode,'text'),bg=required(this.background,'background');
const width=required(bg.getComponent(UITransform),'background UITransform').width;
const state=announcementMotion(this.message,width,this.elapsed);
bg.active=state.visible;text.active=state.visible;
text.setPosition(state.left,this.textY,0);
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4b4591ab-307a-4061-9690-7102b996a3ae",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,10 @@
import {_decorator,Component} from 'cc';
import type {CreateRoomPageContext,CreateRoomPagePort} from '../../framework/sdk/contracts/create-room-page.ts';
const {ccclass}=_decorator;
/** Inherit this component on the subgame prefab root. Platform never inspects its children. */
@ccclass('CreateRoomPage')
export class CreateRoomPage extends Component implements CreateRoomPagePort {
open(_context:CreateRoomPageContext):void{throw Error('Subgame must implement CreateRoomPage.open');}
setBusy(_busy:boolean):void{throw Error('Subgame must implement CreateRoomPage.setBusy');}
close():void{throw Error('Subgame must implement CreateRoomPage.close');}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "956dedf9-3650-418b-af8a-a755a26d991c",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,37 @@
import {_decorator,Button,Component,Label} from 'cc';
import {JoinRoomInput} from '../../framework/presentation/join-room-input.ts';
const {ccclass,property}=_decorator;
export interface JoinRoomViewContext {initialCode:string;submit(code:number):void;cancel():void;paste():Promise<string>;reportError(message:string):void;}
@ccclass('JoinRoomView')
export class JoinRoomView extends Component {
@property([Button]) digitButtons:Button[]=[];
@property([Label]) slots:Label[]=[];
@property(Button) clearButton:Button|null=null;
@property(Button) deleteButton:Button|null=null;
@property(Button) pasteButton:Button|null=null;
@property(Button) submitButton:Button|null=null;
@property([Button]) cancelButtons:Button[]=[];
private input:JoinRoomInput|null=null;
private releases:Array<()=>void>=[];
private generation=0;
private busy=false;
private pasting=false;
open(context:JoinRoomViewContext):void{
this.close();const generation=this.generation;
if(this.digitButtons.length!==10||this.slots.length!==6||!this.clearButton||!this.deleteButton||!this.pasteButton||!this.submitButton||this.cancelButtons.length!==2)throw Error('JoinRoomView requires serialized controls');
const input=this.input=new JoinRoomInput(code=>context.submit(code));input.prefill(context.initialCode);
const bind=(button:Button,action:()=>void)=>{const fn=()=>{if(this.busy||this.pasting)return;try{action();}catch(error){context.reportError(error instanceof Error?error.message:String(error));}finally{this.render();}};
button.node.on(Button.EventType.CLICK,fn);this.releases.push(()=>button.node.off(Button.EventType.CLICK,fn));};
this.digitButtons.forEach((button,digit)=>bind(button,()=>input.digit(digit)));
bind(this.clearButton,()=>input.clear());bind(this.deleteButton,()=>input.remove());bind(this.submitButton,()=>input.confirm());
bind(this.pasteButton,()=>{this.pasting=true;void context.paste().then(text=>{if(this.generation===generation){input.paste(text);this.render();}})
.catch(error=>{if(this.generation===generation)context.reportError(error instanceof Error?error.message:String(error));})
.finally(()=>{if(this.generation===generation)this.pasting=false;});});
this.cancelButtons.forEach(button=>{const fn=()=>context.cancel();button.node.on(Button.EventType.CLICK,fn);this.releases.push(()=>button.node.off(Button.EventType.CLICK,fn));});
this.setBusy(false);this.render();
}
setBusy(busy:boolean):void{this.busy=busy;this.input?.setBusy(busy);for(const b of [...this.digitButtons,this.clearButton,this.deleteButton,this.submitButton,this.pasteButton])if(b)b.interactable=!busy;}
private render():void{const code=this.input?.code;if(code===undefined)return;this.slots.forEach((slot,i)=>{slot.node.active=i<code.length;slot.string=code[i]??'';});}
close():void{this.generation++;this.pasting=false;for(const release of this.releases.splice(0))release();this.input=null;}
onDestroy():void{this.close();}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "033ab9dd-e1a1-48fb-9fd6-5474c1a45eb2",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,17 +1,28 @@
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(Prefab) tasks:Prefab|null=null;
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) redeemCode:Prefab|null=null;
@property(Prefab) qrCode:Prefab|null=null;
@property(Prefab) noticeBoard:Prefab|null=null;
@property(Prefab) ownedRoomList:Prefab|null=null;
@property(Prefab) invite:Prefab|null=null;
@property(Prefab) profile:Prefab|null=null;
@property(Prefab) settings:Prefab|null=null;
@property(Prefab) join:Prefab|null=null;
@property(Prefab) tasks:Prefab|null=null;
@property(Prefab) share:Prefab|null=null;
@property([SpriteFrame]) digits:SpriteFrame[]=[];
@property([SpriteFrame]) switches:SpriteFrame[]=[];
}
@property(Prefab) rank:Prefab|null=null;
@property(Prefab) rankItem:Prefab|null=null;
@property([SpriteFrame]) rankMedals:SpriteFrame[]=[];
@property(SpriteFrame) rankTitle:SpriteFrame|null=null;
@property(SpriteFrame) vipRankTitle:SpriteFrame|null=null;
@property([SpriteFrame]) ownedRoomDigits:SpriteFrame[]=[];
@property([SpriteFrame]) rankSymbols:SpriteFrame[]=[];
@property([SpriteFrame]) digits:SpriteFrame[]=[];
@property([SpriteFrame]) switches:SpriteFrame[]=[];
}
@@ -0,0 +1,37 @@
import {_decorator,Component,Layout,Sprite,SpriteFrame,Toggle,ToggleContainer} from 'cc';
const {ccclass,property}=_decorator;
/** Native ToggleContainer owns selection; serialized frames own the tab appearance. */
@ccclass('NoticeTabsView')
export class NoticeTabsView extends Component {
@property([Toggle]) tabs:Toggle[]=[];
@property([Sprite]) backgrounds:Sprite[]=[];
@property([Sprite]) titles:Sprite[]=[];
@property(SpriteFrame) normalBackground:SpriteFrame|null=null;
@property(SpriteFrame) selectedBackground:SpriteFrame|null=null;
@property([SpriteFrame]) normalTitles:SpriteFrame[]=[];
@property([SpriteFrame]) selectedTitles:SpriteFrame[]=[];
private release:(()=>void)|null=null;
bindPages(visible:readonly boolean[],select:(index:number)=>void):()=>void {
this.release?.();
const count=this.tabs.length,container=this.getComponent(ToggleContainer),layout=this.getComponent(Layout);
if(count===0||visible.length!==count||!visible[0]||!container||container.allowSwitchOff||!layout||layout.type!==Layout.Type.VERTICAL)
throw Error('NoticeTabsView requires a vertical single-selection container and a visible first page');
if(!this.normalBackground||!this.selectedBackground||[this.backgrounds,this.titles,this.normalTitles,this.selectedTitles].some(items=>items.length!==count||items.some(item=>!item)))
throw Error('NoticeTabsView requires complete serialized tab artwork');
this.tabs.forEach((tab,index)=>{if(tab.node.parent!==this.node)throw Error('Notice tabs must be direct ToggleContainer children');tab.node.active=visible[index];});
this.tabs[0].setIsCheckedWithoutNotify(true);
this.tabs.slice(1).forEach(tab=>tab.setIsCheckedWithoutNotify(false));
const paint=()=>this.tabs.forEach((tab,index)=>{
this.backgrounds[index].spriteFrame=tab.isChecked?this.selectedBackground:this.normalBackground;
this.titles[index].spriteFrame=tab.isChecked?this.selectedTitles[index]:this.normalTitles[index];
});
const handlers=this.tabs.map((tab,index)=>()=>{paint();if(tab.isChecked)select(index);});
this.tabs.forEach((tab,index)=>tab.node.on(Toggle.EventType.TOGGLE,handlers[index]));
const release=()=>{this.tabs.forEach((tab,index)=>tab.node.off(Toggle.EventType.TOGGLE,handlers[index]));if(this.release===release)this.release=null;};
this.release=release;paint();layout.updateLayout(true);select(0);
return release;
}
onDestroy():void{this.release?.();}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "fb24d5e9-ba6c-4f8f-a7da-ad8e5888a0b6",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,63 @@
import { _decorator, Animation, AnimationClip, AnimationState, Component, Sprite, SpriteFrame } from 'cc';
const { ccclass, disallowMultiple, property, requireComponent } = _decorator;
/** Optional transition clips decorate a state supplied by the switch's owner.
* Empty on/off clips mean an immediate sprite change. Clips never change business state.
*/
@ccclass('OptionalSwitchAnimation')
@disallowMultiple
@requireComponent([Animation, Sprite])
export class OptionalSwitchAnimation extends Component {
@property({type:AnimationClip,displayName:'开启片段',tooltip:'可选;开启时播放一次,完成后恢复开启状态图片。'})
onClip:AnimationClip|null=null;
@property({type:AnimationClip,displayName:'关闭片段',tooltip:'可选;关闭时播放一次,完成后恢复关闭状态图片。'})
offClip:AnimationClip|null=null;
private stateFrame:SpriteFrame|null=null;
private playing:AnimationState|null=null;
private get animation():Animation {
const animation=this.getComponent(Animation);
if(!animation)throw Error(`${this.node.name} requires Animation`);
return animation;
}
onLoad():void { this.animation.on(Animation.EventType.FINISHED,this.finished,this); }
setState(on:boolean,frame:SpriteFrame,animate:boolean):void {
if(!frame)throw Error(`${this.node.name} requires a switch state SpriteFrame`);
this.playing=null;
const animation=this.animation;
animation.stop();
this.stateFrame=frame;
this.restoreFrame();
const clip=on?this.onClip:this.offClip;
if(!animate||!clip||!this.enabledInHierarchy)return;
const state=animation.addClip(clip);
state.wrapMode=AnimationClip.WrapMode.Normal;
state.repeatCount=1;
this.playing=state;
animation.play(clip.name);
}
private finished(_type:Animation.EventType,state:AnimationState):void {
// A stopped/replaced transition may have queued an event in the previous frame.
if(state!==this.playing||state.isPlaying)return;
this.playing=null;
this.restoreFrame();
}
private restoreFrame():void {
if(!this.stateFrame)return; // No state has been supplied yet; leave the authored sprite intact.
const sprite=this.getComponent(Sprite);
if(!sprite)throw Error(`${this.node.name} requires Sprite`);
sprite.spriteFrame=this.stateFrame;
}
onDisable():void {
this.playing=null;
this.animation.stop();
this.restoreFrame();
}
onDestroy():void { this.animation.off(Animation.EventType.FINISHED,this.finished,this); }
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "3ba9b2d6-3770-42ea-8a7c-56542d25e288",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,23 @@
import { _decorator, Animation, Component } from 'cc';
const { ccclass, disallowMultiple, requireComponent } = _decorator;
/** Animation.defaultClip is optional: empty means the authored static appearance.
* The clip owns its duration and wrap mode; visibility owns playback lifetime.
*/
@ccclass('OptionalUiAnimation')
@disallowMultiple
@requireComponent(Animation)
export class OptionalUiAnimation extends Component {
private get animation(): Animation {
const animation=this.getComponent(Animation);
if(!animation)throw Error(`${this.node.name} requires Animation`);
return animation;
}
onEnable(): void {
const animation=this.animation;
if(animation.defaultClip)animation.play(animation.defaultClip.name);
}
onDisable(): void { this.animation.stop(); }
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "6062e16b-2de1-42cf-91a5-0efcf38bae01",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,21 @@
import {_decorator,Component,Layout,Node,Sprite,SpriteFrame,UITransform,instantiate} from 'cc';
const {ccclass,property}=_decorator;
@ccclass('OwnerRoomItemView')
export class OwnerRoomItemView extends Component {
@property(Sprite) digitTemplate:Sprite|null=null;
@property(Node) digitsRoot:Node|null=null;
render(code:number,frames:readonly SpriteFrame[]):void {
if(!Number.isSafeInteger(code)||code<=0)throw Error('Invalid owned room code');
if(frames.length!==10||frames.some(frame=>!frame))throw Error('Owner room requires ten digit frames');
if(!this.digitTemplate||!this.digitsRoot)throw Error('OwnerRoomItemView requires digit template and container');
const layout=this.digitsRoot.getComponent(Layout),area=this.digitsRoot.getComponent(UITransform);
if(!layout||!area)throw Error('Owner room digits require Layout and UITransform');
for(const child of [...this.digitsRoot.children]){child.removeFromParent();child.destroy();}
for(const char of String(code)){
const digit=instantiate(this.digitTemplate.node);digit.parent=this.digitsRoot;digit.active=true;
digit.getComponent(Sprite)!.spriteFrame=frames[Number(char)];
}
layout.updateLayout(true);
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e40a5810-2a03-40e9-aff2-348909665b03",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,28 @@
import {_decorator,Component,Layout,Node,Prefab,ScrollView,SpriteFrame,UITransform,instantiate} from 'cc';
import {OwnerRoomItemView} from './OwnerRoomItemView.ts';
const {ccclass,property}=_decorator;
@ccclass('OwnerRoomListView')
export class OwnerRoomListView extends Component {
@property(Prefab) itemPrefab:Prefab|null=null;
@property(Node) content:Node|null=null;
@property(ScrollView) scroll:ScrollView|null=null;
render(codes:readonly number[],frames:readonly SpriteFrame[]):Array<{node:Node;code:number}> {
if(!this.itemPrefab||!this.content||!this.scroll)throw Error('OwnerRoomListView requires item prefab, content and ScrollView');
const layout=this.content.getComponent(Layout);
if(!layout||layout.type!==Layout.Type.VERTICAL||layout.resizeMode!==Layout.ResizeMode.CONTAINER||this.scroll.content!==this.content)
throw Error('OwnerRoomListView requires a vertical scrolling content Layout');
for(const child of [...this.content.children]){child.removeFromParent();child.destroy();}
const rows=codes.map(code=>{
const node=instantiate(this.itemPrefab!);node.parent=this.content;node.name=`OwnerRoom${code}`;
const item=node.getComponent(OwnerRoomItemView);if(!item)throw Error('Owner room prefab requires OwnerRoomItemView');
item.render(code,frames);return {node,code};
});
// Empty vertical Layout can produce a negative height from its inter-item spacing.
layout.enabled=rows.length>0;
if(rows.length===0)this.content.getComponent(UITransform)!.height=0;
else layout.updateLayout(true);
this.scroll.stopAutoScroll();this.scroll.scrollToTop(0);
return rows;
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "08da09b2-23b7-49c6-a3dc-489f991cda2d",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,350 +1,443 @@
import { _decorator, assetManager, Component, Director, Label, Node, Prefab, ProgressBar, SceneAsset,
UITransform, Widget, director, instantiate, isValid } from 'cc';
import { resolveRuntimeConfig, type RuntimeConfig } from '../../framework/config/runtime-config.ts';
import { CocosLocalConfigFetcher } from './local-config-fetcher.ts';
import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts';
import { resolveLaunchWaitTime } from '../../framework/config/remote-config.ts';
import { resolveLoginPolicy, type LoginPolicy } from '../../framework/presentation/login-policy.ts';
import { LoginAccounts, type LoginNativeSettings } from '../../framework/adapters/native/login-accounts.ts';
import {VipRoomHistory} from '../../framework/adapters/native/vip-room-history.ts';
import {vipCode} from '../../framework/protocol/contracts/vip-room.ts';
import { WebViewJavascriptBridgeAdapter, type WebViewJavascriptBridgeEnvironment } from '../../framework/adapters/native/webview-javascript-bridge.ts';
import { PlatformRuntime } from '../../framework/platform/runtime.ts';
import { WireClient } from '../../framework/net/wire-client.ts';
import { CocosWebSocketTransport } from '../../framework/net/cocos-transport.ts';
import type { DeviceLoginMode, LoginAccountIdentity, LoginDeviceSnapshot } from '../../framework/protocol/contracts/login-contract.ts';
import { createLoginStageGameEntry } from './login-stage-game-entry.ts';
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 { logNativeCall } from '../../framework/core/native-call-log.ts';
import type { NativeCallName } from '../../framework/adapters/native/native-contracts.ts';
import { resolveLobbyPolicy } from '../../framework/presentation/lobby-model.ts';
import { openLobbyUrl, resolveLobbyLaunchMode } from '../../framework/adapters/native/lobby-links.ts';
import { resolveLobbyMenuConfig, LOBBY_UNAVAILABLE, type LobbyContext } from '../../framework/presentation/lobby-menu.ts';
import { FeedbackPanels } from './feedback-panels.ts';
import { _decorator, assetManager, Component, Director, Label, Node, Prefab, ProgressBar, SceneAsset,
UITransform, Widget, director, instantiate, isValid } from 'cc';
import { resolveRuntimeConfig, type RuntimeConfig } from '../../framework/config/runtime-config.ts';
import { CocosLocalConfigFetcher } from './local-config-fetcher.ts';
import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts';
import { resolveLaunchWaitTime } from '../../framework/config/remote-config.ts';
import { resolveLoginPolicy, type LoginPolicy } from '../../framework/presentation/login-policy.ts';
import { LoginAccounts, type LoginNativeSettings } from '../../framework/adapters/native/login-accounts.ts';
import {OwnedRoomHistory} from '../../framework/adapters/native/owned-room-history.ts';
import {VipRoomHistory} from '../../framework/adapters/native/vip-room-history.ts';
import {vipCode} from '../../framework/protocol/contracts/vip-room.ts';
import { WebViewJavascriptBridgeAdapter, type WebViewJavascriptBridgeEnvironment } from '../../framework/adapters/native/webview-javascript-bridge.ts';
import { PlatformRuntime } from '../../framework/platform/runtime.ts';
import { WireClient } from '../../framework/net/wire-client.ts';
import { CocosWebSocketTransport } from '../../framework/net/cocos-transport.ts';
import type { DeviceLoginMode, LoginAccountIdentity, LoginDeviceSnapshot } from '../../framework/protocol/contracts/login-contract.ts';
import {createTemplateGame} from '../../games/template/template-game.ts';
import {TemplateRoomView} from '../../games/template/TemplateRoomView.ts';
import {SubgameAssets} from './SubgameAssets.ts';
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 { logNativeCall } from '../../framework/core/native-call-log.ts';
import type { NativeCallName } from '../../framework/adapters/native/native-contracts.ts';
import { resolveLobbyPolicy } from '../../framework/presentation/lobby-model.ts';
import { openLobbyUrl, resolveLobbyLaunchMode } from '../../framework/adapters/native/lobby-links.ts';
import { resolveLobbyMenuConfig, LOBBY_UNAVAILABLE, type LobbyContext } from '../../framework/presentation/lobby-menu.ts';
import { FeedbackPanels } from './feedback-panels.ts';
import { isLoginResponse } from '../../framework/protocol/first-slice-routes.ts';
const {ccclass,property}=_decorator;
function required<T>(value:T|null|undefined,name:string):T{if(value==null)throw Error(`PlatformStartup missing ${name}`);return value;}
/** One persistent owner for config/socket/account; scenes own their own visual trees. */
@ccclass('PlatformStartup')
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;
import {readInviteClipboard,readClipboardText} from '../../framework/adapters/native/invite-clipboard.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;}
/** One persistent owner for config/socket/account; scenes own their own visual trees. */
@ccclass('PlatformStartup')
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 lobbyContext:LobbyContext|null=null;
private openExternal:((url:string)=>void)|null=null;
private bridge:WebViewJavascriptBridgeAdapter|null=null;
private restoreShareLogin:(()=>void)|null=null;
private restoreLocation:(()=>void)|null=null;
private startLocation:(()=>void)|null=null;
private location:unknown=null;
private loginResponse:unknown=null;
private unsubscribe:(()=>void)|null=null;
private countdown:ReturnType<typeof createLogoCountdown>|null=null;
private countdownView:CocosLogoCountdownView|null=null;
private progress:ProgressBar|null=null;
private status:Label|null=null;
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}`));
}));
/** 常驻服务按加载场景、配置与桥接、连接、登录场景顺序启动;场景仅拥有自己的 UI。 */
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);
await this.flow.enter(this.loadingScene!.name,()=>this.bindLoading());
if(this.terminal)return;
const injected=window as unknown as {settings?:LoginNativeSettings & {startlocation(type:number):void};app_version?:number|string;app_appversion?:number|string};
const bridgeHost=injected.app_version!==undefined&&Number(injected.app_version)!==0;
const native=bridgeHost?{
getcompareCode:()=>Number(injected.app_appversion),
// uAgent_3 passes the original argument, not its unused local tp variable.
accreditlogin:(provider:number)=>required(this.bridge,'native bridge').call('accreditlogin',provider===2?undefined:provider,()=>{}),
}:injected.settings;
this.config=await resolveRuntimeConfig({hostKind:bridgeHost?'uAgent_3':native?'native-settings':'h5',win:window as any,
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});
if(this.config.isDebugger)console.info('[PlatformStartup] login-config',JSON.stringify(loginConfig));
this.accounts=new LoginAccounts({storage:window.localStorage,settings:native,random:Math.random,now:Date.now,
gameid:this.config.identity.gameid,agentid:this.config.identity.agentid,policy:this.policy,
submit:(account,mode)=>this.login(account,mode),tip:text=>this.tip(text)});
this.lobbyContext={versionState:this.accounts.versionState,
launchMode:resolveLobbyLaunchMode(window.location.search,bridgeHost,(window as any).app_Launchtype)};
const previous=(window as any).sharelogin;
const callback=(openid:unknown,headimgurl:unknown,nickname:unknown,sex:unknown,city:unknown,Province:unknown,unionid:unknown)=>{
if(!this.terminal)this.acceptNative({openid,headimgurl,nickname,sex,city,Province,unionid},'global');
};
(window as any).sharelogin=callback;
this.restoreShareLogin=()=>{if((window as any).sharelogin===callback){if(previous===undefined)delete (window as any).sharelogin;else (window as any).sharelogin=previous;}};
const oldLocation=(window as any).getlocationinfo;
const locationCallback=(data:unknown)=>{
if(this.terminal)return;
try{
const value=typeof data==='string'?JSON.parse(data):data;
if(value===null||typeof value!=='object'||Array.isArray(value))throw Error('Invalid getlocationinfo payload');
this.location=value;
}catch(error){this.tip(error instanceof Error?error.message:String(error));}
};
(window as any).getlocationinfo=locationCallback;
this.restoreLocation=()=>{if((window as any).getlocationinfo===locationCallback){if(oldLocation===undefined)delete (window as any).getlocationinfo;else (window as any).getlocationinfo=oldLocation;}};
if(native){
this.bridge=new WebViewJavascriptBridgeAdapter(window as unknown as WebViewJavascriptBridgeEnvironment);
this.bridge.register('sharelogin',data=>this.acceptNative(data,'bridge'));
this.bridge.register('getlocationinfo',locationCallback);
this.startLocation=()=>{if(bridgeHost)required(this.bridge,'native bridge').call('startlocation',2,()=>{});
else {logNativeCall('settings','startlocation',[2],true);required(injected.settings,'settings').startlocation(2);}};
}else{
this.startLocation=()=>logNativeCall('settings','startlocation',[2],false);
}
required(this.countdown,'countdown').start(resolveLaunchWaitTime(this.config.remoteConfig));
await this.startSession(true);
}catch(error){this.fail(error);}
}
/** 退出只重建账号会话;配置、原生桥和已加载资源由常驻组件保留。 */
private game:ReturnType<typeof createTemplateGame>|null=null;
private roomPending:'create_room'|'self_join_room'|null=null;
private roomTimer:ReturnType<typeof setTimeout>|null=null;
private roomReceipt:{rpc:string;data:Record<string,unknown>}|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 lobbyContext:LobbyContext|null=null;
private openExternal:((url:string)=>void)|null=null;
private bridge:WebViewJavascriptBridgeAdapter|null=null;
private restoreShareLogin:(()=>void)|null=null;
private restoreLocation:(()=>void)|null=null;
private startLocation:(()=>void)|null=null;
private location:unknown=null;
private loginResponse:unknown=null;
private unsubscribe:(()=>void)|null=null;
private countdown:ReturnType<typeof createLogoCountdown>|null=null;
private countdownView:CocosLogoCountdownView|null=null;
private progress:ProgressBar|null=null;
private status:Label|null=null;
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}`));
}));
/** 常驻服务按加载场景、配置与桥接、连接、登录场景顺序启动;场景仅拥有自己的 UI。 */
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);
await this.flow.enter(this.loadingScene!.name,()=>this.bindLoading());
if(this.terminal)return;
const injected=window as unknown as {settings?:LoginNativeSettings & {startlocation(type:number):void};app_version?:number|string;app_appversion?:number|string};
const bridgeHost=injected.app_version!==undefined&&Number(injected.app_version)!==0;
const native=bridgeHost?{
getcompareCode:()=>Number(injected.app_appversion),
// uAgent_3 passes the original argument, not its unused local tp variable.
accreditlogin:(provider:number)=>required(this.bridge,'native bridge').call('accreditlogin',provider===2?undefined:provider,()=>{}),
}:injected.settings;
this.config=await resolveRuntimeConfig({hostKind:bridgeHost?'uAgent_3':native?'native-settings':'h5',win:window as any,
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});
if(this.config.isDebugger)console.info('[PlatformStartup] login-config',JSON.stringify(loginConfig));
this.accounts=new LoginAccounts({storage:window.localStorage,settings:native,random:Math.random,now:Date.now,
gameid:this.config.identity.gameid,agentid:this.config.identity.agentid,policy:this.policy,
submit:(account,mode)=>this.login(account,mode),tip:text=>this.tip(text)});
this.lobbyContext={versionState:this.accounts.versionState,
launchMode:resolveLobbyLaunchMode(window.location.search,bridgeHost,(window as any).app_Launchtype)};
const previous=(window as any).sharelogin;
const callback=(openid:unknown,headimgurl:unknown,nickname:unknown,sex:unknown,city:unknown,Province:unknown,unionid:unknown)=>{
if(!this.terminal)this.acceptNative({openid,headimgurl,nickname,sex,city,Province,unionid},'global');
};
(window as any).sharelogin=callback;
this.restoreShareLogin=()=>{if((window as any).sharelogin===callback){if(previous===undefined)delete (window as any).sharelogin;else (window as any).sharelogin=previous;}};
const oldLocation=(window as any).getlocationinfo;
const locationCallback=(data:unknown)=>{
if(this.terminal)return;
try{
const value=typeof data==='string'?JSON.parse(data):data;
if(value===null||typeof value!=='object'||Array.isArray(value))throw Error('Invalid getlocationinfo payload');
this.location=value;
}catch(error){this.tip(error instanceof Error?error.message:String(error));}
};
(window as any).getlocationinfo=locationCallback;
this.restoreLocation=()=>{if((window as any).getlocationinfo===locationCallback){if(oldLocation===undefined)delete (window as any).getlocationinfo;else (window as any).getlocationinfo=oldLocation;}};
if(native){
this.bridge=new WebViewJavascriptBridgeAdapter(window as unknown as WebViewJavascriptBridgeEnvironment);
this.bridge.register('sharelogin',data=>this.acceptNative(data,'bridge'));
this.bridge.register('getlocationinfo',locationCallback);
this.startLocation=()=>{if(bridgeHost)required(this.bridge,'native bridge').call('startlocation',2,()=>{});
else {logNativeCall('settings','startlocation',[2],true);required(injected.settings,'settings').startlocation(2);}};
}else{
this.startLocation=()=>logNativeCall('settings','startlocation',[2],false);
}
required(this.countdown,'countdown').start(resolveLaunchWaitTime(this.config.remoteConfig));
await this.startSession(true);
}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()});
const identity=required(this.config,'config').identity;
this.game=createTemplateGame(identity.gameid);
this.runtime=new PlatformRuntime({gameEntry:this.game.entry,
roomHistory:new OwnedRoomHistory(window.localStorage,identity.gameid,identity.agentid),
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'&&(isLoginResponse(event.message)||(event.message.route==='agent'&&event.message.rpc==='self_join_room'))){
const data=event.message.data as Record<string,unknown>;
if(data.state===0&&data.shortcode){const id=config.identity;
new VipRoomHistory(window.localStorage,id.gameid,id.agentid).add(vipCode(data.shortcode,'$.data.shortcode'));
if(event.type==='message')this.lobby?.requestReceived(event.message);
if(event.type==='message'&&event.message.route==='agent'&&(event.message.rpc==='create_room'||event.message.rpc==='self_join_room')){
this.roomReceipt={rpc:event.message.rpc,data:event.message.data as Record<string,unknown>};this.endRoomRequest();
}
if(event.type==='close'||event.type==='slow'||event.type==='reconnecting'){this.endRoomRequest();this.lobby?.cancelRoomPage();}
if(event.type==='close'||event.type==='slow'||event.type==='reconnecting'){this.lobby?.cancelTaskRequest();this.lobby?.cancelRankRequest();}
if(event.type==='message'&&(isLoginResponse(event.message)||(event.message.route==='agent'&&(event.message.rpc==='self_join_room'||event.message.rpc==='create_room')))){
const data=event.message.data as Record<string,unknown>;
if(data.state===0&&data.shortcode){const id=config.identity;
new VipRoomHistory(window.localStorage,id.gameid,id.agentid).add(vipCode(data.shortcode,'$.data.shortcode'));
}
}
if(event.type==='open'&&this.reconnecting){
this.reconnecting=false;this.feedback?.reconnect(false);
this.waitingReply=this.busy||this.runtime!.state.players.selfPlayerId!==null;
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:()=>{void this.showLobby().catch(error=>this.fail(error));},
showRoom:()=>{void this.showRoom().catch(error=>this.fail(error));},
showReconnect:()=>{this.waitingReply=false;this.feedback?.loading(false);this.reconnecting=true;this.feedback?.reconnect(true);},
showTips:(message,time,source)=>{if(source==='show_message'){this.endRoomRequest();this.lobby?.cancelTaskRequest();this.lobby?.cancelInviteRequest();this.lobby?.cancelRankRequest();}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.endRoomRequest();this.roomReceipt=null;
if(Number(outcome.data.showerror)===1){
if(typeof outcome.data.error!=='string')throw Error(`${outcome.rpc}.error must be a string`);
this.tip(outcome.data.error);
}
}
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:()=>{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,source)=>{if(source==='show_message')this.lobby?.cancelTaskRequest();this.tip(message,time);},
showKicked:data=>{
const packet=data as {msg?:string;fromOther?:unknown;gameid?:unknown};
const text=packet.msg||`网络出现异常!${packet.fromOther?(packet.gameid==='G2hw0ubng0zcoI0r4mx3H2yr4GejidwO'?2:1):0}#(请重启游戏)`;
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:()=>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=>{
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();
}
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);
this.createStatus();requireNode(logo,'group-32').active=false;
this.progress=required(requireNode(logo,'group-1/Spirit254').getComponent(ProgressBar),'Logo ProgressBar');
const labels=logo.getComponentsInChildren(Label).filter(label=>label.node.name==='Spirit3104');
if(labels.length!==1)throw Error('Logo requires one Spirit3104');
this.countdownView=new CocosLogoCountdownView(requireNode(logo,'group-1/Spirit3103'),labels[0].node,()=>this.countdown?.skip());
this.countdown=createLogoCountdown({now:()=>performance.now(),schedule:(callback,ms)=>{const id=window.setTimeout(callback,ms);return ()=>window.clearTimeout(id);},
render:state=>this.countdownView?.render(state)});
await new Promise<void>(resolve=>director.once(Director.EVENT_AFTER_DRAW,resolve));
this.events.push({kind:'scene',data:'Loading'});
}
private async loadLoginResources():Promise<void>{
const prefab=required(this.loginPrefab,'loginPrefab');
await new Promise<void>((resolve,reject)=>assetManager.loadAny(prefab.uuid,(done,total)=>{
if(!this.terminal&&this.progress&&total>0)this.progress.progress=done/total;
},error=>error?reject(error):resolve()));
if(this.terminal)throw Error('Startup stopped');
this.countdown?.resourcesReady();required(this.progress,'progress').node.active=false;
}
/** 完成真实场景切换后再绑定按钮和尝试缓存登录;弹窗挂在独立 PopupRoot。 */
private async showLogin():Promise<void>{
this.countdownView?.dispose();this.countdownView=null;
await this.flow.enter(required(this.loginScene,'loginScene').name,()=>{
const root=instantiate(required(this.loginPrefab,'loginPrefab'));root.parent=this.canvas();root.setPosition(0,0,0);
const popup=new Node('PopupRoot');popup.parent=this.canvas();popup.layer=root.layer;popup.addComponent(UITransform).setContentSize(1600,720);
this.createStatus();this.panel=new LoginPanel(root,popup,required(this.protocolPrefab,'protocolPrefab'),required(this.phonePrefab,'phonePrefab'),
required(this.accounts,'accounts'),required(this.policy,'policy'),message=>this.tip(message),
()=>!this.busy&&!this.terminal&&this.runtime?.state.app.phase==='connected');
const version=required(requireNode(root,'group-2/版本显示').getComponent(Label),'version Label');
// Beta 展示格式使用当前渠道版本号;只改变显示,不改变登录协议中的数值版本。
version.string=`v${required(this.config,'config').identity.version}.0.0 Beta`;
// Label 自动尺寸更新后重新计算左上角 Widget,避免长版本文字向屏幕外扩展。
required(version.node.getComponent(UITransform),'version UITransform').setAnchorPoint(0,1);
version.updateRenderData(true);
const versionWidget=required(version.node.getComponent(Widget),'version Widget');
versionWidget.target=this.canvas();
versionWidget.updateAlignment();
this.events.push({kind:'scene',data:'Login'});
// Legacy location acquisition is independent of login. Keep the source null
// until a real callback arrives; report host failure without stopping the socket.
try{this.startLocation?.();}catch(error){this.tip(`定位接口调用失败:${error instanceof Error?error.message:String(error)}`);}
if(this.accounts!.cached())this.accounts!.autoLogin();
});
}
private acceptNative(data:unknown,channel:'global'|'bridge'):void{
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),
resolveLobbyMenuConfig(required(this.config,'config').remoteConfig),required(this.lobbyContext,'lobby context'),action=>{
this.events.push({kind:'lobby-action',data:action});
},
showFatal:error=>this.fail(error)},
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=>{
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();
}
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);
this.createStatus();
this.progress=required(requireNode(logo,'group-1/Spirit254').getComponent(ProgressBar),'Logo ProgressBar');
const labels=logo.getComponentsInChildren(Label).filter(label=>label.node.name==='Spirit3104');
if(labels.length!==1)throw Error('Logo requires one Spirit3104');
this.countdownView=new CocosLogoCountdownView(requireNode(logo,'group-1/Spirit3103'),labels[0].node,()=>this.countdown?.skip());
this.countdown=createLogoCountdown({now:()=>performance.now(),schedule:(callback,ms)=>{const id=window.setTimeout(callback,ms);return ()=>window.clearTimeout(id);},
render:state=>this.countdownView?.render(state)});
await new Promise<void>(resolve=>director.once(Director.EVENT_AFTER_DRAW,resolve));
this.events.push({kind:'scene',data:'Loading'});
}
private async loadLoginResources():Promise<void>{
const prefab=required(this.loginPrefab,'loginPrefab');
await new Promise<void>((resolve,reject)=>assetManager.loadAny(prefab.uuid,(done,total)=>{
if(!this.terminal&&this.progress&&total>0)this.progress.progress=done/total;
},error=>error?reject(error):resolve()));
if(this.terminal)throw Error('Startup stopped');
this.countdown?.resourcesReady();required(this.progress,'progress').node.active=false;
}
/** 完成真实场景切换后再绑定按钮和尝试缓存登录;弹窗挂在独立 PopupRoot。 */
private async showLogin():Promise<void>{
this.countdownView?.dispose();this.countdownView=null;
await this.flow.enter(required(this.loginScene,'loginScene').name,()=>{
const root=instantiate(required(this.loginPrefab,'loginPrefab'));root.parent=this.canvas();root.setPosition(0,0,0);
const popup=new Node('PopupRoot');popup.parent=this.canvas();popup.layer=root.layer;popup.addComponent(UITransform).setContentSize(1600,720);
this.createStatus();this.panel=new LoginPanel(root,popup,required(this.protocolPrefab,'protocolPrefab'),required(this.phonePrefab,'phonePrefab'),
required(this.accounts,'accounts'),required(this.policy,'policy'),message=>this.tip(message),
()=>!this.busy&&!this.terminal&&this.runtime?.state.app.phase==='connected');
const version=required(requireNode(root,'group-2/版本显示').getComponent(Label),'version Label');
// Beta 展示格式使用当前渠道版本号;只改变显示,不改变登录协议中的数值版本。
version.string=`v${required(this.config,'config').identity.version}.0.0 Beta`;
// Label 自动尺寸更新后重新计算左上角 Widget,避免长版本文字向屏幕外扩展。
required(version.node.getComponent(UITransform),'version UITransform').setAnchorPoint(0,1);
version.updateRenderData(true);
const versionWidget=required(version.node.getComponent(Widget),'version Widget');
versionWidget.target=this.canvas();
versionWidget.updateAlignment();
this.events.push({kind:'scene',data:'Login'});
// Legacy location acquisition is independent of login. Keep the source null
// until a real callback arrives; report host failure without stopping the socket.
try{this.startLocation?.();}catch(error){this.tip(`定位接口调用失败:${error instanceof Error?error.message:String(error)}`);}
if(this.accounts!.cached())this.accounts!.autoLogin();
});
}
private acceptNative(data:unknown,channel:'global'|'bridge'):void{
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),
resolveLobbyMenuConfig(required(this.config,'config').remoteConfig),required(this.lobbyContext,'lobby context'),action=>{
this.events.push({kind:'lobby-action',data:action});
if(action==='tasks'){required(this.runtime,'runtime').getPlayerTasks();return;}
this.tip(required(LOBBY_UNAVAILABLE[action],`lobby action handler ${action}`));
},taskid=>required(this.runtime,'runtime').claimTask(taskid),(name,data)=>this.callNative(name,data),{
request:codes=>required(this.runtime,'runtime').getVipRooms(codes),
join:code=>required(this.runtime,'runtime').joinVipRoom(code,this.location),
if(action==='create-room'){required(this.lobby,'lobby').openCreate();return;}
this.tip(required(LOBBY_UNAVAILABLE[action],`lobby action handler ${action}`));
},taskid=>required(this.runtime,'runtime').claimTask(taskid),(name,data)=>this.callNative(name,data),{
request:codes=>required(this.runtime,'runtime').getVipRooms(codes),
join:code=>this.requestRoom('self_join_room',()=>required(this.runtime,'runtime').joinVipRoom(code,this.location)),
describe:()=> '子游戏规则描述尚未接入',
},{
query:()=>required(this.runtime,'runtime').getPlayerInvitecode(),
bind:code=>required(this.runtime,'runtime').bindInvitecode(code),
paste:()=>this.pasteInvitecode(),
},{
rank:()=>required(this.runtime,'runtime').getTreasureList(),
loading:show=>required(this.feedback,'feedback').loading(show),
vipRank:limit=>required(this.runtime,'runtime').getVipRankList(limit),
joinOwned:code=>this.requestRoom('self_join_room',()=>required(this.runtime,'runtime').joinOwnedRoom(code,this.location)),
redeem:cardno=>required(this.runtime,'runtime').redeemCard(cardno),
paste:()=>this.pasteInvitecode(true),
room:{createPrefab:required(required(this.getComponent(SubgameAssets),'SubgameAssets').createRoom,'subgame create prefab'),
join:roomcode=>this.requestRoom('self_join_room',()=>required(this.runtime,'runtime').joinRoom({roomcode,...this.roomEnvironment()})),
create:roomtype=>this.requestRoom('create_room',()=>required(this.runtime,'runtime').createRoom({roomtype,...this.roomEnvironment()})),
busy:()=>this.roomPending!==null,close:()=>this.feedback?.loading(false)},
});
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'});
});
popup.setSiblingIndex(this.canvas().children.length-1);
required(this.status,'status').node.setSiblingIndex(this.canvas().children.length-1);
this.events.push({kind:'scene',data:'Lobby'});
});
this.tip('');
}
private callNative(name:string,data:Record<string,unknown>):void {
const win=window as unknown as {settings?:Record<string,unknown>;app_version?:number|string};
if(win.app_version!==undefined&&Number(win.app_version)!==0){
const bridge=this.bridge;
if(bridge?.isReady()){bridge.call(name as NativeCallName,data,()=>{});return;}
logNativeCall('WVJB',name,[data],false);return;
private roomEnvironment():{ip:string;location:unknown}{
const state=required(this.runtime,'runtime').state;
const self=required(state.players.entities[required(state.players.selfPlayerId,'self player id')],'self player');
if(typeof self.ip!=='string')throw Error('Authenticated player is missing ip');
return {ip:self.ip,location:this.location};
}
private requestRoom(rpc:'create_room'|'self_join_room',send:()=>void):void{
if(this.roomPending!==null)return;
const state=required(this.runtime,'runtime').state;
if(state.app.phase!=='logged-in'||state.room.kind!=='outside')throw Error('当前连接状态无法进入房间');
this.roomPending=rpc;this.lobby?.setRoomBusy(true);this.feedback?.loading(true);
this.roomTimer=setTimeout(()=>{this.endRoomRequest();this.tip('房间请求超时,请重试');},15000);
try{send();}catch(error){this.endRoomRequest();throw error;}
}
private endRoomRequest():void{
if(this.roomTimer!==null)clearTimeout(this.roomTimer);this.roomTimer=null;
if(this.roomPending!==null){this.roomPending=null;this.lobby?.setRoomBusy(false);this.feedback?.loading(false);}
}
private async showRoom():Promise<void>{
if(this.terminal)return;
this.saveAcceptedLogin();this.endRoomRequest();
// This callback runs only after the runtime accepted and committed the packet.
const receipt=this.roomReceipt;this.roomReceipt=null;
if(receipt&&Number(receipt.data.state)===0){
const identity=required(this.config,'config').identity;
if(receipt.rpc==='create_room')window.localStorage.setItem('tsgame_roomtype_'+identity.gameid+identity.agentid,JSON.stringify({data:receipt.data.roomtype}));
if(receipt.rpc==='self_join_room'&&receipt.data.shortcode)window.localStorage.setItem('tsgame_shortcode_'+identity.gameid+identity.agentid,String(receipt.data.shortcode));
}
const args=name==='friendsSharetypeUrlToptitleDescript'?
[data.sharefriend,data.type,data.webpageUrl,data.title,data.description]:[data];
const method=win.settings?.[name];
logNativeCall('settings',name,args,typeof method==='function');
if(typeof method==='function')Reflect.apply(method,win.settings,args);
this.panel?.dispose();this.panel=null;this.lobby?.dispose();this.lobby=null;
const assets=required(this.getComponent(SubgameAssets),'SubgameAssets');
await this.flow.enter(required(assets.roomScene,'subgame room scene').name,()=>{
if(required(this.runtime,'runtime').state.room.kind!=='inside')return;
const root=instantiate(required(assets.room,'subgame room prefab'));root.parent=this.canvas();root.setPosition(0,0,0);
const view=required(root.getComponent(TemplateRoomView),'TemplateRoomView');
root.on('room-action-error',(error:unknown)=>this.tip(error instanceof Error?error.message:String(error)));
root.on('room-action-timeout',()=>this.tip('房间操作超时,请重试'));
this.createStatus();required(this.game,'template game').mount(view);this.events.push({kind:'scene',data:'Room'});
});
}
private async pasteInvitecode(raw=false):Promise<string>{
const readText=raw?readClipboardText:readInviteClipboard;
const win=window as unknown as {settings?:{gamepastetext?:()=>unknown};app_version?:number|string};
if(win.app_version!==undefined&&Number(win.app_version)!==0){
const bridge=required(this.bridge,'clipboard bridge');
return readText({kind:'bridge',call:(name,data,callback)=>bridge.call(name,data,callback)});
}
if(win.settings){
const read=required(win.settings.gamepastetext,'settings.gamepastetext');
return readText({kind:'settings',read:()=>read.call(win.settings)});
}
if(!navigator.clipboard?.readText)throw Error('当前环境无法读取剪贴板,请手动输入');
return readText({kind:'browser',read:()=>navigator.clipboard.readText()});
}
private callNative(name:string,data:Record<string,unknown>):void {
const win=window as unknown as {settings?:Record<string,unknown>;app_version?:number|string};
if(win.app_version!==undefined&&Number(win.app_version)!==0){
const bridge=this.bridge;
if(bridge?.isReady()){bridge.call(name as NativeCallName,data,()=>{});return;}
logNativeCall('WVJB',name,[data],false);return;
}
const args=name==='friendsSharetypeUrlToptitleDescript'?
[data.sharefriend,data.type,data.webpageUrl,data.title,data.description]:[data];
const method=win.settings?.[name];
logNativeCall('settings',name,args,typeof method==='function');
if(typeof method==='function')Reflect.apply(method,win.settings,args);
}
private logout():void{
if(this.returningToLogin||this.terminal)return;
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');
if(this.busy||this.terminal)return;
if(!required(this.accounts,'accounts').requireAgreement())return;
if(!runtime.ready)throw Error('登录资源或连接尚未就绪');
const storage=window.localStorage;
let machineid=storage.getItem('tsgame_machineId_');
if(machineid===null){machineid=`${Date.now()}-${1000+Math.floor(Math.random()*8999)}`;storage.setItem('tsgame_machineId_',machineid);}
const host=window as unknown as {returnCitySN?:{ip:string;province:string;city:string}};
const city=host.returnCitySN;
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.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 fail(error:unknown):void{
console.error('[PlatformStartup]',error);
try{this.feedback?.kick(error instanceof Error?error.message:String(error));}
finally{this.release();}
}
/** 幂等释放计时器、订阅和宿主回调,阻止销毁后的迟到消息重新激活流程。 */
this.endRoomRequest();this.roomReceipt=null;
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');
if(this.busy||this.terminal)return;
if(!required(this.accounts,'accounts').requireAgreement())return;
if(!runtime.ready)throw Error('登录资源或连接尚未就绪');
const storage=window.localStorage;
let machineid=storage.getItem('tsgame_machineId_');
if(machineid===null){machineid=`${Date.now()}-${1000+Math.floor(Math.random()*8999)}`;storage.setItem('tsgame_machineId_',machineid);}
const host=window as unknown as {returnCitySN?:{ip:string;province:string;city:string}};
const city=host.returnCitySN;
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.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 fail(error:unknown):void{
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();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();this.feedback?.dispose();}
getDiagnostics(){return {events:[...this.events],ready:this.runtime?.ready,busy:this.busy,terminal:this.terminal};}
}
if(this.terminal)return;this.terminal=true;this.flow.dispose();this.panel?.dispose();this.lobby?.dispose();
this.endRoomRequest();this.countdown?.cancel();this.countdownView?.dispose();this.unsubscribe?.();this.bridge?.dispose();
this.restoreShareLogin?.();this.restoreLocation?.();this.runtime?.stop();
}
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,41 @@
import {_decorator,Component,Label,Layout,Node,Prefab,ScrollView,UITransform,Vec2,instantiate} from 'cc';
const {ccclass,property}=_decorator;
function required<T>(value:T|null,name:string):T{if(value===null)throw Error(`ProfileInfoListView requires ${name}`);return value;}
/** The prefab owns typography, spacing and viewport dimensions. Data owns the row list. */
@ccclass('ProfileInfoListView')
export class ProfileInfoListView extends Component {
@property(Prefab) textItemPrefab:Prefab|null=null;
@property(Prefab) authPrefab:Prefab|null=null;
@property(Node) content:Node|null=null;
@property(ScrollView) scroll:ScrollView|null=null;
@property(Label) signature:Label|null=null;
@property(UITransform) signatureBox:UITransform|null=null;
private rows:Label[]=[];
private signatureMinimumHeight:number|null=null;
render(texts:readonly string[],sign:string):void {
const content=required(this.content,'content'),scroll=required(this.scroll,'ScrollView');
const signature=required(this.signature,'signature'),box=required(this.signatureBox,'signature background');
const item=required(this.textItemPrefab,'text item prefab'),layout=required(content.getComponent(Layout),'vertical Layout');
if(layout.type!==Layout.Type.VERTICAL||layout.resizeMode!==Layout.ResizeMode.CONTAINER)throw Error('ProfileInfoListView requires a vertical container Layout');
if(this.signatureMinimumHeight===null)this.signatureMinimumHeight=box.height;
const offset=scroll.getScrollOffset();
while(this.rows.length>texts.length){const row=this.rows.pop()!;row.node.removeFromParent();row.node.destroy();}
while(this.rows.length<texts.length){
const node=instantiate(item);node.parent=content;node.name=`InfoRow${this.rows.length}`;
this.rows.push(required(node.getComponent(Label),'text item Label'));
}
this.rows.forEach((label,index)=>{
label.node.setSiblingIndex(index);label.string=texts[index];label.updateRenderData(true);
});
signature.string=sign;signature.updateRenderData(true);
const textArea=required(signature.getComponent(UITransform),'signature UITransform');
// The authored text inset is symmetric; the serialized background sets the minimum height.
box.height=Math.max(this.signatureMinimumHeight,textArea.height-2*signature.node.position.y);
box.node.setSiblingIndex(this.rows.length);
layout.updateLayout(true);
scroll.stopAutoScroll();
scroll.scrollToOffset(new Vec2(0,Math.max(0,Math.min(offset.y,scroll.getMaxScrollOffset().y))),0);
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b1570b17-a411-4b15-a66f-256d865d61e5",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,58 @@
import {assetManager,Button,ImageAsset,Label,Layout,Node,Prefab,ScrollView,Sprite,SpriteFrame,Texture2D,UITransform,instantiate,isValid} from 'cc';
import type {RankEntry} from '../../framework/protocol/contracts/rank-list.ts';
import {requireNode} from './node-bindings.ts';
function need<T>(v:T|null,name:string):T{if(v===null)throw Error(`Rank list missing ${name}`);return v;}
/** Rows instantiate the editor-owned VipRankItem; ScrollView and Layout own geometry. */
export class RankListView {
private disposed=false;
private generation=0;
private images:Array<{frame:SpriteFrame;texture:Texture2D}>=[];
constructor(private readonly group:Node,entries:readonly RankEntry[],starName:string,medals:readonly SpriteFrame[],title:SpriteFrame,itemPrefab:Prefab,vip=false){
if(entries.length===0)throw Error('Cannot open empty rank list');
if(medals.length!==3)throw Error('Rank list requires three medal frames');
const node=(id:number)=>requireNode(group,`Spirit${id}`);
need(node(3234).getComponent(Sprite),'title Sprite').spriteFrame=title;
node(609).active=false;
this.text(640,'');
const scrolls=group.getComponentsInChildren(ScrollView);
if(scrolls.length!==1)throw Error('Rank list requires one authored ScrollView');
const scroll=scrolls[0],content=need(scroll.content,'ScrollView content');
const layout=need(content.getComponent(Layout),'content Layout');
if(layout.type!==Layout.Type.VERTICAL||layout.resizeMode!==Layout.ResizeMode.CONTAINER)throw Error('Rank content requires vertical container Layout');
for(const child of [...content.children])child.destroy();
const choose=(index:number)=>{
const row=entries[index];this.text(636,row.name);this.text(637,`ID:${row.playerid}`);this.text(638,String(row.star));
this.text(640,vip?'公告:\n'+(row.destwo?'('+row.destwo+')\n':'')+(row.notice||''):'');
this.text(641,row.signature?`个性签名:\n${row.signature}`:'个性签名:');
const sig=need(node(641).getComponent(Label),'signature');sig.enableWrapText=true;sig.overflow=Label.Overflow.RESIZE_HEIGHT;
need(sig.node.getComponent(UITransform),'signature transform').width=400;
const token=++this.generation;this.avatar(node(634),row.headimg,()=>token===this.generation);
};
entries.forEach((entry,index)=>{
const row=instantiate(itemPrefab);row.name='VipRankItem'+(index+1);row.parent=content;row.active=true;
const itemNode=(id:number)=>requireNode(row,'Spirit'+id);
const medal=itemNode(629);medal.active=index<3;
if(index<3)need(medal.getComponent(Sprite),'medal').spriteFrame=medals[index];
this.avatar(itemNode(630),entry.headimg,()=>true);
need(itemNode(632).getComponent(Label),'name').string=entry.name;
need(itemNode(633).getComponent(Label),'star').string=starName+':'+entry.star;
const button=row.getComponent(Button)||row.addComponent(Button);button.transition=Button.Transition.NONE;
row.on(Button.EventType.CLICK,()=>choose(index));
});
layout.updateLayout();
choose(0);scroll.scrollToTop(0);
}
private text(id:number,value:string):void{need(requireNode(this.group,`Spirit${id}`).getComponent(Label),`Spirit${id} Label`).string=value;}
private avatar(node:Node,url:string,current:()=>boolean):void{
const sprite=need(node.getComponent(Sprite),'avatar');sprite.spriteFrame=null;sprite.sizeMode=Sprite.SizeMode.CUSTOM;
if(!url)return;
assetManager.loadRemote<ImageAsset>(url,{ext:'.png'},(error,image)=>{
if(this.disposed||!isValid(node)||!current())return;
if(error){this.group.emit('rank-image-error',error.message);return;}
const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame;
this.images.push({texture,frame});
});
}
dispose():void{this.disposed=true;for(const {frame,texture} of this.images){frame.destroy();texture.destroy();}this.images=[];}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "9186a6b3-f2f5-4b75-aeba-a958033141c3",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,9 @@
import {_decorator,Component,Prefab,SceneAsset} from 'cc';
const {ccclass,property}=_decorator;
/** App composition root: each game assigns its own pre-room page, room view and scene. */
@ccclass('SubgameAssets')
export class SubgameAssets extends Component {
@property(Prefab) createRoom:Prefab|null=null;
@property(Prefab) room:Prefab|null=null;
@property(SceneAsset) roomScene:SceneAsset|null=null;
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ec19e470-cf36-40e8-b420-e4314e5fbf07",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,66 +1,68 @@
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());
import {Animation, BlockInputEvents, Label, Node, Prefab, UITransform, Widget, instantiate, isValid} 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;}
loading(show:boolean):void{
const animation=requireNode(this.loadingNode,'group-40/载入动画').getComponent(Animation);
if(!animation||!animation.defaultClip)throw Error('Loading requires serialized AnimationClip');
this.loadingNode.active=show&&!this.locked;
if(this.loadingNode.active){this.front(this.loadingNode);if(!animation.getState(animation.defaultClip.name)?.isPlaying)animation.play();}
else animation.stop();
}
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=[];
}
}
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 animation of n.getComponentsInChildren(Animation))animation.stop();n.destroy();}
this.nodes=[];
}
}
@@ -1,388 +1,624 @@
import { assetManager, BlockInputEvents, Button, ImageAsset, Label, Mask, Node, Prefab, ScrollView, Sprite, SpriteFrame, Texture2D, UITransform, Widget, instantiate, isValid } from 'cc';
import type { RuntimeConfig } from '../../framework/config/runtime-config.ts';
import { selectLobbyModel, shouldShowNotice, type LobbyModel, type LobbyPolicy } from '../../framework/presentation/lobby-model.ts';
import type { PlatformState } from '../../framework/platform/stores/platform-types.ts';
import { LobbyAssets } from './LobbyAssets.ts';
import { assetManager, BlockInputEvents, Button, EditBox, ImageAsset, Label, Mask, Node, Prefab, ScrollView, Sprite, SpriteFrame, Texture2D, UITransform, Widget, instantiate, isValid } from 'cc';
import type { RuntimeConfig } from '../../framework/config/runtime-config.ts';
import { selectLobbyModel, shouldShowNotice, type LobbyModel, type LobbyPolicy } from '../../framework/presentation/lobby-model.ts';
import type { PlatformState } from '../../framework/platform/stores/platform-types.ts';
import { LobbyAssets } from './LobbyAssets.ts';
import { TaskListView } from './TaskListView.ts';
import {VipRoomListView} from './VipRoomListView.ts';
import {VipRoomHistory} from '../../framework/adapters/native/vip-room-history.ts';
import type {VipRoom,VipCode} from '../../framework/core/types/vip-room.ts';
import { resolveSharePolicy,sharePayload } from '../../framework/presentation/share-model.ts';
import type { TaskData } from '../../framework/core/types/task.ts';
import { requireNode } from './node-bindings.ts';
import { LOBBY_MENU_ACTIONS, lobbyMenuState, lobbyTrayState, type LobbyAction, type LobbyContext, type LobbyMenuConfig, type LobbyMenuState } from '../../framework/presentation/lobby-menu.ts';
function need<T>(value:T|null|undefined,name:string):T {if(value==null)throw Error(`Lobby missing ${name}`);return value;}
const label=(root:Node,path:string)=>need(requireNode(root,path).getComponent(Label),`${path} Label`);
/** 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;
private readonly menuNodes=new Map<number,Node>();
private readonly taskTip:Node;
private taskData:readonly TaskData[]|undefined;
private taskPending:'list'|'award'|null=null;
private taskTimer:ReturnType<typeof setTimeout>|null=null;
import { AnnouncementView } from './AnnouncementView.ts';
import { NoticeTabsView } from './NoticeTabsView.ts';
import { OwnerRoomListView } from './OwnerRoomListView.ts';
import { OptionalSwitchAnimation } from './OptionalSwitchAnimation.ts';
import { ProfileInfoListView } from './ProfileInfoListView.ts';
import { validateProfileAuth } from '../../framework/presentation/profile-auth.ts';
import {RequestLoading} from '../../framework/presentation/request-loading.ts';
import {JoinRoomView} from './JoinRoomView.ts';
import {CreateRoomPage} from './CreateRoomPage.ts';
import {RankListView} from './RankListView.ts';
import {parseRankList,type RankEntry} from '../../framework/protocol/contracts/rank-list.ts';
import {VipRoomListView} from './VipRoomListView.ts';
import {OwnedRoomHistory} from '../../framework/adapters/native/owned-room-history.ts';
import {VipRoomHistory} from '../../framework/adapters/native/vip-room-history.ts';
import type {VipRoom,VipCode} from '../../framework/core/types/vip-room.ts';
import { resolveSharePolicy,sharePayload,resolveQrCodePolicy } from '../../framework/presentation/share-model.ts';
import type { TaskData } from '../../framework/core/types/task.ts';
import { requireNode } from './node-bindings.ts';
import { LOBBY_MENU_ACTIONS, lobbyLockedTip, lobbyMenuState, lobbyTrayState, type LobbyAction, type LobbyContext, type LobbyMenuConfig, type LobbyMenuState } from '../../framework/presentation/lobby-menu.ts';
function need<T>(value:T|null|undefined,name:string):T {if(value==null)throw Error(`Lobby missing ${name}`);return value;}
const label=(root:Node,path:string)=>need(requireNode(root,path).getComponent(Label),`${path} Label`);
/** 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;
private readonly menuNodes=new Map<number,Node>();
private readonly taskTip:Node;
private taskData:readonly TaskData[]|undefined;
private readonly requestLoading:RequestLoading;
private taskPending:'list'|'award'|null=null;
private taskTimer:ReturnType<typeof setTimeout>|null=null;
private vipData:readonly VipRoom[]|undefined;
constructor(private readonly canvas:Node,private readonly popup:Node,private readonly assets:LobbyAssets,
private readonly config:RuntimeConfig,private readonly policy:LobbyPolicy,state:PlatformState,
private readonly tip:(text:string)=>void,private readonly logout:()=>void,
private readonly location:()=>unknown,private readonly openUrl:(url:string)=>void,
private readonly menuConfig:LobbyMenuConfig,private readonly context:LobbyContext,
private readonly onAction:(action:LobbyAction)=>void,
private readonly claimTask:(taskid:number|string)=>void,
private readonly nativeCall:(name:string,data:Record<string,unknown>)=>void,
private readonly vip?:{request:(codes:readonly VipCode[])=>void;join:(code:VipCode)=>void;describe:(roomtype:readonly unknown[])=>string}){
this.model=selectLobbyModel(state);
if(assets.switches.length!==2)throw Error('LobbyAssets requires 2 switch frames');
this.menu=this.add(assets.menu,canvas);this.header=this.add(assets.header,canvas);this.noticeMenu=this.add(assets.noticeMenu,canvas);
requireNode(this.header,'group-103').active=false;
requireNode(this.noticeMenu,'group-95').active=menuConfig.template.isGameHall;
for(const root of [this.menu,this.header,this.noticeMenu]){
for(const node of root.getComponentsInChildren(UITransform).map(ui=>ui.node)){
const match=/^Spirit(\d+)$/.exec(node.name);
if(match){const id=Number(match[1]);if(this.menuNodes.has(id))throw Error(`Duplicate lobby node ${id}`);this.menuNodes.set(id,node);}
}
}
this.menuNodes.set(10,requireNode(this.header,'group-3/购买房卡'));
this.menuNodes.set(9,requireNode(this.header,'group-3/PlayerInfo/头像框'));
// Original JumpMenuScene hides these transient/obsolete fields.
for(const id of [219,220,157,497])requireNode(this.menu,`group-3/Spirit${id}`).active=false;
private invitePending:'query'|'binding'|null=null;
private inviteTimer:ReturnType<typeof setTimeout>|null=null;
private inviteReceipt:PlatformState['inviteResponse'];
private rankData:readonly RankEntry[]|undefined;
private vipRankReceipt:PlatformState['vipRank'];
private rankPending:'rank'|'vip-rank'|null=null;
private rankTimer:ReturnType<typeof setTimeout>|null=null;
private roomPage:{setBusy(busy:boolean):void;close():void}|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,
private readonly menuConfig:LobbyMenuConfig,private readonly context:LobbyContext,
private readonly onAction:(action:LobbyAction)=>void,
private readonly claimTask:(taskid:number|string)=>void,
private readonly nativeCall:(name:string,data:Record<string,unknown>)=>void,
private readonly vip?:{request:(codes:readonly VipCode[])=>void;join:(code:VipCode)=>void;describe:(roomtype:readonly unknown[])=>string},
private readonly invite?:{query:()=>void;bind:(code:string)=>void;paste:()=>Promise<string>},
private readonly extras?:{rank:()=>void;redeem:(cardno:string)=>void;paste:()=>Promise<string>;vipRank?:(limit:number)=>void;joinOwned?:(code:number)=>void;loading?:(show:boolean)=>void;
room?:{createPrefab:Prefab;join(code:number):void;create(roomtype:readonly unknown[]):void;busy():boolean;close():void}}){
this.requestLoading=new RequestLoading(show=>need(this.extras?.loading,'request Loading UI')(show));
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=menuConfig.template.isGameHall;
for(const root of [this.menu,this.header,this.noticeMenu]){
for(const node of root.getComponentsInChildren(UITransform).map(ui=>ui.node)){
const match=/^Spirit(\d+)$/.exec(node.name);
if(match){const id=Number(match[1]);if(this.menuNodes.has(id))throw Error(`Duplicate lobby node ${id}`);this.menuNodes.set(id,node);}
}
}
this.menuNodes.set(10,requireNode(this.header,'group-3/购买房卡'));
this.menuNodes.set(9,requireNode(this.header,'group-3/PlayerInfo/头像框'));
// Original JumpMenuScene hides these transient/obsolete fields.
for(const id of [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;
});
label(this.noticeMenu,'group-3/Spirit145').string=policy.scroll;
for(const [id,action] of Object.entries(LOBBY_MENU_ACTIONS))this.bind(need(this.menuNodes.get(Number(id)),`menu ${id}`),()=>this.dispatch(action));
this.bind(requireNode(this.noticeMenu,'group-95/Spirit3122'),()=>{requireNode(this.noticeMenu,'group-95').active=false;});
const vipRank=requireNode(this.menu,'group-3/VipRank/Spirit679');
for(const id of [180,193,194,682,178,179]){label(vipRank,`Spirit${id}`).string='';requireNode(vipRank,`Spirit${id}`).active=false;}
label(this.menu,'group-3/Spirit340').string='';requireNode(this.menu,'group-3/Spirit340').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(vipRank,`Spirit${title}`),valueLabel=label(vipRank,`Spirit${value}`);
titleLabel.string=contact.label;valueLabel.string=contact.value;titleLabel.node.active=true;valueLabel.node.active=true;
});
label(this.noticeMenu,'group-3/Spirit145').string=policy.scroll;
for(const [id,action] of Object.entries(LOBBY_MENU_ACTIONS))this.bind(need(this.menuNodes.get(Number(id)),`menu ${id}`),()=>this.dispatch(action));
this.bind(requireNode(this.noticeMenu,'group-95/Spirit3122'),()=>{requireNode(this.noticeMenu,'group-95').active=false;});
this.applyMenuState(lobbyMenuState(menuConfig,context));
if(policy.contacts.length>0)requireNode(this.menu,'group-3/Spirit16').active=false;
this.taskTip=requireNode(this.menu,'group-3/Spirit17/Tip');
need(this.taskTip.getComponent(Sprite),'Spirit17/Tip Sprite');
this.render(state);
const key=`tsgame_gameNoteTime_${config.identity.gameid}${config.identity.agentid}`;
const previous=window.localStorage.getItem(key),time=previous===null?null:Number(previous);
if(time!==null&&!Number.isFinite(time))throw Error(`Invalid ${key}`);
const now=Date.now();
if(context.versionState===0&&shouldShowNotice(policy.noticeHours,time,now,false)){
if(policy.noticeHours>0)window.localStorage.setItem(key,String(now));
this.openNotice();
need(requireNode(this.menu,'group-3/Announcement').getComponent(AnnouncementView),'AnnouncementView').show(policy.scroll);
for(const [id,locked] of Object.entries(menuConfig.locked)){
requireNode(this.menu,`group-3/Spirit${id}/Locked`).active=locked;
}
}
if(policy.contacts.length>0)requireNode(vipRank,'Spirit16').active=false;
this.taskTip=requireNode(this.menu,'group-3/Spirit17/Tip');
need(this.taskTip.getComponent(Sprite),'Spirit17/Tip Sprite');
this.render(state);
const key=`tsgame_gameNoteTime_${config.identity.gameid}${config.identity.agentid}`;
const previous=window.localStorage.getItem(key),time=previous===null?null:Number(previous);
if(time!==null&&!Number.isFinite(time))throw Error(`Invalid ${key}`);
const now=Date.now();
if(context.versionState===0&&shouldShowNotice(policy.noticeHours,time,now,false)){
if(policy.noticeHours>0)window.localStorage.setItem(key,String(now));
this.openNotice();
}
}
private dispatch(action:LobbyAction):void{
switch(action){
case 'buy-roomcard':case 'buy-bean':return; // Payments are retired in the current business.
case 'my-rooms':this.openOwnedRooms();return;
case 'vip-rank':this.requestVipRank();return;
case 'world-rooms':
case 'match':
case 'official-rooms':{
const text=lobbyLockedTip(this.menuConfig,action);
if(text!==null){this.tip(text);return;}
this.onAction(action);return;
}
case 'agent':this.requestInvite('query',()=>need(this.invite,'invite commands').query());return;
case 'share':this.openShare();return;
case 'vip-rooms':this.openVipRooms();return;
case 'tasks':this.requestTask('list',()=>this.onAction('tasks'));return;
case 'settings':this.openSettings();return;
case 'profile':this.openProfile();return;
case 'qrcode':this.openQrCode();return;
case 'rank':this.requestRank();return;
case 'cdkey':this.openCdKey();return;
case 'vip-rooms':this.openVipRooms();return;
case 'tasks':this.requestTask('list',()=>this.onAction('tasks'));return;
case 'settings':this.openSettings();return;
case 'profile':this.openProfile();return;
case 'join-room':this.openJoin();return;
case 'notice':this.openNotice();return;
case 'tray':{
const tray=requireNode(this.noticeMenu,'group-95');
if(tray.active){tray.active=false;return;}
if(this.context.versionState===1)return;
const state=lobbyTrayState(this.menuConfig,this.context);
const bg=requireNode(tray,'Spirit3121');bg.active=true;
need(bg.getComponent(UITransform),'tray background').width=150*Object.values(state.visible).filter(Boolean).length+30;
this.applyMenuState(state);
tray.active=true;return;
}
default:this.onAction(action);
case 'create-room':this.openCreate();return;
case 'notice':this.openNotice();return;
case 'tray':{
const tray=requireNode(this.noticeMenu,'group-95');
if(tray.active){tray.active=false;return;}
if(this.context.versionState===1)return;
const state=lobbyTrayState(this.menuConfig,this.context);
const bg=requireNode(tray,'Spirit3121');bg.active=true;
need(bg.getComponent(UITransform),'tray background').width=150*Object.values(state.visible).filter(Boolean).length+30;
this.applyMenuState(state);
tray.active=true;return;
}
default:this.onAction(action);
}
}
private applyMenuState(state:LobbyMenuState):void{
// A projection includes nodes from the adjacent notice layer. Missing IDs belonging to other layers are intentional.
for(const [id,active] of Object.entries(state.visible)){const node=this.menuNodes.get(Number(id));if(node)node.active=active;}
for(const id of new Set([...Object.keys(state.left),...Object.keys(state.top)])){
const node=this.menuNodes.get(Number(id));if(!node)continue;
const ui=need(node.getComponent(UITransform),`menu ${id} transform`);
const widget=node.getComponent(Widget);
// Original coordinates are top-left in a centered 1280x720 layout; retain the 1600x720 Cocos canvas.
const left=state.left[Number(id)],top=state.top[Number(id)];
if(left!==undefined){
const x=left-640+ui.width*ui.anchorX;
if(widget){widget.isAlignLeft=false;widget.isAlignRight=false;widget.isAlignHorizontalCenter=true;widget.horizontalCenter=x;}
node.setPosition(x,node.position.y,0);
}
if(top!==undefined){
const y=360-top-ui.height*(1-ui.anchorY);
if(widget){widget.isAlignTop=false;widget.isAlignBottom=false;widget.isAlignVerticalCenter=true;widget.verticalCenter=y;}
node.setPosition(node.position.x,y,0);
}
if(widget)widget.updateAlignment();
}
}
private add(prefab:Prefab|null,parent:Node):Node {
const root=instantiate(need(prefab,'prefab'));root.parent=parent;root.setPosition(0,0,0);root.active=true;
const widget=root.getComponent(Widget);
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 button=node.getComponent(Button)??node.addComponent(Button);
button.transition=Button.Transition.NONE;
const listener=()=>{if(this.disposed||!node.activeInHierarchy||!button.interactable||(!modal&&this.modal!==null)||!this.model.canAct)return;
try{action();}catch(error){this.tip(error instanceof Error?error.message:String(error));}};
node.on(Button.EventType.CLICK,listener);
(modal?this.modalReleases:this.releases).push(()=>{if(isValid(node))node.off(Button.EventType.CLICK,listener);});
}
render(state:PlatformState):void {
if(this.disposed)return;this.model=selectLobbyModel(state);
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.
this.taskTip.active=this.model.taskstate!==0;
if(!this.model.canAct){this.cancelTaskRequest();this.cancelInviteRequest();}
if(!this.model.canAct)this.cancelRankRequest();
if(!this.model.canAct)this.cancelRoomPage();
if(state.treasureList!==this.rankData){
this.rankData=state.treasureList;const show=this.rankPending==='rank';if(show)this.cancelRankRequest();
if(show&&this.model.canAct&&state.treasureList)this.openRank(state.treasureList);
}
}
private applyMenuState(state:LobbyMenuState):void{
// A projection includes nodes from the adjacent notice layer. Missing IDs belonging to other layers are intentional.
for(const [id,active] of Object.entries(state.visible)){const node=this.menuNodes.get(Number(id));if(node)node.active=active;}
for(const id of new Set([...Object.keys(state.left),...Object.keys(state.top)])){
const node=this.menuNodes.get(Number(id));if(!node)continue;
const ui=need(node.getComponent(UITransform),`menu ${id} transform`);
const widget=node.getComponent(Widget);
// Original coordinates are top-left in a centered 1280x720 layout; retain the 1600x720 Cocos canvas.
const left=state.left[Number(id)],top=state.top[Number(id)];
if(left!==undefined){
const x=left-640+ui.width*ui.anchorX;
if(widget){widget.isAlignLeft=false;widget.isAlignRight=false;widget.isAlignHorizontalCenter=true;widget.horizontalCenter=x;}
node.setPosition(x,node.position.y,0);
}
if(top!==undefined){
const y=360-top-ui.height*(1-ui.anchorY);
if(widget){widget.isAlignTop=false;widget.isAlignBottom=false;widget.isAlignVerticalCenter=true;widget.verticalCenter=y;}
node.setPosition(node.position.x,y,0);
}
if(widget)widget.updateAlignment();
if(state.vipRank!==this.vipRankReceipt){
this.vipRankReceipt=state.vipRank;
if(this.rankPending==='vip-rank'){this.cancelRankRequest();if(this.model.canAct&&state.vipRank?.entries)this.openRank(state.vipRank.entries,true);}
}
}
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 button=node.getComponent(Button)??node.addComponent(Button);
button.transition=Button.Transition.NONE;
const listener=()=>{if(this.disposed||!node.activeInHierarchy||!button.interactable||(!modal&&this.modal!==null)||!this.model.canAct)return;
try{action();}catch(error){this.tip(error instanceof Error?error.message:String(error));}};
node.on(Button.EventType.CLICK,listener);
(modal?this.modalReleases:this.releases).push(()=>{if(isValid(node))node.off(Button.EventType.CLICK,listener);});
}
render(state:PlatformState):void {
if(this.disposed)return;this.model=selectLobbyModel(state);
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.
this.taskTip.active=this.model.taskstate!==0;
if(!this.model.canAct)this.cancelTaskRequest();
if(state.tasks!==this.taskData){
this.taskData=state.tasks;
const show=this.taskPending==='list'||this.modalKind==='tasks';
this.cancelTaskRequest();
if(show&&state.tasks&&this.model.canAct){this.tip('');this.openTasks(state.tasks);}
if(state.inviteResponse!==this.inviteReceipt){
this.inviteReceipt=state.inviteResponse;
if(state.inviteResponse?.kind===this.invitePending){
const open=this.invitePending==='query';this.cancelInviteRequest();
if(open&&this.model.canAct){
const player=need(state.players.entities[need(state.players.selfPlayerId,'playerid')],'player');
if(player.invitecode===undefined)throw Error('Missing queried invitecode');
this.openInvite(player.invitecode);
}
}
}
this.updateAvatar();
if(state.vipRooms!==this.vipData){this.vipData=state.vipRooms;if(this.modalKind==='vip-rooms'&&state.vipRooms)this.renderVipRooms(state.vipRooms);}
if(this.modalKind==='profile')this.renderProfile();
if(state.tasks!==this.taskData){
this.taskData=state.tasks;
const show=this.taskPending==='list'||this.modalKind==='tasks';
this.cancelTaskRequest();
if(show&&state.tasks&&this.model.canAct){this.tip('');this.openTasks(state.tasks);}
}
this.updateAvatar();
if(state.vipRooms!==this.vipData){this.vipData=state.vipRooms;if(this.modalKind==='vip-rooms'&&state.vipRooms)this.renderVipRooms(state.vipRooms);}
if(this.modalKind==='profile')this.renderProfile();
}
private numbers(name:string,background:string,value:number):void {
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|null):Node {
this.close();requireNode(this.noticeMenu,'group-95').active=false;
const modal=this.add(prefab,this.popup);this.modal=modal;this.modalKind=kind;
if(group!==null)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;
}
private openOwnedRooms():void {
const entries=new OwnedRoomHistory(window.localStorage,this.config.identity.gameid,this.config.identity.agentid).read();
if(this.assets.ownedRoomDigits.length!==10)throw Error('Owned room requires ten digit frames');
const modal=this.open(this.assets.ownedRoomList,'my-rooms','group-98'),group=requireNode(modal,'group-98');
this.bind(requireNode(group,'Spirit103'),()=>this.close(),true);
const view=need(modal.getComponent(OwnerRoomListView),'OwnerRoomListView');
for(const {node,code} of view.render(entries,this.assets.ownedRoomDigits))
this.bind(node,()=>{this.close();need(this.extras?.joinOwned,'owned room command')(code);},true);
}
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 requestVipRank():void {
if(this.rankPending)return;
this.rankPending='vip-rank';
this.rankTimer=setTimeout(()=>{this.cancelRankRequest();this.tip('排行榜请求超时,请重试');},15000);
try{this.requestLoading.begin('getVipRankList');need(this.extras?.vipRank,'VIP rank command')(this.menuConfig.sys.vipRankLimit);}catch(error){this.cancelRankRequest();throw error;}
}
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 requestRank():void {
if(this.rankPending)return;
const mode=Number(this.menuConfig.hall.rankList);
if(mode===1){
const raw=this.config.remoteConfig.getValue('rankList1');
this.openRank(raw?parseRankList(raw,true):[]);return;
}
if(mode!==2)return;
this.rankPending='rank';
this.rankTimer=setTimeout(()=>{this.cancelRankRequest();this.tip('排行榜请求超时,请重试');},15000);
try{need(this.extras,'rank commands').rank();}catch(error){this.cancelRankRequest();throw error;}
}
requestReceived(message:{route:string;rpc:string;data?:unknown}):void{this.requestLoading.received(message);}
cancelRankRequest():void {
this.requestLoading.cancel('getVipRankList');
if(this.rankTimer!==null)clearTimeout(this.rankTimer);
this.rankTimer=null;this.rankPending=null;
}
private openRank(entries:readonly RankEntry[],vip=false):void {
requireNode(this.noticeMenu,'group-95').active=false;
if(entries.length===0)return;
// Legacy Utl.Config.ownerNote and Logic.setOwnerNote, scoped to game + agent.
const ownerNote=window.localStorage.getItem('tsgame_ownerNote_'+this.config.identity.gameid+this.config.identity.agentid);
if(!vip&&ownerNote&&this.menuConfig.hall.isReSetOwnerNote)entries=[{...entries[0],signature:ownerNote.replace(/#/g,'\n')},...entries.slice(1)];
const rawFrame=this.config.remoteConfig.getValue('nameImgFrame_2');
// Logic.getConfig_Succ explicitly defines frame 1 when this optional value is falsy.
const frame=rawFrame?Number(rawFrame):1;
if(!Number.isInteger(frame)||!this.assets.rankSymbols[frame-1])throw Error('Invalid nameImgFrame_2');
const modal=this.open(this.assets.rank,vip?'vip-rank':'rank','group-84'),group=requireNode(modal,'group-84');
need(requireNode(group,'Spirit639').getComponent(Sprite),'rank symbol').spriteFrame=this.assets.rankSymbols[frame-1];
this.bind(requireNode(group,'Spirit605'),()=>this.close(),true);
requireNode(group,'Spirit605/Spirit606').addComponent(BlockInputEvents);
const onError=(message:string)=>this.tip(`排行榜头像加载失败:${message}`);group.on('rank-image-error',onError);
const view=new RankListView(group,entries,this.policy.starName,this.assets.rankMedals,need(vip?this.assets.vipRankTitle:this.assets.rankTitle,'rank title'),need(this.assets.rankItem,'rankItem'),vip);
this.modalReleases.push(()=>{group.off('rank-image-error',onError);view.dispose();});
}
private openCdKey():void {
const modal=this.open(this.assets.redeemCode,'cdkey','group-74'),group=requireNode(modal,'group-74');
this.bind(requireNode(group,'反馈遮罩'),()=>this.close(),true);
requireNode(group,'反馈背景').addComponent(BlockInputEvents);
requireNode(group,'反馈提交').active=false;
const text=label(group,'反馈输入内容');text.string='';text.overflow=Label.Overflow.CLAMP;
const w=text.node.getComponent(Widget);if(w)w.enabled=false;
const area=need(text.node.getComponent(UITransform),'card input transform');area.setContentSize(930,130);area.setAnchorPoint(0,1);
const input=new Node('CardInput');input.layer=group.layer;input.parent=group;input.setPosition(text.node.position);
input.addComponent(UITransform).setContentSize(930,130);
text.node.parent=input;text.node.setPosition(-465,65,0);
const edit=input.addComponent(EditBox);edit.textLabel=text;edit.inputMode=EditBox.InputMode.SINGLE_LINE;
edit.inputFlag=EditBox.InputFlag.DEFAULT;edit.maxLength=-1;
this.modalReleases.push(()=>{edit.blur();edit.string='';});
let pasting=false;
this.bind(requireNode(group,'Spirit3265'),()=>{
if(pasting)return;pasting=true;
void need(this.extras,'card clipboard').paste().then(value=>{if(this.modal===modal&&this.model.canAct&&value)edit.string=value;})
.catch(error=>{if(this.modal===modal)this.tip(error instanceof Error?error.message:String(error));}).finally(()=>{pasting=false;});
},true);
this.bind(requireNode(group,'Spirit3266'),()=>{
const cardno=edit.string;if(cardno===''||/^[ ]+$/.test(cardno))return;
this.close();need(this.extras,'redemption commands').redeem(cardno);
},true);
}
private openQrCode():void {
const policy=resolveQrCodePolicy(this.config);if(!policy)return;
const modal=this.open(this.assets.qrCode,'qrcode','group-42'),group=requireNode(modal,'group-42');
this.bind(requireNode(group,'二维码遮罩'),()=>this.close(),true);
requireNode(group,'Spirit342').addComponent(BlockInputEvents);
this.bind(requireNode(group,'Spirit345'),()=>this.nativeCall('friendsSharetypeUrlToptitleDescript',policy.share),true);
const sprite=need(requireNode(group,'Spirit343').getComponent(Sprite),'QR image Sprite');
sprite.sizeMode=Sprite.SizeMode.CUSTOM;sprite.spriteFrame=null;
assetManager.loadRemote<ImageAsset>(policy.url,{ext:'.png'},(error,image)=>{
if(this.disposed||this.modal!==modal||!isValid(sprite.node))return;
if(error){this.tip(`二维码图片加载失败:${error.message}`);return;}
const texture=new Texture2D();texture.image=image;
const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame;
this.modalReleases.push(()=>{sprite.spriteFrame=null;frame.destroy();texture.destroy();});
});
}
private 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');
const modal=this.open(this.assets.profile,'profile','group-18');
this.bind(requireNode(modal,'group-18/Spirit116'),()=>this.close(),true);this.renderProfile();
const view=need(modal.getComponent(ProfileInfoListView),'ProfileInfoListView');
this.bind(requireNode(modal,'group-18/Spirit484'),()=>this.openAuth(need(view.authPrefab,'auth prefab')),true);
}
private renderProfile():void {
const modal=need(this.modal,'profile');
const values=[this.model.nickname,`ID:${this.model.playerid}`,`IP:${need(this.model.ip,'player ip')}`,
`积分:${this.model.score}`,`${this.policy.starName}:${this.model.bean}`];
const location=this.location() as {errorCode?:number;province?:string;city?:string}|null;
values.push(location?.errorCode===0?`${need(location.province,'location province')}-${need(location.city,'location city')}`:'获取定位失败!','个性签名:');
need(modal.getComponent(ProfileInfoListView),'ProfileInfoListView').render(values,this.model.sign);
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,'退出登录'),()=>{if(this.context.launchMode===1)this.tip('请返回游戏大厅退出登录');else this.logout();},true);
for(const [name,key] of [['音乐底',`tsgame_bgm_${this.config.identity.gameid}`],['音效底',`tsgame_sound_${this.config.identity.gameid}${this.config.identity.agentid}`]] as const){
const sprite=need(requireNode(group,name).getComponent(Sprite),name);
const read=()=>{const raw=window.localStorage.getItem(key);if(raw===null){window.localStorage.setItem(key,'1');return true;}
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);
private openAuth(prefab:Prefab):void {
const modal=this.open(prefab,'auth','group-57'),group=requireNode(modal,'group-57');
const name=need(requireNode(group,'NameInput').getComponent(EditBox),'auth name EditBox');
const id=need(requireNode(group,'IdInput').getComponent(EditBox),'auth ID EditBox');
name.string='';id.string='';
const blur=()=>{name.blur();id.blur();};
this.modalReleases.push(blur);
this.bind(requireNode(group,'Spirit485'),()=>this.close(),true);
this.bind(requireNode(group,'Spirit493'),()=>{
const result=validateProfileAuth(name.string,id.string);
if(result==='name'){this.tip('姓名不正确!');return;}
if(result==='id'){this.tip('身份证信息不正确!');return;}
this.close();this.tip('认证成功!');
},true);
}
openNotice():void {
const modal=this.open(this.assets.notices,'notice','group-12'),group=requireNode(modal,'group-12');
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,'退出登录'),()=>{if(this.context.launchMode===1)this.tip('请返回游戏大厅退出登录');else this.logout();},true);
for(const [name,key] of [['音乐底',`tsgame_bgm_${this.config.identity.gameid}`],['音效底',`tsgame_sound_${this.config.identity.gameid}${this.config.identity.agentid}`]] as const){
const node=requireNode(group,name);
const animation=need(node.getComponent(OptionalSwitchAnimation),`${name} OptionalSwitchAnimation`);
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=(animate:boolean)=>{const on=read();animation.setState(on,this.assets.switches[on?0:1],animate);};render(false);
this.bind(node,()=>{window.localStorage.setItem(key,read()?'0':'1');render(true);},true);
}
for(const name of ['Spirit357','Spirit388'])this.bind(requireNode(group,name),()=>this.tip('原生震动与语音开关尚未接入'),true);
}
openNotice():void {
const modal=this.open(this.assets.noticeBoard,'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);}
// Decorative panel surfaces must consume hits instead of exposing the backdrop behind them.
for(const name of ['通知背景','通知标题','通知标题内容']){
const surface=requireNode(group,name);
if(!surface.getComponent(BlockInputEvents))surface.addComponent(BlockInputEvents);
}
show(0);
}
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();};
});
}
};
const tabs=need(requireNode(group,'NoticeTabs').getComponent(NoticeTabsView),'NoticeTabsView');
this.modalReleases.push(tabs.bindPages(this.policy.notices.map((notice,index)=>index===0||notice.text!==''||notice.image!==''),show));
}
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++)requireNode(modal,`group-5/Spirit${id}`).active=false;
const commands=need(this.extras?.room,'room entry commands');
const view=need(modal.getComponent(JoinRoomView),'JoinRoomView');this.roomPage=view;
const saved=window.localStorage.getItem('tsgame_shortcode_'+this.config.identity.gameid+this.config.identity.agentid);
const current=()=>this.modal===modal&&this.roomPage===view&&this.model.canAct;
view.open({initialCode:saved===null?'':saved,submit:code=>{if(current())commands.join(code);},cancel:()=>{if(this.modal===modal)this.close();},
paste:()=>need(this.extras,'clipboard').paste(),reportError:text=>this.tip(text)});
view.setBusy(commands.busy());
}
openShare():void {
const modal=this.open(this.assets.share,'share','group-11'),policy=resolveSharePolicy(this.config);
this.bind(requireNode(modal,'group-11/Spirit72'),()=>this.close(),true);
for(const [id,friend] of [[74,1],[75,2]] as const)this.bind(requireNode(modal,`group-11/Spirit${id}`),()=>{
const data=sharePayload(policy,friend);if(data)this.nativeCall('friendsSharetypeUrlToptitleDescript',data);
this.close();
},true);
if(policy.background){
const node=requireNode(modal,'group-11/Spirit345'),sprite=need(node.getComponent(Sprite),'share background');
assetManager.loadRemote<ImageAsset>(policy.background,{ext:'.png'},(error,image)=>{
if(this.disposed||this.modal!==modal||!isValid(node))return;
if(error){this.tip(`分享背景加载失败:${error.message}`);return;}
const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame;
this.modalReleases.push(()=>{sprite.spriteFrame=null;frame.destroy();texture.destroy();});
});
openCreate():void {
const commands=need(this.extras?.room,'room entry commands');
const modal=this.open(commands.createPrefab,'create',null);
const page=need(modal.getComponent(CreateRoomPage),'subgame CreateRoomPage');this.roomPage=page;
const text=window.localStorage.getItem('tsgame_roomtype_'+this.config.identity.gameid+this.config.identity.agentid);
let previous:unknown=null;
if(text!==null&&text!==''){
const saved=JSON.parse(text);if(!saved||!Array.isArray(saved.data))throw Error('Invalid saved roomtype');previous=saved.data;
}
const current=()=>this.modal===modal&&this.roomPage===page&&this.model.canAct;
page.open({previousRoomtype:previous,submit:roomtype=>{if(current())commands.create(roomtype);},cancel:()=>{if(this.modal===modal)this.close();},reportError:text=>{if(current())this.tip(text);}});
page.setBusy(commands.busy());
}
private openVipRooms():void {
const modal=this.open(this.assets.header,'vip-rooms','group-103'),group=requireNode(modal,'group-103');
this.menu.active=false;this.noticeMenu.active=false;
this.bind(requireNode(group,'Spirit3182'),()=>this.close(),true);
for(const id of [3190,3191])requireNode(group,`Spirit${id}`).active=false;
const advanced=need(this.model.advanced,'players.self.advanced');
for(const id of advanced?[3170,3171]:[3183,3184,3194,3195,3196,3197,3198,3214,3215,3235])requireNode(group,`Spirit${id}`).active=false;
const view=need(modal.getComponent(VipRoomListView),'VipRoomListView'),port=need(this.vip,'VIP command port');
view.configure(port.describe);
this.bind(requireNode(group,'Spirit3175'),()=>view.showRooms(),true);
this.bind(requireNode(group,'Spirit3184'),()=>{this.close();this.onAction('create-room');},true);
this.bind(requireNode(group,'Spirit3240'),()=>this.openJoin(),true);
this.bind(requireNode(group,'Spirit3231'),()=>this.onAction('vip-rank'),true);
this.bind(requireNode(group,'Spirit3199'),()=>this.tip(`房间 ${view.selectedCode}:短房号排行模块尚未接入`),true);
for(const id of [3194,3195,3214])this.bind(requireNode(group,`Spirit${id}`),()=>this.tip('黑名单管理模块尚未接入'),true);
for(const id of [3196,3197,3215])this.bind(requireNode(group,`Spirit${id}`),()=>this.tip('统一魅力值设置模块尚未接入'),true);
this.bind(requireNode(group,'Spirit3235'),()=>this.tip('好友管理模块尚未接入'),true);
if(advanced){
label(group,'Spirit3198').string='(黑名单内玩家临时禁入房间,请尽快到后台处理好友关系)';
requireNode(group,'Spirit3235').active=!!this.config.remoteConfig.getValue('managerUrl');
}
const join=(code:VipCode)=>{if(this.model.canAct&&this.modal===modal){try{port.join(code);}catch(e){this.tip(e instanceof Error?e.message:String(e));}}};
modal.on('vip-join',join);this.modalReleases.push(()=>modal.off('vip-join',join));
const refresh=()=>{if(this.model.canAct&&this.modal===modal)port.request(this.vipHistory().read());};
const raw=this.config.remoteConfig.getValue('vipFlushTime');
// GameData.sysConfig defines the optional refresh setting as 10000 ms.
const delay=raw==null?10000:Number(raw);if(!Number.isFinite(delay)||delay<=0)throw Error('Invalid vipFlushTime');
const timer=setInterval(()=>{try{if(this.vipHistory().read().length>0)refresh();}catch(e){this.tip(e instanceof Error?e.message:String(e));}},delay);
this.modalReleases.push(()=>clearInterval(timer));
if(this.vipData)this.renderVipRooms(this.vipData);
refresh();
if(!advanced){const wx=this.config.remoteConfig.getValue('vipWx');label(group,'Spirit3171').string=wx==null?'':String(wx);}
}
private vipHistory():VipRoomHistory {return new VipRoomHistory(window.localStorage,this.config.identity.gameid,this.config.identity.agentid);}
private renderVipRooms(rooms:readonly VipRoom[]):void {need(need(this.modal,'VIP modal').getComponent(VipRoomListView),'VipRoomListView').render(rooms,this.vipHistory().read());}
private requestTask(kind:'list'|'award',send:()=>void):void {
if(this.taskPending||!this.model.canAct)return;
this.taskPending=kind;
this.setTaskButtons(false);
this.taskTimer=setTimeout(()=>{this.cancelTaskRequest();this.tip('任务请求超时,请重试');},10000);
try{send();}catch(error){this.cancelTaskRequest();throw error;}
}
/** Server show_message and connection transitions release the local request guard. */
setRoomBusy(busy:boolean):void{this.roomPage?.setBusy(busy);}
cancelRoomPage():void{if(this.roomPage)this.close();}
openShare():void {
const modal=this.open(this.assets.share,'share','group-11'),policy=resolveSharePolicy(this.config);
this.bind(requireNode(modal,'group-11/Spirit72'),()=>this.close(),true);
for(const [id,friend] of [[74,1],[75,2]] as const)this.bind(requireNode(modal,`group-11/Spirit${id}`),()=>{
const data=sharePayload(policy,friend);if(data)this.nativeCall('friendsSharetypeUrlToptitleDescript',data);
this.close();
},true);
if(policy.background){
const node=requireNode(modal,'group-11/Spirit345'),sprite=need(node.getComponent(Sprite),'share background');
assetManager.loadRemote<ImageAsset>(policy.background,{ext:'.png'},(error,image)=>{
if(this.disposed||this.modal!==modal||!isValid(node))return;
if(error){this.tip(`分享背景加载失败:${error.message}`);return;}
const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame;
this.modalReleases.push(()=>{sprite.spriteFrame=null;frame.destroy();texture.destroy();});
});
}
}
private openVipRooms():void {
const modal=this.open(this.assets.header,'vip-rooms','group-103'),group=requireNode(modal,'group-103');
this.menu.active=false;this.noticeMenu.active=false;
this.bind(requireNode(group,'Spirit3182'),()=>this.close(),true);
for(const id of [3190,3191])requireNode(group,`Spirit${id}`).active=false;
const advanced=need(this.model.advanced,'players.self.advanced');
for(const id of advanced?[3170,3171]:[3183,3184,3194,3195,3196,3197,3198,3214,3215,3235])requireNode(group,`Spirit${id}`).active=false;
const view=need(modal.getComponent(VipRoomListView),'VipRoomListView'),port=need(this.vip,'VIP command port');
view.configure(port.describe);
this.bind(requireNode(group,'Spirit3175'),()=>view.showRooms(),true);
this.bind(requireNode(group,'Spirit3184'),()=>this.openCreate(),true);
this.bind(requireNode(group,'Spirit3240'),()=>this.openJoin(),true);
this.bind(requireNode(group,'Spirit3231'),()=>this.requestVipRank(),true);
this.bind(requireNode(group,'Spirit3199'),()=>this.tip(`房间 ${view.selectedCode}:短房号排行模块尚未接入`),true);
for(const id of [3194,3195,3214])this.bind(requireNode(group,`Spirit${id}`),()=>this.tip('黑名单管理模块尚未接入'),true);
for(const id of [3196,3197,3215])this.bind(requireNode(group,`Spirit${id}`),()=>this.tip('统一魅力值设置模块尚未接入'),true);
this.bind(requireNode(group,'Spirit3235'),()=>this.tip('好友管理模块尚未接入'),true);
if(advanced){
label(group,'Spirit3198').string='(黑名单内玩家临时禁入房间,请尽快到后台处理好友关系)';
requireNode(group,'Spirit3235').active=!!this.config.remoteConfig.getValue('managerUrl');
}
const join=(code:VipCode)=>{if(this.model.canAct&&this.modal===modal){try{port.join(code);}catch(e){this.tip(e instanceof Error?e.message:String(e));}}};
modal.on('vip-join',join);this.modalReleases.push(()=>modal.off('vip-join',join));
const refresh=()=>{if(this.model.canAct&&this.modal===modal)port.request(this.vipHistory().read());};
const raw=this.config.remoteConfig.getValue('vipFlushTime');
// GameData.sysConfig defines the optional refresh setting as 10000 ms.
const delay=raw==null?10000:Number(raw);if(!Number.isFinite(delay)||delay<=0)throw Error('Invalid vipFlushTime');
const timer=setInterval(()=>{try{if(this.vipHistory().read().length>0)refresh();}catch(e){this.tip(e instanceof Error?e.message:String(e));}},delay);
this.modalReleases.push(()=>clearInterval(timer));
if(this.vipData)this.renderVipRooms(this.vipData);
refresh();
if(!advanced){const wx=this.config.remoteConfig.getValue('vipWx');label(group,'Spirit3171').string=wx==null?'':String(wx);}
}
private vipHistory():VipRoomHistory {return new VipRoomHistory(window.localStorage,this.config.identity.gameid,this.config.identity.agentid);}
private renderVipRooms(rooms:readonly VipRoom[]):void {need(need(this.modal,'VIP modal').getComponent(VipRoomListView),'VipRoomListView').render(rooms,this.vipHistory().read());}
private requestTask(kind:'list'|'award',send:()=>void):void {
if(this.taskPending||!this.model.canAct)return;
this.taskPending=kind;
this.setTaskButtons(false);
this.taskTimer=setTimeout(()=>{this.cancelTaskRequest();this.tip('任务请求超时,请重试');},10000);
try{this.requestLoading.begin(kind==='list'?'get_player_task':'get_task_award');send();}catch(error){this.cancelTaskRequest();throw error;}
}
/** Server show_message and connection transitions release the local request guard. */
cancelTaskRequest():void {
if(this.taskTimer!==null)clearTimeout(this.taskTimer);
this.taskTimer=null;this.taskPending=null;this.setTaskButtons(true);
this.requestLoading.cancel('get_player_task');this.requestLoading.cancel('get_task_award');
if(this.taskTimer!==null)clearTimeout(this.taskTimer);
this.taskTimer=null;this.taskPending=null;this.setTaskButtons(true);
}
private setTaskButtons(enabled:boolean):void {
if(this.modalKind==='tasks'&&this.modal)for(const button of this.modal.getComponentsInChildren(Button)){
if(button.node.name==='Spirit124'||button.node.name==='Spirit125')button.interactable=enabled;
}
}
private openTasks(tasks:readonly TaskData[]):void {
const modal=this.modalKind==='tasks'?need(this.modal,'tasks'):this.open(this.assets.tasks,'tasks','group-10');
if(this.modalReleases.length===0){
for(const id of [112,115])this.bind(requireNode(modal,`group-10/Spirit${id}`),()=>this.close(),true);
const claim=(taskid:number|string)=>{if(this.model.canAct&&!this.disposed&&this.modal===modal){
try{this.requestTask('award',()=>this.claimTask(taskid));}catch(error){this.tip(error instanceof Error?error.message:String(error));}
}};
const perform=()=>{if(this.model.canAct&&!this.taskPending&&this.modal===modal){this.close();this.openShare();}};
modal.on('task-claim',claim);modal.on('task-do',perform);
this.modalReleases.push(()=>{modal.off('task-claim',claim);modal.off('task-do',perform);});
}
need(modal.getComponent(TaskListView),'TaskListView').render(tasks,this.policy.roomCardName);
}
private requestInvite(kind:'query'|'binding',send:()=>void):void {
if(this.invitePending||!this.model.canAct||this.disposed)return;
this.invitePending=kind;
this.inviteTimer=setTimeout(()=>{this.cancelInviteRequest();this.tip('邀请码请求超时,请重试');},15000);
try{send();}catch(error){this.cancelInviteRequest();throw error;}
}
private setTaskButtons(enabled:boolean):void {
if(this.modalKind==='tasks'&&this.modal)for(const button of this.modal.getComponentsInChildren(Button)){
if(button.node.name==='Spirit124'||button.node.name==='Spirit125')button.interactable=enabled;
}
cancelInviteRequest():void {
if(this.inviteTimer!==null)clearTimeout(this.inviteTimer);
this.inviteTimer=null;this.invitePending=null;
}
private openTasks(tasks:readonly TaskData[]):void {
const modal=this.modalKind==='tasks'?need(this.modal,'tasks'):this.open(this.assets.tasks,'tasks','group-10');
if(this.modalReleases.length===0){
for(const id of [112,115])this.bind(requireNode(modal,`group-10/Spirit${id}`),()=>this.close(),true);
const claim=(taskid:number|string)=>{if(this.model.canAct&&!this.disposed&&this.modal===modal){
try{this.requestTask('award',()=>this.claimTask(taskid));}catch(error){this.tip(error instanceof Error?error.message:String(error));}
}};
const perform=()=>{if(this.model.canAct&&!this.taskPending&&this.modal===modal){this.close();this.openShare();}};
modal.on('task-claim',claim);modal.on('task-do',perform);
this.modalReleases.push(()=>{modal.off('task-claim',claim);modal.off('task-do',perform);});
}
need(modal.getComponent(TaskListView),'TaskListView').render(tasks,this.policy.roomCardName);
private openInvite(code:string|null):void {
const modal=this.open(this.assets.invite,'invite','group-80'),group=requireNode(modal,'group-80');
requireNode(this.noticeMenu,'group-95').active=false;
this.bind(requireNode(group,'Spirit362'),()=>this.close(),true);
const input=requireNode(group,'Spirit365/InputEditBox'),edit=need(input.getComponent(EditBox),'invite EditBox');
if(!edit.textLabel||!edit.placeholderLabel)throw Error('Invite EditBox requires serialized labels');
const bound=!!code;
for(const id of [365,366,367])requireNode(group,`Spirit${id}`).active=!bound;
const message=label(group,'Spirit368');message.node.active=bound;
message.string=bound?`您已绑定邀请码,所绑定邀请码为:${code}`:'';
edit.string='';
if(bound)return;
const changed=()=>{edit.string=edit.string.replace(/[^0-9]/g,'');};
input.on(EditBox.EventType.TEXT_CHANGED,changed);this.modalReleases.push(()=>input.off(EditBox.EventType.TEXT_CHANGED,changed));
let pasting=false;
this.bind(requireNode(group,'Spirit366'),()=>{
if(pasting)return;pasting=true;
void need(this.invite,'invite commands').paste().then(text=>{
if(!this.disposed&&this.model.canAct&&this.modal===modal)edit.string=text;
}).catch(error=>{if(!this.disposed&&this.modal===modal)this.tip(error instanceof Error?error.message:String(error));}).finally(()=>{pasting=false;});
},true);
this.bind(requireNode(group,'Spirit367'),()=>{
const text=edit.string;edit.blur();this.close();
this.requestInvite('binding',()=>need(this.invite,'invite commands').bind(text));
},true);
}
close():void {
this.cancelTaskRequest();
if(this.modalKind==='vip-rooms'){this.menu.active=true;this.noticeMenu.active=true;}
for(const release of this.modalReleases.splice(0))release();
this.modal?.destroy();this.modal=null;this.modalKind='';
}
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();
for(const root of [this.menu,this.header,this.noticeMenu])if(isValid(root))root.destroy();
}
}
if(this.roomPage){this.roomPage.close();this.roomPage=null;this.extras?.room?.close();}
this.cancelRankRequest();
this.cancelInviteRequest();
this.cancelTaskRequest();
if(this.modalKind==='vip-rooms'){this.menu.active=true;this.noticeMenu.active=true;}
for(const release of this.modalReleases.splice(0))release();
this.modal?.destroy();this.modal=null;this.modalKind='';
}
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();
for(const root of [this.menu,this.header,this.noticeMenu])if(isValid(root))root.destroy();
}
}