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
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
export type VipCode = string | number;
/** min_ary_indexof in the legacy source compares short codes with ==. */
export const sameVipCode=(left:VipCode,right:VipCode):boolean=>left==right;
@@ -5,7 +6,7 @@ export interface VipRoom {
readonly shortCode: VipCode;
readonly title: string;
readonly owner: string;
readonly roomcodes: { readonly roomtype: readonly unknown[]; readonly roomlist: readonly VipCode[] } | null;
readonly roomcodes: { readonly roomtype: Roomtype; readonly roomlist: readonly VipCode[] } | null;
readonly playerList: readonly (readonly unknown[])[];
readonly deskState: readonly unknown[];
readonly seatCount: readonly VipCode[];
@@ -1,3 +1,4 @@
import type {Roomtype} from '../sdk/contracts/roomtype.ts';
import { APP } from '../core/constants.ts';
import type { OutboundEnvelope } from '../core/types/envelope.ts';
import type {
@@ -48,7 +49,7 @@ type SubscriptionReleaseResult =
interface BoundRoomGeneration {
readonly roomcode: string;
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly selfSeat: number;
readonly seatCount: number;
}
@@ -341,7 +341,7 @@ export class PlatformRuntime {
this.wireIsOpen = false;
const errors: unknown[] = [];
for (const off of [...this.stateObservers]) {
for (const off of [...this.stateObservers]) {
this.captureCleanupError(errors, off);
}
this.captureCleanupError(errors, () => { this.clearLoginGuard(); });
@@ -556,7 +556,7 @@ export class PlatformRuntime {
this.connectionIntent = { type: 'none' };
this.wireIsOpen = false;
const errors: unknown[] = [];
for (const off of [...this.stateObservers]) {
for (const off of [...this.stateObservers]) {
this.captureCleanupError(errors, off);
}
this.captureCleanupError(errors, () => { this.store.setConnectionPhase('kicked'); });
@@ -722,7 +722,7 @@ export class PlatformRuntime {
this.wireIsOpen = false;
const cleanupErrors: unknown[] = [];
for (const off of [...this.stateObservers]) {
for (const off of [...this.stateObservers]) {
this.captureCleanupError(cleanupErrors, off);
}
this.captureCleanupError(cleanupErrors, () => { this.clearLoginGuard(); });
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
import type { Unsubscribe } from '../../core/reactive.ts';
import {parseVipRooms} from '../../protocol/contracts/vip-room.ts';
import {parseRankList,parseVipRankList} from '../../protocol/contracts/rank-list.ts';
@@ -6,7 +7,8 @@ import type {LoginAccountIdentity} from '../../protocol/contracts/login-contract
import {requireStringOrFiniteNumber} from '../../protocol/contracts/validation.ts';
import {
hasOwn,
requireArray,
requireArray,
requireRoomtype,
requireBoolean,
requireInteger,
requireNumber,
@@ -152,7 +154,7 @@ function verifyRoomtypeSourceGraph(graph: readonly RoomtypeObjectSnapshot[]): vo
}
}
export function cloneAndFreezeRoomtype(value: readonly unknown[]): readonly unknown[] {
export function cloneAndFreezeRoomtype(value: Roomtype): Roomtype {
const copies = new WeakMap<object, object>();
const active = new WeakSet<object>();
const ownedGraph: object[] = [];
@@ -241,7 +243,7 @@ export function cloneAndFreezeRoomtype(value: readonly unknown[]): readonly unkn
return target;
};
const canonical = copy(value, 'roomtype') as readonly unknown[];
const canonical = copy(value, 'roomtype') as Roomtype;
verifyRoomtypeSourceGraph(sourceGraph);
for (let index = ownedGraph.length - 1; index >= 0; index -= 1) {
Object.freeze(ownedGraph[index]);
@@ -287,7 +289,7 @@ function buildRoom(
const raw = requireRecord(rawValue, '$.raw');
const roomcode = requireString(room.roomcode, '$.room.roomcode');
const selfSeat = requireInteger(room.seat, '$.room.seat');
const roomtype = requireArray(room.roomtype, '$.room.roomtype');
const roomtype = requireRoomtype(room.roomtype, '$.room.roomtype');
const sourcePlayers = requireArray(room.players, '$.room.players');
if (requireString(raw.roomcode, '$.raw.roomcode') !== roomcode) {
throw new RangeError('$.room.roomcode: expected value parsed from raw roomcode');
@@ -569,7 +571,7 @@ export class PlatformStore {
this.transitions.push({
state,
previous,
listeners: [...this.listeners],
listeners: [...this.listeners],
});
if (this.publishing) return state;
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
import type { PlatformGameConnectionPhase } from '../../sdk/contracts/snapshots.ts';
import type { TaskData } from '../../core/types/task.ts';
@@ -46,7 +47,7 @@ export interface InsideRoomState {
readonly seatPlayerIds: readonly (number | null)[];
readonly readySeats: readonly number[];
readonly offlineSeats: readonly number[];
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
@@ -1,7 +1,8 @@
export {
hasOwn,
optionalField,
requireArray,
requireArray,
requireRoomtype,
requireBoolean,
requireInteger,
requireNumber,
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
import {
buildFirstSliceEnvelope,
type FirstSliceEnvelope,
@@ -5,7 +6,8 @@ import {
import {
hasOwn,
optionalField,
requireArray,
requireArray,
requireRoomtype,
requireInteger,
requireNumber,
requireRecord,
@@ -22,7 +24,7 @@ export type ReconnectSnapshot =
export interface ParsedRoomSnapshot {
readonly roomcode: string;
readonly seat: number;
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly players: readonly unknown[];
}
@@ -40,7 +42,7 @@ export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot {
export function parseRoomEntryFields(raw: JsonRecord): ParsedRoomSnapshot {
const roomcode = requireString(raw.roomcode, '$.data.roomcode');
const seat = requireInteger(raw.seat, '$.data.seat');
const roomtype = requireArray(raw.roomtype, '$.data.roomtype');
const roomtype = requireRoomtype(raw.roomtype, '$.data.roomtype');
const players = requireArray(raw.players, '$.data.players');
if (seat < 0 || seat >= players.length) {
throw new RangeError(`$.data.seat: expected server seat index within players, received number`);
@@ -246,7 +248,7 @@ export function parseKickServerPayload(value: unknown): ParsedKickPayload {
export interface CreateRoomEnvironment {
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly location: unknown;
readonly ip: string;
}
@@ -267,7 +269,7 @@ export function buildCreateRoomRequest(
agentid: requireStringOrFiniteNumber(identity.agentid, '$.identity.agentid'),
playerid: requireInteger(playerid, '$.playerid'),
gameid: requireStringOrFiniteNumber(identity.gameid, '$.identity.gameid'),
roomtype: requireArray(raw.roomtype, '$.command.roomtype'),
roomtype: requireRoomtype(raw.roomtype, '$.command.roomtype'),
ip: requireString(raw.ip, '$.command.ip'),
location: raw.location,
});
@@ -275,7 +277,7 @@ export function buildCreateRoomRequest(
export interface ParsedCreateRoomResponse {
readonly state: number;
readonly room: { readonly roomcode: string; readonly seat: number; readonly roomtype: readonly unknown[] } | null;
readonly room: { readonly roomcode: string; readonly seat: number; readonly roomtype: Roomtype } | null;
readonly needprepare: number;
readonly infinite: number;
readonly raw: JsonRecord;
@@ -296,7 +298,7 @@ export function parseCreateRoomResponse(value: unknown): ParsedCreateRoomRespons
state,
room: {roomcode: requireString(raw.roomcode, '$.data.roomcode'),
seat: requireInteger(raw.seat, '$.data.seat'),
roomtype: requireArray(raw.roomtype, '$.data.roomtype')},
roomtype: requireRoomtype(raw.roomtype, '$.data.roomtype')},
needprepare: flag('needprepare'), infinite: flag('infinite'), raw,
};
}
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
export type JsonRecord = Readonly<Record<string, unknown>>;
function receivedType(value: unknown): string {
@@ -66,3 +67,9 @@ export function optionalField<T>(
if (!hasOwn(record, key)) return undefined;
return validate(record[key], `${path}.${key}`);
}
/** Validate the transport shape without interpreting game-specific options. */
export function requireRoomtype(value: unknown, path: string): Roomtype {
if (typeof value === 'string' || Array.isArray(value)) return value;
return invalid(path, 'string or array', value);
}
@@ -1,4 +1,4 @@
import {requireArray,requireRecord,requireString} from './validation.ts';
import {requireArray,requireRoomtype,requireRecord,requireString} from './validation.ts';
import type {VipCode,VipRoom} from '../../core/types/vip-room.ts';
export function vipCode(value:unknown,path:string):VipCode {
if(typeof value==='string'&&value.length>0)return value;
@@ -12,7 +12,8 @@ export function parseVipRooms(payload:unknown):readonly VipRoom[]{
const p=`$.data.roomInfo[${index}]`,r=requireRecord(value,p);
const playerList=requireArray(r.playerList,p+'.playerList').map((v,i)=>Object.freeze([...requireArray(v,`${p}.playerList[${i}]`)]));
const codes=r.roomcodes?requireRecord(r.roomcodes,p+'.roomcodes'):null;
const roomcodes=codes?Object.freeze({roomtype:Object.freeze([...requireArray(codes.roomtype,p+'.roomcodes.roomtype')]),roomlist:Object.freeze(requireArray(codes.roomlist,p+'.roomcodes.roomlist').map((v,i)=>vipCode(v,`${p}.roomcodes.roomlist[${i}]`)))}):null;
const rawType=codes?requireRoomtype(codes.roomtype,p+'.roomcodes.roomtype'):null;
const roomcodes=codes?Object.freeze({roomtype:typeof rawType==='string'?rawType:Object.freeze([...(rawType as readonly unknown[])]),roomlist:Object.freeze(requireArray(codes.roomlist,p+'.roomcodes.roomlist').map((v,i)=>vipCode(v,`${p}.roomcodes.roomlist[${i}]`)))}):null;
const deskState=roomcodes?requireArray(r.deskState,p+'.deskState'):[];
const seatCount=roomcodes?requireArray(r.seatCount,p+'.seatCount').map((v,i)=>vipCode(v,`${p}.seatCount[${i}]`)):[];
if(roomcodes&&[roomcodes.roomlist.length,deskState.length,seatCount.length].some(n=>n!==playerList.length))throw TypeError(p+': desk arrays must have matching lengths');
@@ -1,7 +1,8 @@
import type {Roomtype} from './roomtype.ts';
/** Pre-room capability: a game owns its page and validated, opaque wire configuration. */
export interface CreateRoomPageContext {
readonly previousRoomtype:unknown;
submit(roomtype:readonly unknown[]):void;
submit(roomtype: Roomtype):void;
cancel():void;
reportError(message:string):void;
}
@@ -1,3 +1,4 @@
import type {Roomtype} from './roomtype.ts';
import type { GameModule } from './game-module.ts';
/** The one compile-time game identity and module factory for an app. */
@@ -6,6 +7,6 @@ export interface GameEntry {
readonly gameId: string | number;
readonly route: string;
/** Interprets this game's actual server roomtype; no synthetic/default input is required. */
resolveSeatCount(roomtype: readonly unknown[]): number;
resolveSeatCount(roomtype: Roomtype): number;
createModule(): GameModule;
}
@@ -5,3 +5,5 @@ export * from './game-module.ts';
export * from './game-seat-mapper.ts';
export * from './platform-events.ts';
export * from './snapshots.ts';
export type {Roomtype} from './roomtype.ts';
@@ -1,6 +1,7 @@
import type {Roomtype} from './roomtype.ts';
/** Platform lifecycle events delivered to the current game module. */
export type PlatformToGameEvent =
| { readonly type: 'room.entered'; readonly roomtype: readonly unknown[] }
| { readonly type: 'room.entered'; readonly roomtype: Roomtype }
| { readonly type: 'room.player-joined'; readonly seat: number }
| { readonly type: 'room.player-left'; readonly seat: number }
| { readonly type: 'room.player-ready'; readonly seat: number }
@@ -0,0 +1,2 @@
/** Opaque subgame wire configuration. The game defines its contents. */
export type Roomtype = string | readonly unknown[];
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "69bfa9e3-15e3-4ed5-a26a-1fda48ffc906",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,3 +1,4 @@
import type {Roomtype} from './roomtype.ts';
/** Public connection phases available to a game. */
export type PlatformGameConnectionPhase =
| 'connected'
@@ -20,7 +21,7 @@ export interface PlatformGameSelfSnapshot {
/** Plain public room projection. `roomtype` remains opaque server data. */
export interface PlatformGameRoomSnapshot {
readonly roomcode: string;
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
@@ -1,3 +1,4 @@
import type {Roomtype} from '../contracts/roomtype.ts';
import type { strict } from 'node:assert';
import type {
GameEntry,
@@ -16,7 +17,7 @@ export type GameContractHostFactory = () => GameContractHost;
/** The game owns each room configuration, its expected count and matching Host fixture. */
export interface GameContractSample {
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly expectedSeatCount: number;
readonly makeHost: GameContractHostFactory;
}
@@ -701,7 +701,7 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -229.49841260344616,
"x": -231.99801937299284,
"y": 178,
"z": 0
},
@@ -742,8 +742,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 19.996825206892293,
"height": 25.200000000000003
"width": 24.996038745985672,
"height": 31.500000000000004
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -781,10 +781,10 @@
"_string": "消",
"_horizontalAlign": 0,
"_verticalAlign": 1,
"_actualFontSize": 21.083333333333336,
"_fontSize": 20,
"_actualFontSize": 26.354166666666668,
"_fontSize": 25,
"_fontFamily": "Arial",
"_lineHeight": 20,
"_lineHeight": 25,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
@@ -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;
@@ -1,661 +0,0 @@
[
{
"__type__": "cc.SceneAsset",
"_name": "Login",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"scene": {
"__id__": 1
}
},
{
"__type__": "cc.Scene",
"_name": "Login",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 4
}
],
"_active": true,
"_components": [],
"_prefab": {
"__id__": 19
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"autoReleaseAssets": false,
"_globals": {
"__id__": 20
},
"_id": "9490c809-bcb7-4a9e-af1f-acd680227bc4"
},
{
"__type__": "cc.Node",
"_name": "Camera",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 800,
"y": 360,
"z": 1000
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "a68KuGzBVD2IGLPd5z3pYi"
},
{
"__type__": "cc.Camera",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_projection": 0,
"_priority": 0,
"_fov": 45,
"_fovAxis": 0,
"_orthoHeight": 360,
"_near": 1,
"_far": 2000,
"_color": {
"__type__": "cc.Color",
"r": 51,
"g": 51,
"b": 51,
"a": 255
},
"_depth": 1,
"_stencil": 0,
"_clearFlags": 7,
"_rect": {
"__type__": "cc.Rect",
"x": 0,
"y": 0,
"width": 1,
"height": 1
},
"_aperture": 19,
"_shutter": 7,
"_iso": 0,
"_screenScale": 1,
"_visibility": 1117782015,
"_targetTexture": null,
"_postProcess": null,
"_usePostProcess": false,
"_cameraType": -1,
"_trackingType": 0,
"_id": "95vXCLVwZAnpgZ7/Uwwsji"
},
{
"__type__": "cc.Node",
"_name": "Canvas",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 5
},
{
"__id__": 13
}
],
"_active": true,
"_components": [
{
"__id__": 16
},
{
"__id__": 17
},
{
"__id__": 18
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 800,
"y": 360,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "70gbryz/VPqpu+opfSJoWa"
},
{
"__type__": "cc.Node",
"_objFlags": 0,
"_parent": {
"__id__": 4
},
"_prefab": {
"__id__": 6
},
"__editorExtras__": {}
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 5
},
"asset": {
"__uuid__": "b209cefb-b381-4c7c-b765-94a5ab75a89c",
"__expectedType__": "cc.Prefab"
},
"fileId": "33yDavvzhB6IAjia4PLDbg",
"instance": {
"__id__": 7
},
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.PrefabInstance",
"fileId": "ebnOxopupO4ap7jFhZuQbk",
"prefabRootNode": null,
"mountedChildren": [],
"mountedComponents": [],
"propertyOverrides": [
{
"__id__": 8
},
{
"__id__": 10
},
{
"__id__": 11
},
{
"__id__": 12
}
],
"removedComponents": []
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 9
},
"propertyPath": [
"_name"
],
"value": "Login_Layer"
},
{
"__type__": "cc.TargetInfo",
"localID": [
"33yDavvzhB6IAjia4PLDbg"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 9
},
"propertyPath": [
"_lpos"
],
"value": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
}
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 9
},
"propertyPath": [
"_lrot"
],
"value": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
}
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 9
},
"propertyPath": [
"_euler"
],
"value": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
}
},
{
"__type__": "cc.Node",
"_name": "LoginFlow_Node",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 4
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 14
},
{
"__id__": 15
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "fcp86q55FCsoAB/fOiWeHO"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 13
},
"_enabled": true,
"__prefab": null,
"_contentSize": {
"__type__": "cc.Size",
"width": 100,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "50hCb+fYBIgL72OODgBoQD"
},
{
"__type__": "bad5buGaitIB7iC7Qf+eoiB",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 13
},
"_enabled": true,
"__prefab": null,
"_id": "85RWmRT71EzIOWG6Qsha/3"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 4
},
"_enabled": true,
"__prefab": null,
"_contentSize": {
"__type__": "cc.Size",
"width": 1600,
"height": 720
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "c6QNB3wx9GYYzOAodcozbv"
},
{
"__type__": "cc.Canvas",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 4
},
"_enabled": true,
"__prefab": null,
"_cameraComponent": null,
"_alignCanvasWithScreen": true,
"_id": "48VGgW1kRITZOItV2ROj/Y"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 4
},
"_enabled": true,
"__prefab": null,
"_alignFlags": 45,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 1600,
"_originalHeight": 720,
"_alignMode": 1,
"_lockFlags": 0,
"_id": "d9PQijkfZPf7WjO0Xufvgu"
},
{
"__type__": "cc.PrefabInfo",
"root": null,
"asset": null,
"fileId": "9490c809-bcb7-4a9e-af1f-acd680227bc4",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": [
{
"__id__": 5
}
]
},
{
"__type__": "cc.SceneGlobals",
"ambient": {
"__id__": 21
},
"shadows": {
"__id__": 22
},
"_skybox": {
"__id__": 23
},
"fog": {
"__id__": 24
},
"octree": {
"__id__": 25
},
"skin": {
"__id__": 26
},
"lightProbeInfo": {
"__id__": 27
},
"postSettings": {
"__id__": 28
},
"bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false
},
{
"__type__": "cc.AmbientInfo",
"_skyColorHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 0.520833125
},
"_skyColor": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 0.520833125
},
"_skyIllumHDR": 20000,
"_skyIllum": 20000,
"_groundAlbedoHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_groundAlbedo": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_skyColorLDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 1
},
"_skyIllumLDR": 20000,
"_groundAlbedoLDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
}
},
{
"__type__": "cc.ShadowsInfo",
"_enabled": false,
"_type": 0,
"_normal": {
"__type__": "cc.Vec3",
"x": 0,
"y": 1,
"z": 0
},
"_distance": 0,
"_planeBias": 1,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 76
},
"_maxReceived": 4,
"_size": {
"__type__": "cc.Vec2",
"x": 1024,
"y": 1024
}
},
{
"__type__": "cc.SkyboxInfo",
"_envLightingType": 0,
"_envmapHDR": null,
"_envmap": null,
"_envmapLDR": null,
"_diffuseMapHDR": null,
"_diffuseMapLDR": null,
"_enabled": false,
"_useHDR": true,
"_editableMaterial": null,
"_reflectionHDR": null,
"_reflectionLDR": null,
"_rotationAngle": 0
},
{
"__type__": "cc.FogInfo",
"_type": 0,
"_fogColor": {
"__type__": "cc.Color",
"r": 200,
"g": 200,
"b": 200,
"a": 255
},
"_enabled": false,
"_fogDensity": 0.3,
"_fogStart": 0.5,
"_fogEnd": 300,
"_fogAtten": 5,
"_fogTop": 1.5,
"_fogRange": 1.2,
"_accurate": false
},
{
"__type__": "cc.OctreeInfo",
"_enabled": false,
"_minPos": {
"__type__": "cc.Vec3",
"x": -1024,
"y": -1024,
"z": -1024
},
"_maxPos": {
"__type__": "cc.Vec3",
"x": 1024,
"y": 1024,
"z": 1024
},
"_depth": 8
},
{
"__type__": "cc.SkinInfo",
"_enabled": true,
"_blurRadius": 0.01,
"_sssIntensity": 3
},
{
"__type__": "cc.LightProbeInfo",
"_giScale": 1,
"_giSamples": 1024,
"_bounces": 2,
"_reduceRinging": 0,
"_showProbe": true,
"_showWireframe": true,
"_showConvex": false,
"_data": null,
"_lightProbeSphereVolume": 1
},
{
"__type__": "cc.PostSettingsInfo",
"_toneMappingType": 0
}
]
@@ -1,11 +0,0 @@
{
"ver": "1.1.50",
"importer": "scene",
"imported": true,
"uuid": "9490c809-bcb7-4a9e-af1f-acd680227bc4",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}
@@ -395,12 +395,13 @@
},
"_enabled": true,
"__prefab": null,
"gameKey": "erqiwang",
"createRoom": {
"__uuid__": "584dbb85-4acc-4bea-9d9b-e0cbeb8741da",
"__uuid__": "c1608cf0-5b48-4714-8a06-9363c854ab71",
"__expectedType__": "cc.Prefab"
},
"room": {
"__uuid__": "f4582832-23e2-4981-be26-08dfe832aa96",
"__uuid__": "28fcf5a7-d779-4400-9c71-300cf492bcc7",
"__expectedType__": "cc.Prefab"
},
"roomScene": {
@@ -1,449 +0,0 @@
[
{
"__type__": "cc.SceneAsset",
"_name": "scene-2d",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"scene": {
"__id__": 1
}
},
{
"__type__": "cc.Scene",
"_name": "scene-2d",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_components": [],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"autoReleaseAssets": false,
"_globals": {
"__id__": 8
},
"_id": "aa6aad6d-aef0-4b69-95a0-18ffda41f4c7"
},
{
"__type__": "cc.Node",
"_name": "Canvas",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 3
}
],
"_active": true,
"_components": [
{
"__id__": 5
},
{
"__id__": 6
},
{
"__id__": 7
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 800,
"y": 360.00000000000006,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "beI88Z2HpFELqR4T5EMHpg"
},
{
"__type__": "cc.Node",
"_name": "Camera",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 2
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 4
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 1000
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "ebFwiq8gBFaYpqYbdoDODe"
},
{
"__type__": "cc.Camera",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 3
},
"_enabled": true,
"__prefab": null,
"_projection": 0,
"_priority": 0,
"_fov": 45,
"_fovAxis": 0,
"_orthoHeight": 360,
"_near": 0,
"_far": 2000,
"_color": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_depth": 1,
"_stencil": 0,
"_clearFlags": 7,
"_rect": {
"__type__": "cc.Rect",
"x": 0,
"y": 0,
"width": 1,
"height": 1
},
"_aperture": 19,
"_shutter": 7,
"_iso": 0,
"_screenScale": 1,
"_visibility": 1108344832,
"_targetTexture": null,
"_postProcess": null,
"_usePostProcess": false,
"_cameraType": -1,
"_trackingType": 0,
"_id": "63WIch3o5BEYRlXzTT0oWc"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_contentSize": {
"__type__": "cc.Size",
"width": 1600,
"height": 720
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "d6rUX5yfhMlKoWX2bSbawx"
},
{
"__type__": "cc.Canvas",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_cameraComponent": {
"__id__": 4
},
"_alignCanvasWithScreen": true,
"_id": "12O/ljcVlEqLmVm3U2gEOQ"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_alignFlags": 45,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 5.684341886080802e-14,
"_bottom": 5.684341886080802e-14,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": "c5V1EV8IpMtrIvY1OE9t2u"
},
{
"__type__": "cc.SceneGlobals",
"ambient": {
"__id__": 9
},
"shadows": {
"__id__": 10
},
"_skybox": {
"__id__": 11
},
"fog": {
"__id__": 12
},
"octree": {
"__id__": 13
},
"skin": {
"__id__": 14
},
"lightProbeInfo": {
"__id__": 15
},
"postSettings": {
"__id__": 16
},
"bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false
},
{
"__type__": "cc.AmbientInfo",
"_skyColorHDR": {
"__type__": "cc.Vec4",
"x": 0,
"y": 0,
"z": 0,
"w": 0.520833125
},
"_skyColor": {
"__type__": "cc.Vec4",
"x": 0,
"y": 0,
"z": 0,
"w": 0.520833125
},
"_skyIllumHDR": 20000,
"_skyIllum": 20000,
"_groundAlbedoHDR": {
"__type__": "cc.Vec4",
"x": 0,
"y": 0,
"z": 0,
"w": 0
},
"_groundAlbedo": {
"__type__": "cc.Vec4",
"x": 0,
"y": 0,
"z": 0,
"w": 0
},
"_skyColorLDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 1
},
"_skyIllumLDR": 20000,
"_groundAlbedoLDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
}
},
{
"__type__": "cc.ShadowsInfo",
"_enabled": false,
"_type": 0,
"_normal": {
"__type__": "cc.Vec3",
"x": 0,
"y": 1,
"z": 0
},
"_distance": 0,
"_planeBias": 1,
"_shadowColor": {
"__type__": "cc.Color",
"r": 76,
"g": 76,
"b": 76,
"a": 255
},
"_maxReceived": 4,
"_size": {
"__type__": "cc.Vec2",
"x": 512,
"y": 512
}
},
{
"__type__": "cc.SkyboxInfo",
"_envLightingType": 0,
"_envmapHDR": null,
"_envmap": null,
"_envmapLDR": null,
"_diffuseMapHDR": null,
"_diffuseMapLDR": null,
"_enabled": false,
"_useHDR": true,
"_editableMaterial": null,
"_reflectionHDR": null,
"_reflectionLDR": null,
"_rotationAngle": 0
},
{
"__type__": "cc.FogInfo",
"_type": 0,
"_fogColor": {
"__type__": "cc.Color",
"r": 200,
"g": 200,
"b": 200,
"a": 255
},
"_enabled": false,
"_fogDensity": 0.3,
"_fogStart": 0.5,
"_fogEnd": 300,
"_fogAtten": 5,
"_fogTop": 1.5,
"_fogRange": 1.2,
"_accurate": false
},
{
"__type__": "cc.OctreeInfo",
"_enabled": false,
"_minPos": {
"__type__": "cc.Vec3",
"x": -1024,
"y": -1024,
"z": -1024
},
"_maxPos": {
"__type__": "cc.Vec3",
"x": 1024,
"y": 1024,
"z": 1024
},
"_depth": 8
},
{
"__type__": "cc.SkinInfo",
"_enabled": false,
"_blurRadius": 0.01,
"_sssIntensity": 3
},
{
"__type__": "cc.LightProbeInfo",
"_giScale": 1,
"_giSamples": 1024,
"_bounces": 2,
"_reduceRinging": 0,
"_showProbe": true,
"_showWireframe": true,
"_showConvex": false,
"_data": null,
"_lightProbeSphereVolume": 1
},
{
"__type__": "cc.PostSettingsInfo",
"_toneMappingType": 0
}
]
@@ -1,11 +0,0 @@
{
"ver": "1.1.50",
"importer": "scene",
"imported": true,
"uuid": "aa6aad6d-aef0-4b69-95a0-18ffda41f4c7",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}
@@ -1,3 +1,5 @@
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,
UITransform, Widget, director, instantiate, isValid } from 'cc';
import { resolveRuntimeConfig, type RuntimeConfig } from '../../framework/config/runtime-config.ts';
@@ -150,7 +152,9 @@ export class PlatformStartup extends Component {
/** 退出只重建账号会话;配置、原生桥和已加载资源由常驻组件保留。 */
private async startSession(firstStart:boolean):Promise<void>{
const identity=required(this.config,'config').identity;
this.game=createTemplateGame(identity.gameid);
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);
this.runtime=new PlatformRuntime({gameEntry:this.game.entry,
roomHistory:new OwnedRoomHistory(window.localStorage,identity.gameid,identity.agentid),
resolveRuntimeConfig:async()=>required(this.config,'config'),
@@ -359,7 +363,7 @@ export class PlatformStartup extends Component {
await this.flow.enter(required(assets.roomScene,'subgame room scene').name,()=>{
if(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=required(root.getComponent(TemplateRoomView),'TemplateRoomView');
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)));
root.on('room-action-timeout',()=>this.tip('房间操作超时,请重试'));
this.createStatus();required(this.game,'template game').mount(view);this.events.push({kind:'scene',data:'Room'});
@@ -3,6 +3,7 @@ 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;
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../framework/sdk/contracts/roomtype.ts';
import {_decorator,Button,Component,instantiate,Label,Node,Prefab,ScrollView,Sprite,SpriteFrame,UITransform} from 'cc';
import type {VipCode,VipRoom} from '../../framework/core/types/vip-room.ts';
import {sameVipCode} from '../../framework/core/types/vip-room.ts';
@@ -15,8 +16,8 @@ export class VipRoomListView extends Component {
@property([SpriteFrame]) deskFrames:SpriteFrame[]=[];
private rooms:readonly VipRoom[]=[];
private selected:VipCode|null=null;
private describe:(roomtype:readonly unknown[])=>string=()=>{throw Error('VIP room description provider is not configured');};
configure(describe:(roomtype:readonly unknown[])=>string):void {this.describe=describe;this.showRooms();}
private describe:(roomtype: Roomtype)=>string=()=>{throw Error('VIP room description provider is not configured');};
configure(describe:(roomtype: Roomtype)=>string):void {this.describe=describe;this.showRooms();}
get selectedCode():VipCode|null {return this.selected;}
render(rooms:readonly VipRoom[],codes:readonly VipCode[]):void {
this.rooms=orderedVipRooms(rooms,codes);
@@ -1,3 +1,4 @@
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 type { RuntimeConfig } from '../../framework/config/runtime-config.ts';
import { selectLobbyModel, shouldShowNotice, type LobbyModel, type LobbyPolicy } from '../../framework/presentation/lobby-model.ts';
@@ -67,10 +68,10 @@ export class LobbyPanel {
private readonly onAction:(action:LobbyAction)=>void,
private readonly claimTask:(taskid:number|string)=>void,
private readonly nativeCall:(name:string,data:Record<string,unknown>)=>void,
private readonly vip?:{request:(codes:readonly VipCode[])=>void;join:(code:VipCode)=>void;describe:(roomtype:readonly unknown[])=>string},
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;
room?:{createPrefab:Prefab;join(code:number):void;create(roomtype:readonly unknown[]):void;busy():boolean;close():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');
@@ -479,7 +480,7 @@ 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||!Array.isArray(saved.data))throw Error('Invalid saved roomtype');previous=saved.data;
const saved=JSON.parse(text);if(!saved||!(typeof saved.data==='string'||Array.isArray(saved.data)))throw Error('Invalid saved roomtype');previous=saved.data;
}
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);}});
@@ -7,5 +7,8 @@
"fitWidth": true,
"fitHeight": true
}
},
"script": {
"loose": false
}
}
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { readLayerXml, emitPrefabJson } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { ROOT, HOST_PROJECT } from '../../scripts/lib/paths.mjs';
import { ROOT } from '../../scripts/lib/paths.mjs';
// PoC 阶段 3 端到端: Layer00002.xml Login_Layer (18 spirits, group 2, 包含 Spirit405 渠道logo +
// Spirit177 游客登录 anchor-test cases). 验证生成 prefab 与 fixtures/Login_Layer.prefab 在
@@ -19,7 +19,7 @@ import { ROOT, HOST_PROJECT } from '../../scripts/lib/paths.mjs';
// ImgResID 427 的依赖. 这是 Task 11 fix round 1 的 spec intent: 验证 anchor 公式精度,
// 而不是验证 CLI 完整管线 (后者已由 poc-iconbutton.test.mjs / poc-protocol.test.mjs 覆盖).
const FIXTURE_XML = join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'Layer00002.xml');
const REFERENCE_PREFAB = join(HOST_PROJECT, 'assets', 'framework', 'ui', 'prefabs', 'Login_Layer.prefab');
const REFERENCE_PREFAB = join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'Login_Layer.prefab');
function buildFilteredLayer() {
const layer = readLayerXml(FIXTURE_XML);
@@ -98,4 +98,4 @@ test('PoC 阶段 3 - 游客登录 _lpos 误差 < 0.001 (bypass CLI)', () => {
assert.ok(Math.abs(genVisitor._lpos.x - expVisitor._lpos.x) < 0.001, `x mismatch: gen=${genVisitor._lpos.x} exp=${expVisitor._lpos.x}`);
assert.ok(Math.abs(genVisitor._lpos.y - expVisitor._lpos.y) < 0.001, `y mismatch: gen=${genVisitor._lpos.y} exp=${expVisitor._lpos.y}`);
});
});
@@ -824,6 +824,7 @@ test('canonical roomtype preserves sparse length, holes, and negative zero', ()
assert.equal(state.room.kind, 'inside');
if (state.room.kind !== 'inside') return;
assert.ok(Array.isArray(state.room.roomtype));
assert.equal(state.room.roomtype.length, 5);
assert.equal(0 in state.room.roomtype, false);
assert.equal(1 in state.room.roomtype, true);
@@ -604,7 +604,7 @@ for (const mutate of [
setup.session.handleLogin(fixture('player-login-success.json'));
const before = setup.store.getState();
const data = {state: 0, roomcode: '123456', seat: 0, roomtype: [1, {count: 4}]};
setup.gameSession.entry.resolveSeatCount = roomtype => { mutate(roomtype); return 4; };
setup.gameSession.entry.resolveSeatCount = roomtype => { assert.ok(Array.isArray(roomtype)); mutate(roomtype); return 4; };
assert.throws(() => setup.session.handleCreateRoom(data), TypeError);
assert.deepEqual(data.roomtype, [1, {count: 4}]);
assert.equal(Object.isFrozen(data.roomtype), false);
@@ -1,3 +1,4 @@
import {createErqiwangGame} from '../../YouleNexus/assets/games/erqiwang/erqiwang-game.ts';
import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。
const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const});
@@ -736,7 +737,7 @@ test('a denied join retains the lobby root, reports the raw outcome and permits
});
test('runtime resolves seats only from the actual game-owned server roomtype at room entry', async () => {
const roomtypes: Array<readonly unknown[]> = [];
const roomtypes: Array<string | readonly unknown[]> = [];
const attached: GameHost[] = [];
const setup = makeRuntime(runtimeConfig(), makeGameEntry({
resolveSeatCount(roomtype) {
@@ -795,7 +796,7 @@ for(const loginRoute of ['agent','room','platform'])test(`${loginRoute} login de
const before = runtime.state;
runtime.login(ACCOUNT);
const denial = { state: -1, msg: '短信验证码不正确', time: 0 };
assert.equal(wire.sent.at(-1)?.route,'agent','outbound login remains agent');
assert.equal(wire.sent[wire.sent.length - 1]?.route,'agent','outbound login remains agent');
wire.emit({ type: 'message', message: { route: loginRoute, rpc: 'player_login', data: denial } });
assert.equal(runtime.state, before);
@@ -1651,7 +1652,7 @@ test('kick_offline bypasses pending login and stops late navigation', async () =
await runtime.start();wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT);
const data={fromOther:true,gameid:'another-game'};
wire.emit({type:'message',message:{route:'agent',rpc:'kick_offline',data}});
assert.equal(scene.calls.at(-1)?.name,'kicked');assert.equal(scene.calls.at(-1)?.value,data);
assert.equal(scene.calls[scene.calls.length - 1]?.name,'kicked');assert.equal(scene.calls[scene.calls.length - 1]?.value,data);
assert.equal(wire.stopCalls,1);
const count=scene.calls.length;wire.emit({type:'open',server:'ws://agent'});
assert.equal(scene.calls.length,count);
@@ -1663,7 +1664,7 @@ test('show_message preserves text and legacy numeric timeout without navigating'
const before=runtime.state;
for(const time of ['3000','0','-1']){
wire.emit({type:'message',message:{route:'agent',rpc:'show_message',data:{msg:'服务器提示#第二行',time}}});
assert.deepEqual(scene.calls.at(-1),{name:'tips',value:{message:'服务器提示#第二行',time:Number(time)}});
assert.deepEqual(scene.calls[scene.calls.length - 1],{name:'tips',value:{message:'服务器提示#第二行',time:Number(time)}});
assert.equal(runtime.state,before);
}
runtime.stop();
@@ -2394,3 +2395,18 @@ test('create wire outcome enters only on success and a repeated success leaves t
if(runtime.state.room.kind==='inside')assert.equal(runtime.state.room.seatPlayerIds.length,10);
runtime.stop();assert.equal(disposals,1);
});
test('erqiwang creation preserves wire string, three seats and room host exit',async()=>{
const config=runtimeConfig(),game=createErqiwangGame(config.identity.gameid);
const {runtime,wire,scene}=makeRuntime(config,game.entry);await runtime.start();
wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT);
wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data:fixture('player-login-success.json')}});
const roomtype='11011000000';runtime.createRoom({roomtype,location:null,ip:'127.0.0.1'});
assert.equal((wire.sent[wire.sent.length - 1]?.data as {roomtype:unknown}).roomtype,roomtype);
wire.emit({type:'message',message:{route:'agent',rpc:'create_room',data:{state:0,roomcode:'123456',seat:0,roomtype,needprepare:1}}});
assert.equal(runtime.state.room.kind,'inside');
if(runtime.state.room.kind==='inside'){assert.equal(runtime.state.room.roomtype,roomtype);assert.equal(runtime.state.room.seatPlayerIds.length,3);}
assert.equal(scene.calls.filter(x=>x.name==='room').length,1);
let rendered=false;game.mount({render(snapshot,actions){assert.equal(snapshot.room.roomtype,roomtype);rendered=true;actions.exit();},close(){}});
assert.ok(rendered);assert.equal(wire.sent[wire.sent.length - 1]?.rpc,'self_exit_room');runtime.stop();
});
@@ -0,0 +1,36 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {cloneAndFreezeRoomtype} from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts';
import {parseCreateRoomResponse,parseRoomEntryFields} from '../../YouleNexus/assets/framework/protocol/contracts/room-contracts.ts';
import {ErqiwangRoomRules} from '../../YouleNexus/assets/games/erqiwang/erqiwang-room-rules.ts';
import {createRequire} from 'node:module';
const require=createRequire(import.meta.url);
const server=require('../../../server/games/erqiwang/class.config.js');
test('all 32 UI combinations match actual server config, round-trip and fees',()=>{
for(let mask=0;mask<32;mask++){
const r=new ErqiwangRoomRules();r.open(null);
for(let group=0;group<5;group++)r.select(group,(mask>>group)&1);
const code=r.build();assert.match(code,/^[01]{5}0{6}$/);
const cfg=server.parse(code);
assert.deepEqual([cfg.aset12,cfg.deductAA,cfg.nocheck,cfg.bangwang,cfg.climb],r.selection.map(Boolean));
const copy=new ErqiwangRoomRules();copy.open(code);assert.deepEqual(copy.selection,r.selection);
assert.equal(r.roomCards,(cfg.aset12?2:1)*(cfg.deductAA?1:2));
assert.equal(r.seatCount(code),3);
}
});
test('required groups have defaults and malformed saved configuration is rejected',()=>{
const r=new ErqiwangRoomRules();r.open(null);assert.equal(r.build(),'00000000000');
for(const invalid of ['00000','00000000001','2'.repeat(11),[],{},123])assert.throws(()=>r.open(invalid));
assert.throws(()=>r.select(5,0));assert.throws(()=>r.select(0,2));
});
test('platform preserves erqiwang string through canonical state and entry responses',()=>{
const code='10100000000';
assert.equal(cloneAndFreezeRoomtype(code as never),code);
assert.equal(parseCreateRoomResponse({state:0,roomcode:'123456',seat:0,roomtype:code}).room?.roomtype,code);
assert.equal(parseRoomEntryFields({roomcode:'123456',seat:0,roomtype:code,players:[{},null,null]}).roomtype,code);
const array=[1,[2]];const copied=cloneAndFreezeRoomtype(array);
assert.deepEqual(copied,array);assert.notEqual(copied,array);
assert.ok(Object.isFrozen(copied));
});
@@ -11,5 +11,5 @@ test('template module attaches only when opened and releases its view and subscr
let actions:any;game.mount({render:(s,a)=>{views.push(s);actions=a;},close:()=>views.push('closed')});
actions.exit();actions.prepare();assert.deepEqual(commands,[{type:'room.exit'},{type:'room.prepare'}]);
const deskinfo={nested:[1,2]};module.restore(deskinfo);assert.equal(game.restoredSnapshot,deskinfo);
module.dispose();assert.equal(unsubscribed,1);assert.equal(views.at(-1),'closed');assert.throws(()=>actions.exit());
module.dispose();assert.equal(unsubscribed,1);assert.equal(views[views.length - 1],'closed');assert.throws(()=>actions.exit());
});
@@ -0,0 +1,39 @@
# 二七王创建房间接入
核对日期:2026-09-07。本篇记录创建房间阶段的实际实现,不代表二七王牌桌玩法已迁移完成。
## 资源与入口
工程为 `cocoscreator_projects/YouleNexus`。`PlatformStartup.scene` 的 `PlatformServices / SubgameAssets` 已配置:
- `gameKey = erqiwang`。
- `createRoom`:`assets/games/erqiwang/ErqiwangCreateRoom.prefab`。
- `room`:`assets/games/erqiwang/ErqiwangRoom.prefab`。
- `roomScene`:沿用 `assets/scenes/TemplateRoom.scene` 的通用场景容器。
联调 gameid 按用户确认沿用默认 `J9r4022t80pxlb01zOK76DN1WD4QewFl`,身份仍由运行配置提供,不在创建页重复定义。
## 页面与规则
`ErqiwangCreateRoomView` 继承平台 `CreateRoomPage`,只通过上下文提交、取消和报告错误。平台统一处理登录身份、IP、定位、请求信封、等待、超时和回包。
Inspector 绑定三组 `ToggleContainer`,`allowSwitchOff=false`;八个 Toggle 依次为 6 局、12 局、房主扣卡、AA 每人扣卡、可查牌、不查牌、傍王、爬坡。最后两个 Toggle 没有 ToggleContainer,可以独立勾选或取消。费用由局数与扣卡方式联动计算。
规则模型 `erqiwang-room-rules.ts` 是前端选项与编码转换的唯一来源。界面顺序为局数、扣卡、查牌、傍王、爬坡;实际服务器编码顺序仍为局数、扣卡、傍王、爬坡、查牌。尾部六位填零,总长度十一位。默认 `00000000000`。协议依据见 [服务端协议 §0.5](../../../../server/games/erqiwang/docs/protocol/packet_protocol.md#05-roomtype-房间选项位串),不能按界面顺序直接发包。
模型拒绝错误长度、非二进制选项、非零预留位以及数组型历史配置;历史来自平台成功回包保存的 `tsgame_roomtype_ + gameid + agentid`,格式仍为 JSON `{data: roomtype}`。没有历史时使用规则来源定义的默认选择,不静默修补非法历史。
## 平台契约与房间交接
公共 SDK 的 `Roomtype = string | readonly unknown[]` 用于创建页面、请求、成功响应、状态、GameEntry、GameHost 快照及事件。字符串保持原值,数组仍复制并冻结;平台不解释二七王位含义。VIP 房间配置同样保留字符串。
创建成功后,`createErqiwangGame` 校验配置并提供三个服务器座位,平台维护权威状态,临时房间页显示房号、玩家以及“玩法尚未接入”。退出使用本房间 GameHost。准备按钮暂不展示,避免进入尚未迁移的牌桌流程。玩法消息处理未实现,收到会显式报错;deskinfo 仅保留原值,尚无玩法恢复表现。
## 验证与限制
- `framework-tests/presentation/erqiwang-room-entry.test.ts`:穷举 32 种选项组合,对照实际服务端 `class.config.js` 验证编码、恢复、费用和人数。
- `framework-tests/platform/runtime.test.ts`:模拟登录、创建出站包、成功回包、三座位状态、房间显示与退出命令。
- 相关回归测试 157 项通过;Cocos 工程与框架 TypeScript 检查、导入边界检查通过。
- Game View 实际坐标点击验证 11 项:必选互斥、费用、独立开关、精确编码、busy、防重复、关闭清理及历史恢复。新 prefab 共 52 处 UUID 引用有效。
本次未修改服务器代码,未在真实服务器创建房间。本地模拟通过不等于实际服务端创建成功;后续联调从大厅“创建房间”入口进行,并检查出站 roomtype 类型与值。