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:
2026-09-09 03:13:18 +08:00
parent a7ec8aa6a7
commit 35216f75e7
1028 changed files with 835556 additions and 21415 deletions
@@ -0,0 +1,6 @@
import {_decorator} from 'cc';
import {GAME_DEFINITIONS} from './game-definition.ts';
@_decorator.ccclass('GameEntry_Game_Surface_3')
export class TemplateGameEntry {
static readonly definition=GAME_DEFINITIONS.Game_Surface_3;
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ca0ce008-3ed5-4075-bec9-91aa79785ae7",
"files": [],
"subMetas": {},
"userData": {}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "584dbb85-4acc-4bea-9d9b-e0cbeb8741da",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "Layer27_CreateRoom"
}
}
@@ -27,7 +27,7 @@ export class TemplateCreateRoomView extends CreateRoomPage {
toggle.node.on(Toggle.EventType.TOGGLE,changed);this.releases.push(()=>toggle.node.off(Toggle.EventType.TOGGLE,changed));
});
this.groups.forEach(group=>group.allowSwitchOff=false);
const bind=(button:Button,action:()=>void)=>{const fn=()=>{if(!this.busy&&this.context)try{action();}catch(error){context.reportError(error instanceof Error?error.message:String(error));}};
const bind=(button:Button,action:()=>void)=>{const fn=()=>{if(!this.busy&&this.context)try{action();}catch(error){console.error('[TemplateCreateRoomView]',error);}};
button.node.on(Button.EventType.CLICK,fn);this.releases.push(()=>button.node.off(Button.EventType.CLICK,fn));};
bind(this.submitButton,()=>context.submit(this.rules.build()));
this.cancelButtons.forEach(button=>{const cancel=()=>context.cancel();button.node.on(Button.EventType.CLICK,cancel);this.releases.push(()=>button.node.off(Button.EventType.CLICK,cancel));});this.setBusy(false);
@@ -1,39 +1,5 @@
import {_decorator,Button,Component,Label} from 'cc';
import type {PlatformGameSnapshot} from '../../framework/sdk/contracts/index.ts';
import type {TemplateRoomActions,TemplateRoomPresentation} from './template-game.ts';
const {ccclass,property}=_decorator;
/** Template lobby-to-room handoff surface; concrete subgames replace this presentation. */
import {_decorator} from 'cc';
import {CommonRoomViewBase} from '../../scripts/platform-login/CommonRoomViewBase.ts';
const {ccclass}=_decorator;
@ccclass('TemplateRoomView')
export class TemplateRoomView extends Component implements TemplateRoomPresentation {
@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:TemplateRoomActions|null=null;
private bound=false;
private pending:'prepare'|'exit'|null=null;
private timer:ReturnType<typeof setTimeout>|null=null;
private snapshot:PlatformGameSnapshot|null=null;
private readonly prepare=()=>this.invoke('prepare');
private readonly exit=()=>this.invoke('exit');
render(snapshot:PlatformGameSnapshot,actions:TemplateRoomActions):void{
if(!this.roomLabel||!this.playersLabel||!this.prepareButton||!this.exitButton)throw Error('TemplateRoomView missing controls');
this.actions=actions;this.snapshot=snapshot;
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=this.pending===null&&snapshot.connection.phase==='logged-in'&&snapshot.room.stage===0&&!self?.ready;
this.exitButton.interactable=this.pending===null&&snapshot.connection.phase==='logged-in';
}
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.node.emit('room-action-timeout');},15000);
}catch(error){this.node.emit('room-action-error',error);}
}
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();}
}
export class TemplateRoomView extends CommonRoomViewBase {}
@@ -0,0 +1,2 @@
/** XML is the sole identity/version source. This config only locates it. */
export const gameConfig = Object.freeze({versionResource:'Game_Surface_3/version',assetRoot:'games/template'});
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "69288246-9bc5-4ec2-9afa-54574e136a4f",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,13 @@
import {gameConfig} from './game-config.ts';
import {createTemplateGame} from './template-game.ts';
import {roomChatConfig} from './room-chat-config.ts';
import type {GameDefinition} from '../../framework/sdk/contracts/game-definition.ts';
export const GAME_DEFINITIONS = {
Game_Surface_3:{
config:gameConfig,key:'Game_Surface_3',route:'Game_Surface_3',
createGame:function(this:GameDefinition,gameId:string|number){return createTemplateGame(gameId,this);},
chat:roomChatConfig,roomMenu:{mainSceneButton:false,vipInfinite:false},
createPageClass:'TemplateCreateRoomView',roomViewClass:'TemplateRoomView',
resources:{createRoom:'games/template/Layer27_CreateRoom.prefab',room:'games/template/TemplateRoom.prefab',roomScene:'scenes/TemplateRoom.scene'},
},
} as const satisfies Readonly<Record<string,GameDefinition>>;
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e3fb84ba-d2b5-42f8-8f86-897f14cdceb0",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,14 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "ebfeb186-fb9e-403e-86f4-bce2d7f5411f",
"files": [],
"subMetas": {},
"userData": {
"isBundle": true,
"bundleConfigID": "default",
"bundleName": "version-Game_Surface_3",
"priority": 2
}
}
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "00fe2433-722e-42a6-86b7-d646b79b2ca4",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<game>
<agent id="1B2h0ccl205c390Y28m1Ajdplkuu4wgy" name="天盛网络"/>
<game id="8x4l0rGjf026f60c48h0mbUAhK5vV16f" name="友乐游戏"/>
<channel id="aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG" name="友乐互动游戏"/>
<version value="1" name="1.1"/>
</game>
@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "f3c5f3ca-cd7c-4571-9a64-830629c787e9",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "afdab568-f194-4b97-a3ea-bb55baaa8360",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,29 +1,39 @@
import type {GameRoomProjection,GameRoomProjectionSource} from '../../framework/sdk/contracts/game-room-projection.ts';
import type {GameEntry,GameHost,GameModule,PlatformGameSnapshot} from '../../framework/sdk/contracts/index.ts';
import {TemplateRoomRules} from './template-room-rules.ts';
import type {PlatformToGameEvent} from '../../framework/sdk/contracts/platform-events.ts';
import {requireRoomCapabilities,type RoomCapabilities} from '../../framework/sdk/contracts/index.ts';
export interface TemplateRoomActions {prepare():void;exit():void;}
export interface TemplateRoomPresentation {render(snapshot:PlatformGameSnapshot,actions:TemplateRoomActions):void;close():void;}
export interface TemplateRoomPresentation {render(snapshot:PlatformGameSnapshot,actions:TemplateRoomActions,projection?:GameRoomProjection|null):void;close():void;}
/** Template hooks are intentionally empty, matching 02_SubGame_Input.js. No gameplay is inferred. */
export function createTemplateGame(gameId:string|number){
export function createTemplateGame(gameId:string|number,identity:Pick<GameEntry,'key'|'route'>){
let active:TemplateModule|null=null;
class TemplateModule implements GameModule {
private host:GameHost|null=null;
private capabilities:RoomCapabilities|null=null;
readonly roomProjection:GameRoomProjectionSource|null=null;
private unsubscribeProjection:(()=>void)|null=null;
private unsubscribe:(()=>void)|null=null;
private view:TemplateRoomPresentation|null=null;
restored:unknown=null;
attach(host:GameHost):void{
if(this.host||active)throw Error('Template module already attached');
this.host=host;active=this;this.unsubscribe=host.subscribe(()=>this.render());
if(this.capabilities||active)throw Error('Template module already attached');
this.capabilities=requireRoomCapabilities(host);active=this;this.unsubscribe=this.capabilities.room.subscribe(()=>this.render());
this.unsubscribeProjection=this.roomProjection?.subscribe(()=>this.render())??null;
}
mount(view:TemplateRoomPresentation):void{this.view?.close();this.view=view;this.render();}
private requireHost():GameHost{if(!this.host)throw Error('Template room session disposed');return this.host;}
private render():void{this.view?.render(this.requireHost().getSnapshot(),{prepare:()=>this.requireHost().execute({type:'room.prepare'}),exit:()=>this.requireHost().execute({type:'room.exit'})});}
handlePlatformEvent():void{this.render();}
private requireCapabilities():RoomCapabilities{if(!this.capabilities)throw Error('Template room session disposed');return this.capabilities;}
private render():void{this.view?.render(this.requireCapabilities().room.getSnapshot(),{prepare:()=>this.requireCapabilities().room.prepare(),exit:()=>this.requireCapabilities().room.requestExit()},this.roomProjection?.getSnapshot()??null);}
handlePlatformEvent(event:PlatformToGameEvent):void{
if(event.type==='room.dissolution-confirmed')throw Error('模板游戏解散结算界面尚未接入');
this.render();
}
handleGameMessage():void{/* Original template _ReceiveData hook is empty. */}
restore(deskinfo:unknown):void{this.requireHost();this.restored=deskinfo;}
dispose():void{this.unsubscribe?.();this.unsubscribe=null;this.view?.close();this.view=null;this.host=null;this.restored=null;if(active===this)active=null;}
restore(deskinfo:unknown):void{this.requireCapabilities();this.restored=deskinfo;}
dispose():void{this.unsubscribeProjection?.();this.unsubscribeProjection=null;this.unsubscribe?.();this.unsubscribe=null;this.view?.close();this.view=null;this.capabilities=null;this.restored=null;if(active===this)active=null;}
}
const rules=new TemplateRoomRules();
const entry:GameEntry={key:'Game_Surface_3',gameId,route:'Game_Surface_3',resolveSeatCount:roomtype=>rules.seatCount(roomtype),createModule:()=>new TemplateModule()};
const entry:GameEntry={key:identity.key,gameId,route:identity.route,resolveSeatCount:roomtype=>rules.seatCount(roomtype),createModule:()=>new TemplateModule()};
return {entry,mount(view:TemplateRoomPresentation){if(!active)throw Error('Template room not attached');active.mount(view);},
get roomProjection(){return active?.roomProjection??null;},
get restoredSnapshot():unknown{return active?.restored;}};
}
@@ -13,7 +13,7 @@ export class TemplateRoomRules {
if(!Number.isInteger(index)||index<0||index>1)throw Error('Invalid template option');
this.selected[group]=index;
}
build():unknown[]{return [1,4,1,2,2,[1,1,[1,2000,10],null,null,1],[1,0,5]];}
build():Roomtype[]{return [1,4,1,2,2,[1,1,[1,2000,10],null,null,1],[1,0,5]];}
seatCount(roomtype: Roomtype):number{
if(!Array.isArray(roomtype))throw Error('Template roomtype must be an array');
// Desk.Create uses Game_Config.Max.PlayerCnt because the template defines no getMaxPlayerCount hook.