feat: integrate room framework and isolated subgame bundles
Complete room UI and protocol integration, move game definitions and resources behind bundle entries, publish authoritative version XML, and document single-game builds. Include all current resource changes and experiment artifacts.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import {GameComposition} from './platform-login/GameComposition.ts';
|
||||
import { _decorator, Component, director } from 'cc';
|
||||
import { PlatformSession } from '../framework/platform/session.ts';
|
||||
import { NetClient, type NetClientEvents } from '../framework/net/net-client.ts';
|
||||
@@ -60,7 +61,10 @@ export class LoginFlow extends Component {
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const composition=this.getComponent(GameComposition);
|
||||
if(!composition)throw Error('LoginFlow requires GameComposition');
|
||||
const result = await resolveBootstrap({
|
||||
buildIdentity:await composition.loadBuildIdentity(),
|
||||
win: window as any,
|
||||
search: window.location.search,
|
||||
fetcher: new HttpConfigFetcher(),
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import {BlockInputEvents,Button,Label,Layout,Node,Prefab,ScrollView,Widget,instantiate,isValid} from 'cc';
|
||||
import type {PlatformState} from '../../framework/platform/stores/platform-types.ts';
|
||||
import {selectApplyVote,type ApplyVoteModel} from './apply-vote-model.ts';
|
||||
import {requireNode} from './node-bindings.ts';
|
||||
|
||||
function need<T>(value:T|null,name:string):T {if(value===null)throw Error(`ApplyVotePanel missing ${name}`);return value;}
|
||||
type Commands={agree():void;refuse():void;acknowledge():void;error(message:string):void};
|
||||
|
||||
/** Scene-owned Layer606 view. All player identity and voting state come from PlatformStore. */
|
||||
export class ApplyVotePanel {
|
||||
private root:Node|null=null;
|
||||
private state:PlatformState|null=null;
|
||||
private timer:ReturnType<typeof setInterval>|null=null;
|
||||
private pending=false;
|
||||
private disposed=false;
|
||||
private rows=new Map<number,Node>();
|
||||
private buttons:Button[]=[];
|
||||
private releases:Array<()=>void>=[];
|
||||
constructor(private readonly canvas:Node,private readonly prefab:Prefab,private readonly itemPrefab:Prefab,
|
||||
private readonly commands:Commands){}
|
||||
|
||||
render(state:PlatformState):void {
|
||||
if(this.disposed)return;
|
||||
const previous=this.state?.dissolution;
|
||||
this.state=state;
|
||||
const model=selectApplyVote(state,Date.now());
|
||||
if(!model){this.close();return;}
|
||||
const vote=state.dissolution!;
|
||||
if(vote.kind==='result'||!previous||previous.kind==='result'||previous.roomcode!==vote.roomcode
|
||||
||previous.kind==='active'&&vote.kind==='active'&&previous.deadline!==vote.deadline
|
||||
||state.app.phase!=='logged-in')this.pending=false;
|
||||
if(!this.root)this.open();
|
||||
this.text('group-29/Spirit191').string=model.message;
|
||||
this.renderRows(model);
|
||||
requireNode(this.root!,'group-32').active=model.result;
|
||||
for(const button of this.buttons){button.node.active=!model.result&&model.canVote;button.interactable=model.canVote&&!this.pending;}
|
||||
const countdown=this.text('group-29/Spirit222');countdown.node.active=model.seconds!==null;
|
||||
if(model.seconds!==null)countdown.string=String(model.seconds);
|
||||
if(model.result||model.seconds===0)this.stopTimer();
|
||||
else if(this.timer===null)this.timer=setInterval(()=>this.tick(),200);
|
||||
}
|
||||
private open():void {
|
||||
const root=instantiate(this.prefab);this.root=root;root.parent=this.canvas;root.setPosition(0,0,0);
|
||||
const widget=root.getComponent(Widget);if(widget){widget.target=this.canvas;widget.updateAlignment();}
|
||||
// Keep platform connection/error overlays above this modal.
|
||||
const overlay=this.canvas.children.find(n=>['Layer614_Loading','Layer615_Reconnect','Layer612_Tips','Layer616_Kick'].includes(n.name));
|
||||
if(overlay)root.setSiblingIndex(overlay.getSiblingIndex());
|
||||
root.addComponent(BlockInputEvents);
|
||||
requireNode(root,'group-29').active=true;
|
||||
const content=need(need(requireNode(root,'group-29/PlayerVotes').getComponent(ScrollView),'ScrollView').content,'content');
|
||||
for(const child of [...content.children]){child.removeFromParent();child.destroy();}
|
||||
for(const [name,action] of [['Spirit201','agree'],['Spirit202','refuse']] as const){
|
||||
const button=this.bind(requireNode(root,'group-29/'+name),()=>{
|
||||
if(this.pending||!this.state||!selectApplyVote(this.state,Date.now())?.canVote)return;
|
||||
this.pending=true;for(const b of this.buttons)b.interactable=false;
|
||||
try{this.commands[action]();}catch(error){this.pending=false;this.render(this.state);throw error;}
|
||||
});this.buttons.push(button);
|
||||
}
|
||||
this.bind(requireNode(root,'group-32/Spirit223'),()=>this.commands.acknowledge());
|
||||
}
|
||||
private bind(node:Node,action:()=>void):Button {
|
||||
const button=node.getComponent(Button)||node.addComponent(Button);button.transition=Button.Transition.NONE;
|
||||
const listener=()=>{if(this.disposed||!node.activeInHierarchy||!button.interactable)return;
|
||||
try{action();}catch(error){console.error('[ApplyVotePanel]',error);}};
|
||||
node.on(Button.EventType.CLICK,listener);this.releases.push(()=>{if(isValid(node))node.off(Button.EventType.CLICK,listener);});return button;
|
||||
}
|
||||
private renderRows(model:ApplyVoteModel):void {
|
||||
const scroll=need(requireNode(this.root!,'group-29/PlayerVotes').getComponent(ScrollView),'ScrollView');
|
||||
const content=need(scroll.content,'content'),layout=need(content.getComponent(Layout),'Layout');
|
||||
const seats=new Set(model.rows.map(row=>row.seat));let changed=false;
|
||||
for(const [seat,node] of this.rows)if(!seats.has(seat)){node.removeFromParent();node.destroy();this.rows.delete(seat);changed=true;}
|
||||
model.rows.forEach((row,index)=>{
|
||||
let node=this.rows.get(row.seat);
|
||||
if(!node){node=instantiate(this.itemPrefab);node.name=`PlayerVoteItem_${row.seat}`;node.parent=content;node.setPosition(0,0,0);this.rows.set(row.seat,node);changed=true;}
|
||||
node.setSiblingIndex(index);
|
||||
need(requireNode(node,'Spirit166').getComponent(Label),'vote status').string=row.status;
|
||||
need(requireNode(node,'Spirit264').getComponent(Label),'nickname').string=row.nickname;
|
||||
});
|
||||
if(changed){layout.updateLayout();scroll.scrollToTop(0);}
|
||||
}
|
||||
private text(path:string):Label{return need(requireNode(this.root!,path).getComponent(Label),path);}
|
||||
private tick():void {
|
||||
if(!this.root||!this.state)return;
|
||||
const vote=this.state.dissolution;if(!vote||vote.kind!=='active'){this.stopTimer();return;}
|
||||
const seconds=Math.max(0,Math.ceil((vote.deadline-Date.now())/1000));
|
||||
this.text('group-29/Spirit222').string=String(seconds);if(seconds===0)this.stopTimer();
|
||||
}
|
||||
private stopTimer():void {if(this.timer!==null)clearInterval(this.timer);this.timer=null;}
|
||||
private close():void {
|
||||
this.stopTimer();for(const release of this.releases.splice(0))release();this.rows.clear();this.buttons=[];this.pending=false;
|
||||
this.root?.destroy();this.root=null;
|
||||
}
|
||||
dispose():void {if(this.disposed)return;this.disposed=true;this.close();this.state=null;}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ec19e470-cf36-40e8-b420-e4314e5fbf07",
|
||||
"uuid": "f6157c1a-6f9f-4c80-b3f9-3a490c7b9e22",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {_decorator,Button,Component,Label} from 'cc';
|
||||
import type {PlatformGameSnapshot} from '../../framework/sdk/contracts/index.ts';
|
||||
import type {RoomPresentationActions,RoomPresentation} from '../../framework/sdk/contracts/game-definition.ts';
|
||||
import {roomExitAction} from '../../framework/presentation/room-exit-action.ts';
|
||||
import type {GameRoomProjection} from '../../framework/sdk/contracts/game-room-projection.ts';
|
||||
import {composeGameRoomPresentation,gameAllowsRoomAction} from '../../framework/presentation/game-room-presentation.ts';
|
||||
const {ccclass,property}=_decorator;
|
||||
/** Template lobby-to-room handoff surface; concrete subgames replace this presentation. */
|
||||
@ccclass('CommonRoomViewBase')
|
||||
export class CommonRoomViewBase extends Component implements RoomPresentation {
|
||||
@property(Label) roomLabel:Label|null=null;
|
||||
@property(Label) playersLabel:Label|null=null;
|
||||
@property(Button) prepareButton:Button|null=null;
|
||||
@property(Button) exitButton:Button|null=null;
|
||||
private actions:RoomPresentationActions|null=null;
|
||||
private bound=false;
|
||||
private pending:'prepare'|'exit'|null=null;
|
||||
private timer:ReturnType<typeof setTimeout>|null=null;
|
||||
private snapshot:PlatformGameSnapshot|null=null;
|
||||
private projection:GameRoomProjection|null=null;
|
||||
private readonly prepare=()=>this.invoke('prepare');
|
||||
private readonly exit=()=>this.invoke('exit');
|
||||
render(snapshot:PlatformGameSnapshot,actions:RoomPresentationActions,projection:GameRoomProjection|null=null):void{
|
||||
const combined=composeGameRoomPresentation(snapshot,projection);
|
||||
if(!this.roomLabel||!this.playersLabel||!this.prepareButton||!this.exitButton)throw Error('CommonRoomViewBase missing controls');
|
||||
this.actions=actions;this.snapshot=snapshot;this.projection=projection;
|
||||
if(!this.bound){this.prepareButton.node.on(Button.EventType.CLICK,this.prepare);this.exitButton.node.on(Button.EventType.CLICK,this.exit);this.bound=true;}
|
||||
this.roomLabel.string=`房间号:${snapshot.room.roomcode}`;
|
||||
this.playersLabel.string=snapshot.seats.map(p=>`${p.seat+1} ${p.nickname}${p.playerId===snapshot.self.playerId?'(我)':''} ${!p.online?'离线':p.ready?'已准备':'未准备'}`).join('\n');
|
||||
const self=snapshot.seats.find(p=>p.playerId===snapshot.self.playerId);
|
||||
if(self?.ready||snapshot.connection.phase!=='logged-in')this.clearPending();
|
||||
this.prepareButton.interactable=gameAllowsRoomAction(this.pending===null&&snapshot.connection.phase==='logged-in'&&snapshot.room.stage===0&&!self?.ready,combined.game,'prepare');
|
||||
this.exitButton.interactable=gameAllowsRoomAction(this.pending===null&&snapshot.connection.phase==='logged-in',combined.game,'exit');
|
||||
const exitLabel=this.exitButton.getComponent(Label);
|
||||
if(!exitLabel)throw Error('CommonRoomViewBase exit button missing Label');
|
||||
exitLabel.string=roomExitAction(snapshot.room).label;
|
||||
}
|
||||
private invoke(kind:'prepare'|'exit'):void{
|
||||
if(!this.actions||this.pending)return;
|
||||
try{this.actions[kind]();this.pending=kind;this.prepareButton!.interactable=false;this.exitButton!.interactable=false;
|
||||
this.timer=setTimeout(()=>{this.clearPending();if(this.snapshot&&this.actions)this.render(this.snapshot,this.actions,this.projection);this.node.emit('room-action-timeout');},15000);
|
||||
}catch(error){this.node.emit('room-action-error',error);}
|
||||
}
|
||||
dissolutionReceived():void{
|
||||
if(this.pending!=='exit'||!this.snapshot||roomExitAction(this.snapshot.room).command!=='room.apply-dissolution')return;
|
||||
this.clearPending();if(this.actions)this.render(this.snapshot,this.actions,this.projection);
|
||||
}
|
||||
private clearPending():void{if(this.timer!==null)clearTimeout(this.timer);this.timer=null;this.pending=null;}
|
||||
close():void{this.clearPending();if(this.bound){this.prepareButton?.node.off(Button.EventType.CLICK,this.prepare);this.exitButton?.node.off(Button.EventType.CLICK,this.exit);}this.bound=false;this.actions=null;}
|
||||
onDestroy():void{this.close();}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c183b3f7-ecdb-47af-baa9-62004a7920a5",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type {ChannelIdentity} from '../../framework/config/identity.ts';
|
||||
import {_decorator,Component,Prefab,SceneAsset,Node,Label,Button} from 'cc';
|
||||
import type {GameDefinition,RoomPresentation} from '../../framework/sdk/contracts/game-definition.ts';
|
||||
const {ccclass}=_decorator;
|
||||
export interface CommonRoomView extends RoomPresentation {
|
||||
readonly roomLabel:Label|null;
|
||||
readonly exitButton:Button|null;
|
||||
dissolutionReceived():void;
|
||||
}
|
||||
/** Public startup depends on this injected component contract, never a game registry. */
|
||||
@ccclass('GameComposition')
|
||||
export abstract class GameComposition extends Component {
|
||||
abstract readonly gameKey:string;
|
||||
abstract readonly createRoom:Prefab|null;
|
||||
abstract readonly room:Prefab|null;
|
||||
abstract readonly roomScene:SceneAsset|null;
|
||||
abstract get displayVersion():string;
|
||||
abstract loadBuildIdentity():Promise<Readonly<ChannelIdentity>>;
|
||||
abstract get definition():GameDefinition;
|
||||
abstract resolveRoomView(root:Node):CommonRoomView;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "cf22a2ad-ab7a-46b2-bb0a-ffccf298983d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -20,12 +20,12 @@ export class JoinRoomView extends Component {
|
||||
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();}};
|
||||
const bind=(button:Button,action:()=>void)=>{const fn=()=>{if(this.busy||this.pasting)return;try{action();}catch(error){console.error('[JoinRoomView]',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));})
|
||||
.catch(error=>{if(this.generation===generation)console.error('[JoinRoomView]',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();
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import {BlockInputEvents,Label,Node,Prefab,UITransform} from 'cc';
|
||||
import type {NumericToolsPort} from '../../framework/sdk/contracts/game-capabilities.ts';
|
||||
import {NumericInputModel} from '../../framework/presentation/numeric-input-model.ts';
|
||||
import {MultiplierModel} from '../../framework/presentation/multiplier-model.ts';
|
||||
import {requireNode} from './node-bindings.ts';
|
||||
import {RoomUi,roomNeed} from './RoomUi.ts';
|
||||
|
||||
export interface NumericToolsPanelOptions {
|
||||
readonly canvas:Node;
|
||||
readonly inputPrefab:Prefab;
|
||||
readonly calPrefab:Prefab;
|
||||
readonly storage:Pick<Storage,'getItem'|'setItem'>;
|
||||
readonly gameid:string|number;
|
||||
readonly agentid:string|number;
|
||||
readonly readClipboard:()=>Promise<string>;
|
||||
readonly error:(message:string)=>void;
|
||||
}
|
||||
|
||||
/** Room-scoped presentation; subgames supply both input callbacks and calculator originals. */
|
||||
export class NumericToolsPanel implements NumericToolsPort {
|
||||
private readonly ui:RoomUi;
|
||||
private readonly inputRoot:Node;
|
||||
private readonly calRoot:Node;
|
||||
private readonly calEntry:Node;
|
||||
private readonly calDialog:Node;
|
||||
private input:NumericInputModel|null=null;
|
||||
private calculator:MultiplierModel|null=null;
|
||||
private inputGeneration=0;
|
||||
private pasting=false;
|
||||
private cancelInput:(()=>void)|null=null;
|
||||
private cancelCalculator:(()=>void)|null=null;
|
||||
private readonly inputDisplayWidth:number;
|
||||
|
||||
constructor(private readonly options:NumericToolsPanelOptions){
|
||||
this.ui=new RoomUi(options.error);
|
||||
this.inputRoot=this.ui.mount(options.inputPrefab,options.canvas);
|
||||
this.calRoot=this.ui.mount(options.calPrefab,options.canvas);
|
||||
this.calEntry=requireNode(this.calRoot,'group-78');
|
||||
this.calDialog=requireNode(this.calRoot,'group-79');
|
||||
// Label dimensions can be changed by its font assembler; the backdrop owns the input bounds.
|
||||
this.inputDisplayWidth=roomNeed(this.inputNode(3002).getComponent(UITransform),'input backdrop bounds').width-36;
|
||||
this.inputRoot.addComponent(BlockInputEvents);
|
||||
this.calDialog.addComponent(BlockInputEvents);
|
||||
for(let digit=0;digit<10;digit++){
|
||||
this.ui.bind(this.inputNode(661+digit),()=>{const model=this.needInput();this.tip(model.inputDigit(digit));this.renderInput();});
|
||||
this.ui.bind(this.calNode(392+digit),()=>{const model=this.needCalculator();this.tip(model.inputDigit(digit));this.renderCalculator();});
|
||||
}
|
||||
this.ui.bind(this.inputNode(671),()=>{this.needInput().backspace();this.renderInput();});
|
||||
this.ui.bind(this.inputNode(3000),()=>this.needInput().confirm());
|
||||
this.ui.bind(this.inputNode(3004),()=>this.closeNumberInput());
|
||||
this.ui.bind(this.inputNode(3217),()=>this.paste());
|
||||
this.ui.bind(requireNode(this.calEntry,'Spirit283'),()=>{
|
||||
this.needCalculator();this.calDialog.active=true;this.raise(this.calRoot);this.renderCalculator();
|
||||
});
|
||||
this.ui.bind(this.calNode(402),()=>{this.tip(this.needCalculator().inputDecimal());this.renderCalculator();});
|
||||
this.ui.bind(this.calNode(391),()=>{this.needCalculator().backspace();this.renderCalculator();});
|
||||
this.ui.bind(this.calNode(403),()=>{
|
||||
const model=this.needCalculator();model.confirm();
|
||||
// A result callback may synchronously replace or dismiss this presentation.
|
||||
if(this.calculator===model){this.calDialog.active=false;this.renderCalculator();}
|
||||
});
|
||||
this.ui.bind(this.calNode(375),()=>{if(this.cancelCalculator)this.hideMultiplier();else this.calDialog.active=false;});
|
||||
this.close();
|
||||
}
|
||||
|
||||
openNumberInput(request:Parameters<NumericToolsPort['openNumberInput']>[0]):void {
|
||||
this.requireActive();this.closeNumberInput();this.input=new NumericInputModel(request);this.cancelInput=request.onCancel??null;
|
||||
this.ui.text(this.inputRoot,'group-87/Spirit3001').string=this.input.message;
|
||||
this.renderInput();this.inputRoot.active=true;this.raise(this.inputRoot);
|
||||
}
|
||||
closeNumberInput():void {
|
||||
const cancel=this.cancelInput;this.cancelInput=null;
|
||||
this.inputGeneration++;this.pasting=false;this.input=null;this.inputRoot.active=false;cancel?.();
|
||||
}
|
||||
showMultiplier(request:Parameters<NumericToolsPort['showMultiplier']>[0]):void {
|
||||
this.requireActive();this.hideMultiplier();this.cancelCalculator=request.onCancel??null;
|
||||
this.calculator=new MultiplierModel({originalValues:request.values,onConfirm:request.onConfirm,
|
||||
storage:this.options.storage,gameid:this.options.gameid,agentid:this.options.agentid});
|
||||
this.calRoot.active=true;this.calEntry.active=true;this.calDialog.active=false;this.renderCalculator();
|
||||
}
|
||||
hideMultiplier():void {
|
||||
const cancel=this.cancelCalculator;this.cancelCalculator=null;
|
||||
this.calculator=null;this.calDialog.active=false;this.calEntry.active=false;this.calRoot.active=false;cancel?.();
|
||||
}
|
||||
close():void {this.closeNumberInput();this.hideMultiplier();}
|
||||
dispose():void {this.close();this.ui.dispose();}
|
||||
|
||||
private inputNode(id:number):Node{return requireNode(this.inputRoot,`group-87/Spirit${id}`);}
|
||||
private calNode(id:number):Node{return requireNode(this.calDialog,`Spirit${id}`);}
|
||||
private needInput():NumericInputModel{return roomNeed(this.input,'numeric input context');}
|
||||
private needCalculator():MultiplierModel{return roomNeed(this.calculator,'calculator context');}
|
||||
private requireActive():void{if(this.ui.disposed)throw Error('Numeric tools panel is disposed');}
|
||||
private tip(message:string|null):void {if(message!==null)this.options.error(message);}
|
||||
private renderInput():void {
|
||||
const model=this.needInput();const label=this.ui.text(this.inputRoot,'group-87/Spirit3003');
|
||||
this.numberLabel(label,model.text,51,90);
|
||||
const transform=roomNeed(label.node.getComponent(UITransform),'input display transform');
|
||||
transform.width=Math.min(transform.width,this.inputDisplayWidth);
|
||||
}
|
||||
private renderCalculator():void {
|
||||
const model=this.needCalculator();
|
||||
this.numberLabel(this.ui.text(this.calRoot,'group-78/Spirit370'),model.text,28,72);
|
||||
this.numberLabel(this.ui.text(this.calRoot,'group-79/Spirit373'),model.text,36,90);
|
||||
const entry=requireNode(this.calEntry,'Spirit283');
|
||||
roomNeed(entry.getComponent(UITransform),'calculator entry transform').width=203+model.text.length*28;
|
||||
}
|
||||
private numberLabel(label:Label,text:string,charWidth:number,height:number):void {
|
||||
label.string=text.replace('b','.');label.enableWrapText=false;label.overflow=Label.Overflow.SHRINK;
|
||||
roomNeed(label.node.getComponent(UITransform),'numeric label transform').setContentSize(Math.max(1,text.length*charWidth),height);
|
||||
}
|
||||
private paste():void {
|
||||
if(this.pasting)return;
|
||||
const input=this.needInput(),generation=this.inputGeneration;this.pasting=true;
|
||||
void Promise.resolve().then(()=>this.options.readClipboard()).then(text=>{
|
||||
if(this.ui.disposed||generation!==this.inputGeneration||this.input!==input)return;
|
||||
this.tip(input.paste(text));this.renderInput();
|
||||
}).catch(error=>{
|
||||
if(!this.ui.disposed&&generation===this.inputGeneration)console.error('[NumericToolsPanel]',error);
|
||||
}).finally(()=>{if(generation===this.inputGeneration)this.pasting=false;});
|
||||
}
|
||||
private raise(root:Node):void {
|
||||
const parent=this.options.canvas;
|
||||
root.setSiblingIndex(parent.children.length-1);
|
||||
const overlay=parent.children.find(node=>['Layer614_Loading','Layer615_Reconnect','Layer612_Tips','Layer616_Kick'].includes(node.name));
|
||||
root.setSiblingIndex(overlay?overlay.getSiblingIndex():parent.children.length-1);
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "1c145198-d199-4ac7-bb93-6be164d2fe00",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
+202
-51
@@ -1,6 +1,4 @@
|
||||
import {createErqiwangGame} from '../../games/erqiwang/erqiwang-game.ts';
|
||||
import {ErqiwangRoomView} from '../../games/erqiwang/ErqiwangRoomView.ts';
|
||||
import { _decorator, assetManager, Component, Director, Label, Node, Prefab, ProgressBar, SceneAsset,
|
||||
import { _decorator, assetManager, BitmapFont, SpriteFrame, sys, 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';
|
||||
@@ -16,9 +14,9 @@ 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 type {GameBinding} from '../../framework/sdk/contracts/game-definition.ts';
|
||||
import {GameComposition,type CommonRoomView} from './GameComposition.ts';
|
||||
import type {RoomScope} from '../../framework/platform/room/room-scope.ts';
|
||||
import { CocosLogoCountdownView, createLogoCountdown } from './logo-countdown.ts';
|
||||
import { requireNode } from './node-bindings.ts';
|
||||
import { SceneFlow } from './scene-flow.ts';
|
||||
@@ -26,7 +24,22 @@ 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 type { NativeCallName } from '../../framework/adapters/native/native-contracts.ts';
|
||||
import {startPlatformLocation,type PlatformNativeHost} from '../../framework/adapters/native/platform-native.ts';
|
||||
import {PlatformSettings} from '../../framework/presentation/platform-settings.ts';
|
||||
import {ApplyVotePanel} from './ApplyVotePanel.ts';
|
||||
import {RoomPlatformPanels} from './RoomPlatformPanels.ts';
|
||||
import {RoomChatPanel} from './RoomChatPanel.ts';
|
||||
import {NumericToolsPanel} from './NumericToolsPanel.ts';
|
||||
import {DeferredNumericToolsPort} from '../../framework/presentation/deferred-numeric-tools.ts';
|
||||
import {RoomHud} from './RoomHud.ts';
|
||||
import {RoomNoticePanel} from './RoomNoticePanel.ts';
|
||||
import {RoomDeviceStatusAdapter,createRoomDeviceStatusSubscriptions} from '../../framework/adapters/native/room-device-status.ts';
|
||||
import {RoomVoiceAdapter,createRoomVoiceSubscriptions} from '../../framework/adapters/native/room-voice.ts';
|
||||
import {resolveRoomChatPolicy} from '../../framework/presentation/room-chat-config.ts';
|
||||
import {resolveRoomMenuContext} from '../../framework/presentation/room-menu-context.ts';
|
||||
import {shouldShowOwnerNotice} from '../../framework/presentation/room-owner-notice.ts';
|
||||
import type {PlatformState} from '../../framework/platform/stores/platform-types.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';
|
||||
@@ -50,12 +63,45 @@ export class PlatformStartup extends Component {
|
||||
@property(Prefab) tipsPrefab:Prefab|null=null;
|
||||
@property(Prefab) waitPrefab:Prefab|null=null;
|
||||
@property(Prefab) reconnectPrefab:Prefab|null=null;
|
||||
@property(Prefab) kickPrefab:Prefab|null=null;
|
||||
@property(Prefab) kickPrefab:Prefab|null=null;
|
||||
@property(Prefab) applyPrefab:Prefab|null=null;
|
||||
@property(Prefab) playerVoteItemPrefab:Prefab|null=null;
|
||||
@property(Prefab) roomMainPrefab:Prefab|null=null;
|
||||
@property(Prefab) roomMenuPrefab:Prefab|null=null;
|
||||
@property(Prefab) roomCheckPrefab:Prefab|null=null;
|
||||
@property(Prefab) roomChatPrefab:Prefab|null=null;
|
||||
@property(Prefab) chatPhrasePrefab:Prefab|null=null;
|
||||
@property(Prefab) chatHistoryPrefab:Prefab|null=null;
|
||||
@property(Prefab) roomHelpPrefab:Prefab|null=null;
|
||||
@property(Prefab) roomScenePrefab:Prefab|null=null;
|
||||
@property(Prefab) roomNoticePrefab:Prefab|null=null;
|
||||
@property(Prefab) numberInputPrefab:Prefab|null=null;
|
||||
@property(Prefab) multiplierPrefab:Prefab|null=null;
|
||||
@property([SpriteFrame]) roomNetworkFrames:SpriteFrame[]=[];
|
||||
@property([SpriteFrame]) roomBatteryFrames:SpriteFrame[]=[];
|
||||
@property(BitmapFont) roomClockFont:BitmapFont|null=null;
|
||||
@property(BitmapFont) roomAppleClockFont:BitmapFont|null=null;
|
||||
@property(SpriteFrame) roomClockSeparator:SpriteFrame|null=null;
|
||||
@property(SpriteFrame) roomAppleClockSeparator:SpriteFrame|null=null;
|
||||
private numericTools:NumericToolsPanel|null=null;
|
||||
private readonly numericToolsPort=new DeferredNumericToolsPort();
|
||||
private roomHud:RoomHud|null=null;
|
||||
private roomNotice:RoomNoticePanel|null=null;
|
||||
private roomDevice:RoomDeviceStatusAdapter|null=null;
|
||||
private roomPanels:RoomPlatformPanels|null=null;
|
||||
private roomChat:RoomChatPanel|null=null;
|
||||
private roomVoice:RoomVoiceAdapter|null=null;
|
||||
private nativeHost:PlatformNativeHost|null=null;
|
||||
private announcementsRead=0;
|
||||
private lastAnnouncements:readonly string[]|undefined;
|
||||
private voiceMessageCount=0;
|
||||
private applyPanel:ApplyVotePanel|null=null;
|
||||
private dissolutionReceipt:PlatformState['dissolution']=undefined;
|
||||
private feedback:FeedbackPanels|null=null;
|
||||
private waitingReply=false;
|
||||
private reconnecting=false;
|
||||
private runtime:PlatformRuntime|null=null;
|
||||
private game:ReturnType<typeof createTemplateGame>|null=null;
|
||||
private game:GameBinding|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;
|
||||
@@ -69,14 +115,16 @@ export class PlatformStartup extends Component {
|
||||
private bridge:WebViewJavascriptBridgeAdapter|null=null;
|
||||
private restoreShareLogin:(()=>void)|null=null;
|
||||
private restoreLocation:(()=>void)|null=null;
|
||||
private startLocation:(()=>void)|null=null;
|
||||
private startLocation:(()=>void)|null=null;
|
||||
private settings:PlatformSettings|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 status:Label|null=null;
|
||||
private roomView:CommonRoomView|null=null;
|
||||
private device:LoginDeviceSnapshot|null=null;
|
||||
private busy=false;
|
||||
private terminal=false;
|
||||
@@ -103,7 +151,8 @@ export class PlatformStartup extends Component {
|
||||
// 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,
|
||||
this.config=await resolveRuntimeConfig({
|
||||
buildIdentity: await required(this.getComponent(GameComposition),'GameComposition').loadBuildIdentity(),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);
|
||||
@@ -132,7 +181,7 @@ export class PlatformStartup extends Component {
|
||||
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));}
|
||||
}catch(error){console.error('[PlatformStartup]',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;}};
|
||||
@@ -140,11 +189,13 @@ export class PlatformStartup extends Component {
|
||||
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);
|
||||
}
|
||||
}
|
||||
const platformHost:PlatformNativeHost=bridgeHost?
|
||||
{kind:'bridge',call:(name,data)=>required(this.bridge,'native bridge').call(name,data,()=>{})}:
|
||||
injected.settings?{kind:'settings',settings:injected.settings}:{kind:'browser'};
|
||||
this.nativeHost=platformHost;
|
||||
this.startLocation=()=>startPlatformLocation(platformHost,locationCallback);
|
||||
this.settings=new PlatformSettings(window.localStorage,this.config.identity.gameid,this.config.identity.agentid,platformHost);
|
||||
required(this.countdown,'countdown').start(resolveLaunchWaitTime(this.config.remoteConfig));
|
||||
await this.startSession(true);
|
||||
}catch(error){this.fail(error);}
|
||||
@@ -152,10 +203,12 @@ export class PlatformStartup extends Component {
|
||||
/** 退出只重建账号会话;配置、原生桥和已加载资源由常驻组件保留。 */
|
||||
private async startSession(firstStart:boolean):Promise<void>{
|
||||
const identity=required(this.config,'config').identity;
|
||||
const gameKey=required(this.getComponent(SubgameAssets),'SubgameAssets').gameKey;
|
||||
if(gameKey!=='erqiwang'&&gameKey!=='Game_Surface_3')throw Error('Unsupported subgame: '+gameKey);
|
||||
this.game=gameKey==='erqiwang'?createErqiwangGame(identity.gameid):createTemplateGame(identity.gameid);
|
||||
const definition=required(this.getComponent(GameComposition),'GameComposition').definition;
|
||||
this.game=definition.createGame(identity.gameid);
|
||||
if(this.game.entry.key!==definition.key||this.game.entry.route!==definition.route)throw Error('Game definition identity mismatch');
|
||||
this.runtime=new PlatformRuntime({gameEntry:this.game.entry,
|
||||
feedback:{beginLoading:()=>required(this.feedback,'feedback').beginLoading(),showBusinessTip:(message,time)=>required(this.feedback,'feedback').showBusinessTip(message,time)},
|
||||
numericTools:this.numericToolsPort,
|
||||
roomHistory:new OwnedRoomHistory(window.localStorage,identity.gameid,identity.agentid),
|
||||
resolveRuntimeConfig:async()=>required(this.config,'config'),
|
||||
createWireClient:config=>{
|
||||
@@ -187,13 +240,13 @@ export class PlatformStartup extends Component {
|
||||
},
|
||||
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));},
|
||||
showRoom:scope=>this.showRoom(scope),
|
||||
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);},
|
||||
showTips:(message,time,source)=>{if(source==='show_message'){this.roomPanels?.requestFailed();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));
|
||||
try{this.feedback?.kick(text);}finally{this.release();}
|
||||
},
|
||||
showServerDenial:outcome=>{
|
||||
this.busy=false;this.panel?.setBusy(false);
|
||||
@@ -218,12 +271,29 @@ export class PlatformStartup extends Component {
|
||||
});
|
||||
this.unsubscribe=this.runtime.subscribeState(state=>{
|
||||
if(firstStart)this.countdown?.socketReady(state.app.phase==='connected'||state.app.phase==='logged-in');
|
||||
this.lobby?.render(state);
|
||||
this.renderPlatformState(state);
|
||||
},error=>this.fail(error));
|
||||
await this.runtime.start();
|
||||
}
|
||||
await this.runtime.start();
|
||||
}
|
||||
private renderPlatformState(state:PlatformState):void {
|
||||
// A dissolution switch ends with free_room, not player_login. Only an accepted
|
||||
// result receipt clears connection overlays; malformed packets never reach here.
|
||||
if(state.dissolution!==this.dissolutionReceipt){
|
||||
this.dissolutionReceipt=state.dissolution;
|
||||
if(state.dissolution)this.roomView?.dissolutionReceived();
|
||||
if(state.dissolution?.kind==='result'){
|
||||
this.waitingReply=false;this.reconnecting=false;
|
||||
this.feedback?.loading(false);this.feedback?.reconnect(false);
|
||||
}
|
||||
}
|
||||
this.lobby?.render(state);this.applyPanel?.render(state);
|
||||
this.renderRoomPanels(state);
|
||||
}
|
||||
private canvas():Node{return required(director.getScene(),'scene').getChildByName('Canvas') ?? (()=>{throw Error('Scene requires Canvas');})();}
|
||||
private createStatus():void{
|
||||
private createStatus():void{
|
||||
this.disposeRoomPanels();
|
||||
this.roomView?.close();this.roomView=null;
|
||||
this.applyPanel?.dispose();this.applyPanel=null;
|
||||
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;
|
||||
@@ -262,8 +332,8 @@ export class PlatformStartup extends Component {
|
||||
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`;
|
||||
// 展示版本来自 XML name;登录协议使用 XML value 的数值版本。
|
||||
version.string=`v${required(this.getComponent(GameComposition),'GameComposition').displayVersion}`;
|
||||
// Label 自动尺寸更新后重新计算左上角 Widget,避免长版本文字向屏幕外扩展。
|
||||
required(version.node.getComponent(UITransform),'version UITransform').setAnchorPoint(0,1);
|
||||
version.updateRenderData(true);
|
||||
@@ -271,15 +341,15 @@ export class PlatformStartup extends Component {
|
||||
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)}`);}
|
||||
// Location acquisition is independent of login; the browser adapter supplies
|
||||
// the legacy test callback, while native hosts own their real callbacks.
|
||||
try{this.startLocation?.();}catch(error){console.error('[PlatformStartup] location',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));}
|
||||
try{required(this.accounts,'accounts').acceptNative(data,channel);}catch(error){console.error('[PlatformStartup]',error);}
|
||||
}
|
||||
private saveAcceptedLogin():void{
|
||||
if(this.loginResponse!==null){
|
||||
@@ -292,8 +362,10 @@ export class PlatformStartup extends Component {
|
||||
if(this.terminal)return;
|
||||
this.saveAcceptedLogin();
|
||||
this.panel?.dispose();this.panel=null;
|
||||
await this.flow.enterOnce(required(this.lobbyScene,'lobbyScene').name,()=>{
|
||||
this.createStatus();
|
||||
await this.flow.enterOnce(required(this.lobbyScene,'lobbyScene').name,()=>{
|
||||
this.createStatus();
|
||||
try{required(this.settings,'platform settings').synchronizeVoice();}
|
||||
catch(error){console.error('[PlatformStartup]',error);}
|
||||
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'),
|
||||
@@ -313,13 +385,14 @@ export class PlatformStartup extends Component {
|
||||
bind:code=>required(this.runtime,'runtime').bindInvitecode(code),
|
||||
paste:()=>this.pasteInvitecode(),
|
||||
},{
|
||||
settings:required(this.settings,'platform settings'),
|
||||
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'),
|
||||
room:{createPrefab:required(required(this.getComponent(GameComposition),'GameComposition').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)},
|
||||
@@ -348,8 +421,8 @@ export class PlatformStartup extends Component {
|
||||
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;
|
||||
private async showRoom(scope:RoomScope):Promise<void>{
|
||||
if(this.terminal||!scope.active)return;
|
||||
this.saveAcceptedLogin();this.endRoomRequest();
|
||||
// This callback runs only after the runtime accepted and committed the packet.
|
||||
const receipt=this.roomReceipt;this.roomReceipt=null;
|
||||
@@ -359,14 +432,27 @@ export class PlatformStartup extends Component {
|
||||
if(receipt.rpc==='self_join_room'&&receipt.data.shortcode)window.localStorage.setItem('tsgame_shortcode_'+identity.gameid+identity.agentid,String(receipt.data.shortcode));
|
||||
}
|
||||
this.panel?.dispose();this.panel=null;this.lobby?.dispose();this.lobby=null;
|
||||
const assets=required(this.getComponent(SubgameAssets),'SubgameAssets');
|
||||
const assets=required(this.getComponent(GameComposition),'GameComposition');
|
||||
await this.flow.enter(required(assets.roomScene,'subgame room scene').name,()=>{
|
||||
if(required(this.runtime,'runtime').state.room.kind!=='inside')return;
|
||||
if(!scope.active||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=assets.gameKey==='erqiwang'?required(root.getComponent(ErqiwangRoomView),'ErqiwangRoomView'):required(root.getComponent(TemplateRoomView),'TemplateRoomView');
|
||||
root.on('room-action-error',(error:unknown)=>this.tip(error instanceof Error?error.message:String(error)));
|
||||
scope.own(()=>{if(isValid(root))root.destroy();});
|
||||
const view=assets.resolveRoomView(root);
|
||||
scope.own(()=>{if(this.roomView===view){this.roomView=null;view.close();}});
|
||||
scope.own(()=>this.disposeRoomPanels());
|
||||
scope.own(()=>{this.applyPanel?.dispose();this.applyPanel=null;});
|
||||
root.on('room-action-error',(error:unknown)=>console.error('[PlatformStartup]',error));
|
||||
root.on('room-action-timeout',()=>this.tip('房间操作超时,请重试'));
|
||||
this.createStatus();required(this.game,'template game').mount(view);this.events.push({kind:'scene',data:'Room'});
|
||||
this.createStatus();this.roomView=view;required(this.game,'template game').mount(view);
|
||||
this.mountRoomPanels();
|
||||
required(view.exitButton,'room exit button').node.active=false;
|
||||
required(view.roomLabel,'room label').node.on(Node.EventType.TOUCH_END,()=>this.roomPanels?.openRoomInfo());
|
||||
this.applyPanel=new ApplyVotePanel(this.canvas(),required(this.applyPrefab,'applyPrefab'),required(this.playerVoteItemPrefab,'playerVoteItemPrefab'),{
|
||||
agree:()=>required(this.runtime,'runtime').agreeDissolution(),refuse:()=>required(this.runtime,'runtime').refuseDissolution(),
|
||||
acknowledge:()=>required(this.runtime,'runtime').acknowledgeDissolution(),error:text=>console.error('[ApplyVotePanel]',text),
|
||||
});
|
||||
this.applyPanel.render(required(this.runtime,'runtime').state);
|
||||
this.events.push({kind:'scene',data:'Room'});
|
||||
});
|
||||
}
|
||||
private async pasteInvitecode(raw=false):Promise<string>{
|
||||
@@ -431,17 +517,82 @@ export class PlatformStartup extends Component {
|
||||
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 fail(error:unknown):void{
|
||||
console.error('[PlatformStartup]',error);
|
||||
this.release();
|
||||
}
|
||||
/** 幂等释放计时器、订阅和宿主回调,阻止销毁后的迟到消息重新激活流程。 */
|
||||
private release():void{
|
||||
if(this.terminal)return;this.terminal=true;this.flow.dispose();this.panel?.dispose();this.lobby?.dispose();
|
||||
this.disposeRoomPanels();
|
||||
this.roomView?.close();this.roomView=null;
|
||||
if(this.terminal)return;this.terminal=true;this.flow.dispose();this.panel?.dispose();this.lobby?.dispose();
|
||||
this.applyPanel?.dispose();this.applyPanel=null;
|
||||
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};}
|
||||
}
|
||||
getDiagnostics(){return {events:[...this.events],ready:this.runtime?.ready,busy:this.busy,terminal:this.terminal};}
|
||||
update(deltaSeconds:number):void{this.roomHud?.update(deltaSeconds);this.roomNotice?.update(deltaSeconds);}
|
||||
private ensureNumericTools():NumericToolsPanel{
|
||||
if(!this.numericTools){const identity=required(this.config,'config').identity;this.numericTools=new NumericToolsPanel({canvas:this.canvas(),inputPrefab:required(this.numberInputPrefab,'numberInputPrefab'),calPrefab:required(this.multiplierPrefab,'multiplierPrefab'),storage:window.localStorage,gameid:identity.gameid,agentid:identity.agentid,readClipboard:()=>this.pasteInvitecode(true),error:message=>this.tip(message)});}
|
||||
return this.numericTools;
|
||||
}
|
||||
private mountRoomPanels():void{
|
||||
const config=required(this.config,'config'),assets=required(this.getComponent(LobbyAssets),'LobbyAssets');
|
||||
const definition=required(this.getComponent(GameComposition),'GameComposition').definition;
|
||||
const chatConfig=definition.chat;
|
||||
const runtime=required(this.runtime,'runtime'),host=required(this.nativeHost,'native host');
|
||||
this.roomDevice=new RoomDeviceStatusAdapter({host,subscribe:createRoomDeviceStatusSubscriptions(host,window as unknown as Record<string,unknown>,this.bridge||undefined)});
|
||||
const apple=sys.os===sys.OS.IOS;
|
||||
this.roomHud=new RoomHud({canvas:this.canvas(),mainScene:required(this.roomScenePrefab,'roomScenePrefab'),device:this.roomDevice,networkFrames:this.roomNetworkFrames,batteryFrames:this.roomBatteryFrames,clockFont:required(apple?this.roomAppleClockFont:this.roomClockFont,'room clock font'),clockSeparator:required(apple?this.roomAppleClockSeparator:this.roomClockSeparator,'room clock separator'),openRoomInfo:()=>this.roomPanels?.openRoomInfo(),reportError:message=>console.error('[RoomHud]',message)});
|
||||
this.roomHud.root.setSiblingIndex(0);
|
||||
this.roomNotice=new RoomNoticePanel(this.canvas(),required(this.roomNoticePrefab,'roomNoticePrefab'),message=>console.error('[RoomNoticePanel]',message));
|
||||
this.voiceMessageCount=runtime.state.roomSocial?.history.length||0;
|
||||
this.roomVoice=new RoomVoiceAdapter({host,subscribe:createRoomVoiceSubscriptions(host,window as unknown as Record<string,unknown>,this.bridge||undefined),
|
||||
sendVoice:(url,time)=>runtime.sendRoomVoice(url,time),onPlayback:(user,playing)=>this.roomChat?.setPlayback(user-chatConfig.voiceHistoryTag,playing)});
|
||||
this.roomChat=new RoomChatPanel(this.canvas(),required(this.roomChatPrefab,'roomChatPrefab'),required(this.chatPhrasePrefab,'chatPhrasePrefab'),required(this.chatHistoryPrefab,'chatHistoryPrefab'),chatConfig,{
|
||||
text:(text,announcement)=>runtime.sendRoomText(text,announcement),phrase:text=>runtime.sendRoomPhrase(text),expression:id=>runtime.sendRoomExpression(id),
|
||||
voice:(url,index)=>required(this.roomVoice,'room voice').playVoice(url,2,chatConfig.voiceHistoryTag+index),error:message=>this.tip(message),
|
||||
readAnnouncements:()=>{this.announcementsRead=runtime.state.roomSocial?.announcements.length||0;this.roomPanels?.renderNotice(0,null);},
|
||||
});
|
||||
this.roomPanels=new RoomPlatformPanels(this.canvas(),{main:required(this.roomMainPrefab,'roomMainPrefab'),menu:required(this.roomMenuPrefab,'roomMenuPrefab'),check:required(this.roomCheckPrefab,'roomCheckPrefab'),help:required(this.roomHelpPrefab,'roomHelpPrefab'),settings:required(assets.settings,'settings prefab'),switches:assets.switches},required(this.settings,'settings'),{
|
||||
context:()=>resolveRoomMenuContext(runtime.state,config,definition.roomMenu,{h5Version:host.kind==='browser'?1:0,versionState:required(this.lobbyContext,'lobby context').versionState}),
|
||||
command:(command,count)=>{switch(command){case 'room.exit':runtime.exitRoom();break;case 'room.break':runtime.breakRoom();break;case 'room.apply-dissolution':runtime.applyDissolution();break;case 'room.change':runtime.changeRoom();break;case 'room.escape':runtime.surrenderRoom(required(count,'surrender count'));break;default:throw Error('Unknown room action '+command);}},
|
||||
chat:()=>required(this.roomChat,'room chat').open(),record:()=>required(this.roomVoice,'room voice').startRecording(),error:message=>this.tip(message),external:url=>required(this.openExternal,'external browser')(url),
|
||||
copyRoom:()=>{const room=runtime.state.room;if(room.kind!=='inside')throw Error('No room');
|
||||
if(host.kind==='bridge'){host.call('gameCopytext',room.roomcode);this.tip('复制成功!');}
|
||||
else if(host.kind==='settings'){const method=(host.settings as Record<string,unknown>).gameCopytext;if(typeof method!=='function')throw Error('Native settings missing gameCopytext');Reflect.apply(method,host.settings,[room.roomcode]);this.tip('复制成功!');}
|
||||
else {if(!navigator.clipboard)throw Error('当前浏览器无法访问剪贴板');void navigator.clipboard.writeText(room.roomcode).then(()=>this.tip('复制成功!'),error=>console.error('[PlatformStartup]',error));}},
|
||||
describeRoom:()=>{const room=runtime.state.room;if(room.kind!=='inside')throw Error('No room');return room.matchId?'比赛房':room.infinite===1?'系统房':'房号:'+room.roomcode;},
|
||||
autoOwnerNotice:()=>shouldShowOwnerNotice(window.localStorage,String(config.identity.gameid),String(config.identity.agentid),Date.now()),
|
||||
numberInput:(message,onConfirm)=>this.ensureNumericTools().openNumberInput({message,onConfirm:value=>{this.numericTools?.closeNumberInput();if(typeof value!=='number')throw Error('Surrender input must be numeric');onConfirm(value);}}),
|
||||
});
|
||||
this.renderRoomPanels(runtime.state);
|
||||
this.numericToolsPort.bind(this.ensureNumericTools());
|
||||
}
|
||||
private renderRoomPanels(state:PlatformState):void{
|
||||
if(!this.roomPanels||!this.roomChat)return;if(state.room.kind!=='inside'){this.disposeRoomPanels();return;}this.roomPanels.render(state);
|
||||
this.roomHud?.render(state);this.roomNotice?.render(state);
|
||||
const policy=resolveRoomChatPolicy(required(this.config,'config').remoteConfig);
|
||||
const self=required(state.players.entities[required(state.players.selfPlayerId,'self player')],'self player');
|
||||
this.roomChat.render(state,policy.noticeLimit>=0&&self.bean>=policy.noticeLimit,`发送系统公告将消耗${resolveLobbyPolicy(required(this.config,'config').remoteConfig).starName}数量:${policy.noticeCost}`);
|
||||
const history=state.roomSocial?.history;
|
||||
if(history){const from=this.voiceMessageCount;this.voiceMessageCount=history.length;
|
||||
for(const message of history.slice(from))if(message.kind==='voice'&&message.autoPlay){
|
||||
if(state.room.kind!=='inside')throw Error('Voice playback requires room');
|
||||
const seat=(message.sender.seat-state.room.selfSeat+state.room.seatPlayerIds.length)%state.room.seatPlayerIds.length;
|
||||
try{required(this.roomVoice,'room voice').playVoice(required(message.voiceurl,'voice URL'),1,seat);}catch(error){console.error('[PlatformStartup]',error);}
|
||||
}
|
||||
}
|
||||
const news=state.roomSocial?.announcements;
|
||||
if(news){const changed=this.lastAnnouncements!==news;this.lastAnnouncements=news;if(this.announcementsRead>news.length)this.announcementsRead=0;this.roomPanels.renderNotice(Math.max(0,news.length-this.announcementsRead),changed&&news.length>0&&!this.roomChat.root.active?news[news.length-1]:null);}
|
||||
}
|
||||
private disposeRoomPanels():void{
|
||||
const owned=[this.numericTools,this.roomNotice,this.roomHud,this.roomDevice,this.roomVoice,this.roomChat,this.roomPanels];
|
||||
this.numericTools=null;this.roomNotice=null;this.roomHud=null;this.roomDevice=null;this.roomVoice=null;this.roomChat=null;this.roomPanels=null;
|
||||
this.announcementsRead=0;this.lastAnnouncements=undefined;this.voiceMessageCount=0;
|
||||
const errors:unknown[]=[];try{this.numericToolsPort.unbind();}catch(error){errors.push(error);}
|
||||
for(const resource of owned)try{resource?.dispose();}catch(error){errors.push(error);}
|
||||
if(errors.length)throw Object.assign(Error('Room panels cleanup failed'),{errors});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import {Director,director,BlockInputEvents,Button,EditBox,Label,Layout,Node,Prefab,ScrollView,Sprite,UITransform,isValid} from 'cc';
|
||||
import type {PlatformState,RoomSocialMessage} from '../../framework/platform/stores/platform-types.ts';
|
||||
import {resolveRoomChatPhrase,type RoomChatConfig,validateRoomChatText} from '../../framework/presentation/room-chat-config.ts';
|
||||
import {KeyedRows} from '../../framework/presentation/keyed-rows.ts';
|
||||
import {requireNode} from './node-bindings.ts';
|
||||
import {RoomUi,roomNeed} from './RoomUi.ts';
|
||||
|
||||
export interface RoomChatActions {
|
||||
text(text:string,announcement:boolean):void;phrase(text:string):void;expression(id:number):void;
|
||||
voice(url:string,index:number):void;error(message:string):void;readAnnouncements():void;
|
||||
}
|
||||
/** The platform store owns messages; this view only owns its instantiated rows. */
|
||||
export class RoomChatPanel {
|
||||
readonly root:Node;
|
||||
private readonly ui:RoomUi;
|
||||
private readonly historyRows:KeyedRows<RoomSocialMessage,{node:Node;ui:RoomUi;index:number;message:RoomSocialMessage}>;
|
||||
private pendingRender:(()=>void)|null=null;
|
||||
private readonly flush=()=>{const render=this.pendingRender;this.pendingRender=null;render?.();};
|
||||
private readonly visited=new Set<string>();
|
||||
private readonly input:EditBox;
|
||||
private page:'Phrases'|'Expressions'|'History'|'Announcements'='Phrases';
|
||||
private announcement=false;
|
||||
private allowed=false;
|
||||
private history:readonly RoomSocialMessage[]|undefined;
|
||||
private announcements:readonly string[]|undefined;
|
||||
private connected=false;
|
||||
private readonly playback=new Set<number>();
|
||||
constructor(canvas:Node,prefab:Prefab,private readonly phrasePrefab:Prefab,private readonly historyPrefab:Prefab,
|
||||
private readonly config:RoomChatConfig,private readonly actions:RoomChatActions){
|
||||
this.ui=new RoomUi(actions.error);this.root=this.ui.mount(prefab,canvas);
|
||||
this.historyRows=new KeyedRows({key:message=>message.id,create:(message,index)=>this.createHistoryRow(message,index),
|
||||
update:(row,message,index)=>{if(row.message!==message){row.ui.dispose();Object.assign(row,this.createHistoryRow(message,index));}row.index=index;},remove:row=>{row.node.removeFromParent();row.ui.dispose();}});
|
||||
this.root.addComponent(BlockInputEvents);this.root.active=false;
|
||||
this.input=roomNeed(this.node('聊天输入框').getComponent(EditBox),'chat input');
|
||||
this.ui.bind(this.node('聊天遮罩'),()=>this.close());
|
||||
for(const name of ['聊天背景','聊天底'])this.node(name).addComponent(BlockInputEvents);
|
||||
for(const [name,page] of [['常用语','Phrases'],['Spirit699','Phrases'],['表情','Expressions'],['Spirit161','Expressions'],['Spirit698','History'],['Spirit162','History'],['Spirit3107','Announcements'],['Spirit3108','Announcements']] as const)this.ui.bind(this.node(name),()=>this.select(page));
|
||||
this.ui.bind(this.node('发送'),()=>{if(!this.connected||this.input.string===''||/^[ ]+$/.test(this.input.string))return;validateRoomChatText(this.config,this.input.string,this.announcement);actions.text(this.input.string,this.announcement);this.input.string='';});
|
||||
this.ui.bind(this.node('Spirit3111'),()=>{if(!this.allowed)return;this.announcement=!this.announcement;this.renderAnnouncement();});
|
||||
const phrases=this.content('Phrases');this.clear(phrases);
|
||||
config.phrases.forEach((phrase,index)=>{const row=this.ui.mount(phrasePrefab,phrases);row.name=`ChatPhraseItem_${index}`;this.ui.text(row,'Text').string=phrase.text;this.ui.bind(row,()=>{if(this.connected)actions.phrase(phrase.text);});});
|
||||
this.layout('Phrases');
|
||||
for(const node of this.content('Expressions').children){const id=Number(node.name.replace('Expression',''));if(!Number.isInteger(id)||id<1)throw Error('Invalid expression item');this.ui.bind(node,()=>{if(this.connected)actions.expression(id);});}
|
||||
this.layout('Expressions');this.clear(this.content('History'));this.clear(this.content('Announcements'));
|
||||
this.select('Phrases');
|
||||
}
|
||||
private node(name:string):Node{return requireNode(this.root,'group-30/'+name);}
|
||||
private scroll(page:string):ScrollView{return roomNeed(this.node(page).getComponent(ScrollView),page);}
|
||||
private content(page:string):Node{return roomNeed(this.scroll(page).content,page+' content');}
|
||||
private clear(parent:Node):void{for(const n of [...parent.children]){n.removeFromParent();n.destroy();}}
|
||||
private layout(page:string):void{roomNeed(this.content(page).getComponent(Layout),page+' layout').updateLayout();}
|
||||
open():void{this.root.active=true;const parent=this.root.parent!;const overlay=parent.children.find(n=>['Layer614_Loading','Layer615_Reconnect','Layer612_Tips','Layer616_Kick'].includes(n.name));this.root.setSiblingIndex(overlay?overlay.getSiblingIndex():parent.children.length-1);this.select(this.page);}
|
||||
close():void{this.root.active=false;this.input.blur();}
|
||||
private select(page:typeof this.page):void{this.page=page;for(const name of ['Phrases','Expressions','History','Announcements'])this.node(name).active=name===page;this.layout(page);if(!this.visited.has(page)&&(page==='History'||page==='Announcements'))this.scroll(page).scrollToBottom(0);this.visited.add(page);if(page==='Announcements'&&this.root.active)this.actions.readAnnouncements();}
|
||||
render(state:PlatformState,announcementAllowed:boolean,announcementCost:string):void{
|
||||
if(this.pendingRender===null)director.once(Director.EVENT_BEFORE_DRAW,this.flush);
|
||||
this.pendingRender=()=>this.renderState(state,announcementAllowed,announcementCost);
|
||||
}
|
||||
private renderState(state:PlatformState,announcementAllowed:boolean,announcementCost:string):void{
|
||||
this.connected=state.app.phase==='logged-in'&&state.room.kind==='inside';this.allowed=announcementAllowed;
|
||||
if(!this.allowed)this.announcement=false;this.renderAnnouncement();
|
||||
const cost=this.ui.text(this.root,'group-30/Spirit3117');cost.string=announcementCost;cost.node.active=this.allowed;
|
||||
const social=state.roomSocial;if(!social)return;
|
||||
if(this.history!==social.history){this.history=social.history;this.renderHistory(social.history);}
|
||||
if(this.announcements!==social.announcements){this.announcements=social.announcements;this.renderNews(social.announcements);if(this.page==='Announcements'&&this.root.active)this.actions.readAnnouncements();}
|
||||
}
|
||||
private renderAnnouncement():void{
|
||||
this.node('Spirit3111').active=this.allowed;this.node('Spirit3113').active=this.allowed;
|
||||
this.node('Spirit3111').setScale(this.announcement?1.1:1,this.announcement?1.1:1,1);
|
||||
this.input.maxLength=this.announcement?this.config.announcementLimit:this.config.limitLength;
|
||||
}
|
||||
private renderHistory(messages:readonly RoomSocialMessage[]):void{
|
||||
const scroll=this.scroll('History'),offset=scroll.getScrollOffset(),follow=offset.y>=scroll.getMaxScrollOffset().y-2;
|
||||
this.historyRows.render(messages);this.layout('History');
|
||||
if(follow)scroll.scrollToBottom(0);else scroll.scrollToOffset(offset,0);
|
||||
}
|
||||
private createHistoryRow(message:RoomSocialMessage,index:number):{node:Node;ui:RoomUi;index:number;message:RoomSocialMessage}{
|
||||
const ui=new RoomUi(this.actions.error),row=ui.mount(this.historyPrefab,this.content('History'));row.name=`ChatHistoryItem_${message.id}`;
|
||||
const holder={node:row,ui,index,message};
|
||||
ui.text(row,'Spirit3110').string=message.sender.nickname;
|
||||
ui.avatar(requireNode(row,'Spirit697'),message.sender.avatar);
|
||||
const label=ui.text(row,'Spirit164'),duration=ui.text(row,'Spirit480');label.enableWrapText=true;
|
||||
requireNode(row,'Spirit165').active=message.kind==='voice';duration.node.active=message.kind==='voice';
|
||||
label.string=message.kind==='text'?roomNeed(message.text,'chat text'):message.kind==='phrase'?resolveRoomChatPhrase(this.config,roomNeed(message.index,'phrase index')).text:message.kind==='expression'?'': '点击播放语音';
|
||||
if(message.kind==='voice'){duration.string=String(roomNeed(message.time,'voice duration'))+'秒';ui.bind(row,()=>this.actions.voice(roomNeed(message.voiceurl,'voice URL'),holder.index));if(this.playback.has(index))requireNode(row,'Spirit165').setScale(1.25,1.25,1);}
|
||||
if(message.kind==='expression'){
|
||||
const expressions=this.content('Expressions'),count=expressions.children.length;if(count===0)throw Error('Missing expression assets');
|
||||
const source=requireNode(expressions,'Expression'+((roomNeed(message.index,'expression index')-1)%count+1));
|
||||
const emoji=new Node('Expression');emoji.parent=row;emoji.layer=row.layer;emoji.setPosition(118,-65);emoji.addComponent(UITransform).setContentSize(48,48);emoji.addComponent(Sprite).spriteFrame=roomNeed(source.getComponent(Sprite),'expression sprite').spriteFrame;
|
||||
}
|
||||
const lines=Math.max(1,Math.ceil(label.string.replace(/[^\x00-\xff]/g,'**').length/40));const height=Math.max(105,48+lines*24);
|
||||
roomNeed(row.getComponent(UITransform),'history row').height=height;
|
||||
roomNeed(label.node.getComponent(UITransform),'history text').height=height-48;label.lineHeight=24;
|
||||
label.node.setPosition(290,-(height+25)/2);const bubble=requireNode(row,'Spirit163');roomNeed(bubble.getComponent(UITransform),'bubble').height=height-40;bubble.setPosition(290,-(height+25)/2);
|
||||
|
||||
return holder;
|
||||
}
|
||||
private renderNews(messages:readonly string[]):void{
|
||||
const scroll=this.scroll('Announcements'),offset=scroll.getScrollOffset(),follow=offset.y>=scroll.getMaxScrollOffset().y-2;
|
||||
const content=this.content('Announcements');
|
||||
for(const row of [...content.children])if(Number(row.name.replace('Announcement_',''))>=messages.length){row.removeFromParent();row.destroy();}
|
||||
messages.forEach((message,index)=>{let row=content.getChildByName(`Announcement_${index}`);if(!row){row=new Node(`Announcement_${index}`);row.parent=content;row.layer=content.layer;row.addComponent(UITransform).setAnchorPoint(0,1);const label=row.addComponent(Label);label.fontSize=20;label.lineHeight=26;label.overflow=Label.Overflow.RESIZE_HEIGHT;label.horizontalAlign=Label.HorizontalAlign.LEFT;label.color=this.ui.text(this.root,'group-30/Spirit3117').color;}
|
||||
roomNeed(row.getComponent(UITransform),'announcement row').setContentSize(500,Math.max(60,Math.ceil(message.length/22)*26+20));roomNeed(row.getComponent(Label),'announcement label').string=message.replace(/#/g,'\n');});
|
||||
this.layout('Announcements');if(follow)scroll.scrollToBottom(0);else scroll.scrollToOffset(offset,0);
|
||||
}
|
||||
setPlayback(index:number,playing:boolean):void{if(playing)this.playback.add(index);else this.playback.delete(index);const message=this.history?.[index],row=message?this.historyRows.get(message.id)?.node:undefined;if(row&&isValid(row))requireNode(row,'Spirit165').setScale(playing?1.25:1,playing?1.25:1,1);}
|
||||
dispose():void{director.off(Director.EVENT_BEFORE_DRAW,this.flush);this.pendingRender=null;try{this.historyRows.dispose();}finally{this.ui.dispose();}}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "0bda0501-4d0e-42c6-90ef-e185e491dc4a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import {BitmapFont,Label,Node,Prefab,Sprite,SpriteFrame,UIOpacity,UITransform,Widget,isValid} from 'cc';
|
||||
import type {RoomDeviceStatusAdapter} from '../../framework/adapters/native/room-device-status.ts';
|
||||
import type {PlatformState} from '../../framework/platform/stores/platform-types.ts';
|
||||
import {RoomHudClock,roomHudCycles,roomHudTime} from '../../framework/presentation/room-hud-model.ts';
|
||||
import {RoomUi,roomNeed} from './RoomUi.ts';
|
||||
import {requireNode} from './node-bindings.ts';
|
||||
|
||||
export interface RoomHudOptions {
|
||||
canvas:Node;
|
||||
mainScene:Prefab;
|
||||
device:RoomDeviceStatusAdapter;
|
||||
/** Zero-indexed arrays contain legacy frames 1..4 and 1..3 respectively. */
|
||||
networkFrames:readonly SpriteFrame[];
|
||||
batteryFrames:readonly SpriteFrame[];
|
||||
/** Caller selects original 00069/00070 or Apple 00174/00173 from authoritative OS. */
|
||||
clockFont:BitmapFont;
|
||||
clockSeparator:SpriteFrame;
|
||||
openRoomInfo():void;
|
||||
reportError(message:string):void;
|
||||
/** Date source is optional for deterministic tests; production uses local Date. */
|
||||
now?:()=>Date;
|
||||
}
|
||||
|
||||
/** Layer50 status binding. The caller owns sibling ordering and native adapter lifetime. */
|
||||
export class RoomHud {
|
||||
readonly root:Node;
|
||||
private readonly overlay:Node;
|
||||
private readonly ui:RoomUi;
|
||||
private readonly gate:Node;
|
||||
private readonly hours:Label;
|
||||
private readonly minutes:Label;
|
||||
private readonly roomCode:Label;
|
||||
private readonly codeTitle:Node;
|
||||
private readonly wifi:Sprite;
|
||||
private readonly battery:Sprite;
|
||||
private readonly batteryBackground:Sprite;
|
||||
private readonly colon:Sprite;
|
||||
private readonly colonOpacity:UIOpacity;
|
||||
private readonly clock=new RoomHudClock();
|
||||
private roomKey:string|null=null;
|
||||
private cycles=false;
|
||||
private readonly now:()=>Date;
|
||||
|
||||
constructor(private readonly options:RoomHudOptions) {
|
||||
this.ui=new RoomUi(options.reportError);
|
||||
this.now=options.now===undefined?()=>new Date():options.now;
|
||||
try {
|
||||
if(options.networkFrames.length!==4||options.networkFrames.some(frame=>!frame))throw Error('RoomHud requires network frames 00113_1..4');
|
||||
if(options.batteryFrames.length!==3||options.batteryFrames.some(frame=>!frame))throw Error('RoomHud requires battery frames 00112_1..3');
|
||||
roomNeed(options.clockFont,'clockFont');roomNeed(options.clockSeparator,'clockSeparator');
|
||||
this.root=this.ui.mount(options.mainScene,options.canvas);
|
||||
const node=(id:number)=>requireNode(this.root,'group-21/Spirit'+id);
|
||||
const sprite=(id:number)=>roomNeed(node(id).getComponent(Sprite),'Spirit'+id+' Sprite');
|
||||
this.gate=node(149);
|
||||
this.hours=roomNeed(node(154).getComponent(Label),'Spirit154 Label');
|
||||
this.minutes=roomNeed(node(155).getComponent(Label),'Spirit155 Label');
|
||||
this.roomCode=roomNeed(node(3141).getComponent(Label),'Spirit3141 Label');
|
||||
this.codeTitle=node(3142);
|
||||
this.wifi=sprite(336);this.batteryBackground=sprite(337);this.battery=sprite(338);this.colon=sprite(156);
|
||||
this.colonOpacity=roomNeed(this.colon.getComponent(UIOpacity)||this.colon.addComponent(UIOpacity),'Spirit156 UIOpacity');
|
||||
this.hours.font=options.clockFont;this.minutes.font=options.clockFont;this.colon.spriteFrame=options.clockSeparator;
|
||||
this.batteryBackground.spriteFrame=options.batteryFrames[0];
|
||||
// Horizontal fill clips the source texture from the left, matching legacy set_clip.
|
||||
this.battery.type=Sprite.Type.FILLED;this.battery.fillType=Sprite.FillType.HORIZONTAL;this.battery.fillStart=0;
|
||||
this.ui.bind(node(284),()=>options.openRoomInfo());
|
||||
this.ui.bind(node(3141),()=>options.openRoomInfo());
|
||||
node(291).active=false;
|
||||
// The platform background stays below the subgame; status remains above it.
|
||||
this.overlay=new Node('RoomStatus');this.overlay.layer=this.root.layer;this.overlay.parent=options.canvas;
|
||||
this.overlay.addComponent(UITransform);
|
||||
const align=roomNeed(this.overlay.addComponent(Widget),'RoomStatus Widget');align.target=options.canvas;
|
||||
align.isAlignTop=align.isAlignBottom=align.isAlignLeft=align.isAlignRight=true;
|
||||
align.top=align.bottom=align.left=align.right=0;align.updateAlignment();
|
||||
for(const id of [284,154,155,156,336,337,338,3141,3142,291])node(id).parent=this.overlay;
|
||||
const feedback=options.canvas.children.find(n=>['Layer614_Loading','Layer615_Reconnect','Layer612_Tips','Layer616_Kick'].includes(n.name));
|
||||
if(feedback)this.overlay.setSiblingIndex(feedback.getSiblingIndex());
|
||||
this.overlay.active=false;this.ui.releases.push(()=>{if(isValid(this.overlay))this.overlay.destroy();});
|
||||
this.ui.releases.push(options.device.subscribe(()=>this.ui.run(()=>this.draw())));
|
||||
this.root.active=false;
|
||||
}catch(error){this.ui.dispose();throw error;}
|
||||
}
|
||||
|
||||
render(state:PlatformState):void {
|
||||
if(this.ui.disposed)return;
|
||||
const inside=state.app.phase==='logged-in'&&state.room.kind==='inside';
|
||||
this.root.active=inside;
|
||||
this.overlay.active=inside;
|
||||
if(state.room.kind!=='inside'||!inside){this.roomKey=null;this.clock.reset();return;}
|
||||
if(this.roomKey!==state.room.roomcode){this.roomKey=state.room.roomcode;this.clock.reset();}
|
||||
this.cycles=roomHudCycles(state.room);
|
||||
this.roomCode.string=state.room.roomcode;
|
||||
this.clock.update(0,this.gate.activeInHierarchy,this.cycles);
|
||||
this.draw();
|
||||
}
|
||||
|
||||
update(dt:number):void {
|
||||
if(this.ui.disposed||!this.root.activeInHierarchy)return;
|
||||
this.clock.update(dt,this.gate.activeInHierarchy,this.cycles);
|
||||
this.draw();
|
||||
}
|
||||
|
||||
dispose():void {this.ui.dispose();this.clock.reset();this.roomKey=null;}
|
||||
|
||||
private draw():void {
|
||||
if(this.ui.disposed||!this.root.activeInHierarchy)return;
|
||||
const visible=this.gate.activeInHierarchy;
|
||||
const showStatus=visible&&!this.clock.showRoomCode;
|
||||
const time=roomHudTime(this.now());
|
||||
if(visible){this.hours.string=time.hours;this.minutes.string=time.minutes;this.colonOpacity.opacity=this.clock.colonOpacity;}
|
||||
this.hours.node.active=showStatus;this.minutes.node.active=showStatus;this.colon.node.active=showStatus;
|
||||
this.roomCode.node.active=visible&&this.clock.showRoomCode;this.codeTitle.active=visible&&this.clock.showRoomCode;
|
||||
const status=this.options.device.snapshot();
|
||||
this.wifi.node.active=showStatus&&status.wifiFrame!==null;
|
||||
if(status.wifiFrame!==null)this.wifi.spriteFrame=this.options.networkFrames[status.wifiFrame-1];
|
||||
this.batteryBackground.node.active=showStatus;
|
||||
this.battery.node.active=showStatus&&status.battery!==null&&status.batteryFrame!==null;
|
||||
if(status.batteryFrame!==null)this.battery.spriteFrame=this.options.batteryFrames[status.batteryFrame-1];
|
||||
if(status.battery!==null)this.battery.fillRange=status.battery;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "5fec65ea-08a0-4560-a1dd-a17ed777fbe7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {Label,Mask,Node,Prefab,UITransform} from 'cc';
|
||||
import type {PlatformState} from '../../framework/platform/stores/platform-types.ts';
|
||||
import {RoomBroadcastQueue} from '../../framework/presentation/room-broadcast-queue.ts';
|
||||
import {requireNode} from './node-bindings.ts';
|
||||
import {RoomUi,roomNeed} from './RoomUi.ts';
|
||||
|
||||
/** Layer402: broadcast/msgtype=1 only; chat announcements have their own list. */
|
||||
export class RoomNoticePanel {
|
||||
readonly root:Node;
|
||||
private readonly ui:RoomUi;
|
||||
private readonly queue:RoomBroadcastQueue;
|
||||
private readonly label:Label;
|
||||
private readonly textTransform:UITransform;
|
||||
private readonly clipWidth:number;
|
||||
private roomcode:string|null=null;
|
||||
private receipts:readonly string[]|undefined;
|
||||
constructor(canvas:Node,prefab:Prefab,error:(message:string)=>void){
|
||||
this.ui=new RoomUi(error);this.root=this.ui.mount(prefab,canvas);this.root.active=false;
|
||||
const viewport=requireNode(this.root,'group-56/Viewport');
|
||||
roomNeed(viewport.getComponent(Mask),'broadcast clip mask');
|
||||
this.clipWidth=roomNeed(viewport.getComponent(UITransform),'broadcast viewport').width;
|
||||
this.queue=new RoomBroadcastQueue(this.clipWidth);
|
||||
this.label=this.ui.text(this.root,'group-56/Viewport/Spirit390');
|
||||
this.textTransform=roomNeed(this.label.node.getComponent(UITransform),'broadcast text');
|
||||
this.label.enableWrapText=false;this.label.overflow=Label.Overflow.NONE;
|
||||
// Legacy closeBroadcastfromplayer is empty: no click handler consumes/skips a message.
|
||||
}
|
||||
render(state:PlatformState):void {
|
||||
if(this.ui.disposed)return;
|
||||
const roomcode=state.room.kind==='inside'?state.room.roomcode:null;
|
||||
const receipts=roomcode===null?undefined:state.roomBroadcasts;
|
||||
if(roomcode!==this.roomcode||receipts===undefined){
|
||||
this.queue.clear();this.receipts=undefined;this.roomcode=roomcode;
|
||||
}
|
||||
if(receipts!==undefined&&receipts!==this.receipts){
|
||||
const previous=this.receipts;
|
||||
const appended=previous!==undefined&&receipts.length>=previous.length&&previous.every((text,index)=>text===receipts[index]);
|
||||
if(previous!==undefined&&!appended)this.queue.clear();
|
||||
const start=appended?previous.length:0;
|
||||
for(let index=start;index<receipts.length;index++)this.queue.enqueue(receipts[index]);
|
||||
this.receipts=receipts;
|
||||
}
|
||||
this.paint();
|
||||
}
|
||||
update(deltaSeconds:number):void {
|
||||
if(this.ui.disposed)return;
|
||||
this.queue.advance(deltaSeconds);this.paint();
|
||||
}
|
||||
private paint():void {
|
||||
const frame=this.queue.current;this.root.active=frame!==null;
|
||||
if(frame===null)return;
|
||||
this.label.string=frame.text;this.textTransform.width=frame.textWidth;
|
||||
this.label.node.setPosition(frame.offsetX-this.clipWidth/2,0,0);
|
||||
}
|
||||
dispose():void {this.queue.clear();this.receipts=undefined;this.roomcode=null;this.ui.dispose();}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "7bd9fc1d-809d-47fd-afc5-a80bb6ca3797",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import {BlockInputEvents,Label,Layout,Node,Prefab,Sprite,SpriteFrame,UITransform,Widget,isValid} from 'cc';
|
||||
import type {PlatformState} from '../../framework/platform/stores/platform-types.ts';
|
||||
import {buildRoomMenuModel,roomMenuAction,roomEscapeConfirmation,type RoomMenuContext,type RoomMenuAction,type RoomMenuSpiritId} from '../../framework/presentation/room-menu-model.ts';
|
||||
import type {PlatformSettings} from '../../framework/presentation/platform-settings.ts';
|
||||
import {OptionalSwitchAnimation} from './OptionalSwitchAnimation.ts';
|
||||
import {RoomUi,roomNeed} from './RoomUi.ts';
|
||||
import {requireNode} from './node-bindings.ts';
|
||||
|
||||
export interface RoomPanelAssets {main:Prefab;menu:Prefab;check:Prefab;settings:Prefab;help:Prefab;switches:SpriteFrame[];}
|
||||
export interface RoomPanelActions {
|
||||
context():RoomMenuContext;command(command:string,count?:number):void;chat():void;record():void;
|
||||
numberInput(message:string,onConfirm:(value:number)=>void):void;
|
||||
error(message:string):void;external(url:string):void;copyRoom():void;describeRoom():string;autoOwnerNotice():boolean;
|
||||
}
|
||||
const BUTTON_NAMES:Readonly<Record<RoomMenuSpiritId,string>>={151:'游戏界面设置',150:'游戏界面帮助',185:'解散',233:'退出按钮',3014:'更换房间',513:'逃跑',656:'下载'};
|
||||
/** Platform-owned room toolbar, menu and confirmations. */
|
||||
export class RoomPlatformPanels {
|
||||
private readonly ui:RoomUi;
|
||||
readonly main:Node;
|
||||
readonly menu:Node;
|
||||
private modal:RoomUi|null=null;
|
||||
private state:PlatformState|null=null;
|
||||
private pending=false;
|
||||
private timer:ReturnType<typeof setTimeout>|null=null;
|
||||
private pendingCommand:string|null=null;
|
||||
private noticeTimer:ReturnType<typeof setTimeout>|null=null;
|
||||
private ownerNotice:string|null=null;
|
||||
constructor(private readonly canvas:Node,private readonly assets:RoomPanelAssets,private readonly settings:PlatformSettings,private readonly actions:RoomPanelActions){
|
||||
this.ui=new RoomUi(actions.error);this.main=this.ui.mount(assets.main,canvas);this.menu=this.ui.mount(assets.menu,canvas);
|
||||
this.ui.bind(requireNode(this.main,'group-21/Spirit153'),()=>actions.chat());
|
||||
for(const name of ['Spirit3115','Spirit3118','Spirit3119'])this.ui.bind(requireNode(this.main,'group-21/'+name),()=>actions.chat());
|
||||
const record=requireNode(this.main,'group-21/Spirit152');const fn=()=>this.ui.run(()=>{if(this.state?.app.phase==='logged-in')actions.record();});record.on(Node.EventType.TOUCH_START,fn);this.ui.releases.push(()=>{if(isValid(record))record.off(Node.EventType.TOUCH_START,fn);});
|
||||
this.ui.bind(requireNode(this.menu,'group-21/Spirit107'),()=>{const group=requireNode(this.menu,'group-99');if(group.active)group.active=false;else this.openMenu();});
|
||||
this.ui.bind(requireNode(this.menu,'group-99/Spirit3123'),()=>this.closeMenu());
|
||||
requireNode(this.menu,'group-99/Spirit108').addComponent(BlockInputEvents);
|
||||
for(const [id,name] of Object.entries(BUTTON_NAMES))this.ui.bind(requireNode(this.menu,'group-99/Buttons/'+name),()=>this.perform(roomMenuAction(Number(id) as RoomMenuSpiritId,actions.context())));
|
||||
for(const name of ['Spirit109','Spirit3124'])this.ui.bind(requireNode(this.menu,'group-100/'+name),()=>{requireNode(this.menu,'group-100').active=false;});
|
||||
this.ui.bind(requireNode(this.menu,'group-100/Spirit512'),()=>actions.copyRoom());
|
||||
this.ui.bind(requireNode(this.menu,'group-81/Spirit3149'),()=>this.showOwnerNotice(!requireNode(this.menu,'group-81/Spirit3148').active));
|
||||
for(const name of ['Spirit3148','Spirit3150'])this.ui.bind(requireNode(this.menu,'group-81/'+name),()=>this.showOwnerNotice(false));
|
||||
for(const name of ['Spirit3115','Spirit3116','Spirit3118','Spirit3119','Spirit3117']){const n=this.main.getChildByPath('group-21/'+name);if(n)n.active=false;}
|
||||
}
|
||||
private closeMenu():void{requireNode(this.menu,'group-99').active=false;}
|
||||
private openMenu():void{
|
||||
if(this.pending||this.state?.app.phase!=='logged-in')return;
|
||||
const visible=new Set(buildRoomMenuModel(this.actions.context()).map(x=>x.spiritId));
|
||||
for(const [id,name] of Object.entries(BUTTON_NAMES))requireNode(this.menu,'group-99/Buttons/'+name).active=visible.has(Number(id) as RoomMenuSpiritId);
|
||||
requireNode(this.menu,'group-100').active=false;requireNode(this.menu,'group-99').active=true;
|
||||
roomNeed(requireNode(this.menu,'group-99/Buttons').getComponent(Layout),'menu Layout').updateLayout();
|
||||
}
|
||||
openRoomInfo():void{this.closeMenu();const g=requireNode(this.menu,'group-100');this.ui.text(g,'房间号').string=this.actions.describeRoom();this.ui.text(g,'Spirit111').string='';g.active=true;}
|
||||
private perform(action:RoomMenuAction):void{
|
||||
this.closeMenu();
|
||||
switch(action.kind){
|
||||
case 'tip':this.actions.error(action.message);return;
|
||||
case 'download':this.actions.external(action.url);return;
|
||||
case 'panel':if(action.panel==='settings')this.openSettings();else this.openHelp();return;
|
||||
case 'command':this.send(action.command);return;
|
||||
case 'confirm':this.confirm(action);return;
|
||||
case 'input':this.escapeInput(action.message);return;
|
||||
}
|
||||
}
|
||||
private send(command:string,count?:number):void{
|
||||
if(this.pending||this.state?.app.phase!=='logged-in')return;
|
||||
this.pending=true;this.pendingCommand=command;
|
||||
this.timer=setTimeout(()=>{this.clearPending();this.actions.error('房间操作超时,请重试');},15000);
|
||||
try{this.actions.command(command,count);}catch(error){this.clearPending();throw error;}
|
||||
}
|
||||
requestFailed():void{this.clearPending();}
|
||||
private clearPending():void{this.pending=false;this.pendingCommand=null;if(this.timer!==null)clearTimeout(this.timer);this.timer=null;}
|
||||
render(state:PlatformState):void{
|
||||
const old=this.state;this.state=state;
|
||||
if(state.app.phase!=='logged-in'||state.room.kind!=='inside'||old&&(
|
||||
this.pendingCommand==='room.apply-dissolution'&&old.dissolution!==state.dissolution||
|
||||
this.pendingCommand==='room.change'&&old.room.kind==='inside'&&old.room.roomcode!==state.room.roomcode))this.clearPending();
|
||||
if(state.app.phase!=='logged-in'){this.closeMenu();this.closeModal();}
|
||||
const notice=state.room.kind==='inside'?state.room.ownerNotice:null;
|
||||
const noticeGroup=requireNode(this.menu,'group-81');noticeGroup.active=!!notice;
|
||||
if(notice!==this.ownerNotice){this.ownerNotice=notice||null;this.showOwnerNotice(!!notice&&this.actions.autoOwnerNotice());}
|
||||
}
|
||||
private showOwnerNotice(show:boolean):void{
|
||||
const g=requireNode(this.menu,'group-81'),bg=requireNode(g,'Spirit3148'),label=this.ui.text(g,'Spirit3150');
|
||||
bg.active=label.node.active=show&&!!this.ownerNotice;if(!bg.active)return;
|
||||
label.string='房主公告:\n'+this.ownerNotice;label.enableWrapText=true;label.overflow=Label.Overflow.CLAMP;label.horizontalAlign=Label.HorizontalAlign.LEFT;label.fontSize=20;label.lineHeight=28;
|
||||
const height=Math.max(90,Math.ceil(label.string.length/20)*28+30);
|
||||
for(const n of [bg,label.node]){const w=n.getComponent(Widget);if(w)w.enabled=false;}
|
||||
roomNeed(bg.getComponent(Sprite),'owner notice background').sizeMode=Sprite.SizeMode.CUSTOM;
|
||||
roomNeed(bg.getComponent(UITransform),'notice background').setContentSize(420,height);bg.setPosition(210,350-height/2);
|
||||
roomNeed(label.node.getComponent(UITransform),'notice label').setContentSize(390,height-30);label.node.setPosition(210,350-height/2);
|
||||
}
|
||||
private closeModal():void{this.modal?.dispose();this.modal=null;}
|
||||
private show(prefab:Prefab):{ui:RoomUi;root:Node}{this.closeModal();const ui=new RoomUi(this.actions.error);this.modal=ui;const root=ui.mount(prefab,this.canvas);root.addComponent(BlockInputEvents);return {ui,root};}
|
||||
private confirm(action:Extract<RoomMenuAction,{kind:'confirm'}>):void{
|
||||
const {ui,root}=this.show(this.assets.check);ui.text(root,'group-36/Spirit361').string=action.message;
|
||||
for(const name of ['Spirit242','Spirit246','Spirit247'])ui.bind(requireNode(root,'group-36/'+name),()=>this.closeModal());
|
||||
for(const name of ['Spirit243','Spirit244'])requireNode(root,'group-36/'+name).addComponent(BlockInputEvents);
|
||||
ui.bind(requireNode(root,'group-36/Spirit245'),()=>{this.closeModal();this.send(action.command,action.count);});
|
||||
}
|
||||
private escapeInput(message:string):void{
|
||||
this.closeModal();this.actions.numberInput(message,value=>this.perform(roomEscapeConfirmation(this.actions.context(),value)));
|
||||
}
|
||||
private openSettings():void{
|
||||
const {ui,root}=this.show(this.assets.settings);const g=requireNode(root,'group-6');g.active=true;
|
||||
ui.bind(requireNode(g,'设置遮罩'),()=>this.closeModal());
|
||||
requireNode(g,'邀请好友').active=false;
|
||||
ui.bind(requireNode(g,'退出登录'),()=>this.actions.error('请返回游戏大厅退出登录'));
|
||||
const dissolve=requireNode(g,'申请解散');dissolve.active=buildRoomMenuModel(this.actions.context()).some(x=>x.spiritId===185);
|
||||
ui.bind(dissolve,()=>{this.closeModal();this.perform(roomMenuAction(185,this.actions.context()));});
|
||||
for(const [name,key] of [['音乐底','bgm'],['音效底','sound'],['Spirit357','shake'],['Spirit388','voice']] as const){const n=requireNode(g,name),animation=roomNeed(n.getComponent(OptionalSwitchAnimation),name);const render=(animate:boolean)=>{const on=this.settings.read(key);animation.setState(on,roomNeed(this.assets.switches[on?0:1],'switch sprite'),animate);};render(false);ui.bind(n,()=>{try{this.settings.toggle(key);}finally{render(true);}});}
|
||||
}
|
||||
private openHelp():void{const {ui,root}=this.show(this.assets.help);requireNode(root,'group-19').active=true;for(const name of ['帮助遮罩','关闭帮助'])ui.bind(requireNode(root,'group-19/'+name),()=>this.closeModal());requireNode(root,'group-19/帮助背景').addComponent(BlockInputEvents);}
|
||||
renderNotice(unread:number,preview:string|null):void{
|
||||
const badge=requireNode(this.main,'group-21/Spirit3118'),count=this.ui.text(this.main,'group-21/Spirit3119');badge.active=count.node.active=unread>0;count.string=unread>99?'99+':String(unread);roomNeed(badge.getComponent(UITransform),'notice badge').width=unread<10?27:unread<100?37:47;
|
||||
if(preview!==null){const node=requireNode(this.main,'group-21/Spirit3115');node.active=true;const label=this.ui.text(node,'NoticeText');label.string=preview.replace(/#/g,'\n');if(this.noticeTimer!==null)clearTimeout(this.noticeTimer);this.noticeTimer=setTimeout(()=>{if(isValid(node))node.active=false;this.noticeTimer=null;},1800);}
|
||||
}
|
||||
dispose():void{this.clearPending();if(this.noticeTimer!==null)clearTimeout(this.noticeTimer);this.closeModal();this.ui.dispose();}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "97b778ff-e788-45e9-b18a-e377683013ed",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {assetManager,Button,ImageAsset,Label,Node,Prefab,Sprite,SpriteFrame,Texture2D,UITransform,Widget,instantiate,isValid} from 'cc';
|
||||
import {requireNode} from './node-bindings.ts';
|
||||
import {roomAvatarCache} from './avatar-cache.ts';
|
||||
export function roomNeed<T>(value:T|null|undefined,name:string):T{if(value==null)throw Error(`Room UI missing ${name}`);return value;}
|
||||
/** Owns listeners and remote avatar allocations for one room UI lifetime. */
|
||||
export class RoomUi {
|
||||
readonly releases:Array<()=>void>=[];
|
||||
disposed=false;
|
||||
private readonly avatarOwners=new Map<Node,()=>void>();
|
||||
constructor(readonly error:(message:string)=>void){}
|
||||
run(action:()=>void):void{try{action();}catch(error){console.error('[RoomUi]',error);}}
|
||||
mount(prefab:Prefab,parent:Node):Node{const n=instantiate(prefab);n.parent=parent;n.setPosition(0,0,0);const w=n.getComponent(Widget);if(w){w.target=parent;w.updateAlignment();}const overlay=parent.children.find(c=>['Layer614_Loading','Layer615_Reconnect','Layer612_Tips','Layer616_Kick'].includes(c.name));if(overlay)n.setSiblingIndex(overlay.getSiblingIndex());this.releases.push(()=>{if(isValid(n))n.destroy();});return n;}
|
||||
bind(n:Node,action:()=>void):Button{const b=n.getComponent(Button)||n.addComponent(Button);b.transition=Button.Transition.NONE;const fn=()=>{if(!this.disposed&&n.activeInHierarchy&&b.interactable)this.run(action);};n.on(Button.EventType.CLICK,fn);this.releases.push(()=>{if(isValid(n))n.off(Button.EventType.CLICK,fn);});return b;}
|
||||
text(root:Node,path:string):Label{return roomNeed(requireNode(root,path).getComponent(Label),path);}
|
||||
avatar(node:Node,url:string):void{
|
||||
this.avatarOwners.get(node)?.();this.avatarOwners.delete(node);
|
||||
const sprite=roomNeed(node.getComponent(Sprite),'avatar');if(url==='')return;
|
||||
const original=sprite.spriteFrame,lease=roomAvatarCache.acquire(url);let released=false;
|
||||
const release=()=>{if(released)return;released=true;if(isValid(node))sprite.spriteFrame=original;lease.release();};this.avatarOwners.set(node,release);
|
||||
void lease.value.then(({frame})=>{if(released||this.disposed||!isValid(node))return;sprite.sizeMode=Sprite.SizeMode.CUSTOM;sprite.spriteFrame=frame;},error=>{if(!released&&!this.disposed)console.error('[RoomUi]',error);});
|
||||
}
|
||||
dispose():void{
|
||||
if(this.disposed)return;this.disposed=true;const errors:unknown[]=[];
|
||||
for(const release of [...this.avatarOwners.values(),...this.releases.splice(0).reverse()])try{release();}catch(error){errors.push(error);}
|
||||
this.avatarOwners.clear();if(errors.length)throw Object.assign(Error('Room UI cleanup failed'),{errors});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "18fe9a15-6004-4b15-a4bf-ad99253dc8a7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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 gameKey:string='Game_Surface_3';
|
||||
@property(Prefab) createRoom:Prefab|null=null;
|
||||
@property(Prefab) room:Prefab|null=null;
|
||||
@property(SceneAsset) roomScene:SceneAsset|null=null;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type {PlatformState} from '../../framework/platform/stores/platform-types.ts';
|
||||
|
||||
export interface ApplyVoteModel {
|
||||
readonly message:string;
|
||||
readonly rows:readonly {seat:number;nickname:string;status:string}[];
|
||||
readonly seconds:number|null;
|
||||
readonly canVote:boolean;
|
||||
readonly result:boolean;
|
||||
}
|
||||
/** GameUI.OpenApply/OpenApplyResult. Server snapshots alone determine the result. */
|
||||
export function selectApplyVote(state:PlatformState,now:number):ApplyVoteModel|null {
|
||||
const vote=state.dissolution;
|
||||
if(!vote||vote.kind==='result'&&vote.freeNow)return null;
|
||||
if(vote.kind==='result'&&!vote.passed){
|
||||
const refused=vote.participants.find(p=>p.seat===vote.refusedSeat);
|
||||
if(!refused)throw Error('Dissolution result missing refusing player');
|
||||
return {message:`投票结果:\n\n拒绝解散玩家:【${refused.nickname}】`,rows:[],seconds:null,canVote:false,result:true};
|
||||
}
|
||||
if(!vote.initiator)throw Error('Dissolution missing initiator');
|
||||
const rows=vote.participants.filter(p=>p.seat!==vote.initiator!.seat&&(vote.kind==='active'||p.state===1))
|
||||
.map(p=>({seat:p.seat,nickname:`【${p.nickname}】`,status:p.state===1?'同意':p.state===2?'拒绝':'等待'}));
|
||||
if(vote.kind==='result')return {message:`投票结果:\n\n投票发起:【${vote.initiator.nickname}】`,rows,seconds:null,canVote:false,result:true};
|
||||
const self=vote.participants.find(p=>p.seat===vote.selfSeat);
|
||||
if(!self)throw Error('Dissolution missing self participant');
|
||||
return {message:`发起投票:【${vote.initiator.nickname}】\n请问是否同意?(倒计时结束未选择,则默认同意)`,rows,
|
||||
seconds:Math.max(0,Math.ceil((vote.deadline-now)/1000)),
|
||||
canVote:state.app.phase==='logged-in'&&self.state===-1,result:false};
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e1dec3fb-6d05-4242-9e7c-b718a168c3f3",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {assetManager,ImageAsset,SpriteFrame,Texture2D} from 'cc';
|
||||
import {ResourceLeaseCache} from '../../framework/presentation/resource-lease-cache.ts';
|
||||
/** One bounded application cache; only referenced textures are leased by views. */
|
||||
export const roomAvatarCache=new ResourceLeaseCache<{frame:SpriteFrame;texture:Texture2D}>({
|
||||
idleCapacity:32,
|
||||
load:url=>new Promise((resolve,reject)=>assetManager.loadRemote<ImageAsset>(url,{ext:'.png'},(error,image)=>{
|
||||
if(error){reject(error);return;}
|
||||
const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;resolve({frame,texture});
|
||||
})),
|
||||
destroy:({frame,texture})=>{frame.destroy();texture.destroy();},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "34b523da-476f-4081-be46-ca91fdd90342",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import {OwnedFeedback} from '../../framework/presentation/owned-feedback.ts';
|
||||
import type {OwnerHandle} from '../../framework/sdk/contracts/game-capabilities.ts';
|
||||
import {Animation, BlockInputEvents, Label, Node, Prefab, UITransform, Widget, instantiate, isValid} from 'cc';
|
||||
import {requireNode} from './node-bindings.ts';
|
||||
|
||||
@@ -10,6 +12,7 @@ export class FeedbackPanels {
|
||||
private reconnectNode:Node;
|
||||
private kickNode:Node;
|
||||
private locked=false;
|
||||
private readonly owned:OwnedFeedback;
|
||||
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);
|
||||
@@ -19,6 +22,7 @@ export class FeedbackPanels {
|
||||
};
|
||||
this.loadingNode=mount(assets.loading);this.reconnectNode=mount(assets.reconnect);
|
||||
this.tipNode=mount(assets.tips);this.kickNode=mount(assets.kick);
|
||||
this.owned=new OwnedFeedback({loading:show=>this.renderLoading(show),tip:(message,time)=>this.renderTips(message,time),closeTip:()=>this.hideTip()});
|
||||
requireNode(this.tipNode,'group-89').active=false;
|
||||
this.tipNode.on(Node.EventType.TOUCH_END,()=>this.closeTips());
|
||||
}
|
||||
@@ -36,20 +40,27 @@ export class FeedbackPanels {
|
||||
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();
|
||||
showTips(message:string,time=3000):void{if(this.locked||!message)return;this.owned.showPlatformTip(message,time);}
|
||||
showBusinessTip(message:string,time:number):OwnerHandle{if(this.locked)throw Error('Feedback is locked by server kick');return this.owned.showBusinessTip(message,time);}
|
||||
beginLoading():OwnerHandle{if(this.locked)throw Error('Feedback is locked by server kick');return this.owned.beginLoading();}
|
||||
private renderTips(message:string,time:number):void{
|
||||
if(this.locked||!message)return;this.hideTip();
|
||||
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();
|
||||
}
|
||||
closeTips():void{this.owned.dismissTip();}
|
||||
private hideTip():void{if(this.timer!==null)clearTimeout(this.timer);this.timer=null;if(isValid(this.tipNode))this.tipNode.active=false;}
|
||||
loading(show:boolean):void{this.owned.setPlatformLoading(show);}
|
||||
private renderLoading(show:boolean):void{
|
||||
// Scene replacement may destroy this panel before its owner is disposed.
|
||||
if(!show&&!isValid(this.loadingNode))return;
|
||||
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();
|
||||
}
|
||||
reconnect(show:boolean):void{
|
||||
const animation=requireNode(this.reconnectNode,'group-35/重连动画').getComponent(Animation);
|
||||
if(!animation||!animation.defaultClip)throw Error('Reconnect requires serialized AnimationClip');
|
||||
@@ -57,12 +68,15 @@ export class FeedbackPanels {
|
||||
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.locked=true;this.owned.clear();this.reconnect(false);
|
||||
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();}
|
||||
const errors:unknown[]=[];
|
||||
try{this.owned.dispose();}catch(error){errors.push(error);}
|
||||
for(const n of this.nodes){if(!isValid(n))continue;try{for(const animation of n.getComponentsInChildren(Animation))animation.stop();}catch(error){errors.push(error);}finally{try{n.destroy();}catch(error){errors.push(error);}}}
|
||||
this.nodes=[];
|
||||
if(errors.length)throw Object.assign(new Error('Feedback panel cleanup failed'),{errors:Object.freeze(errors)});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {Roomtype} from '../../framework/sdk/contracts/roomtype.ts';
|
||||
import { assetManager, BlockInputEvents, Button, EditBox, ImageAsset, Label, Mask, Node, Prefab, ScrollView, Sprite, SpriteFrame, Texture2D, UITransform, Widget, instantiate, isValid } from 'cc';
|
||||
import {requireRoomtype} from '../../framework/protocol/contracts/validation.ts';
|
||||
import { assetManager, BlockInputEvents, Button, EditBox, ImageAsset, Label, Layout, 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';
|
||||
@@ -9,6 +10,7 @@ import { AnnouncementView } from './AnnouncementView.ts';
|
||||
import { NoticeTabsView } from './NoticeTabsView.ts';
|
||||
import { OwnerRoomListView } from './OwnerRoomListView.ts';
|
||||
import { OptionalSwitchAnimation } from './OptionalSwitchAnimation.ts';
|
||||
import type {PlatformSettings} from '../../framework/presentation/platform-settings.ts';
|
||||
import { ProfileInfoListView } from './ProfileInfoListView.ts';
|
||||
import { validateProfileAuth } from '../../framework/presentation/profile-auth.ts';
|
||||
import {RequestLoading} from '../../framework/presentation/request-loading.ts';
|
||||
@@ -46,7 +48,8 @@ export class LobbyPanel {
|
||||
private cardValue:number|null=null;
|
||||
private beanValue:number|null=null;
|
||||
private readonly menuNodes=new Map<number,Node>();
|
||||
private readonly taskTip:Node;
|
||||
private readonly taskTip:Node;
|
||||
private readonly buttonBar:Node;
|
||||
private taskData:readonly TaskData[]|undefined;
|
||||
private readonly requestLoading:RequestLoading;
|
||||
private taskPending:'list'|'award'|null=null;
|
||||
@@ -70,12 +73,18 @@ export class LobbyPanel {
|
||||
private readonly nativeCall:(name:string,data:Record<string,unknown>)=>void,
|
||||
private readonly vip?:{request:(codes:readonly VipCode[])=>void;join:(code:VipCode)=>void;describe:(roomtype: Roomtype)=>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;
|
||||
private readonly extras?:{settings?:PlatformSettings;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: Roomtype):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);
|
||||
this.menu=this.add(assets.menu,canvas);this.header=this.add(assets.header,canvas);this.noticeMenu=this.add(assets.noticeMenu,canvas);
|
||||
this.buttonBar=requireNode(this.menu,'group-3/ButtonBar');
|
||||
need(this.buttonBar.getComponent(Layout),'ButtonBar Layout');
|
||||
for(const node of this.buttonBar.children){
|
||||
const widget=node.getComponent(Widget);
|
||||
if(widget)widget.isAlignLeft=widget.isAlignRight=widget.isAlignHorizontalCenter=false;
|
||||
}
|
||||
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]){
|
||||
@@ -89,7 +98,7 @@ export class LobbyPanel {
|
||||
// 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.
|
||||
const vipRank=requireNode(this.menu,'group-3/VipRank/Spirit679');
|
||||
const vipRank=requireNode(this.menu,'group-3/MiddleButton/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='客服信息';
|
||||
@@ -104,10 +113,10 @@ export class LobbyPanel {
|
||||
this.applyMenuState(lobbyMenuState(menuConfig,context));
|
||||
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;
|
||||
requireNode(this.menu,`group-3/MiddleButton/Spirit${id}/Locked`).active=locked;
|
||||
}
|
||||
if(policy.contacts.length>0)requireNode(vipRank,'Spirit16').active=false;
|
||||
this.taskTip=requireNode(this.menu,'group-3/Spirit17/Tip');
|
||||
this.taskTip=requireNode(this.menu,'group-3/ButtonBar/Spirit17/Tip');
|
||||
need(this.taskTip.getComponent(Sprite),'Spirit17/Tip Sprite');
|
||||
this.render(state);
|
||||
const key=`tsgame_gameNoteTime_${config.identity.gameid}${config.identity.agentid}`;
|
||||
@@ -160,7 +169,10 @@ export class LobbyPanel {
|
||||
// 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 node=this.menuNodes.get(Number(id));if(!node)continue;
|
||||
// ButtonBar now owns horizontal placement. Legacy hideSpid uses an offscreen
|
||||
// top coordinate; remove that entry from the layout instead of placing it back.
|
||||
if(node.parent===this.buttonBar){if(state.top[Number(id)]!==undefined)node.active=false;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.
|
||||
@@ -175,8 +187,9 @@ export class LobbyPanel {
|
||||
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(widget)widget.updateAlignment();
|
||||
}
|
||||
need(this.buttonBar.getComponent(Layout),'ButtonBar Layout').updateLayout();
|
||||
}
|
||||
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;
|
||||
@@ -189,7 +202,7 @@ export class LobbyPanel {
|
||||
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));}};
|
||||
try{action();}catch(error){console.error('[lobby-panel]',error);}};
|
||||
node.on(Button.EventType.CLICK,listener);
|
||||
(modal?this.modalReleases:this.releases).push(()=>{if(isValid(node))node.off(Button.EventType.CLICK,listener);});
|
||||
}
|
||||
@@ -256,7 +269,7 @@ export class LobbyPanel {
|
||||
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;}
|
||||
if(error){console.error('[lobby-panel]',error);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();
|
||||
@@ -317,7 +330,7 @@ export class LobbyPanel {
|
||||
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 onError=(message:string)=>console.error('[lobby-panel] rank image',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();});
|
||||
}
|
||||
@@ -339,7 +352,7 @@ export class LobbyPanel {
|
||||
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;});
|
||||
.catch(error=>{if(this.modal===modal)console.error('[lobby-panel]',error);}).finally(()=>{pasting=false;});
|
||||
},true);
|
||||
this.bind(requireNode(group,'Spirit3266'),()=>{
|
||||
const cardno=edit.string;if(cardno===''||/^[ ]+$/.test(cardno))return;
|
||||
@@ -356,7 +369,7 @@ export class LobbyPanel {
|
||||
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;}
|
||||
if(error){console.error('[lobby-panel]',error);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();});
|
||||
@@ -399,15 +412,13 @@ export class LobbyPanel {
|
||||
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 settings=need(this.extras?.settings,'platform settings');
|
||||
for(const [name,key] of [['音乐底','bgm'],['音效底','sound'],['Spirit357','shake'],['Spirit388','voice']] 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);
|
||||
const render=(animate:boolean)=>{const on=settings.read(key);animation.setState(on,this.assets.switches[on?0:1],animate);};render(false);
|
||||
this.bind(node,()=>{try{settings.toggle(key);}finally{render(true);}},true);
|
||||
}
|
||||
}
|
||||
openNotice():void {
|
||||
const modal=this.open(this.assets.noticeBoard,'notice','group-12'),group=requireNode(modal,'group-12');
|
||||
@@ -453,7 +464,7 @@ export class LobbyPanel {
|
||||
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;}
|
||||
if(error){console.error('[lobby-panel]',error);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();};
|
||||
@@ -470,7 +481,7 @@ export class LobbyPanel {
|
||||
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)});
|
||||
paste:()=>need(this.extras,'clipboard').paste(),reportError:text=>console.error('[JoinRoomView]',text)});
|
||||
view.setBusy(commands.busy());
|
||||
}
|
||||
openCreate():void {
|
||||
@@ -480,10 +491,10 @@ export class LobbyPanel {
|
||||
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||!(typeof saved.data==='string'||Array.isArray(saved.data)))throw Error('Invalid saved roomtype');previous=saved.data;
|
||||
const saved=JSON.parse(text);previous=requireRoomtype(saved?.data,'saved.roomtype');
|
||||
}
|
||||
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.open({previousRoomtype:previous,submit:roomtype=>{if(current())commands.create(roomtype);},cancel:()=>{if(this.modal===modal)this.close();},reportError:text=>{if(current())console.error('[CreateRoomPage]',text);}});
|
||||
page.setBusy(commands.busy());
|
||||
}
|
||||
setRoomBusy(busy:boolean):void{this.roomPage?.setBusy(busy);}
|
||||
@@ -499,7 +510,7 @@ export class LobbyPanel {
|
||||
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;}
|
||||
if(error){console.error('[lobby-panel]',error);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();});
|
||||
});
|
||||
@@ -518,21 +529,21 @@ export class LobbyPanel {
|
||||
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);
|
||||
this.bind(requireNode(group,'Spirit3199'),()=>console.error('[lobby-panel]',`房间 ${view.selectedCode}:短房号排行模块尚未接入`),true);
|
||||
for(const id of [3194,3195,3214])this.bind(requireNode(group,`Spirit${id}`),()=>console.error('[lobby-panel] 黑名单管理模块尚未接入'),true);
|
||||
for(const id of [3196,3197,3215])this.bind(requireNode(group,`Spirit${id}`),()=>console.error('[lobby-panel] 统一魅力值设置模块尚未接入'),true);
|
||||
this.bind(requireNode(group,'Spirit3235'),()=>console.error('[lobby-panel] 好友管理模块尚未接入'),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));}}};
|
||||
const join=(code:VipCode)=>{if(this.model.canAct&&this.modal===modal){try{port.join(code);}catch(e){console.error('[lobby-panel]',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);
|
||||
const timer=setInterval(()=>{try{if(this.vipHistory().read().length>0)refresh();}catch(e){console.error('[lobby-panel]',e);}},delay);
|
||||
this.modalReleases.push(()=>clearInterval(timer));
|
||||
if(this.vipData)this.renderVipRooms(this.vipData);
|
||||
refresh();
|
||||
@@ -563,7 +574,7 @@ export class LobbyPanel {
|
||||
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));}
|
||||
try{this.requestTask('award',()=>this.claimTask(taskid));}catch(error){console.error('[lobby-panel]',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);
|
||||
@@ -600,7 +611,7 @@ export class LobbyPanel {
|
||||
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;});
|
||||
}).catch(error=>{if(!this.disposed&&this.modal===modal)console.error('[lobby-panel]',error);}).finally(()=>{pasting=false;});
|
||||
},true);
|
||||
this.bind(requireNode(group,'Spirit367'),()=>{
|
||||
const text=edit.string;edit.blur();this.close();
|
||||
|
||||
@@ -45,7 +45,7 @@ export class LoginPanel {
|
||||
const sprite=requireNode(logining,'登录中头像').getComponent(Sprite);if(!sprite)throw Error('登录中头像 requires Sprite');
|
||||
assetManager.loadRemote<ImageAsset>(cached.avatar,{ext:'.png'},(error,image)=>{
|
||||
if(this.disposed||!isValid(sprite))return;
|
||||
if(error){this.tip(`登录头像加载失败:${error.message}`);return;}
|
||||
if(error){console.error('[login-panel]',error);return;}
|
||||
const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame;
|
||||
this.releases.push(()=>{if(isValid(sprite))sprite.spriteFrame=null;frame.destroy();texture.destroy();});
|
||||
});
|
||||
@@ -62,7 +62,7 @@ export class LoginPanel {
|
||||
this.bind(requireNode(group,'隐藏快速登录-2'),()=>accounts.hiddenSecond());
|
||||
}
|
||||
private bind(node:Node,action:()=>void,modal=false){
|
||||
const listener=()=>{if(this.disposed||!this.canAct())return;try{action();}catch(error){this.tip(error instanceof Error?error.message:String(error));}};
|
||||
const listener=()=>{if(this.disposed||!this.canAct())return;try{action();}catch(error){console.error('[login-panel]',error);}};
|
||||
node.on(Node.EventType.TOUCH_END,listener);
|
||||
(modal?this.modalReleases:this.releases).push(()=>node.off(Node.EventType.TOUCH_END,listener));
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ export class SceneFlow {
|
||||
if(this.disposed)throw Error('SceneFlow disposed');
|
||||
await bind();
|
||||
});
|
||||
this.pending=next;
|
||||
// A failed destination must not permanently poison later, explicitly requested transitions.
|
||||
this.pending=next.catch(()=>{});
|
||||
return next;
|
||||
}
|
||||
enterOnce(name:string,bind:()=>void|Promise<void>):Promise<void>{
|
||||
|
||||
Reference in New Issue
Block a user