feat: integrate erqiwang rooms and use spec-compliant Web compilation

This commit is contained in:
2026-09-07 22:36:24 +08:00
parent 30a1b4114c
commit a7ec8aa6a7
49 changed files with 11603 additions and 1183 deletions
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "66d74934-1469-4fe1-ad7d-92b5cd70e688",
"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": "c1608cf0-5b48-4714-8a06-9363c854ab71",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "ErqiwangCreateRoom"
}
}
@@ -0,0 +1,40 @@
import {_decorator,Button,Label,Toggle,ToggleContainer} from 'cc';
import {CreateRoomPage} from '../../scripts/platform-login/CreateRoomPage.ts';
import type {CreateRoomPageContext} from '../../framework/sdk/contracts/create-room-page.ts';
import {ErqiwangRoomRules} from './erqiwang-room-rules.ts';
const {ccclass,property}=_decorator;
@ccclass('ErqiwangCreateRoomView')
export class ErqiwangCreateRoomView extends CreateRoomPage {
@property([ToggleContainer]) groups:ToggleContainer[]=[];
@property([Toggle]) options:Toggle[]=[];
@property([Label]) optionLabels:Label[]=[];
@property(Button) submitButton:Button|null=null;
@property([Button]) cancelButtons:Button[]=[];
@property(Label) feeLabel:Label|null=null;
private readonly rules=new ErqiwangRoomRules();
private context:CreateRoomPageContext|null=null;
private busy=false;
private releases:Array<()=>void>=[];
open(context:CreateRoomPageContext):void{
this.close();
if(this.groups.length!==3||this.options.length!==8||this.optionLabels.length!==8||!this.feeLabel||!this.submitButton||this.cancelButtons.length!==2)throw Error('ErqiwangCreateRoomView requires serialized controls');
this.rules.open(context.previousRoomtype);this.context=context;
// Permit all toggles to reset before restoring each mandatory group.
this.groups.forEach(group=>group.allowSwitchOff=true);
this.optionLabels.forEach((label,i)=>label.string=['6局','12局','房主扣卡','AA每人扣卡','可查牌','不查牌','傍王','爬坡'][i]);
this.options.forEach((toggle,i)=>{
toggle.isChecked=i<6?this.rules.selection[Math.floor(i/2)]===i%2:this.rules.selection[i-3]===1;
const changed=()=>{if(this.busy||!this.context)return;if(i<6&&!toggle.isChecked)return;this.rules.select(i<6?Math.floor(i/2):i-3,i<6?i%2:Number(toggle.isChecked));this.renderFee();};
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));}};
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=()=>{if(this.context===context)context.cancel();};button.node.on(Button.EventType.CLICK,cancel);this.releases.push(()=>button.node.off(Button.EventType.CLICK,cancel));});this.setBusy(false);this.renderFee();
}
private renderFee():void{if(this.feeLabel)this.feeLabel.string=`${this.rules.selection[1]===1?'每人':'房主'}消耗 ${this.rules.roomCards} 张房卡`; }
setBusy(busy:boolean):void{this.busy=busy;this.options.forEach(toggle=>toggle.interactable=!busy);if(this.submitButton)this.submitButton.interactable=!busy;}
close():void{for(const release of this.releases.splice(0))release();this.context=null;}
onDestroy():void{this.close();}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "987021eb-b5b3-4b73-bba9-39f3ea97c6b9",
"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": "28fcf5a7-d779-4400-9c71-300cf492bcc7",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "ErqiwangRoom"
}
}
@@ -0,0 +1,14 @@
import {_decorator} from 'cc';
import {TemplateRoomView} from '../template/TemplateRoomView.ts';
import type {TemplateRoomActions} from '../template/template-game.ts';
import type {PlatformGameSnapshot} from '../../framework/sdk/contracts/index.ts';
const {ccclass}=_decorator;
/** Temporary room receipt surface for the create-room integration stage. */
@ccclass('ErqiwangRoomView')
export class ErqiwangRoomView extends TemplateRoomView {
render(snapshot:PlatformGameSnapshot,actions:TemplateRoomActions):void{
super.render(snapshot,actions);
if(this.roomLabel)this.roomLabel.string='二七王 房间号:'+snapshot.room.roomcode+'\n玩法尚未接入';
if(this.prepareButton)this.prepareButton.node.active=false;
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "aeb4bcf6-864b-4d21-bfce-19b69f5c526c",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,29 @@
import type {GameEntry,GameHost,GameModule,PlatformGameSnapshot} from '../../framework/sdk/contracts/index.ts';
import {ErqiwangRoomRules} from './erqiwang-room-rules.ts';
export interface ErqiwangRoomActions {prepare():void;exit():void;}
export interface ErqiwangRoomPresentation {render(snapshot:PlatformGameSnapshot,actions:ErqiwangRoomActions):void;close():void;}
/** Creation-stage room handoff. Gameplay and deskinfo rendering are not migrated yet. */
export function createErqiwangGame(gameId:string|number){
let active:ErqiwangModule|null=null;
class ErqiwangModule implements GameModule {
private host:GameHost|null=null;
private unsubscribe:(()=>void)|null=null;
private view:ErqiwangRoomPresentation|null=null;
restored:unknown=null;
attach(host:GameHost):void{
if(this.host||active)throw Error('Erqiwang module already attached');
this.host=host;active=this;this.unsubscribe=host.subscribe(()=>this.render());
}
mount(view:ErqiwangRoomPresentation):void{this.view?.close();this.view=view;this.render();}
private requireHost():GameHost{if(!this.host)throw Error('Erqiwang 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();}
handleGameMessage():void{throw Error('二七王玩法消息处理尚未接入');}
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;}
}
const rules=new ErqiwangRoomRules();
const entry:GameEntry={key:'erqiwang',gameId,route:'erqiwang',resolveSeatCount:roomtype=>rules.seatCount(roomtype),createModule:()=>new ErqiwangModule()};
return {entry,mount(view:ErqiwangRoomPresentation){if(!active)throw Error('Erqiwang room not attached');active.mount(view);},
get restoredSnapshot():unknown{return active?.restored;}};
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "9ed79bcf-75f3-44b9-b171-b0f465b7b2fc",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,26 @@
import type {Roomtype} from '../../framework/sdk/contracts/roomtype.ts';
/** UI order differs from server class.config.js: aset, deduct, bangwang, climb, nocheck. */
const WIRE_INDEX = [0,1,4,2,3] as const;
export class ErqiwangRoomRules {
private selected=[0,0,0,0,0];
get selection():readonly number[]{return [...this.selected];}
get roomCards():number{return (this.selected[0]===1?2:1)*(this.selected[1]===1?1:2);}
open(previous:unknown):void{
if(previous===null){this.selected=[0,0,0,0,0];return;}
this.validate(previous);
this.selected=WIRE_INDEX.map(index=>Number((previous as string)[index]));
}
select(group:number,value:number):void{
if(!Number.isInteger(group)||group<0||group>=5||(value!==0&&value!==1))throw Error('Invalid erqiwang option');
this.selected[group]=value;
}
build():string{
const bits=Array<string>(11).fill('0');
WIRE_INDEX.forEach((wire,group)=>bits[wire]=String(this.selected[group]));
return bits.join('');
}
seatCount(roomtype:Roomtype):number{this.validate(roomtype);return 3;}
private validate(value:unknown):void{
if(typeof value!=='string'||!/^[01]{5}0{6}$/.test(value))throw Error('二七王房间配置必须为 11 位字符串,后 6 位为 0');
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "574b385e-9068-4418-8a63-110ad96d0915",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../framework/sdk/contracts/roomtype.ts';
/** Source: 01_SubGame_modify.js. Options are presentation state, not a rule-array encoder. */
export class TemplateRoomRules {
readonly labels=['10局(房卡X1)','20局(房卡X2)','房主扣卡','每人扣卡'] as const;
@@ -13,7 +14,7 @@ export class TemplateRoomRules {
this.selected[group]=index;
}
build():unknown[]{return [1,4,1,2,2,[1,1,[1,2000,10],null,null,1],[1,0,5]];}
seatCount(roomtype:readonly unknown[]):number{
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.
return 10;