Files
joywayer 35216f75e7 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.
2026-09-09 03:13:18 +08:00

32 lines
4.1 KiB
TypeScript

import {readFileSync} from 'node:fs';
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {PlatformStore} from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts';
import {parseLoginResponse,parseOtherExitRoomPayload} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts';
function setup(){const data=JSON.parse(readFileSync(new URL('../fixtures/contracts/player-login-room.json',import.meta.url),'utf8')).data;data.players[0]={...data.players[1],playerid:777,nickname:'other'};const store=new PlatformStore();store.applyLoginSuccess(parseLoginResponse(data));return store;}
test('local message IDs remain stable across append and do not come from raw server input',()=>{
const store=setup();store.applyRoomSocial('send_text',{seat:1,text:'same',id:999});
const first=store.getState().roomSocial!.history[0];store.applyRoomSocial('send_text',{seat:1,text:'same',id:999});
const history=store.getState().roomSocial!.history;assert.equal(history[0],first);assert.notEqual(history[1].id,first.id);assert.notEqual(first.id,999);
});
test('chat retains sender snapshot after leaving, accepts raw phrase indices and announcement without seat',()=>{const store=setup();store.applyRoomSocial('send_text',{seat:0,text:'你好',type:0});const first=store.getState().roomSocial!.history[0];store.playerExit(parseOtherExitRoomPayload({seat:0}));assert.equal(store.getState().roomSocial!.history[0],first);assert.ok(Object.isFrozen(first.sender));store.applyRoomSocial('send_text',{seat:1,text:'2',type:3});assert.equal(store.getState().roomSocial!.history[1].kind,'phrase');store.applyRoomSocial('send_text',{text:'公告',type:1});assert.deepEqual(store.getState().roomSocial!.announcements,['公告']);});
test('invalid social input never commits, voice retains original duration, room exit clears history',()=>{const store=setup();store.applyRoomSocial('send_voice',{seat:1,voiceurl:'https://audio/one',time:'1.25'});assert.equal(store.getState().roomSocial!.history[0].time,1.25);const state=store.getState();assert.throws(()=>store.applyRoomSocial('send_phiz',{seat:1,text:0}));assert.equal(store.getState(),state);store.clearRoom();assert.equal(store.getState().roomSocial,undefined);});
test('change_seat swaps occupancy and own seat without altering sender history',()=>{const store=setup();store.applyRoomSocial('send_text',{seat:1,text:'a'});const before=store.getState().room;assert.ok(before.kind==='inside');store.changeSeats({seat1:1,seat2:0});const room=store.getState().room;assert.ok(room.kind==='inside');assert.equal(room.selfSeat,0);assert.equal(room.seatPlayerIds[0],before.seatPlayerIds[1]);assert.equal(store.getState().roomSocial!.history[0].sender.seat,1);});
test('ordinary voice auto-plays while robot voice remains history only',()=>{
const store=setup();
for(const type of [undefined,0,'0',2,'2'])store.applyRoomSocial('send_voice',{seat:1,voiceurl:'https://audio',time:1,...(type===undefined?{}:{type}),...(Number(type)===2?{info:{nickname:'客服'}}:{})});
assert.deepEqual(store.getState().roomSocial!.history.map(message=>message.autoPlay),[true,true,true,false,false]);
const snapshot=store.getState();assert.throws(()=>store.applyRoomSocial('send_voice',{type:1,seat:1,voiceurl:'url',time:1}));assert.equal(store.getState(),snapshot);
});
test('room owner notice comes only from ownerNotice and empty source has explicit null semantics',()=>{
const source=()=>JSON.parse(readFileSync(new URL('../fixtures/contracts/player-login-room.json',import.meta.url),'utf8')).data;
for(const value of [undefined,null,'','房主公告内容']){
const data=source();if(value!==undefined)data.ownerNotice=value;data.ownerNote='private note must not replace notice';
const store=new PlatformStore();store.applyLoginSuccess(parseLoginResponse(data));const room=store.getState().room;assert.ok(room.kind==='inside');assert.equal(room.ownerNotice,value?value:null);
}
const store=setup(),before=store.getState(),bad=source();bad.ownerNotice={text:'not string'};assert.throws(()=>store.applyLoginSuccess(parseLoginResponse(bad)));assert.equal(store.getState(),before);
});