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,84 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {runInNewContext} from 'node:vm';
import {buildRoomMenuModel, roomMenuAction, roomEscapeConfirmation, type RoomMenuContext} from '../../YouleNexus/assets/framework/presentation/room-menu-model.ts';
import {resolveRoomChatPhrase, validateRoomChatText, roomChatByteLength, resolveRoomChatPolicy} from '../../YouleNexus/assets/framework/presentation/room-chat-config.ts';
import {getGameDefinition} from '../fixtures/game-definitions.ts';
const getRoomChatConfig=(key:string)=>getGameDefinition(key).chat;
import {resolveRoomMenuContext} from '../../YouleNexus/assets/framework/presentation/room-menu-context.ts';
const context: RoomMenuContext = {status:1,canexit:1,stage:0,playercnt:2,roomMode:1,infinite:0,shortcode:0,matchId:0,h5Version:1,appdownload:'https://example.test/app',hideEscape:false,mainSceneButton:false,vipInfinite:false,versionState:0,roomCardName:'房卡',bean:200,starName:'金币'};
const ids=(patch:Partial<RoomMenuContext>)=>buildRoomMenuModel({...context,...patch}).map(button=>button.spiritId);
test('legacy panel keeps ordered, compact visible buttons and final infinite override',()=>{
assert.deepEqual(ids({}),[151,150,185,233,513,656]);
assert.deepEqual(buildRoomMenuModel(context).map(button=>button.y),[75,168,261,354,447,540]);
assert.deepEqual(ids({stage:1,canexit:0}),[151,150,185,656]);
assert.deepEqual(ids({stage:1,canexit:1}),[151,150,233,656]);
assert.deepEqual(ids({stage:1,canexit:0,infinite:1}),[151,150,233,3014,656]);
assert.deepEqual(ids({infinite:1,shortcode:123}),[151,150,233,656]);
assert.deepEqual(ids({infinite:1,vipInfinite:true}),[151,150,233,656]);
});
test('legacy escape guards and match hide ordering are preserved',()=>{
for(const patch of [{status:2},{playercnt:3},{roomMode:0},{hideEscape:true},{mainSceneButton:true}])assert.ok(!ids(patch).includes(513));
assert.ok(ids({matchId:3}).includes(185));
assert.ok(!ids({h5Version:0}).includes(656));
assert.ok(!ids({appdownload:''}).includes(656));
});
test('menu actions distinguish owner exit, pregame break and in-game dissolution',()=>{
assert.deepEqual(roomMenuAction(233,context),{kind:'confirm',checkType:2,message:'房间仍存在,是否退出?',command:'room.exit'});
assert.deepEqual(roomMenuAction(233,{...context,status:2}),{kind:'command',command:'room.exit'});
assert.deepEqual(roomMenuAction(185,context),{kind:'confirm',checkType:1,message:'解散房间不扣房卡,是否解散?',command:'room.break'});
assert.equal(roomMenuAction(185,{...context,versionState:1}).message,' 是否解散房间?');
assert.deepEqual(roomMenuAction(185,{...context,stage:1,canexit:0}),{kind:'command',command:'room.apply-dissolution'});
assert.throws(()=>roomMenuAction(3014,context),/not visible/);
});
test('escape asks for quantity then keeps legacy confirmation and balance error',()=>{
assert.deepEqual(roomMenuAction(513,context),{kind:'input',message:'请输入投降扣除数量(底翻)'});
assert.deepEqual(roomEscapeConfirmation(context,30),{kind:'confirm',checkType:3,message:'将扣除30金币,确定逃跑?',command:'room.escape',count:30});
assert.deepEqual(roomEscapeConfirmation(context,300),{kind:'tip',message:'输入大于您拥有的300金币数:200!'});
assert.throws(()=>roomEscapeConfirmation(context,NaN));
});
test('chat config uses exact game source, paired audio and one-based incoming indexes',()=>{
const template=getRoomChatConfig('Game_Surface_3'),eqw=getRoomChatConfig('erqiwang');
assert.equal(resolveRoomChatPhrase(template,1).text,'你好');
assert.equal(resolveRoomChatPhrase(eqw,1).text,'快点吧,我等的花儿都谢了。');
assert.equal(resolveRoomChatPhrase(eqw,7).text,'我先走了,后会有期');
assert.ok(eqw.phrases.every(phrase=>phrase.audio===''));
assert.equal(eqw.limitLength,40);assert.equal(eqw.showChatMs,2000);
assert.throws(()=>getRoomChatConfig('unknown'),/Unknown/);
assert.equal(resolveRoomChatPhrase(eqw,8).text,eqw.phrases[0].text);
for(const index of [0,1.5,NaN])assert.throws(()=>resolveRoomChatPhrase(eqw,index),/index/);
});
test('chat validation preserves source byte count and substring filtering',()=>{
const config=getRoomChatConfig('Game_Surface_3');
assert.equal(roomChatByteLength('a^你😀'),9);
assert.doesNotThrow(()=>validateRoomChatText(config,'你好'));
assert.throws(()=>validateRoomChatText(config,' '),/请输入/);
assert.doesNotThrow(()=>validateRoomChatText(config,'你'.repeat(40)));
assert.throws(()=>validateRoomChatText(config,'你'.repeat(41)),/40/);
assert.doesNotThrow(()=>validateRoomChatText(config,'你'.repeat(21),true));
assert.throws(()=>validateRoomChatText(config,'毛泽东'),/内容含有敏感词汇请重新输入!/);
assert.equal(config.announcementLimit,10000);assert.equal(config.voiceHistoryTag,20000);
assert.deepEqual(resolveRoomChatPolicy({raw:{},getValue:()=>undefined}),{noticeLimit:-1,noticeCost:0});
assert.deepEqual(resolveRoomChatPolicy({raw:{},getValue:key=>key==='noticelimit'?100:2}),{noticeLimit:100,noticeCost:2});
});
test('room menu source resolver reports missing authoritative room state',()=>{
assert.throws(()=>resolveRoomMenuContext({room:{kind:'outside'}} as any,{} as any,getGameDefinition('Game_Surface_3').roomMenu,{h5Version:1,versionState:0}),/inside room/);
const state:any={room:{kind:'inside',roomcode:'123456',stage:0,infinite:0,roomMode:1,status:1,canexit:1,shortcode:null,matchId:null,seatPlayerIds:[1,null]},players:{selfPlayerId:1,entities:{1:{bean:100}}}};
const config:any={identity:{},remoteConfig:{raw:{},getValue:()=>undefined}};
const resolved=resolveRoomMenuContext(state,config,getGameDefinition('Game_Surface_3').roomMenu,{h5Version:1,versionState:0});
assert.equal(resolved.playercnt,1);assert.equal(resolved.shortcode,null);assert.equal(resolved.bean,100);
assert.equal(resolved.roomCardName,'房卡');assert.equal(resolved.appdownload,'');
delete state.room.status;
assert.throws(()=>resolveRoomMenuContext(state,config,getGameDefinition('Game_Surface_3').roomMenu,{h5Version:1,versionState:0}),/status/);
});
test('both configured banned corpora match their own authoritative legacy source',()=>{
for(const gameKey of ['Game_Surface_3','erqiwang']){
const source=readFileSync(new URL(`../../../projects/${gameKey}/js/00_Surface/03_Banwords.js`,import.meta.url),'utf8');
const literal=/var banwords\s*=\s*("[\s\S]*?");/.exec(source);
assert.ok(literal);
const words=String(runInNewContext(literal[1])).split('、');
assert.deepEqual(getRoomChatConfig(gameKey).bannedWords,words);
}
});