feat: integrate platform room entry, UI migration and erqiwang documentation

This commit is contained in:
2026-09-07 21:01:08 +08:00
parent 9c4510697c
commit 30a1b4114c
259 changed files with 84995 additions and 30201 deletions
@@ -0,0 +1,19 @@
type ClipboardHost=
|{kind:'settings';read:()=>unknown}
|{kind:'bridge';call:(name:'gamepastetext',data:string,callback:(value:unknown)=>void)=>void}
|{kind:'browser';read:()=>Promise<string>};
/** 05_Func: settings takes no arguments; WVJB takes "" and returns through its callback. */
export async function readClipboardText(host:ClipboardHost):Promise<string>{
const value=host.kind==='bridge'?await new Promise<unknown>((resolve,reject)=>{
const timer=setTimeout(()=>reject(Error('读取剪贴板超时,请手动输入')),10000);
try{host.call('gamepastetext','',value=>{clearTimeout(timer);resolve(value);});}
catch(error){clearTimeout(timer);reject(error);}
}):await host.read();
if(typeof value!=='string')throw Error('Invalid clipboard text');
return value;
}
export async function readInviteClipboard(host:ClipboardHost):Promise<string>{
return (await readClipboardText(host)).replace(/[^0-9]/g,'');
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4aeb3de8-f982-4246-95be-7dca058bc99d",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,25 @@
import {requireRecord} from '../../protocol/contracts/validation.ts';
/** Logic.readRoomList/addRoomToList/removeRoomFromList. This is owner-return history, not recently played rooms. */
export class OwnedRoomHistory {
private readonly key:string;
constructor(private readonly storage:Pick<Storage,'getItem'|'setItem'>,gameid:string|number,agentid:string|number){this.key='tsgame_roomlist_'+gameid+agentid;}
read():readonly number[]{
const text=this.storage.getItem(this.key);
if(!text){this.save([]);return [];}
const raw=requireRecord(JSON.parse(text),this.key);
if(!Array.isArray(raw.data)||raw.data.some(n=>typeof n!=='number'||!Number.isSafeInteger(n)||n<=0))throw Error('Invalid '+this.key);
return Object.freeze([...raw.data]);
}
private code(value:unknown):number {
if(!(typeof value==='string'&&/^[0-9]+$/.test(value)||typeof value==='number'))throw Error('Invalid room history roomcode');
const code=Number(value);if(!Number.isSafeInteger(code)||code<=0)throw Error('Invalid room history roomcode');return code;
}
private save(data:readonly number[]):void{this.storage.setItem(this.key,JSON.stringify({data}));}
joined(raw:Readonly<Record<string,unknown>>):void{
if(Number(raw.state)===99||Number(raw.state)===0&&Number(raw.isowner)===1){const code=this.code(raw.roomcode);this.save(this.read().filter(n=>n!==code));}
}
exited(raw:Readonly<Record<string,unknown>>,infinite:number):void{
if(Number(raw.isowner)!==1||infinite!==0)return;
const code=this.code(raw.roomcode),list=this.read();if(!list.includes(code))this.save([code,...list]);
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ca074aba-afc9-428f-93ea-199d30dba6ef",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,17 +1,19 @@
import type { ChannelIdentity } from '../identity.ts';
/** Legacy GameData launchWaitTime (04_Data.js): timer units are milliseconds. */
export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10;
/**
* 构建期默认渠道身份(旧 version.js 等价物)。
* gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。
*/
export const BUILD_IDENTITY: ChannelIdentity = {
// This game's entry in config_test/update_jsonv2.txt belongs to this agent/channel.
agentid: '1B2h0ccl205c390Y28m1Ajdplkuu4wgy',
channelid: 'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG',
gameid: 'J9r4022t80pxlb01zOK76DN1WD4QewFl',
marketid: 4,
version: 1, // 数字 versionCode(联调时改成本地服要求的版本号)
};
import type { ChannelIdentity } from '../identity.ts';
/** Legacy GameData launchWaitTime (04_Data.js): timer units are milliseconds. */
export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10;
/**
* 构建期默认渠道身份(旧 version.js 等价物)。
* gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。
*/
export const BUILD_IDENTITY: ChannelIdentity = {
// This game's entry in config_test/update_jsonv2.txt belongs to this agent/channel.
agentid: '1B2h0ccl205c390Y28m1Ajdplkuu4wgy',
channelid: 'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG',
// agentid: 'veRa0qrBf0df2K1G4de2tgfmVxB2jxpv',
// channelid: 'FtJf073aa0d6rI1xD8J1Y42fINTm0ziK',
gameid: 'J9r4022t80pxlb01zOK76DN1WD4QewFl',
marketid: 4,
version: 1, // 数字 versionCode(联调时改成本地服要求的版本号)
};
@@ -1,133 +1,181 @@
import type { RuntimeConfig } from '../config/runtime-config.ts';
import { buildFirstSliceEnvelope } from '../protocol/first-slice-routes.ts';
import type { OutboundEnvelope } from '../core/types/envelope.ts';
import {
buildExitRoomRequest,
buildJoinRoomRequest,
buildPrepareRequest,
type JoinRoomEnvironment,
type RoomIdentity,
} from '../protocol/contracts/index.ts';
import { PlatformStore } from './stores/platform-store.ts';
import type { InsideRoomState, PlatformState } from './stores/platform-types.ts';
/** The complete-envelope send capability used by platform command adapters. */
export interface OutboundEnvelopeSendPort {
send(envelope: OutboundEnvelope): void;
}
export interface PlatformCommandsOptions {
readonly runtimeConfig: RuntimeConfig;
readonly store: PlatformStore;
readonly send: OutboundEnvelopeSendPort;
}
export interface JoinRoomCommand extends JoinRoomEnvironment {
readonly roomcode: string;
}
export class UnsupportedGameHostCommandError extends Error {
constructor(message: string) {
super(message);
this.name = 'UnsupportedGameHostCommandError';
}
}
/** Builds platform-owned room requests exclusively through the first-slice contracts. */
export class PlatformCommands {
constructor(private readonly options: PlatformCommandsOptions) {}
getVipRooms(shortcodeList:readonly (string|number)[]):void {
this.taskPlayerId();
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('getInfoByShortCode',{agentid,gameid,shortcodeList:[...shortcodeList]}));
}
joinVipRoom(roomcode:string|number,location:unknown):void {
const playerid=this.taskPlayerId(),state=this.options.store.getState();
const ip=state.players.entities[playerid].ip;
if(typeof ip!=='string')throw Error('VIP join requires player login ip');
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:0,location,ip}));
}
getPlayerTasks(): void {
const playerid=this.taskPlayerId();
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('get_player_task',{agentid,playerid,gameid}));
}
claimTask(taskid:number|string):void {
const playerid=this.taskPlayerId();
const task=this.options.store.getState().tasks?.find(task=>String(task.taskid)===String(taskid));
if(!task||task.state!==1)throw Error('Task is not claimable');
// 11_GameUI claim click sends these three fields; Net._SendData adds none.
this.options.send.send(buildFirstSliceEnvelope('get_task_award',{
agentid:this.options.runtimeConfig.identity.agentid,playerid,taskid:task.taskid,
}));
}
private taskPlayerId():number {
const state=this.options.store.getState();
if(state.app.phase!=='logged-in'||state.room.kind!=='outside')throw Error('Task command requires logged-in lobby');
return this.requireAuthenticatedPlayerId(state);
}
prepare(): void {
const state = this.options.store.getState();
this.options.send.send(buildPrepareRequest(
this.options.runtimeConfig,
this.roomIdentity(state, this.requireInsideRoom(state)),
));
}
exitRoom(): void {
const state = this.options.store.getState();
const room = this.requireInsideRoom(state);
if (room.stage !== 0 && room.infinite !== 1) {
throw new UnsupportedGameHostCommandError('room.exit requires stage=0 unless infinite=1');
}
this.options.send.send(buildExitRoomRequest(
this.options.runtimeConfig,
this.roomIdentity(state, room),
));
}
joinRoom(command: JoinRoomCommand): void {
const state = this.options.store.getState();
if (state.room.kind !== 'outside') {
throw new UnsupportedGameHostCommandError('room.join requires player to be outside a room');
}
const playerid = this.requireAuthenticatedPlayerId(state);
this.options.send.send(buildJoinRoomRequest(
this.options.runtimeConfig,
{ playerid, roomcode: command.roomcode },
command,
));
}
private requireInsideRoom(state: PlatformState): InsideRoomState {
if (state.room.kind !== 'inside') {
throw new UnsupportedGameHostCommandError('room command requires player to be inside a room');
}
return state.room;
}
private requireAuthenticatedPlayerId(state: PlatformState): number {
const playerid = state.players.selfPlayerId;
if (playerid === null) {
throw new UnsupportedGameHostCommandError('room command requires an authenticated player');
}
if (state.players.entities[playerid] === undefined) {
throw new Error('players.entities: missing authenticated player');
}
return playerid;
}
private roomIdentity(state: PlatformState, room: InsideRoomState): RoomIdentity {
return {
playerid: this.requireAuthenticatedPlayerId(state),
roomcode: room.roomcode,
};
}
}
import type { RuntimeConfig } from '../config/runtime-config.ts';
import { buildFirstSliceEnvelope } from '../protocol/first-slice-routes.ts';
import type { OutboundEnvelope } from '../core/types/envelope.ts';
import {
buildCreateRoomRequest,
type CreateRoomEnvironment,
buildExitRoomRequest,
buildJoinRoomRequest,
buildPrepareRequest,
type JoinRoomEnvironment,
type RoomIdentity,
} from '../protocol/contracts/index.ts';
import { cloneAndFreezeRoomtype, PlatformStore } from './stores/platform-store.ts';
import type { InsideRoomState, PlatformState } from './stores/platform-types.ts';
/** The complete-envelope send capability used by platform command adapters. */
export interface OutboundEnvelopeSendPort {
send(envelope: OutboundEnvelope): void;
}
export interface PlatformCommandsOptions {
readonly runtimeConfig: RuntimeConfig;
readonly store: PlatformStore;
readonly send: OutboundEnvelopeSendPort;
}
export interface JoinRoomCommand extends JoinRoomEnvironment {
readonly roomcode: string | number;
}
export interface CreateRoomCommand extends CreateRoomEnvironment {}
export class UnsupportedGameHostCommandError extends Error {
constructor(message: string) {
super(message);
this.name = 'UnsupportedGameHostCommandError';
}
}
/** Builds platform-owned room requests exclusively through the first-slice contracts. */
export class PlatformCommands {
constructor(private readonly options: PlatformCommandsOptions) {}
getPlayerInvitecode():void {
const playerid=this.taskPlayerId(),player=this.options.store.getState().players.entities[playerid];
const {openid,unionid}=player;
if(typeof openid!=='string'||!(typeof unionid==='string'||typeof unionid==='number'&&Number.isFinite(unionid)))throw Error('Invite query requires login openid and unionid');
this.options.send.send(buildFirstSliceEnvelope('get_player_invitecode',{
agentid:this.options.runtimeConfig.identity.agentid,playerid,unionid,openid,
}));
}
bindInvitecode(invitecode:string):void {
const playerid=this.taskPlayerId();
if(typeof invitecode!=='string')throw Error('invitecode must be a string');
this.options.send.send(buildFirstSliceEnvelope('binding_invitecode',{
agentid:this.options.runtimeConfig.identity.agentid,playerid,invitecode,
}));
}
getVipRooms(shortcodeList:readonly (string|number)[]):void {
this.taskPlayerId();
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('getInfoByShortCode',{agentid,gameid,shortcodeList:[...shortcodeList]}));
}
joinVipRoom(roomcode:string|number,location:unknown):void {
const playerid=this.taskPlayerId(),state=this.options.store.getState();
const ip=state.players.entities[playerid].ip;
if(typeof ip!=='string')throw Error('VIP join requires player login ip');
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:0,location,ip}));
}
getPlayerTasks(): void {
const playerid=this.taskPlayerId();
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('get_player_task',{agentid,playerid,gameid}));
}
getVipRankList(limit:number):void {
this.taskPlayerId();if(!Number.isInteger(limit)||limit<=0)throw Error('Invalid vipRankLimit');
this.options.send.send(buildFirstSliceEnvelope('getVipRankList',{agentid:this.options.runtimeConfig.identity.agentid,limit}));
}
joinOwnedRoom(roomcode:number,location:unknown):void {
const playerid=this.taskPlayerId(),state=this.options.store.getState();
if(!Number.isSafeInteger(roomcode)||roomcode<=0)throw Error('Invalid owned roomcode');
const ip=state.players.entities[playerid].ip;if(typeof ip!=='string')throw Error('Owned room join requires player login ip');
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:1,location,ip}));
}
getTreasureList():void {
this.taskPlayerId();const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('get_treasurelist',{agentid,gameid}));
}
redeemCard(cardno:string):void {
const playerid=this.taskPlayerId();
if(typeof cardno!=='string'||cardno===''||/^[ ]+$/.test(cardno))throw Error('Invalid cardno');
this.options.send.send(buildFirstSliceEnvelope('topup_card',{agentid:this.options.runtimeConfig.identity.agentid,playerid,cardno}));
}
claimTask(taskid:number|string):void {
const playerid=this.taskPlayerId();
const task=this.options.store.getState().tasks?.find(task=>String(task.taskid)===String(taskid));
if(!task||task.state!==1)throw Error('Task is not claimable');
// 11_GameUI claim click sends these three fields; Net._SendData adds none.
this.options.send.send(buildFirstSliceEnvelope('get_task_award',{
agentid:this.options.runtimeConfig.identity.agentid,playerid,taskid:task.taskid,
}));
}
private taskPlayerId():number {
const state=this.options.store.getState();
if(state.app.phase!=='logged-in'||state.room.kind!=='outside')throw Error('Task command requires logged-in lobby');
return this.requireAuthenticatedPlayerId(state);
}
prepare(): void {
const state = this.options.store.getState();
this.options.send.send(buildPrepareRequest(
this.options.runtimeConfig,
this.roomIdentity(state, this.requireInsideRoom(state)),
));
}
exitRoom(): void {
const state = this.options.store.getState();
const room = this.requireInsideRoom(state);
if (room.stage !== 0 && room.infinite !== 1) {
throw new UnsupportedGameHostCommandError('room.exit requires stage=0 unless infinite=1');
}
this.options.send.send(buildExitRoomRequest(
this.options.runtimeConfig,
this.roomIdentity(state, room),
));
}
createRoom(command: CreateRoomCommand): void {
const playerid = this.taskPlayerId();
const roomtype = cloneAndFreezeRoomtype(command.roomtype);
this.options.send.send(buildCreateRoomRequest(this.options.runtimeConfig, playerid, {...command, roomtype}));
}
joinRoom(command: JoinRoomCommand): void {
const state = this.options.store.getState();
if (state.room.kind !== 'outside') {
throw new UnsupportedGameHostCommandError('room.join requires player to be outside a room');
}
const playerid = this.requireAuthenticatedPlayerId(state);
this.options.send.send(buildJoinRoomRequest(
this.options.runtimeConfig,
{ playerid, roomcode: command.roomcode },
command,
));
}
private requireInsideRoom(state: PlatformState): InsideRoomState {
if (state.room.kind !== 'inside') {
throw new UnsupportedGameHostCommandError('room command requires player to be inside a room');
}
return state.room;
}
private requireAuthenticatedPlayerId(state: PlatformState): number {
const playerid = state.players.selfPlayerId;
if (playerid === null) {
throw new UnsupportedGameHostCommandError('room command requires an authenticated player');
}
if (state.players.entities[playerid] === undefined) {
throw new Error('players.entities: missing authenticated player');
}
return playerid;
}
private roomIdentity(state: PlatformState, room: InsideRoomState): RoomIdentity {
return {
playerid: this.requireAuthenticatedPlayerId(state),
roomcode: room.roomcode,
};
}
}
@@ -1,243 +1,276 @@
import type { GameHost } from '../sdk/contracts/index.ts';
import {
extractGameSessionOpenFaultErrors,
GameSessionHost,
} from '../sdk/runtime/game-session-host.ts';
import {
parseLoginResponse,
parseSelfJoinRoomResponse,
type ParsedSelfExitPayload,
type ReconnectSnapshot,
} from '../protocol/contracts/index.ts';
import { PlatformStore } from './stores/platform-store.ts';
import type { PlatformState } from './stores/platform-types.ts';
export interface GameHostLeasePort {
readonly host: GameHost;
invalidate(): void;
}
export type GameHostLeaseFactory = () => GameHostLeasePort;
/** A valid nonzero server outcome, delivered without changing canonical state. */
export interface ServerDenial {
readonly rpc: 'player_login' | 'self_join_room';
readonly state: number;
readonly data: Readonly<Record<string, unknown>>;
}
export interface RuntimeSessionOptions {
readonly store: PlatformStore;
readonly gameSession: GameSessionHost;
readonly createGameHostLease: GameHostLeaseFactory;
readonly onServerDenial: (outcome: ServerDenial) => void;
}
export type RuntimeSessionState = 'operational' | 'faulted' | 'kicked';
export class RuntimeSessionFault extends Error {
readonly errors: readonly unknown[];
constructor(operation: string, errors: readonly unknown[]) {
super(`RuntimeSession fault during ${operation}`);
this.name = 'RuntimeSessionFault';
this.errors = Object.freeze([...errors]);
}
}
/** Coordinates the canonical platform snapshot with the one active game session. */
export class RuntimeSession {
private loginIsPending = false;
private wasKicked = false;
private terminalFault: RuntimeSessionFault | null = null;
private activeLease: GameHostLeasePort | null = null;
constructor(private readonly options: RuntimeSessionOptions) {}
get loginPending(): boolean { return this.loginIsPending; }
get kicked(): boolean { return this.wasKicked; }
get state(): RuntimeSessionState {
if (this.wasKicked) return 'kicked';
return this.terminalFault !== null ? 'faulted' : 'operational';
}
get fault(): Error | null { return this.terminalFault; }
markLoginPending(): void {
this.requireOperational();
this.loginIsPending = true;
}
handleLogin(data: unknown): void {
this.requireOperational();
this.loginIsPending = false;
const parsed = parseLoginResponse(data);
if (parsed.state !== 0) {
this.options.onServerDenial({ rpc: 'player_login', state: parsed.state, data: parsed.raw });
return;
}
const committed = this.options.store.applyLoginSuccess(parsed);
if (parsed.room === null) {
this.releaseOrFault('login lobby replacement');
return;
}
this.openCommittedRoom(committed, parsed.reconnect, 'login room open');
}
handleSelfJoin(data: unknown): void {
this.requireOperational();
const parsed = parseSelfJoinRoomResponse(data);
if (parsed.state !== 0) {
this.options.onServerDenial({ rpc: 'self_join_room', state: parsed.state, data: parsed.raw });
return;
}
const committed = this.options.store.replaceRoom(parsed);
this.openCommittedRoom(committed, parsed.reconnect, 'self join room open');
}
handleSelfExit(parsed: ParsedSelfExitPayload): void {
this.requireOperational();
const state = this.options.store.getState();
if (state.room.kind !== 'inside') {
if (this.options.gameSession.state === 'idle' && this.activeLease === null) return;
const errors = [
new Error('self_exit_room found an active game without room state'),
...this.releaseGame(),
];
this.throwFault('self exit inconsistent state', errors);
}
if (parsed.seat !== undefined && parsed.seat !== state.room.selfSeat) {
throw new RangeError(
`self_exit_room seat mismatch: expected ${state.room.selfSeat}, received ${parsed.seat}`,
);
}
if (this.options.gameSession.state !== 'active' || this.activeLease === null) {
const errors = [
new Error('self_exit_room requires an active game session and host lease'),
...this.releaseGame(),
];
this.throwFault('self exit inconsistent state', errors);
}
try {
this.options.store.clearRoom();
} catch (error) {
this.throwFault('self exit state commit', [error, ...this.releaseGame()]);
}
this.releaseOrFault('self exit cleanup');
}
handleServerSwitch(switchConnection: () => void): void {
this.requireOperational();
const errors = this.releaseGame();
try {
switchConnection();
} catch (error) {
errors.push(error);
}
if (errors.length > 0) this.throwFault('server switch', errors);
}
handleKick(_data: unknown, stopConnection: () => void): void {
if (this.wasKicked) return;
this.requireOperational();
this.loginIsPending = false;
this.wasKicked = true;
const errors = this.releaseGame();
try {
stopConnection();
} catch (error) {
errors.push(error);
}
if (errors.length > 0) this.throwFault('kick terminal cleanup', errors);
}
private openCommittedRoom(
state: PlatformState,
reconnect: ReconnectSnapshot,
operation: string,
): void {
if (state.room.kind !== 'inside') {
this.throwFault(operation, [new Error('Cannot open a game session without committed room state')]);
}
const priorCleanupErrors = this.releaseGame();
if (priorCleanupErrors.length > 0) this.throwFault(operation, priorCleanupErrors);
if (this.gameWasDisposed()) return;
let lease: GameHostLeasePort;
try {
lease = this.options.createGameHostLease();
if (!lease || typeof lease !== 'object' || typeof lease.invalidate !== 'function') {
throw new TypeError('createGameHostLease must return { host, invalidate }');
}
} catch (error) {
this.throwFault(operation, [error]);
}
this.activeLease = lease;
try {
this.options.gameSession.open(lease.host);
if (this.gameWasDisposed()) return;
this.options.gameSession.publish({
type: 'room.entered',
roomtype: state.room.roomtype,
});
if (this.gameWasDisposed()) return;
if (reconnect.present) this.options.gameSession.restore(reconnect.value);
} catch (error) {
const openErrors = extractGameSessionOpenFaultErrors(error);
const errors = openErrors === null
? [error]
: [openErrors[0], openErrors[1]];
errors.push(...this.releaseGame());
this.throwFault(operation, errors);
}
}
private releaseOrFault(operation: string): void {
const errors = this.releaseGame();
if (errors.length > 0) this.throwFault(operation, errors);
}
private gameWasDisposed(): boolean {
return this.options.gameSession.state === 'disposed';
}
private releaseGame(): unknown[] {
const errors: unknown[] = [];
const lease = this.activeLease;
this.activeLease = null;
if (this.options.gameSession.state !== 'idle' && !this.gameWasDisposed()) {
try {
this.options.gameSession.close();
} catch (error) {
errors.push(error);
}
}
if (lease !== null) {
try {
lease.invalidate();
} catch (error) {
errors.push(error);
}
}
return errors;
}
private throwFault(operation: string, errors: readonly unknown[]): never {
const fault = new RuntimeSessionFault(operation, errors);
this.loginIsPending = false;
this.terminalFault = fault;
throw fault;
}
private requireOperational(): void {
if (this.terminalFault !== null) throw this.terminalFault;
if (this.wasKicked) throw new Error('RuntimeSession is kicked');
}
}
import type { GameHost } from '../sdk/contracts/index.ts';
import {
extractGameSessionOpenFaultErrors,
GameSessionHost,
} from '../sdk/runtime/game-session-host.ts';
import {
parseCreateRoomResponse,
parseLoginResponse,
parseSelfJoinRoomResponse,
type ParsedSelfExitPayload,
type ReconnectSnapshot,
} from '../protocol/contracts/index.ts';
import { cloneAndFreezeRoomtype, PlatformStore } from './stores/platform-store.ts';
import type { PlatformState } from './stores/platform-types.ts';
import type {LoginAccountIdentity} from '../protocol/contracts/login-contract.ts';
export interface GameHostLeasePort {
readonly host: GameHost;
invalidate(): void;
}
export type GameHostLeaseFactory = () => GameHostLeasePort;
/** A valid nonzero server outcome, delivered without changing canonical state. */
export interface ServerDenial {
readonly rpc: 'player_login' | 'self_join_room' | 'create_room';
readonly state: number;
readonly data: Readonly<Record<string, unknown>>;
}
export interface RuntimeSessionOptions {
readonly roomHistory?: import('../adapters/native/owned-room-history.ts').OwnedRoomHistory;
readonly store: PlatformStore;
readonly gameSession: GameSessionHost;
readonly createGameHostLease: GameHostLeaseFactory;
readonly onServerDenial: (outcome: ServerDenial) => void;
}
export type RuntimeSessionState = 'operational' | 'faulted' | 'kicked';
export class RuntimeSessionFault extends Error {
readonly errors: readonly unknown[];
constructor(operation: string, errors: readonly unknown[]) {
super(`RuntimeSession fault during ${operation}`);
this.name = 'RuntimeSessionFault';
this.errors = Object.freeze([...errors]);
}
}
/** Coordinates the canonical platform snapshot with the one active game session. */
export class RuntimeSession {
private loginIsPending = false;
private lastJoinedRoomcode: string | null = null;
/** Non-device login owns identity in authorization; device login owns it in the response. */
private authorizedIdentity:Pick<LoginAccountIdentity,'openid'|'unionid'>|undefined;
private wasKicked = false;
private terminalFault: RuntimeSessionFault | null = null;
private activeLease: GameHostLeasePort | null = null;
constructor(private readonly options: RuntimeSessionOptions) {}
get loginPending(): boolean { return this.loginIsPending; }
get kicked(): boolean { return this.wasKicked; }
get state(): RuntimeSessionState {
if (this.wasKicked) return 'kicked';
return this.terminalFault !== null ? 'faulted' : 'operational';
}
get fault(): Error | null { return this.terminalFault; }
markLoginPending(identity?:Pick<LoginAccountIdentity,'openid'|'unionid'>|null): void {
this.requireOperational();
// No argument is a reconnect of the current account; null selects device response identity.
if(identity!==undefined)this.authorizedIdentity=identity===null?undefined:identity;
this.loginIsPending = true;
}
handleLogin(data: unknown): void {
this.requireOperational();
this.loginIsPending = false;
const parsed = parseLoginResponse(data);
if (parsed.state !== 0) {
this.options.onServerDenial({ rpc: 'player_login', state: parsed.state, data: parsed.raw });
return;
}
this.lastJoinedRoomcode = null;
const committed = this.options.store.applyLoginSuccess(parsed,this.authorizedIdentity);
if (parsed.room === null) {
this.releaseOrFault('login lobby replacement');
return;
}
this.openCommittedRoom(committed, parsed.reconnect, 'login room open');
}
handleCreateRoom(data: unknown): void {
this.requireOperational();
const parsed = parseCreateRoomResponse(data);
if (parsed.state !== 0) {
this.options.onServerDenial({rpc: 'create_room', state: parsed.state, data: parsed.raw});
return;
}
if (parsed.room === null) throw new Error('create_room missing parsed room');
const current = this.options.store.getState().room;
if (current.kind === 'inside' && current.roomcode === parsed.room.roomcode
&& this.options.gameSession.state === 'active') return;
// Subgame code receives an owned immutable value, never the server response graph.
const roomtype = cloneAndFreezeRoomtype(parsed.room.roomtype);
const count = this.options.gameSession.entry.resolveSeatCount(roomtype);
const committed = this.options.store.createRoom(parsed, count);
this.openCommittedRoom(committed, {present: false}, 'create room open');
}
handleSelfJoin(data: unknown): void {
this.requireOperational();
const parsed = parseSelfJoinRoomResponse(data);
this.options.roomHistory?.joined(parsed.raw);
if (parsed.state !== 0) {
this.options.onServerDenial({ rpc: 'self_join_room', state: parsed.state, data: parsed.raw });
return;
}
if (parsed.room !== null && this.lastJoinedRoomcode === parsed.room.roomcode
&& this.options.gameSession.state === 'active') return;
const committed = this.options.store.replaceRoom(parsed);
this.openCommittedRoom(committed, parsed.reconnect, 'self join room open');
this.lastJoinedRoomcode = parsed.room!.roomcode;
}
handleSelfExit(parsed: ParsedSelfExitPayload): void {
this.requireOperational();
const state = this.options.store.getState();
if (state.room.kind !== 'inside') {
if (this.options.gameSession.state === 'idle' && this.activeLease === null) return;
const errors = [
new Error('self_exit_room found an active game without room state'),
...this.releaseGame(),
];
this.throwFault('self exit inconsistent state', errors);
}
if (parsed.seat !== undefined && parsed.seat !== state.room.selfSeat) {
throw new RangeError(
`self_exit_room seat mismatch: expected ${state.room.selfSeat}, received ${parsed.seat}`,
);
}
if (this.options.gameSession.state !== 'active' || this.activeLease === null) {
const errors = [
new Error('self_exit_room requires an active game session and host lease'),
...this.releaseGame(),
];
this.throwFault('self exit inconsistent state', errors);
}
try {
this.options.roomHistory?.exited(parsed.raw,state.room.infinite);
this.options.store.clearRoom();
} catch (error) {
this.throwFault('self exit state commit', [error, ...this.releaseGame()]);
}
this.releaseOrFault('self exit cleanup');
}
handleServerSwitch(switchConnection: () => void): void {
this.requireOperational();
const errors = this.releaseGame();
try {
switchConnection();
} catch (error) {
errors.push(error);
}
if (errors.length > 0) this.throwFault('server switch', errors);
}
handleKick(_data: unknown, stopConnection: () => void): void {
if (this.wasKicked) return;
this.requireOperational();
this.loginIsPending = false;
this.wasKicked = true;
const errors = this.releaseGame();
try {
stopConnection();
} catch (error) {
errors.push(error);
}
if (errors.length > 0) this.throwFault('kick terminal cleanup', errors);
}
private openCommittedRoom(
state: PlatformState,
reconnect: ReconnectSnapshot,
operation: string,
): void {
if (state.room.kind !== 'inside') {
this.throwFault(operation, [new Error('Cannot open a game session without committed room state')]);
}
const priorCleanupErrors = this.releaseGame();
if (priorCleanupErrors.length > 0) this.throwFault(operation, priorCleanupErrors);
if (this.gameWasDisposed()) return;
let lease: GameHostLeasePort;
try {
lease = this.options.createGameHostLease();
if (!lease || typeof lease !== 'object' || typeof lease.invalidate !== 'function') {
throw new TypeError('createGameHostLease must return { host, invalidate }');
}
} catch (error) {
this.throwFault(operation, [error]);
}
this.activeLease = lease;
try {
this.options.gameSession.open(lease.host);
if (this.gameWasDisposed()) return;
this.options.gameSession.publish({
type: 'room.entered',
roomtype: state.room.roomtype,
});
if (this.gameWasDisposed()) return;
if (reconnect.present) this.options.gameSession.restore(reconnect.value);
} catch (error) {
const openErrors = extractGameSessionOpenFaultErrors(error);
const errors = openErrors === null
? [error]
: [openErrors[0], openErrors[1]];
errors.push(...this.releaseGame());
this.throwFault(operation, errors);
}
}
private releaseOrFault(operation: string): void {
const errors = this.releaseGame();
if (errors.length > 0) this.throwFault(operation, errors);
}
private gameWasDisposed(): boolean {
return this.options.gameSession.state === 'disposed';
}
private releaseGame(): unknown[] {
this.lastJoinedRoomcode = null;
const errors: unknown[] = [];
const lease = this.activeLease;
this.activeLease = null;
if (this.options.gameSession.state !== 'idle' && !this.gameWasDisposed()) {
try {
this.options.gameSession.close();
} catch (error) {
errors.push(error);
}
}
if (lease !== null) {
try {
lease.invalidate();
} catch (error) {
errors.push(error);
}
}
return errors;
}
private throwFault(operation: string, errors: readonly unknown[]): never {
const fault = new RuntimeSessionFault(operation, errors);
this.loginIsPending = false;
this.terminalFault = fault;
throw fault;
}
private requireOperational(): void {
if (this.terminalFault !== null) throw this.terminalFault;
if (this.wasKicked) throw new Error('RuntimeSession is kicked');
}
}
File diff suppressed because it is too large Load Diff
@@ -1,16 +1,16 @@
import type { ServerDenial } from './runtime-session.ts';
/** Semantic scene intents; the headless runtime never imports Cocos. */
export interface ScenePort {
showLoading(): void;
showLogin(): void;
showLobby(): void;
showRoom(): void;
showReconnect(): void;
showKicked(data: unknown): void;
/** Required when the host supports the server's generic message UI. */
showTips?(message: string, time: number, source?:'show_message'|'update_roomcard'): void;
/** Recoverable server result; the consumer interprets the original failure payload. */
showServerDenial(outcome: ServerDenial): void;
showFatal(error: Error): void;
}
import type { ServerDenial } from './runtime-session.ts';
/** Semantic scene intents; the headless runtime never imports Cocos. */
export interface ScenePort {
showLoading(): void;
showLogin(): void;
showLobby(): void;
showRoom(): void;
showReconnect(): void;
showKicked(data: unknown): void;
/** Required when the host supports the server's generic message UI. */
showTips?(message: string, time: number, source?:'show_message'|'update_roomcard'|'update_bean'|'binding_invitecode'): void;
/** Recoverable server result; the consumer interprets the original failure payload. */
showServerDenial(outcome: ServerDenial): void;
showFatal(error: Error): void;
}
File diff suppressed because it is too large Load Diff
@@ -1,61 +1,68 @@
import type { PlatformGameConnectionPhase } from '../../sdk/contracts/snapshots.ts';
import type { TaskData } from '../../core/types/task.ts';
export type PlatformConnectionPhase =
| 'idle'
| 'connecting'
| PlatformGameConnectionPhase;
export interface PlatformAppState {
readonly phase: PlatformConnectionPhase;
}
export interface PlatformPlayer {
readonly playerid: number;
readonly nickname: string;
readonly avatar: string;
readonly sex: number;
readonly bean: number;
/** Account-only fields are absent on other room players; never default them in views. */
import type { PlatformGameConnectionPhase } from '../../sdk/contracts/snapshots.ts';
import type { TaskData } from '../../core/types/task.ts';
export type PlatformConnectionPhase =
| 'idle'
| 'connecting'
| PlatformGameConnectionPhase;
export interface PlatformAppState {
readonly phase: PlatformConnectionPhase;
}
export interface PlatformPlayer {
readonly playerid: number;
readonly nickname: string;
readonly avatar: string;
readonly sex: number;
readonly bean: number;
/** Account-only fields are absent on other room players; never default them in views. */
readonly roomcard?: number;
readonly taskstate?: number;
readonly advanced?: number;
readonly score?: number;
readonly sign?: string | null;
readonly ip?: string;
readonly isprepare?: number;
readonly onstate?: number;
}
export interface PlayerDirectoryState {
readonly selfPlayerId: number | null;
readonly entities: Readonly<Record<number, PlatformPlayer>>;
}
export interface OutsideRoomState {
readonly kind: 'outside';
}
export interface InsideRoomState {
readonly kind: 'inside';
readonly roomcode: string;
readonly selfSeat: number;
readonly seatPlayerIds: readonly (number | null)[];
readonly readySeats: readonly number[];
readonly offlineSeats: readonly number[];
readonly roomtype: readonly unknown[];
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
}
export type RoomState = OutsideRoomState | InsideRoomState;
readonly openid?: string;
readonly unionid?: string | number;
readonly invitecode?: string | null;
readonly taskstate?: number;
readonly advanced?: number;
readonly score?: number;
readonly sign?: string | null;
readonly ip?: string;
readonly isprepare?: number;
readonly onstate?: number;
}
export interface PlayerDirectoryState {
readonly selfPlayerId: number | null;
readonly entities: Readonly<Record<number, PlatformPlayer>>;
}
export interface OutsideRoomState {
readonly kind: 'outside';
}
export interface InsideRoomState {
readonly kind: 'inside';
readonly roomcode: string;
readonly selfSeat: number;
readonly seatPlayerIds: readonly (number | null)[];
readonly readySeats: readonly number[];
readonly offlineSeats: readonly number[];
readonly roomtype: readonly unknown[];
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
}
export type RoomState = OutsideRoomState | InsideRoomState;
export interface PlatformState {
readonly vipRooms?: readonly import('../../core/types/vip-room.ts').VipRoom[];
/** Absent until a task list response arrives; cleared by login replacement. */
readonly tasks?: readonly TaskData[];
readonly app: PlatformAppState;
readonly players: PlayerDirectoryState;
readonly room: RoomState;
}
readonly vipRank?:{readonly entries:readonly import('../../protocol/contracts/rank-list.ts').RankEntry[]|null};
readonly treasureList?:readonly import('../../protocol/contracts/rank-list.ts').RankEntry[];
/** Receipt identity allows repeated queries with an unchanged code to open the view. */
readonly inviteResponse?: {readonly kind:'query'|'binding'};
readonly vipRooms?: readonly import('../../core/types/vip-room.ts').VipRoom[];
/** Absent until a task list response arrives; cleared by login replacement. */
readonly tasks?: readonly TaskData[];
readonly app: PlatformAppState;
readonly players: PlayerDirectoryState;
readonly room: RoomState;
}
@@ -0,0 +1,25 @@
/** Game_Surface_3 Logic.InputRoomCode/pasteRoomCode and GameUI Spirit602. */
export class JoinRoomInput {
private value='';
private busy=false;
constructor(private readonly submit:(code:number)=>void){}
get code():string{return this.value;}
setBusy(value:boolean):void{this.busy=value;}
prefill(value:string):void{
if(!/^\d{0,6}$/.test(value))throw Error('Invalid saved room shortcode');
this.value=value;
}
digit(value:number):void{
if(!Number.isInteger(value)||value<0||value>9)throw Error('Invalid room digit');
if(this.busy)return;
if(this.value.length<6)this.value+=value;
if(this.value.length===6)this.submit(Number(this.value));
}
remove():void{if(!this.busy)this.value=this.value.slice(0,-1);}
clear():void{if(!this.busy)this.value='';}
paste(value:string):void{
if(this.busy)return;this.value=value.replace(/[^0-9]/g,'').slice(0,6);
if(this.value.length===6)this.submit(Number(this.value));
}
confirm():void{if(!this.busy&&Number(this.value)>0)this.submit(Number(this.value));}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "9301c0f4-8779-44f8-8d19-5f540b499e82",
"files": [],
"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": "a834856f-7a53-445b-b517-6588ad346e59",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,121 +1,131 @@
import type { RemoteConfig } from '../config/remote-config.ts';
export interface LobbyTemplate {
isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean;
}
/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */
export const LOBBY_TEMPLATE:Readonly<LobbyTemplate>=Object.freeze({
isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false,
});
export interface LobbyContext {versionState:0|1;launchMode:0|1;}
const HALL_DEFAULTS=Object.freeze({androidPay:0,rankList:0 as number|string,storeUrl:'',bagUrl:'',actData:0 as unknown,worldRoomList:0 as number|string,wareHouse:0 as number|string});
const SYS_DEFAULTS=Object.freeze({hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1,
ofcShortCode:null as unknown,hideSpid:null as unknown});
function object(value:unknown,name:string):Record<string,unknown>{
if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`);
return value as Record<string,unknown>;
}
/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */
export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly<LobbyTemplate>=LOBBY_TEMPLATE){
const rawHall=remote.getValue('hall_config');
const merged=rawHall==null?{}:object(rawHall,'hall_config');
const hall={...HALL_DEFAULTS,...merged};
for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){
// 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData.
if(key==='actData')continue;
const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value});
}
for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){
if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`);
if(key==='androidPay')hall[key]=Number(hall[key]);
}
for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`);
const sys={...SYS_DEFAULTS};
for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof SYS_DEFAULTS)[]){
const value=remote.getValue(key);if(value!=null)Object.assign(sys,{[key]:value});
}
for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){
if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`);
if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]);
}
if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid');
const rawAgent=remote.getValue('agentmode');
const agentmode=rawAgent==null?1:Number(rawAgent);
if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode');
return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template});
import type { RemoteConfig } from '../config/remote-config.ts';
export interface LobbyTemplate {
isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean;
}
/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */
export const LOBBY_TEMPLATE:Readonly<LobbyTemplate>=Object.freeze({
isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false,
});
export interface LobbyContext {versionState:0|1;launchMode:0|1;}
const HALL_DEFAULTS=Object.freeze({androidPay:0,rankList:0 as number|string,isReSetOwnerNote:0 as number|string,storeUrl:'',bagUrl:'',actData:0 as unknown,worldRoomList:0 as number|string,wareHouse:0 as number|string});
const SYS_DEFAULTS=Object.freeze({vipRankLimit:10,hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1,
ofcShortCode:null as unknown,hideSpid:null as unknown});
function object(value:unknown,name:string):Record<string,unknown>{
if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`);
return value as Record<string,unknown>;
}
/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */
export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly<LobbyTemplate>=LOBBY_TEMPLATE){
const rawHall=remote.getValue('hall_config');
const merged=rawHall==null?{}:object(rawHall,'hall_config');
const hall={...HALL_DEFAULTS,...merged};
for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){
// 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData.
if(key==='actData')continue;
const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value});
}
for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){
if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`);
if(key==='androidPay')hall[key]=Number(hall[key]);
}
for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`);
const sys={...SYS_DEFAULTS};
for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof SYS_DEFAULTS)[]){
const value=remote.getValue(key);if(value!=null)Object.assign(sys,{[key]:value});
}
if(!Number.isInteger(Number(sys.vipRankLimit))||Number(sys.vipRankLimit)<=0)throw Error('Invalid vipRankLimit');
sys.vipRankLimit=Number(sys.vipRankLimit);
for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){
if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`);
if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]);
}
if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid');
const rawAgent=remote.getValue('agentmode');
const agentmode=rawAgent==null?1:Number(rawAgent);
if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode');
// Original GameUI draw cases 568/483/3139: missing vipConfig means unopened;
// the earlier matchhtml condition is commented out in the source.
const vipConfig=remote.getValue('vipConfig');
const locked=Object.freeze({568:!hall.worldRoomList,483:!vipConfig,3139:!sys.ofcShortCode});
return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template,locked});
}
export type LobbyMenuConfig=ReturnType<typeof resolveLobbyMenuConfig>;
export interface LobbyMenuState {visible:Record<number,boolean>;left:Record<number,number>;top:Record<number,number>;}
/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */
export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const {hall,sys,template,agentmode}=config;
const visible:Record<number,boolean>={},left:Record<number,number>={},top:Record<number,number>={};
const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;};
// Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X.
if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;}
hide([219,220]);
if(context.versionState===1){
if(context.launchMode===0)hide([234]);
hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604,
286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]);
}else{
if(agentmode===1)hide([10,509]);
visible[235]=false;
if(hall.androidPay===1){visible[10]=true;visible[509]=true;}
visible[3018]=!template.isGameHall&&Array.isArray(hall.actData);
visible[3016]=Boolean(hall.storeUrl);
if(template.isGameHall||template.vipInfinite)hide([3139,679]);
if(template.mainMenuButton)hide([11,12,568,483]);
if(template.hideNotice)hide([16,180,193,194,682,178,179,145]);
if(sys.hideTaskBtn)hide([17]);
if(sys.miniPro||sys.hideWxShare)hide([14]);
if(!template.isGameHall){
const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);});
}
visible[3233]=Boolean(hall.rankList);
visible[3256]=false; // Both original deviceLogin branches hide this entry.
if(template.isGameHall){
hide([14,13,17,3120,99,3016,76,234,3057,145,3139,3232,3233,604]);
visible[235]=agentmode!==1;visible[15]=true;visible[5]=true;
const shown=[235,15,5,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=300+520*index/(shown.length-1);top[id]=642;});
}
}
if(sys.hideRoomCard===2)hide([286,10,287,3161]);
visible[3264]=sys.cdKey===2;
return {visible,left,top};
/** Original click messages intentionally use different comma characters. */
export function lobbyLockedTip(config:LobbyMenuConfig,action:'world-rooms'|'match'|'official-rooms'):string|null{
if(action==='world-rooms')return config.locked[568]?'暂未开放,敬请期待':null;
return config.locked[action==='match'?483:3139]?'暂未开放,敬请期待':null;
}
/** openPallet runs its own visibility pass each time before placing visible entries. */
export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const visible:Record<number,boolean>={15:false,604:Boolean(config.hall.rankList),
235:config.agentmode!==1&&!config.sys.miniPro,3017:Boolean(config.hall.bagUrl),3057:Boolean(config.hall.wareHouse),5:true};
const left:Record<number,number>={3121:200},top:Record<number,number>={};
if(context.versionState===1)return {visible:Object.fromEntries(Object.keys(visible).map(id=>[id,false])),left,top};
const shown=[15,604,235,3017,3057,5].filter(id=>visible[id]);
shown.forEach((id,index)=>{left[id]=216+155*index;top[id]=540;});
return {visible,left,top};
}
/** IDs remain source traceability only; Cocos owns hit testing and prefab lifetimes. */
export const LOBBY_MENU_ACTIONS=Object.freeze({
7:'settings',9:'profile',11:'create-room',12:'join-room',13:'records',14:'share',17:'tasks',679:'vip-rooms',
10:'buy-roomcard',286:'buy-roomcard',287:'buy-roomcard',3161:'buy-roomcard',
508:'buy-bean',509:'buy-bean',496:'buy-bean',3162:'buy-bean',
483:'match',568:'world-rooms',3016:'store',3018:'activity',99:'my-rooms',76:'qrcode',
3139:'official-rooms',234:'more-games',3120:'tray',3233:'rank',3232:'vip-rank',3256:'bind-phone',3264:'cdkey',
5:'notice',3017:'bag',235:'agent',604:'rank',3057:'warehouse',15:'feedback',
} as const);
export type LobbyAction=typeof LOBBY_MENU_ACTIONS[keyof typeof LOBBY_MENU_ACTIONS];
export const LOBBY_UNAVAILABLE:Readonly<Partial<Record<LobbyAction,string>>>=Object.freeze({
'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块',
'buy-roomcard':'购买房卡:尚未接入支付模块','buy-bean':'购买星星:尚未接入支付模块',
match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口',
activity:'活动:尚未接入活动身份参数与原生接口','my-rooms':'我的房间:尚未接入房间记录与进房',qrcode:'二维码:尚未接入分享模块',
'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口',
rank:'排行:尚未接入排行榜', 'vip-rank':'VIP 排行:尚未接入排行榜','bind-phone':'绑定手机:尚未接入绑定流程',
cdkey:'兑换码:尚未接入兑换请求',bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口',
warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交',
});
export interface LobbyMenuState {visible:Record<number,boolean>;left:Record<number,number>;top:Record<number,number>;}
/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */
export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const {hall,sys,template,agentmode}=config;
const visible:Record<number,boolean>={},left:Record<number,number>={},top:Record<number,number>={};
const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;};
// Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X.
if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;}
hide([219,220]);
if(context.versionState===1){
if(context.launchMode===0)hide([234]);
hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604,
286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]);
}else{
if(agentmode===1)hide([10,509]);
visible[235]=false;
if(hall.androidPay===1){visible[10]=true;visible[509]=true;}
visible[3018]=!template.isGameHall&&Array.isArray(hall.actData);
visible[3016]=Boolean(hall.storeUrl);
if(template.isGameHall||template.vipInfinite)hide([3139,679]);
if(template.mainMenuButton)hide([11,12,568,483]);
if(template.hideNotice)hide([16,180,193,194,682,178,179,145]);
if(sys.hideTaskBtn)hide([17]);
if(sys.miniPro||sys.hideWxShare)hide([14]);
if(!template.isGameHall){
const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);});
}
visible[3233]=Boolean(hall.rankList);
visible[3256]=false; // Both original deviceLogin branches hide this entry.
if(template.isGameHall){
hide([14,13,17,3120,99,3016,76,234,3057,145,3139,3232,3233,604]);
visible[235]=agentmode!==1;visible[15]=true;visible[5]=true;
const shown=[235,15,5,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=300+520*index/(shown.length-1);top[id]=642;});
}
}
if(sys.hideRoomCard===2)hide([286,10,287,3161]);
visible[3264]=sys.cdKey===2;
return {visible,left,top};
}
/** openPallet runs its own visibility pass each time before placing visible entries. */
export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const visible:Record<number,boolean>={15:false,604:Boolean(config.hall.rankList),
235:config.agentmode!==1&&!config.sys.miniPro,3017:Boolean(config.hall.bagUrl),3057:Boolean(config.hall.wareHouse),5:true};
const left:Record<number,number>={3121:200},top:Record<number,number>={};
if(context.versionState===1)return {visible:Object.fromEntries(Object.keys(visible).map(id=>[id,false])),left,top};
const shown=[15,604,235,3017,3057,5].filter(id=>visible[id]);
shown.forEach((id,index)=>{left[id]=216+155*index;top[id]=540;});
return {visible,left,top};
}
/** IDs remain source traceability only; Cocos owns hit testing and prefab lifetimes. */
export const LOBBY_MENU_ACTIONS=Object.freeze({
7:'settings',9:'profile',11:'create-room',12:'join-room',13:'records',14:'share',17:'tasks',679:'vip-rooms',
10:'buy-roomcard',286:'buy-roomcard',287:'buy-roomcard',3161:'buy-roomcard',
508:'buy-bean',509:'buy-bean',496:'buy-bean',3162:'buy-bean',
483:'match',568:'world-rooms',3016:'store',3018:'activity',99:'my-rooms',76:'qrcode',
3139:'official-rooms',234:'more-games',3120:'tray',3233:'rank',3232:'vip-rank',3256:'bind-phone',3264:'cdkey',
5:'notice',3017:'bag',235:'agent',604:'rank',3057:'warehouse',15:'feedback',
} as const);
export type LobbyAction=typeof LOBBY_MENU_ACTIONS[keyof typeof LOBBY_MENU_ACTIONS];
export const LOBBY_UNAVAILABLE:Readonly<Partial<Record<LobbyAction,string>>>=Object.freeze({
'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块',
match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口',
activity:'活动:尚未接入活动身份参数与原生接口',
'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口',
'bind-phone':'绑定手机:尚未接入绑定流程',
bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口',
warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交',
});
@@ -1,66 +1,74 @@
import type { RemoteConfig } from '../config/remote-config.ts';
import type { PlatformState } from '../platform/stores/platform-types.ts';
import { selectSelfPlayer } from '../platform/stores/selectors.ts';
// Presentation checks required view inputs without depending on the protocol layer.
function requireNumber(value:unknown,path:string):number {if(typeof value!=='number'||!Number.isFinite(value))throw Error(`${path}: expected number`);return value;}
function requireInteger(value:unknown,path:string):number {const result=requireNumber(value,path);if(!Number.isInteger(result))throw Error(`${path}: expected integer`);return result;}
function requireString(value:unknown,path:string):string {if(typeof value!=='string')throw Error(`${path}: expected string`);return value;}
function requireRecord(value:unknown,path:string):Record<string,unknown> {if(!value||typeof value!=='object'||Array.isArray(value))throw Error(`${path}: expected object`);return value as Record<string,unknown>;}
export function selectLobbyModel(state:PlatformState) {
const self=selectSelfPlayer(state);
if(!self)throw Error('lobby requires authenticated player');
return Object.freeze({...self,
roomcard:requireNumber(self.roomcard,'players.self.roomcard'),
taskstate:requireInteger(self.taskstate,'players.self.taskstate'),
score:requireNumber(self.score,'players.self.score'),
sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~',
canAct:state.app.phase==='logged-in'&&state.room.kind==='outside',
});
}
import type { RemoteConfig } from '../config/remote-config.ts';
import type { PlatformState } from '../platform/stores/platform-types.ts';
import { selectSelfPlayer } from '../platform/stores/selectors.ts';
// Presentation checks required view inputs without depending on the protocol layer.
function requireNumber(value:unknown,path:string):number {if(typeof value!=='number'||!Number.isFinite(value))throw Error(`${path}: expected number`);return value;}
function requireInteger(value:unknown,path:string):number {const result=requireNumber(value,path);if(!Number.isInteger(result))throw Error(`${path}: expected integer`);return result;}
function requireString(value:unknown,path:string):string {if(typeof value!=='string')throw Error(`${path}: expected string`);return value;}
function requireRecord(value:unknown,path:string):Record<string,unknown> {if(!value||typeof value!=='object'||Array.isArray(value))throw Error(`${path}: expected object`);return value as Record<string,unknown>;}
export function selectLobbyModel(state:PlatformState) {
const self=selectSelfPlayer(state);
if(!self)throw Error('lobby requires authenticated player');
return Object.freeze({...self,
roomcard:requireNumber(self.roomcard,'players.self.roomcard'),
taskstate:requireInteger(self.taskstate,'players.self.taskstate'),
score:requireNumber(self.score,'players.self.score'),
sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~',
canAct:state.app.phase==='logged-in'&&state.room.kind==='outside',
});
}
export type LobbyModel=ReturnType<typeof selectLobbyModel>;
export interface Notice {type:0|1; text:string; image:string; url:string; height:number;}
export interface LobbyPolicy {noticeHours:number; notices:readonly Notice[]; scroll:string;
starName:string; roomCardName:string; contacts:readonly {label:string;value:string}[];}
/** Source defaults: 04_Data.sysConfig and advertInfo_0/1/2; 12_Logic.get_paravalue. */
export function resolveLobbyPolicy(remote:RemoteConfig):LobbyPolicy {
// 04_Data starts with 房卡; getConfig_Succ replaces only a truthy roomCardName.
const cardName=remote.getValue('roomCardName');
const roomCardName=cardName?requireString(cardName,'roomCardName'):'房卡';
const interval=remote.getValue('gameNoticeShow');
const noticeHours=interval==null?0:Number(interval);
if(!Number.isFinite(noticeHours)||noticeHours< -1)throw Error('Invalid gameNoticeShow');
const menuValue=remote.getValue('menunotice');
const menu=menuValue==null?{}:requireRecord(menuValue,'menunotice');
const notices:Notice[]=[];
for(let index=0;index<3;index++){
const value=menu[`advert${index}`];
if(value==null){
const message=index===0?remote.getValue('noticemsg'):null;
notices.push({type:0,text:message==null?'':requireString(message,'noticemsg').replace(/#/g,'\n'),image:'',url:'',height:1});
continue;
}
const data=requireRecord(value,`menunotice.advert${index}`);
const type=requireInteger(data.type,`menunotice.advert${index}.type`);
if(type!==0&&type!==1)throw Error(`menunotice.advert${index}.type must be 0 or 1`);
// Empty text and URL explicitly mean no content/action in the source.
const field=(key:string)=>data[key]==null?'':requireString(data[key],`menunotice.advert${index}.${key}`);
notices.push({type,text:type===0?field('msg').replace(/#/g,'\n'):'',image:type===1?field('imgUrl'):'',
url:field(type===0?'url':'imgAdUrl'),height:type===1?requireNumber(data.height,`menunotice.advert${index}.height`):1});
}
const scroll=remote.getValue('scrollmsg');
const star=remote.getValue('starName');
const contacts:Array<{label:string;value:string}>=[];
for(const [key,title] of [['service_wechat','客服微信:'],['service_qq','投诉建议:'],['service_tel','客服手机:']]){
const value=remote.getValue(key);
if(value!=null&&value!=='')contacts.push({label:title,value:requireString(value,key)});
}
return Object.freeze({roomCardName,noticeHours,notices:Object.freeze(notices),scroll:scroll==null?'':requireString(scroll,'scrollmsg'),
starName:star==null?'星星':requireString(star,'starName'),contacts:Object.freeze(contacts)});
}
/** Exact JudgeShow expiry: equality is not expired; reconnect never auto-opens. */
export function shouldShowNotice(hours:number,previous:number|null,now:number,reconnect:boolean):boolean {
if(reconnect||hours===-1)return false;
return hours===0||!previous||previous+hours*3600000<now;
/** Game_Config.Notice: speed 0.1 px/ms, 15 px per gblen byte. */
export function announcementMotion(text:string,width:number,elapsedSeconds:number){
if(!Number.isFinite(width)||width<=0)throw Error('Invalid announcement width');
if(!Number.isFinite(elapsedSeconds)||elapsedSeconds<0)throw Error('Invalid announcement elapsed time');
const textWidth=text.replace(/[^\x00-\xff]/g,'**').length*15;
const duration=(width+textWidth)/100;
return {visible:text.length>0,textWidth,duration,left:width-(elapsedSeconds%duration)*100};
}
export interface Notice {type:0|1; text:string; image:string; url:string; height:number;}
export interface LobbyPolicy {noticeHours:number; notices:readonly Notice[]; scroll:string;
starName:string; roomCardName:string; contacts:readonly {label:string;value:string}[];}
/** Source defaults: 04_Data.sysConfig and advertInfo_0/1/2; 12_Logic.get_paravalue. */
export function resolveLobbyPolicy(remote:RemoteConfig):LobbyPolicy {
// 04_Data starts with 房卡; getConfig_Succ replaces only a truthy roomCardName.
const cardName=remote.getValue('roomCardName');
const roomCardName=cardName?requireString(cardName,'roomCardName'):'房卡';
const interval=remote.getValue('gameNoticeShow');
const noticeHours=interval==null?0:Number(interval);
if(!Number.isFinite(noticeHours)||noticeHours< -1)throw Error('Invalid gameNoticeShow');
const menuValue=remote.getValue('menunotice');
const menu=menuValue==null?{}:requireRecord(menuValue,'menunotice');
const notices:Notice[]=[];
for(let index=0;index<3;index++){
const value=menu[`advert${index}`];
if(value==null){
const message=index===0?remote.getValue('noticemsg'):null;
notices.push({type:0,text:message==null?'':requireString(message,'noticemsg').replace(/#/g,'\n'),image:'',url:'',height:1});
continue;
}
const data=requireRecord(value,`menunotice.advert${index}`);
const type=requireInteger(data.type,`menunotice.advert${index}.type`);
if(type!==0&&type!==1)throw Error(`menunotice.advert${index}.type must be 0 or 1`);
// Empty text and URL explicitly mean no content/action in the source.
const field=(key:string)=>data[key]==null?'':requireString(data[key],`menunotice.advert${index}.${key}`);
notices.push({type,text:type===0?field('msg').replace(/#/g,'\n'):'',image:type===1?field('imgUrl'):'',
url:field(type===0?'url':'imgAdUrl'),height:type===1?requireNumber(data.height,`menunotice.advert${index}.height`):1});
}
const scroll=remote.getValue('scrollmsg');
const star=remote.getValue('starName');
const contacts:Array<{label:string;value:string}>=[];
for(const [key,title] of [['service_wechat','客服微信:'],['service_qq','投诉建议:'],['service_tel','客服手机:']]){
const value=remote.getValue(key);
if(value!=null&&value!=='')contacts.push({label:title,value:requireString(value,key)});
}
return Object.freeze({roomCardName,noticeHours,notices:Object.freeze(notices),scroll:scroll==null?'':requireString(scroll,'scrollmsg'),
starName:star==null?'星星':requireString(star,'starName'),contacts:Object.freeze(contacts)});
}
/** Exact JudgeShow expiry: equality is not expired; reconnect never auto-opens. */
export function shouldShowNotice(hours:number,previous:number|null,now:number,reconnect:boolean):boolean {
if(reconnect||hours===-1)return false;
return hours===0||!previous||previous+hours*3600000<now;
}
@@ -0,0 +1,24 @@
import {LEGACY_AUTH_DISTRICTS} from './legacy-auth-districts.ts';
/** Game_Surface_3 05_Func.IDValidator + 11_GameUI cases 484/493.
* Local format validation only. Deliberately retain the source's 15-digit,
* parent-district and month/day range semantics; this is not server identity proof.
*/
export function validateProfileAuth(name:string,id:string):'name'|'id'|null {
if(name.length<=1)return 'name';
const code=id.toUpperCase();
if(code.length!==15&&code.length!==18)return 'id';
const body=code.length===18?code.slice(0,17):code;
const address=body.slice(0,6);
const hasAddress=(key:string)=>Object.prototype.hasOwnProperty.call(LEGACY_AUTH_DISTRICTS,key);
if(!hasAddress(address)&&!hasAddress(address.slice(0,4)+'00')&&!hasAddress(address.slice(0,2)+'0000'))return 'id';
const birth=code.length===18?body.slice(6,14):body.slice(6,12);
const month=parseInt(birth.slice(-4,-2),10),day=parseInt(birth.slice(-2),10);
if(month>12||month===0||day>31||day===0)return 'id';
if(code.length===15)return null;
let sum=0;
for(let index=0;index<body.length;index++)sum+=parseInt(body[index],10)*(Math.pow(2,17-index)%11);
const value=12-sum%11;
const check=value===10?'X':String(value>10?value%11:value);
return check===code.slice(-1)?null:'id';
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "11f5830e-5f4b-46fb-9e06-b2b65edbed89",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,14 @@
export type LoadingRpc='get_player_task'|'get_task_award'|'getVipRankList';
/** Wire receipt, rather than a successful model update, completes request loading. */
export class RequestLoading {
private readonly pending=new Set<LoadingRpc>();
constructor(private readonly show:(visible:boolean)=>void){}
begin(rpc:LoadingRpc):void{const empty=this.pending.size===0;this.pending.add(rpc);if(empty)this.show(true);}
cancel(rpc:LoadingRpc):void{if(this.pending.delete(rpc)&&this.pending.size===0)this.show(false);}
received(message:{route:string;rpc:string;data?:unknown}):void{
if(message.route!=='agent')return;
if(message.rpc==='show_message'){this.clear();return;}
if(message.rpc==='get_player_task'||message.rpc==='get_task_award'||message.rpc==='getVipRankList')this.cancel(message.rpc);
}
clear():void{if(this.pending.size){this.pending.clear();this.show(false);}}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8bb6a294-bfcf-4126-98f1-0845b58c72db",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,19 +1,27 @@
import type { RuntimeConfig } from '../config/runtime-config.ts';
/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */
export function resolveSharePolicy(config:RuntimeConfig){
const remote=config.remoteConfig,raw=remote.getValue('hall_config');
if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config');
const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record<string,unknown>;
for(const key of Object.keys(hall)){const value=remote.getValue(key);if(value!=null)hall[key]=value;}
const text=(value:unknown,name:string)=>{if(typeof value!=='string')throw Error(`Invalid ${name}`);return value;};
const download=remote.getValue('url_download'),name=remote.getValue('sharename'),id=config.identity;
return {url:download?`${text(download,'url_download')}?agentid=${id.agentid}&channelid=${id.channelid}&gameid=${id.gameid}&marketid=${id.marketid}`:'',
title:name?text(name,'sharename'):'',description:hall.shareDesc?text(hall.shareDesc,'shareDesc'):'hello world',
background:text(hall.shareBgUrl,'shareBgUrl'),image:text(hall.shareImgUrl,'shareImgUrl'),imageTimeline:!!hall.shareType};
}
export function sharePayload(policy:ReturnType<typeof resolveSharePolicy>,friend:1|2){
if(friend===2&&!policy.imageTimeline)return null; // Legacy case 75 has no link-sharing call.
return {sharefriend:friend,type:friend===1?1:3,webpageUrl:friend===1?policy.url:policy.image||policy.url,
title:friend===1?policy.title||policy.title+policy.description:policy.title+policy.description,
description:friend===1?policy.description:undefined,sharetype:0};
/** Open_ewm uses this exact config key; absent configuration intentionally does nothing. */
export function resolveQrCodePolicy(config:RuntimeConfig){
const url=config.remoteConfig.getValue('WechatPublicNo_ewm');
if(!url)return null;
if(typeof url!=='string')throw Error('Invalid WechatPublicNo_ewm');
const policy=resolveSharePolicy(config);
return {url,share:{sharefriend:1,type:3,webpageUrl:url,title:policy.title+policy.description,description:undefined,sharetype:0}};
}
/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */
export function resolveSharePolicy(config:RuntimeConfig){
const remote=config.remoteConfig,raw=remote.getValue('hall_config');
if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config');
const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record<string,unknown>;
for(const key of Object.keys(hall)){const value=remote.getValue(key);if(value!=null)hall[key]=value;}
const text=(value:unknown,name:string)=>{if(typeof value!=='string')throw Error(`Invalid ${name}`);return value;};
const download=remote.getValue('url_download'),name=remote.getValue('sharename'),id=config.identity;
return {url:download?`${text(download,'url_download')}?agentid=${id.agentid}&channelid=${id.channelid}&gameid=${id.gameid}&marketid=${id.marketid}`:'',
title:name?text(name,'sharename'):'',description:hall.shareDesc?text(hall.shareDesc,'shareDesc'):'hello world',
background:text(hall.shareBgUrl,'shareBgUrl'),image:text(hall.shareImgUrl,'shareImgUrl'),imageTimeline:!!hall.shareType};
}
export function sharePayload(policy:ReturnType<typeof resolveSharePolicy>,friend:1|2){
if(friend===2&&!policy.imageTimeline)return null; // Legacy case 75 has no link-sharing call.
return {sharefriend:friend,type:friend===1?1:3,webpageUrl:friend===1?policy.url:policy.image||policy.url,
title:friend===1?policy.title||policy.title+policy.description:policy.title+policy.description,
description:friend===1?policy.description:undefined,sharetype:0};
}
@@ -1,52 +1,56 @@
export {
hasOwn,
optionalField,
requireArray,
requireBoolean,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
export {
buildLoginRequest,
parseLoginResponse,
type DeviceLoginMode,
type FirstSliceRuntimeConfig,
type LoginAccountIdentity,
type LoginDeviceSnapshot,
type LoginPlayerIdMode,
type LoginRequestPayload,
type ParsedLoginResponse,
} from './login-contract.ts';
export {
buildExitRoomRequest,
buildJoinRoomRequest,
buildPrepareRequest,
parseConnectAgentserverPayload,
parseConnectRoomserverPayload,
parseKickServerPayload,
parseOtherExitRoomPayload,
parseOtherJoinRoomPayload,
parseOtherOfflinePayload,
parseOtherOnlinePayload,
parsePlayerPreparePayload,
parseSelfExitRoomPayload,
parseSelfJoinRoomResponse,
type JoinRoomEnvironment,
type JoinRoomPayload,
type ParsedAgentserverPayload,
type ParsedKickPayload,
type ParsedOnlinePayload,
type ParsedRoomEntry,
type ParsedRoomResponse,
type ParsedRoomserverPayload,
type ParsedRoomSnapshot,
type ParsedSelfExitPayload,
type ParsedSeatPayload,
type ReconnectSnapshot,
type RoomActionPayload,
type RoomIdentity,
} from './room-contracts.ts';
export {
hasOwn,
optionalField,
requireArray,
requireBoolean,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
export {
buildLoginRequest,
parseLoginResponse,
type DeviceLoginMode,
type FirstSliceRuntimeConfig,
type LoginAccountIdentity,
type LoginDeviceSnapshot,
type LoginPlayerIdMode,
type LoginRequestPayload,
type ParsedLoginResponse,
} from './login-contract.ts';
export {
buildCreateRoomRequest,
parseCreateRoomResponse,
type ParsedCreateRoomResponse,
type CreateRoomEnvironment,
buildExitRoomRequest,
buildJoinRoomRequest,
buildPrepareRequest,
parseConnectAgentserverPayload,
parseConnectRoomserverPayload,
parseKickServerPayload,
parseOtherExitRoomPayload,
parseOtherJoinRoomPayload,
parseOtherOfflinePayload,
parseOtherOnlinePayload,
parsePlayerPreparePayload,
parseSelfExitRoomPayload,
parseSelfJoinRoomResponse,
type JoinRoomEnvironment,
type JoinRoomPayload,
type ParsedAgentserverPayload,
type ParsedKickPayload,
type ParsedOnlinePayload,
type ParsedRoomEntry,
type ParsedRoomResponse,
type ParsedRoomserverPayload,
type ParsedRoomSnapshot,
type ParsedSelfExitPayload,
type ParsedSeatPayload,
type ReconnectSnapshot,
type RoomActionPayload,
type RoomIdentity,
} from './room-contracts.ts';
@@ -0,0 +1,27 @@
import {requireArray,requireRecord,requireString,requireStringOrFiniteNumber} from './validation.ts';
export interface RankEntry {readonly name:string;readonly headimg:string;readonly playerid:string|number;readonly star:string|number;readonly signature:string;readonly notice?:string;readonly destwo?:string;}
/** Server rank entries are consumed as sent. Only rankList1 config replaces # line breaks. */
export function parseRankList(value:unknown,fromConfig=false):readonly RankEntry[]{
return Object.freeze(requireArray(value,'rank list').map((item,index)=>{
const p=`rank list[${index}]`,r=requireRecord(item,p);
const name=requireString(r.name,p+'.name'),signature=r.signature?requireString(r.signature,p+'.signature'):'';
return Object.freeze({name:fromConfig?configRankName(name):name,headimg:requireString(r.headimg,p+'.headimg'),playerid:requireStringOrFiniteNumber(r.playerid,p+'.playerid'),
star:requireStringOrFiniteNumber(r.star,p+'.star'),signature:fromConfig?signature.replace(/#/g,'\n'):signature});
}));
}
/** Func.subString(name,16,true): Latin-1 counts one, other UTF-16 units two. */
function configRankName(name:string):string {
let length=0;
for(let i=0;i<name.length;i++){
length+=name.charCodeAt(i)>255?2:1;
if(length>16)return name.slice(0,i)+'...';
}
return name;
}
export function parseVipRankList(value:unknown):readonly RankEntry[]{
const raw=requireArray(value,'VIP rank list'),entries=parseRankList(raw);
return Object.freeze(entries.map((entry,i)=>{const item=requireRecord(raw[i],'VIP rank entry');
return Object.freeze({...entry,notice:item.notice?requireString(item.notice,'notice'):'',destwo:item.destwo?requireString(item.destwo,'destwo'):''});}));
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "744452fb-2d7e-4a0e-9c49-2e6994f5df84",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,243 +1,302 @@
import {
buildFirstSliceEnvelope,
type FirstSliceEnvelope,
} from '../first-slice-routes.ts';
import {
hasOwn,
optionalField,
requireArray,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
import type { FirstSliceRuntimeConfig } from './login-contract.ts';
export type ReconnectSnapshot =
| { readonly present: false }
| { readonly present: true; readonly value: unknown };
export interface ParsedRoomSnapshot {
readonly roomcode: string;
readonly seat: number;
readonly roomtype: readonly unknown[];
readonly players: readonly unknown[];
}
export interface ParsedRoomEntry {
readonly room: ParsedRoomSnapshot;
readonly reconnect: ReconnectSnapshot;
readonly raw: JsonRecord;
}
export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot {
if (!Boolean(raw.deskinfo)) return { present: false };
return { present: true, value: raw.deskinfo };
}
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 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`);
}
return { roomcode, seat, roomtype, players };
}
export interface RoomIdentity {
readonly playerid: number;
readonly roomcode: string;
}
export interface RoomActionPayload extends Readonly<Record<string, unknown>> {
readonly agentid: string | number;
readonly playerid: number;
readonly gameid: string | number;
readonly roomcode: string;
}
function baseRoomAction(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): RoomActionPayload {
const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity');
const roomRecord = requireRecord(room, '$.room');
return {
agentid: requireStringOrFiniteNumber(
identity.agentid,
'$.runtimeConfig.identity.agentid',
),
playerid: requireInteger(roomRecord.playerid, '$.room.playerid'),
gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'),
};
}
export function buildPrepareRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): FirstSliceEnvelope<'player_prepare', RoomActionPayload> {
return buildFirstSliceEnvelope('player_prepare', baseRoomAction(runtimeConfig, room));
}
export function buildExitRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): FirstSliceEnvelope<'self_exit_room', RoomActionPayload> {
return buildFirstSliceEnvelope('self_exit_room', baseRoomAction(runtimeConfig, room));
}
export interface JoinRoomEnvironment {
readonly location: unknown;
readonly ip: string;
readonly vipMatch?: 1;
readonly match_id?: string | number;
}
export interface JoinRoomPayload extends RoomActionPayload {
readonly location: unknown;
readonly ip: string;
readonly vipMatch?: 1;
readonly match_id?: string | number;
}
export function buildJoinRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
environment: JoinRoomEnvironment,
): FirstSliceEnvelope<'self_join_room', JoinRoomPayload> {
const environmentRecord = requireRecord(environment, '$.environment');
if (!hasOwn(environmentRecord, 'location') || environmentRecord.location === undefined) {
throw new TypeError('$.environment.location: expected present field, received undefined');
}
if (environmentRecord.location !== null) {
requireRecord(environmentRecord.location, '$.environment.location');
}
const data: Record<string, unknown> = {
...baseRoomAction(runtimeConfig, room),
location: environmentRecord.location,
ip: requireString(environmentRecord.ip, '$.environment.ip'),
};
const vipMatch = optionalField(
environmentRecord,
'vipMatch',
'$.environment',
(value, path): 1 => {
const parsed = requireNumber(value, path);
if (parsed !== 1) {
throw new RangeError(`${path}: expected number 1, received number`);
}
return 1;
},
);
if (vipMatch !== undefined) data.vipMatch = vipMatch;
const matchId = optionalField(
environmentRecord,
'match_id',
'$.environment',
requireStringOrFiniteNumber,
);
if (matchId !== undefined) data.match_id = matchId;
return buildFirstSliceEnvelope('self_join_room', data as JoinRoomPayload);
}
export interface ParsedRoomResponse {
readonly state: number;
readonly room: ParsedRoomSnapshot | null;
readonly reconnect: ReconnectSnapshot;
readonly stage: 0 | 1;
readonly raw: JsonRecord;
}
export function parseSelfJoinRoomResponse(value: unknown): ParsedRoomResponse {
const raw = requireRecord(value, '$.data');
const state = requireInteger(raw.state, '$.data.state');
const room = state === 0 ? parseRoomEntryFields(raw) : null;
const parsedDeskwar = optionalField(raw, 'deskwar', '$.data', requireInteger);
if (parsedDeskwar !== undefined && parsedDeskwar !== 0 && parsedDeskwar !== 1) {
throw new RangeError('$.data.deskwar: expected protocol flag 0 or 1');
}
const reconnect = reconnectFrom(raw);
const stage = reconnect.present ? 1 : parsedDeskwar ?? 0;
return { state, room, reconnect, stage, raw };
}
export interface ParsedSeatPayload {
readonly seat: number;
readonly raw: JsonRecord;
}
function parseSeatPayload(value: unknown): ParsedSeatPayload {
const raw = requireRecord(value, '$.data');
const seat = requireInteger(raw.seat, '$.data.seat');
if (seat < 0) throw new RangeError('$.data.seat: expected non-negative server seat, received number');
return { seat, raw };
}
export const parsePlayerPreparePayload = parseSeatPayload;
export const parseOtherJoinRoomPayload = parseSeatPayload;
export const parseOtherExitRoomPayload = parseSeatPayload;
export const parseOtherOfflinePayload = parseSeatPayload;
export interface ParsedSelfExitPayload {
readonly seat?: number;
readonly raw: JsonRecord;
}
export function parseSelfExitRoomPayload(value: unknown): ParsedSelfExitPayload {
const raw = requireRecord(value, '$.data');
const seat = optionalField(raw, 'seat', '$.data', requireInteger);
if (seat !== undefined && seat < 0) {
throw new RangeError('$.data.seat: expected non-negative server seat, received number');
}
return { ...(seat === undefined ? {} : { seat }), raw };
}
export interface ParsedOnlinePayload extends ParsedSeatPayload {
readonly ip: string;
}
export function parseOtherOnlinePayload(value: unknown): ParsedOnlinePayload {
const parsed = parseSeatPayload(value);
return { seat: parsed.seat, ip: requireString(parsed.raw.ip, '$.data.ip'), raw: parsed.raw };
}
export interface ParsedRoomserverPayload {
readonly roomserver: string;
readonly raw: JsonRecord;
}
export function parseConnectRoomserverPayload(value: unknown): ParsedRoomserverPayload {
const raw = requireRecord(value, '$.data');
return { roomserver: requireString(raw.roomserver, '$.data.roomserver'), raw };
}
export interface ParsedAgentserverPayload {
readonly agentserver: string;
readonly opt: string;
readonly raw: JsonRecord;
}
export function parseConnectAgentserverPayload(value: unknown): ParsedAgentserverPayload {
const raw = requireRecord(value, '$.data');
return {
agentserver: requireString(raw.agentserver, '$.data.agentserver'),
opt: requireString(raw.opt, '$.data.opt'),
raw,
};
}
export interface ParsedKickPayload {
readonly msg: string;
readonly raw: JsonRecord;
}
export function parseKickServerPayload(value: unknown): ParsedKickPayload {
const raw = requireRecord(value, '$.data');
return { msg: requireString(raw.msg, '$.data.msg'), raw };
}
import {
buildFirstSliceEnvelope,
type FirstSliceEnvelope,
} from '../first-slice-routes.ts';
import {
hasOwn,
optionalField,
requireArray,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
import type { FirstSliceRuntimeConfig } from './login-contract.ts';
export type ReconnectSnapshot =
| { readonly present: false }
| { readonly present: true; readonly value: unknown };
export interface ParsedRoomSnapshot {
readonly roomcode: string;
readonly seat: number;
readonly roomtype: readonly unknown[];
readonly players: readonly unknown[];
}
export interface ParsedRoomEntry {
readonly room: ParsedRoomSnapshot;
readonly reconnect: ReconnectSnapshot;
readonly raw: JsonRecord;
}
export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot {
if (!Boolean(raw.deskinfo)) return { present: false };
return { present: true, value: raw.deskinfo };
}
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 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`);
}
return { roomcode, seat, roomtype, players };
}
export interface RoomIdentity {
readonly playerid: number;
readonly roomcode: string;
}
export interface RoomActionPayload extends Readonly<Record<string, unknown>> {
readonly agentid: string | number;
readonly playerid: number;
readonly gameid: string | number;
readonly roomcode: string;
}
function baseRoomAction(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): RoomActionPayload {
const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity');
const roomRecord = requireRecord(room, '$.room');
return {
agentid: requireStringOrFiniteNumber(
identity.agentid,
'$.runtimeConfig.identity.agentid',
),
playerid: requireInteger(roomRecord.playerid, '$.room.playerid'),
gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'),
};
}
export function buildPrepareRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): FirstSliceEnvelope<'player_prepare', RoomActionPayload> {
return buildFirstSliceEnvelope('player_prepare', baseRoomAction(runtimeConfig, room));
}
export function buildExitRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): FirstSliceEnvelope<'self_exit_room', RoomActionPayload> {
return buildFirstSliceEnvelope('self_exit_room', baseRoomAction(runtimeConfig, room));
}
export interface JoinRoomEnvironment {
readonly location: unknown;
readonly ip: string;
readonly vipMatch?: 1;
readonly match_id?: string | number;
}
export interface JoinRoomPayload extends Omit<RoomActionPayload, 'roomcode'> {
readonly roomcode: string | number;
readonly location: unknown;
readonly ip: string;
readonly vipMatch?: 1;
readonly match_id?: string | number;
}
export function buildJoinRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: { readonly playerid: number; readonly roomcode: string | number },
environment: JoinRoomEnvironment,
): FirstSliceEnvelope<'self_join_room', JoinRoomPayload> {
const environmentRecord = requireRecord(environment, '$.environment');
if (!hasOwn(environmentRecord, 'location') || environmentRecord.location === undefined) {
throw new TypeError('$.environment.location: expected present field, received undefined');
}
if (environmentRecord.location !== null) {
requireRecord(environmentRecord.location, '$.environment.location');
}
const data: Record<string, unknown> = {
...baseRoomAction(runtimeConfig, {playerid: room.playerid, roomcode: String(room.roomcode)}),
roomcode: requireStringOrFiniteNumber(room.roomcode, '$.room.roomcode'),
location: environmentRecord.location,
ip: requireString(environmentRecord.ip, '$.environment.ip'),
};
const vipMatch = optionalField(
environmentRecord,
'vipMatch',
'$.environment',
(value, path): 1 => {
const parsed = requireNumber(value, path);
if (parsed !== 1) {
throw new RangeError(`${path}: expected number 1, received number`);
}
return 1;
},
);
if (vipMatch !== undefined) data.vipMatch = vipMatch;
const matchId = optionalField(
environmentRecord,
'match_id',
'$.environment',
requireStringOrFiniteNumber,
);
if (matchId !== undefined) data.match_id = matchId;
return buildFirstSliceEnvelope('self_join_room', data as JoinRoomPayload);
}
export interface ParsedRoomResponse {
readonly state: number;
readonly room: ParsedRoomSnapshot | null;
readonly reconnect: ReconnectSnapshot;
readonly stage: 0 | 1;
readonly raw: JsonRecord;
}
export function parseSelfJoinRoomResponse(value: unknown): ParsedRoomResponse {
const raw = requireRecord(value, '$.data');
const state = requireInteger(raw.state, '$.data.state');
const room = state === 0 ? parseRoomEntryFields(raw) : null;
const parsedDeskwar = optionalField(raw, 'deskwar', '$.data', requireInteger);
if (parsedDeskwar !== undefined && parsedDeskwar !== 0 && parsedDeskwar !== 1) {
throw new RangeError('$.data.deskwar: expected protocol flag 0 or 1');
}
const reconnect = reconnectFrom(raw);
const stage = reconnect.present ? 1 : parsedDeskwar ?? 0;
return { state, room, reconnect, stage, raw };
}
export interface ParsedSeatPayload {
readonly seat: number;
readonly raw: JsonRecord;
}
function parseSeatPayload(value: unknown): ParsedSeatPayload {
const raw = requireRecord(value, '$.data');
const seat = requireInteger(raw.seat, '$.data.seat');
if (seat < 0) throw new RangeError('$.data.seat: expected non-negative server seat, received number');
return { seat, raw };
}
export const parsePlayerPreparePayload = parseSeatPayload;
export const parseOtherJoinRoomPayload = parseSeatPayload;
export const parseOtherExitRoomPayload = parseSeatPayload;
export const parseOtherOfflinePayload = parseSeatPayload;
export interface ParsedSelfExitPayload {
readonly seat?: number;
readonly raw: JsonRecord;
}
export function parseSelfExitRoomPayload(value: unknown): ParsedSelfExitPayload {
const raw = requireRecord(value, '$.data');
const seat = optionalField(raw, 'seat', '$.data', requireInteger);
if (seat !== undefined && seat < 0) {
throw new RangeError('$.data.seat: expected non-negative server seat, received number');
}
return { ...(seat === undefined ? {} : { seat }), raw };
}
export interface ParsedOnlinePayload extends ParsedSeatPayload {
readonly ip: string;
}
export function parseOtherOnlinePayload(value: unknown): ParsedOnlinePayload {
const parsed = parseSeatPayload(value);
return { seat: parsed.seat, ip: requireString(parsed.raw.ip, '$.data.ip'), raw: parsed.raw };
}
export interface ParsedRoomserverPayload {
readonly roomserver: string;
readonly raw: JsonRecord;
}
export function parseConnectRoomserverPayload(value: unknown): ParsedRoomserverPayload {
const raw = requireRecord(value, '$.data');
return { roomserver: requireString(raw.roomserver, '$.data.roomserver'), raw };
}
export interface ParsedAgentserverPayload {
readonly agentserver: string;
readonly opt: string;
readonly raw: JsonRecord;
}
export function parseConnectAgentserverPayload(value: unknown): ParsedAgentserverPayload {
const raw = requireRecord(value, '$.data');
return {
agentserver: requireString(raw.agentserver, '$.data.agentserver'),
opt: requireString(raw.opt, '$.data.opt'),
raw,
};
}
export interface ParsedKickPayload {
readonly msg: string;
readonly raw: JsonRecord;
}
export function parseKickServerPayload(value: unknown): ParsedKickPayload {
const raw = requireRecord(value, '$.data');
return { msg: requireString(raw.msg, '$.data.msg'), raw };
}
export interface CreateRoomEnvironment {
readonly roomtype: readonly unknown[];
readonly location: unknown;
readonly ip: string;
}
/** Transport contract only: roomtype contents belong to the subgame. */
export function buildCreateRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig,
playerid: number,
command: CreateRoomEnvironment,
): FirstSliceEnvelope<'create_room', Readonly<Record<string, unknown>>> {
const identity = requireRecord(runtimeConfig.identity, '$.runtimeConfig.identity');
const raw = requireRecord(command, '$.command');
if (!hasOwn(raw, 'location') || raw.location === undefined) {
throw new TypeError('$.command.location: expected present field');
}
if (raw.location !== null) requireRecord(raw.location, '$.command.location');
return buildFirstSliceEnvelope('create_room', {
agentid: requireStringOrFiniteNumber(identity.agentid, '$.identity.agentid'),
playerid: requireInteger(playerid, '$.playerid'),
gameid: requireStringOrFiniteNumber(identity.gameid, '$.identity.gameid'),
roomtype: requireArray(raw.roomtype, '$.command.roomtype'),
ip: requireString(raw.ip, '$.command.ip'),
location: raw.location,
});
}
export interface ParsedCreateRoomResponse {
readonly state: number;
readonly room: { readonly roomcode: string; readonly seat: number; readonly roomtype: readonly unknown[] } | null;
readonly needprepare: number;
readonly infinite: number;
readonly raw: JsonRecord;
}
export function parseCreateRoomResponse(value: unknown): ParsedCreateRoomResponse {
const raw = requireRecord(value, '$.data');
const state = requireInteger(raw.state, '$.data.state');
if (state !== 0) return {state, room: null, needprepare: 0, infinite: 0, raw};
// Desk.Init initializes these flags to zero; create_room overrides only when provided.
const flag = (key: string): number => {
if (!hasOwn(raw, key)) return 0;
const value = requireInteger(raw[key], `$.data.${key}`);
if (value !== 0 && value !== 1) throw new RangeError(`$.data.${key}: expected protocol flag`);
return value;
};
return {
state,
room: {roomcode: requireString(raw.roomcode, '$.data.roomcode'),
seat: requireInteger(raw.seat, '$.data.seat'),
roomtype: requireArray(raw.roomtype, '$.data.roomtype')},
needprepare: flag('needprepare'), infinite: flag('infinite'), raw,
};
}
@@ -1,77 +1,91 @@
import { APP } from '../core/constants.ts';
/** Legacy 12_Logic dispatches login replies from all three platform routes by rpc.
* This inbound contract is independent of the outbound agent route below. */
export function isLoginResponse(message:{route:string;rpc:string}):boolean{
return message.rpc==='player_login'&&(message.route==='agent'||message.route==='room'||message.route==='platform');
}
export type FirstSliceRpc =
| 'getInfoByShortCode'
| 'get_player_task'
| 'get_task_award'
| 'update_roomcard'
| 'player_finish_task'
| 'player_login'
| 'self_join_room'
| 'player_prepare'
| 'self_exit_room'
| 'other_join_room'
| 'other_exit_room'
| 'other_offline'
| 'other_online'
| 'connect_roomserver'
| 'connect_agentserver'
| 'kick_server'
| 'kick_offline'
| 'show_message';
export type FirstSliceRoute = 'platform' | 'agent' | 'room';
/** 第一纵向切片 rpc → route 的唯一权威映射。 */
export const FIRST_SLICE_RPC_ROUTE = {
getInfoByShortCode: 'agent',
get_player_task: 'agent',
get_task_award: 'agent',
update_roomcard: 'agent',
player_finish_task: 'agent',
player_login: 'agent',
self_join_room: 'agent',
player_prepare: 'room',
self_exit_room: 'room',
other_join_room: 'room',
other_exit_room: 'room',
other_offline: 'room',
other_online: 'room',
connect_roomserver: 'room',
connect_agentserver: 'agent',
kick_server: 'agent',
kick_offline: 'agent',
show_message: 'agent',
} as const satisfies Record<FirstSliceRpc, 'platform' | 'agent' | 'room'>;
export function routeForFirstSlice<Rpc extends FirstSliceRpc>(
rpc: Rpc,
): (typeof FIRST_SLICE_RPC_ROUTE)[Rpc] {
if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, rpc)) {
throw new Error(`FIRST_SLICE_RPC_ROUTE 未定义 rpc: ${String(rpc)}`);
}
return FIRST_SLICE_RPC_ROUTE[rpc];
}
export interface FirstSliceEnvelope<
Rpc extends FirstSliceRpc,
Data extends Readonly<Record<string, unknown>>,
> {
readonly app: 'youle';
readonly route: (typeof FIRST_SLICE_RPC_ROUTE)[Rpc];
readonly rpc: Rpc;
readonly data: Data;
}
export function buildFirstSliceEnvelope<
Rpc extends FirstSliceRpc,
Data extends Readonly<Record<string, unknown>>,
>(rpc: Rpc, data: Data): FirstSliceEnvelope<Rpc, Data> {
return { app: APP, route: routeForFirstSlice(rpc), rpc, data };
}
import { APP } from '../core/constants.ts';
/** Legacy 12_Logic dispatches login replies from all three platform routes by rpc.
* This inbound contract is independent of the outbound agent route below. */
export function isLoginResponse(message:{route:string;rpc:string}):boolean{
return message.rpc==='player_login'&&(message.route==='agent'||message.route==='room'||message.route==='platform');
}
export type FirstSliceRpc =
| 'getVipRankList'
| 'get_treasurelist'
| 'topup_card'
| 'get_player_invitecode'
| 'binding_invitecode'
| 'getInfoByShortCode'
| 'get_player_task'
| 'get_task_award'
| 'update_bean'
| 'update_roomcard'
| 'player_finish_task'
| 'player_login'
| 'create_room'
| 'self_join_room'
| 'player_prepare'
| 'self_exit_room'
| 'other_join_room'
| 'other_exit_room'
| 'other_offline'
| 'other_online'
| 'connect_roomserver'
| 'connect_agentserver'
| 'kick_server'
| 'kick_offline'
| 'show_message';
export type FirstSliceRoute = 'platform' | 'agent' | 'room';
/** 第一纵向切片 rpc → route 的唯一权威映射。 */
export const FIRST_SLICE_RPC_ROUTE = {
getVipRankList: 'agent',
get_treasurelist: 'agent',
topup_card: 'agent',
get_player_invitecode: 'agent',
binding_invitecode: 'agent',
getInfoByShortCode: 'agent',
get_player_task: 'agent',
get_task_award: 'agent',
update_bean: 'agent',
update_roomcard: 'agent',
player_finish_task: 'agent',
player_login: 'agent',
create_room: 'agent',
self_join_room: 'agent',
player_prepare: 'room',
self_exit_room: 'room',
other_join_room: 'room',
other_exit_room: 'room',
other_offline: 'room',
other_online: 'room',
connect_roomserver: 'room',
connect_agentserver: 'agent',
kick_server: 'agent',
kick_offline: 'agent',
show_message: 'agent',
} as const satisfies Record<FirstSliceRpc, 'platform' | 'agent' | 'room'>;
export function routeForFirstSlice<Rpc extends FirstSliceRpc>(
rpc: Rpc,
): (typeof FIRST_SLICE_RPC_ROUTE)[Rpc] {
if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, rpc)) {
throw new Error(`FIRST_SLICE_RPC_ROUTE 未定义 rpc: ${String(rpc)}`);
}
return FIRST_SLICE_RPC_ROUTE[rpc];
}
export interface FirstSliceEnvelope<
Rpc extends FirstSliceRpc,
Data extends Readonly<Record<string, unknown>>,
> {
readonly app: 'youle';
readonly route: (typeof FIRST_SLICE_RPC_ROUTE)[Rpc];
readonly rpc: Rpc;
readonly data: Data;
}
export function buildFirstSliceEnvelope<
Rpc extends FirstSliceRpc,
Data extends Readonly<Record<string, unknown>>,
>(rpc: Rpc, data: Data): FirstSliceEnvelope<Rpc, Data> {
return { app: APP, route: routeForFirstSlice(rpc), rpc, data };
}
@@ -1,206 +1,228 @@
import type { InboundMessage } from '../core/types/envelope.ts';
import type { GameSessionHost } from '../sdk/runtime/game-session-host.ts';
import type { ConnectionControlPort } from '../platform/connection-intent.ts';
import type { RuntimeSession } from '../platform/runtime-session.ts';
import type { PlatformStore } from '../platform/stores/platform-store.ts';
import { requireRecord, requireString } from './contracts/validation.ts';
import {
parseConnectAgentserverPayload,
parseConnectRoomserverPayload,
parseKickServerPayload,
parseOtherExitRoomPayload,
parseOtherJoinRoomPayload,
parseOtherOfflinePayload,
parseOtherOnlinePayload,
parsePlayerPreparePayload,
parseSelfExitRoomPayload,
} from './contracts/index.ts';
import {
buildFirstSliceEnvelope,
isLoginResponse,
FIRST_SLICE_RPC_ROUTE,
routeForFirstSlice,
type FirstSliceRpc,
} from './first-slice-routes.ts';
export interface PlatformHandlersOptions {
readonly store: PlatformStore;
readonly gameSession: GameSessionHost;
readonly session: RuntimeSession;
readonly connection: ConnectionControlPort;
readonly showMessage?: (message:string,time:number,source:'show_message'|'update_roomcard')=>void;
}
export type PlatformDispatchResult = 'handled-platform';
/** Strict first-slice platform RPC dispatcher. */
export class PlatformHandlers {
constructor(private readonly options: PlatformHandlersOptions) {}
dispatch(message: InboundMessage): PlatformDispatchResult {
if (!isPlatformRoute(message.route)) {
throw new Error(
`PlatformHandlers received non-platform route: route=${message.route} rpc=${message.rpc}`,
);
}
if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, message.rpc)) {
throw new Error(
`Unsupported platform rpc: route=${message.route} rpc=${message.rpc}`,
);
}
const rpc = message.rpc as FirstSliceRpc;
const expectedRoute = routeForFirstSlice(rpc);
if (message.route !== expectedRoute && !isLoginResponse(message)) {
throw new Error(
`Platform rpc route mismatch: rpc=${rpc} expected=${expectedRoute} received=${message.route}`,
);
}
switch (rpc) {
case 'getInfoByShortCode':this.options.store.applyVipRooms(message.data);break;
case 'update_roomcard': {
const raw=requireRecord(message.data,'$.data');
const text=raw.text?requireString(raw.text,'$.data.text'):null;
this.options.store.applyRoomcardUpdate(raw);
if(raw.change===undefined&&text){
if(!this.options.showMessage)throw Error('update_roomcard requires a message UI');
this.options.showMessage(text,0,'update_roomcard');
}
break;
}
case 'get_player_task':this.options.store.applyTaskList(message.data);break;
case 'get_task_award':this.options.store.applyTaskAward(message.data);break;
case 'player_finish_task':this.options.store.applyTaskFinished(message.data);break;
case 'player_login':
this.options.session.handleLogin(message.data);
break;
case 'self_join_room':
this.options.session.handleSelfJoin(message.data);
break;
case 'player_prepare': {
const parsed = parsePlayerPreparePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerReady(parsed);
this.options.gameSession.publish({ type: 'room.player-ready', seat: parsed.seat });
break;
}
case 'self_exit_room': {
const parsed = parseSelfExitRoomPayload(message.data);
this.options.session.handleSelfExit(parsed);
break;
}
case 'other_join_room': {
const parsed = parseOtherJoinRoomPayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerJoin(parsed);
this.options.gameSession.publish({ type: 'room.player-joined', seat: parsed.seat });
break;
}
case 'other_exit_room': {
const parsed = parseOtherExitRoomPayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerExit(parsed);
this.options.gameSession.publish({ type: 'room.player-left', seat: parsed.seat });
break;
}
case 'other_offline': {
const parsed = parseOtherOfflinePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerOffline(parsed);
this.options.gameSession.publish({ type: 'room.player-offline', seat: parsed.seat });
break;
}
case 'other_online': {
const parsed = parseOtherOnlinePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerOnline(parsed);
this.options.gameSession.publish({ type: 'room.player-online', seat: parsed.seat });
break;
}
case 'connect_roomserver': {
const parsed = parseConnectRoomserverPayload(message.data);
const target = parseSwitchTarget(parsed.roomserver, '$.data.roomserver');
const resend = buildFirstSliceEnvelope('connect_roomserver', parsed.raw);
this.options.session.handleServerSwitch(
() => this.options.connection.switchServer(target, resend),
);
break;
}
case 'connect_agentserver': {
const parsed = parseConnectAgentserverPayload(message.data);
const target = parseSwitchTarget(parsed.agentserver, '$.data.agentserver');
const resend = buildFirstSliceEnvelope('connect_agentserver', parsed.raw);
this.options.session.handleServerSwitch(
() => this.options.connection.switchServer(target, resend),
);
break;
}
case 'show_message': {
const data=requireRecord(message.data,'$.data');
const text=requireString(data.msg,'$.data.msg');
// Desk.show_message converts the original time with Number; only >0 auto-closes.
const time=Number(data.time);
if(!this.options.showMessage)throw Error('show_message requires a message UI');
this.options.showMessage(text,time,'show_message');
break;
}
case 'kick_offline': {
const data=requireRecord(message.data,'$.data');
this.options.session.handleKick(data,()=>this.options.connection.stopForKick(data));
break;
}
case 'kick_server': {
const parsed = parseKickServerPayload(message.data);
this.options.session.handleKick(
parsed.raw,
() => this.options.connection.stopForKick(parsed.raw),
);
break;
}
}
return 'handled-platform';
}
private requireActiveGame(rpc: string): void {
if (this.options.gameSession.state !== 'active') {
throw new Error(`${rpc} requires an active game session`);
}
}
}
function isPlatformRoute(route: string): route is 'platform' | 'agent' | 'room' {
return route === 'platform' || route === 'agent' || route === 'room';
}
function parseSwitchTarget(address: string, path: string): string {
if (address.length === 0 || address.trim() !== address) {
throw new TypeError(`${path}: expected a non-empty WebSocket server address`);
}
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(address);
const target = scheme ? address : `ws://${address}`;
const authority = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#]*)/.exec(target)?.[1];
if (authority === undefined || authority.length === 0) {
throw new TypeError(`${path}: expected a WebSocket address with a non-empty host`);
}
if (target.includes('#') || authority.includes('@')) {
throw new TypeError(`${path}: expected a WebSocket address without fragment or userinfo`);
}
let parsed: URL;
try {
parsed = new URL(target);
} catch {
throw new TypeError(`${path}: expected a valid WebSocket server address`);
}
const protocol = parsed.protocol.toLowerCase();
if (
(protocol !== 'ws:' && protocol !== 'wss:')
|| parsed.hostname.length === 0
|| parsed.username.length > 0
|| parsed.password.length > 0
|| parsed.hash.length > 0
) {
throw new TypeError(`${path}: expected a valid WebSocket server address`);
}
return target;
}
import type { InboundMessage } from '../core/types/envelope.ts';
import type { GameSessionHost } from '../sdk/runtime/game-session-host.ts';
import type { ConnectionControlPort } from '../platform/connection-intent.ts';
import type { RuntimeSession } from '../platform/runtime-session.ts';
import type { PlatformStore } from '../platform/stores/platform-store.ts';
import { requireRecord, requireString } from './contracts/validation.ts';
import {
parseConnectAgentserverPayload,
parseConnectRoomserverPayload,
parseKickServerPayload,
parseOtherExitRoomPayload,
parseOtherJoinRoomPayload,
parseOtherOfflinePayload,
parseOtherOnlinePayload,
parsePlayerPreparePayload,
parseSelfExitRoomPayload,
} from './contracts/index.ts';
import {
buildFirstSliceEnvelope,
isLoginResponse,
FIRST_SLICE_RPC_ROUTE,
routeForFirstSlice,
type FirstSliceRpc,
} from './first-slice-routes.ts';
export interface PlatformHandlersOptions {
readonly store: PlatformStore;
readonly gameSession: GameSessionHost;
readonly session: RuntimeSession;
readonly connection: ConnectionControlPort;
readonly showMessage?: (message:string,time:number,source:'show_message'|'update_roomcard'|'update_bean'|'binding_invitecode')=>void;
}
export type PlatformDispatchResult = 'handled-platform';
/** Strict first-slice platform RPC dispatcher. */
export class PlatformHandlers {
constructor(private readonly options: PlatformHandlersOptions) {}
dispatch(message: InboundMessage): PlatformDispatchResult {
if (!isPlatformRoute(message.route)) {
throw new Error(
`PlatformHandlers received non-platform route: route=${message.route} rpc=${message.rpc}`,
);
}
if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, message.rpc)) {
throw new Error(
`Unsupported platform rpc: route=${message.route} rpc=${message.rpc}`,
);
}
const rpc = message.rpc as FirstSliceRpc;
const expectedRoute = routeForFirstSlice(rpc);
if (message.route !== expectedRoute && !isLoginResponse(message) && !(rpc==='update_bean'&&message.route==='room')) {
throw new Error(
`Platform rpc route mismatch: rpc=${rpc} expected=${expectedRoute} received=${message.route}`,
);
}
switch (rpc) {
case 'getVipRankList':this.options.store.applyVipRank(message.data);break;
case 'get_treasurelist':this.options.store.applyTreasureList(message.data);break;
case 'topup_card':break; // Original response is empty; updates arrive separately.
case 'get_player_invitecode':this.options.store.applyInvitecode(message.data);break;
case 'binding_invitecode':{
if(!this.options.showMessage)throw Error('binding_invitecode requires a message UI');
const text=this.options.store.applyInviteBinding(message.data);
this.options.showMessage(text,3000,'binding_invitecode');break;
}
case 'getInfoByShortCode':this.options.store.applyVipRooms(message.data);break;
case 'update_bean': {
const raw=requireRecord(message.data,'$.data');
const text=raw.text?requireString(raw.text,'$.data.text'):null;
this.options.store.applyBeanUpdate(raw);
if(raw.change===undefined&&raw.seat===undefined&&text){
if(!this.options.showMessage)throw Error('update_bean requires a message UI');
this.options.showMessage(text,0,'update_bean');
}
break;
}
case 'update_roomcard': {
const raw=requireRecord(message.data,'$.data');
const text=raw.text?requireString(raw.text,'$.data.text'):null;
this.options.store.applyRoomcardUpdate(raw);
if(raw.change===undefined&&text){
if(!this.options.showMessage)throw Error('update_roomcard requires a message UI');
this.options.showMessage(text,0,'update_roomcard');
}
break;
}
case 'get_player_task':this.options.store.applyTaskList(message.data);break;
case 'get_task_award':this.options.store.applyTaskAward(message.data);break;
case 'player_finish_task':this.options.store.applyTaskFinished(message.data);break;
case 'player_login':
this.options.session.handleLogin(message.data);
break;
case 'create_room':
this.options.session.handleCreateRoom(message.data);
break;
case 'self_join_room':
this.options.session.handleSelfJoin(message.data);
break;
case 'player_prepare': {
const parsed = parsePlayerPreparePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerReady(parsed);
this.options.gameSession.publish({ type: 'room.player-ready', seat: parsed.seat });
break;
}
case 'self_exit_room': {
const parsed = parseSelfExitRoomPayload(message.data);
this.options.session.handleSelfExit(parsed);
break;
}
case 'other_join_room': {
const parsed = parseOtherJoinRoomPayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerJoin(parsed);
this.options.gameSession.publish({ type: 'room.player-joined', seat: parsed.seat });
break;
}
case 'other_exit_room': {
const parsed = parseOtherExitRoomPayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerExit(parsed);
this.options.gameSession.publish({ type: 'room.player-left', seat: parsed.seat });
break;
}
case 'other_offline': {
const parsed = parseOtherOfflinePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerOffline(parsed);
this.options.gameSession.publish({ type: 'room.player-offline', seat: parsed.seat });
break;
}
case 'other_online': {
const parsed = parseOtherOnlinePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerOnline(parsed);
this.options.gameSession.publish({ type: 'room.player-online', seat: parsed.seat });
break;
}
case 'connect_roomserver': {
const parsed = parseConnectRoomserverPayload(message.data);
const target = parseSwitchTarget(parsed.roomserver, '$.data.roomserver');
const resend = buildFirstSliceEnvelope('connect_roomserver', parsed.raw);
this.options.session.handleServerSwitch(
() => this.options.connection.switchServer(target, resend),
);
break;
}
case 'connect_agentserver': {
const parsed = parseConnectAgentserverPayload(message.data);
const target = parseSwitchTarget(parsed.agentserver, '$.data.agentserver');
const resend = buildFirstSliceEnvelope('connect_agentserver', parsed.raw);
this.options.session.handleServerSwitch(
() => this.options.connection.switchServer(target, resend),
);
break;
}
case 'show_message': {
const data=requireRecord(message.data,'$.data');
const text=requireString(data.msg,'$.data.msg');
// Desk.show_message converts the original time with Number; only >0 auto-closes.
const time=Number(data.time);
if(!this.options.showMessage)throw Error('show_message requires a message UI');
this.options.showMessage(text,time,'show_message');
break;
}
case 'kick_offline': {
const data=requireRecord(message.data,'$.data');
this.options.session.handleKick(data,()=>this.options.connection.stopForKick(data));
break;
}
case 'kick_server': {
const parsed = parseKickServerPayload(message.data);
this.options.session.handleKick(
parsed.raw,
() => this.options.connection.stopForKick(parsed.raw),
);
break;
}
}
return 'handled-platform';
}
private requireActiveGame(rpc: string): void {
if (this.options.gameSession.state !== 'active') {
throw new Error(`${rpc} requires an active game session`);
}
}
}
function isPlatformRoute(route: string): route is 'platform' | 'agent' | 'room' {
return route === 'platform' || route === 'agent' || route === 'room';
}
function parseSwitchTarget(address: string, path: string): string {
if (address.length === 0 || address.trim() !== address) {
throw new TypeError(`${path}: expected a non-empty WebSocket server address`);
}
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(address);
const target = scheme ? address : `ws://${address}`;
const authority = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#]*)/.exec(target)?.[1];
if (authority === undefined || authority.length === 0) {
throw new TypeError(`${path}: expected a WebSocket address with a non-empty host`);
}
if (target.includes('#') || authority.includes('@')) {
throw new TypeError(`${path}: expected a WebSocket address without fragment or userinfo`);
}
let parsed: URL;
try {
parsed = new URL(target);
} catch {
throw new TypeError(`${path}: expected a valid WebSocket server address`);
}
const protocol = parsed.protocol.toLowerCase();
if (
(protocol !== 'ws:' && protocol !== 'wss:')
|| parsed.hostname.length === 0
|| parsed.username.length > 0
|| parsed.password.length > 0
|| parsed.hash.length > 0
) {
throw new TypeError(`${path}: expected a valid WebSocket server address`);
}
return target;
}
@@ -0,0 +1,12 @@
/** Pre-room capability: a game owns its page and validated, opaque wire configuration. */
export interface CreateRoomPageContext {
readonly previousRoomtype:unknown;
submit(roomtype:readonly unknown[]):void;
cancel():void;
reportError(message:string):void;
}
export interface CreateRoomPagePort {
open(context:CreateRoomPageContext):void;
setBusy(busy:boolean):void;
close():void;
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "fd4c9495-d6d5-49da-8476-a510df24f1c4",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,6 +1,7 @@
export * from './game-entry.ts';
export * from './game-host.ts';
export * from './game-module.ts';
export * from './game-seat-mapper.ts';
export * from './platform-events.ts';
export * from './snapshots.ts';
export * from './create-room-page.ts';
export * from './game-host.ts';
export * from './game-module.ts';
export * from './game-seat-mapper.ts';
export * from './platform-events.ts';
export * from './snapshots.ts';
@@ -0,0 +1,91 @@
[
{
"__type__": "cc.AnimationClip",
"_name": "AnnouncementHorn",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"sample": 2,
"speed": 1,
"wrapMode": 2,
"enableTrsBlending": false,
"_duration": 2,
"_hash": 0,
"_tracks": [
{
"__id__": 1
}
],
"_exoticAnimation": null,
"_events": [],
"_embeddedPlayers": [],
"_additiveSettings": {
"__id__": 6
},
"_auxiliaryCurveEntries": []
},
{
"__type__": "cc.animation.ObjectTrack",
"_binding": {
"__type__": "cc.animation.TrackBinding",
"path": {
"__id__": 2
},
"proxy": null
},
"_channel": {
"__id__": 4
}
},
{
"__type__": "cc.animation.TrackPath",
"_paths": [
{
"__id__": 3
},
"spriteFrame"
]
},
{
"__type__": "cc.animation.ComponentPath",
"component": "cc.Sprite"
},
{
"__type__": "cc.animation.Channel",
"_curve": {
"__id__": 5
}
},
{
"__type__": "cc.ObjectCurve",
"_times": [
0,
0.5,
1,
1.5
],
"_values": [
{
"__uuid__": "151439f3-c9bd-4fb2-97b5-5afc295d76e8@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "5647b603-4a35-4bd8-b955-a8ceca2a3f43@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "151439f3-c9bd-4fb2-97b5-5afc295d76e8@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "151439f3-c9bd-4fb2-97b5-5afc295d76e8@f9941",
"__expectedType__": "cc.SpriteFrame"
}
]
},
{
"__type__": "cc.AnimationClipAdditiveSettings",
"enabled": false,
"refClip": null
}
]
@@ -0,0 +1,13 @@
{
"ver": "2.0.4",
"importer": "animation-clip",
"imported": true,
"uuid": "aed03d13-bce1-42a0-9d23-ebd829d9ee55",
"files": [
".bin"
],
"subMetas": {},
"userData": {
"name": "AnnouncementHorn"
}
}
@@ -0,0 +1,92 @@
[
{
"__type__": "cc.AnimationClip",
"_name": "RequestLoading",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"sample": 60,
"speed": 1,
"wrapMode": 2,
"enableTrsBlending": false,
"_duration": 1,
"_hash": 0,
"_tracks": [
{
"__id__": 1
}
],
"_exoticAnimation": null,
"_events": [],
"_embeddedPlayers": [],
"_additiveSettings": {
"__id__": 5
},
"_auxiliaryCurveEntries": []
},
{
"__type__": "cc.animation.RealTrack",
"_binding": {
"__type__": "cc.animation.TrackBinding",
"path": {
"__id__": 2
},
"proxy": null
},
"_channel": {
"__id__": 3
}
},
{
"__type__": "cc.animation.TrackPath",
"_paths": [
"angle"
]
},
{
"__type__": "cc.animation.Channel",
"_curve": {
"__id__": 4
}
},
{
"__type__": "cc.RealCurve",
"_times": [
0,
1
],
"_values": [
{
"__type__": "cc.RealKeyframeValue",
"interpolationMode": 0,
"tangentWeightMode": 0,
"value": 0,
"rightTangent": 0,
"rightTangentWeight": 0,
"leftTangent": 0,
"leftTangentWeight": 0,
"easingMethod": 0,
"__editorExtras__": null
},
{
"__type__": "cc.RealKeyframeValue",
"interpolationMode": 0,
"tangentWeightMode": 0,
"value": -360,
"rightTangent": 0,
"rightTangentWeight": 0,
"leftTangent": 0,
"leftTangentWeight": 0,
"easingMethod": 0,
"__editorExtras__": null
}
],
"preExtrapolation": 1,
"postExtrapolation": 1
},
{
"__type__": "cc.AnimationClipAdditiveSettings",
"enabled": false,
"refClip": null
}
]
@@ -0,0 +1,13 @@
{
"ver": "2.0.4",
"importer": "animation-clip",
"imported": true,
"uuid": "987771c5-7f29-487a-b61a-476981cc9216",
"files": [
".bin"
],
"subMetas": {},
"userData": {
"name": "RequestLoading"
}
}
@@ -1,134 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a",
"displayName": "00042",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@f9941",
"displayName": "00042",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 108,
"height": 98,
"rawWidth": 108,
"rawHeight": 98,
"borderTop": 48,
"borderBottom": 48,
"borderLeft": 53,
"borderRight": 53,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-54,
-49,
0,
54,
-49,
0,
-54,
49,
0,
54,
49,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
98,
108,
98,
0,
0,
108,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-54,
-49,
0
],
"maxPos": [
54,
49,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a",
"atlasUuid": "",
"trimType": "none"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a"
}
}
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a",
"displayName": "00042",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@f9941",
"displayName": "00042",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 108,
"height": 98,
"rawWidth": 108,
"rawHeight": 98,
"borderTop": 26,
"borderBottom": 34,
"borderLeft": 30,
"borderRight": 31,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-54,
-49,
0,
54,
-49,
0,
-54,
49,
0,
54,
49,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
98,
108,
98,
0,
0,
108,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-54,
-49,
0
],
"maxPos": [
54,
49,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a",
"atlasUuid": "",
"trimType": "none"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -45,9 +45,9 @@
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1600,
"width": 1280,
"height": 720,
"rawWidth": 1600,
"rawWidth": 1280,
"rawHeight": 720,
"borderTop": 0,
"borderBottom": 0,
@@ -60,16 +60,16 @@
"meshType": 0,
"vertices": {
"rawPosition": [
-800,
-640,
-360,
0,
800,
640,
-360,
0,
-800,
-640,
360,
0,
800,
640,
360,
0
],
@@ -84,11 +84,11 @@
"uv": [
0,
720,
1600,
1280,
720,
0,
0,
1600,
1280,
0
],
"nuv": [
@@ -102,12 +102,12 @@
1
],
"minPos": [
-800,
-640,
-360,
0
],
"maxPos": [
800,
640,
360,
0
]
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "ad5e871f-b5d8-439a-852b-53d4d1d9f61b",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,538 @@
[
{
"__type__": "cc.Prefab",
"_name": "OwnerRoomItem",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "OwnerRoomItem",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 8
},
{
"__id__": 14
}
],
"_active": true,
"_components": [
{
"__id__": 20
},
{
"__id__": 22
}
],
"_prefab": {
"__id__": 24
},
"_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": "5dxzP/UG5Fea3nNr557ORP"
},
{
"__type__": "cc.Node",
"_name": "Spirit100",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
}
],
"_prefab": {
"__id__": 7
},
"_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": "9blwBxZBxC4LDYs+VIpNy1"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 264,
"height": 80
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "90IWdamq1CaK2+fcIY8I4G"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "l3ocOicG3kovqa6+Yow7SQ"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 6
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "5f783910-7fd0-4482-a206-8dcb57238c74@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": "e2Q9Z8vy1NVpG+xdDhdD9s"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "VMQNp5XtZ86iR1Z+hSYFOg"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "0lhvW5OWHIVi/ncFScuMKQ",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Spirit102",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": false,
"_components": [
{
"__id__": 9
},
{
"__id__": 11
}
],
"_prefab": {
"__id__": 13
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 39,
"y": 2.5,
"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": "89d19HpytGVox/SvGWJFDr"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": {
"__id__": 10
},
"_contentSize": {
"__type__": "cc.Size",
"width": 14,
"height": 19
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "50k3QzjPhHhJYWzNgtUGBk"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "1sIXmawSIfDNdF9tSIh6ig"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": {
"__id__": 12
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "9d49bb70-608e-46d2-8672-5afb78bbd8d7@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": "32xJfAFBFPL57FrZT1Njpw"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "dYeagt3oXF7LLcLyBNmxcQ"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "xOj6F8iDH5ro1bLGRx3c5g",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Digits",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 15
},
{
"__id__": 17
}
],
"_prefab": {
"__id__": 19
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 32,
"y": 2.5,
"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": "85eISb6stE0oj1mZHpP6zK"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 14
},
"_enabled": true,
"__prefab": {
"__id__": 16
},
"_contentSize": {
"__type__": "cc.Size",
"width": 14,
"height": 19
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_id": "44soAEXzBJSaDlfZI1MuoB"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "41Li0cKhzYiSg6SbbaoAGg"
},
{
"__type__": "cc.Layout",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 14
},
"_enabled": true,
"__prefab": {
"__id__": 18
},
"_resizeMode": 1,
"_layoutType": 1,
"_cellSize": {
"__type__": "cc.Size",
"width": 40,
"height": 40
},
"_startAxis": 0,
"_paddingLeft": 0,
"_paddingRight": 0,
"_paddingTop": 0,
"_paddingBottom": 0,
"_spacingX": 0,
"_spacingY": 0,
"_verticalDirection": 1,
"_horizontalDirection": 0,
"_constraint": 0,
"_constraintNum": 2,
"_affectedByScale": false,
"_isAlign": false,
"_id": "53I4gVRnlCNo+Wqps6VeDy"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "k0owfG3taNqw+EIUHZ8V9g"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "lC5WHe9437aSbTecw6lUiQ",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 21
},
"_contentSize": {
"__type__": "cc.Size",
"width": 264,
"height": 80
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "77ZqoCi+xJ/7Bjxxb0o4bl"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "KLcB/F2HEtr5tYEndV8qbg"
},
{
"__type__": "e40a5gQKgNA6a/yNIkJZlsD",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 23
},
"digitTemplate": {
"__id__": 11
},
"digitsRoot": {
"__id__": 14
},
"_id": "306fBHxpJHuLQI/kcAnGQS"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "RCmOaA1Wm5nUHFrcVQJpOw"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "0oOd9F3D7yWBfO1u4tL3BA",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
}
]
@@ -2,12 +2,12 @@
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "b13f487a-451e-41ce-a810-1fab0d52248f",
"uuid": "67117675-9f6d-4def-a67c-db0e9fb19b98",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "Layer9_Feedback"
"syncNodeName": "OwnerRoomItem"
}
}
@@ -0,0 +1,170 @@
[
{
"__type__": "cc.Prefab",
"_name": "PlayerInfoTextItem",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "PlayerInfoTextItem",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [],
"_active": true,
"_components": [
{
"__id__": 2
},
{
"__id__": 4
}
],
"_prefab": {
"__id__": 6
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 10,
"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": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 3
},
"_contentSize": {
"__type__": "cc.Size",
"width": 371,
"height": 22.680000000000003
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 1
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "JYB5ndvc0hWZFdTsDPujQg"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 5
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_string": "资料文字",
"_horizontalAlign": 0,
"_verticalAlign": 0,
"_actualFontSize": 18.975,
"_fontSize": 18,
"_fontFamily": "Arial",
"_lineHeight": 18,
"_overflow": 3,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": false,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": false,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "wX+Ww4pBz/TnLcrPEXSuAA"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "Pn6tcB1uaJ1EOUf2nLgpzw",
"instance": null,
"targetOverrides": null
}
]
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "9e6dbfc6-26c5-4eac-9cc8-8060a677d5ee",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "PlayerInfoTextItem"
}
}
@@ -1,13 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "b9f6f400-7b1b-4e51-a841-cc84dcddca82",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "TaskItem"
}
}
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "b9f6f400-7b1b-4e51-a841-cc84dcddca82",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "TaskItem"
}
}
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "5925d1d2-f4b2-44c1-a37a-fb2bcdcf664e",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "VipRankItem"
}
}
@@ -1,13 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "fed2e71c-22ac-4e48-a42a-68e48a1929a7",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "VipRoomItem1"
}
}
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "fed2e71c-22ac-4e48-a42a-68e48a1929a7",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "VipRoomItem1"
}
}
@@ -1,13 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "8cb43df4-d8fa-4b3c-ab01-497b23c68435",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "VipRoomItem2"
}
}
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "8cb43df4-d8fa-4b3c-ab01-497b23c68435",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "VipRoomItem2"
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -25,14 +25,14 @@
"_active": true,
"_components": [
{
"__id__": 24
"__id__": 25
},
{
"__id__": 26
"__id__": 27
}
],
"_prefab": {
"__id__": 28
"__id__": 29
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -82,14 +82,14 @@
"_active": true,
"_components": [
{
"__id__": 19
"__id__": 20
},
{
"__id__": 21
"__id__": 22
}
],
"_prefab": {
"__id__": 23
"__id__": 24
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -186,6 +186,7 @@
"__id__": 5
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1600,
"height": 720
},
@@ -313,10 +314,13 @@
},
{
"__id__": 16
},
{
"__id__": 18
}
],
"_prefab": {
"__id__": 18
"__id__": 19
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -360,6 +364,7 @@
"__id__": 13
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
},
@@ -455,6 +460,29 @@
"__type__": "cc.CompPrefabInfo",
"fileId": "3wlBO4ciKrxeTYsQdlQvBg"
},
{
"__type__": "cc.Animation",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 11
},
"_enabled": true,
"__prefab": null,
"playOnLoad": false,
"_clips": [
{
"__uuid__": "987771c5-7f29-487a-b61a-476981cc9216",
"__expectedType__": "cc.AnimationClip"
}
],
"_defaultClip": {
"__uuid__": "987771c5-7f29-487a-b61a-476981cc9216",
"__expectedType__": "cc.AnimationClip"
},
"_id": "1b3VMKLpRFPKzD9o608yTZ"
},
{
"__type__": "cc.PrefabInfo",
"root": {
@@ -478,9 +506,10 @@
},
"_enabled": true,
"__prefab": {
"__id__": 20
"__id__": 21
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1600,
"height": 720
},
@@ -505,7 +534,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 22
"__id__": 23
},
"_alignFlags": 45,
"_target": null,
@@ -554,9 +583,10 @@
},
"_enabled": true,
"__prefab": {
"__id__": 25
"__id__": 26
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1600,
"height": 720
},
@@ -581,7 +611,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 27
"__id__": 28
},
"_alignFlags": 45,
"_target": null,
@@ -1,7 +1,7 @@
[
{
"__type__": "cc.Prefab",
"_name": "Login_Layer",
"_name": "Login",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
@@ -13,7 +13,7 @@
},
{
"__type__": "cc.Node",
"_name": "Login_Layer",
"_name": "Login",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
@@ -1,14 +1,14 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "b209cefb-b381-4c7c-b765-94a5ab75a89c",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "Login_Layer",
"hasIcon": false
}
}
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "b209cefb-b381-4c7c-b765-94a5ab75a89c",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "Login",
"hasIcon": false
}
}
@@ -8,6 +8,6 @@
],
"subMetas": {},
"userData": {
"syncNodeName": "Layer4_MainMenu"
"syncNodeName": "MainMenu"
}
}
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "e7a05100-7de4-4935-896e-a59061377b87",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "NoticeBoard"
}
}
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "260ea2dc-696c-4a65-a2fd-109dd794bb45",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "OwnedRoomList"
}
}
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "95ee9b79-8c30-4f54-a2cf-19d02083cd28",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "QrCode"
}
}
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "ada34847-93c7-48f2-bb0a-5c1db4b112f6",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "RedeemCode"
}
}
@@ -1,7 +1,7 @@
[
{
"__type__": "cc.Prefab",
"_name": "Layer1_Logo",
"_name": "ResourceLoading",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
@@ -13,22 +13,19 @@
},
{
"__type__": "cc.Node",
"_name": "Layer1_Logo",
"_name": "ResourceLoading",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 50
}
],
"_active": true,
"_components": [],
"_prefab": {
"__id__": 60
"__id__": 50
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -1130,241 +1127,6 @@
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "group-32",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 51
}
],
"_active": true,
"_components": [],
"_prefab": {
"__id__": 59
},
"_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": ""
},
{
"__type__": "cc.Node",
"_name": "Spirit221",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 50
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 52
},
{
"__id__": 54
},
{
"__id__": 56
}
],
"_prefab": {
"__id__": 58
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -1612.5,
"y": 1099.952,
"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": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 51
},
"_enabled": true,
"__prefab": {
"__id__": 53
},
"_contentSize": {
"__type__": "cc.Size",
"width": 63,
"height": 63
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "Y8tq/cDuhVvttPn+x9+bww"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 51
},
"_enabled": true,
"__prefab": {
"__id__": 55
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "974bf7e8-69fd-49ff-95fa-ed7ddb8d6a14@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "t+vRkZQc/kDtmwdvT1STKA"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 51
},
"_enabled": true,
"__prefab": {
"__id__": 57
},
"_alignFlags": 16,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": -1612.5,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "K2+PudUKVOrBYR09Lp3sAA"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "J1IKV6k3EB5NkpQVaZVxiA",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "JccA+NmPM7lVqq5kAuMdjQ",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.PrefabInfo",
"root": {
@@ -8,6 +8,6 @@
],
"subMetas": {},
"userData": {
"syncNodeName": "Layer1_Logo"
"syncNodeName": "ResourceLoading"
}
}
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "bfaf200b-f8f5-4fcb-b9e3-7e4e831177ce",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "c593ddf6-0173-48e8-96cb-dab1273e43d8",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,38 @@
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 {TemplateRoomRules} from './template-room-rules.ts';
const {ccclass,property}=_decorator;
@ccclass('TemplateCreateRoomView')
export class TemplateCreateRoomView extends CreateRoomPage {
@property([ToggleContainer]) groups:ToggleContainer[]=[];
@property([Toggle]) options:Toggle[]=[];
@property([Label]) optionLabels:Label[]=[];
@property(Button) submitButton:Button|null=null;
@property([Button]) cancelButtons:Button[]=[];
private readonly rules=new TemplateRoomRules();
private context:CreateRoomPageContext|null=null;
private busy=false;
private releases:Array<()=>void>=[];
open(context:CreateRoomPageContext):void{
this.close();
if(this.groups.length!==2||this.options.length!==4||this.optionLabels.length!==4||!this.submitButton||this.cancelButtons.length!==2)throw Error('TemplateCreateRoomView requires serialized controls');
this.rules.open(context.previousRoomtype);this.context=context;
// A saved source index may have no matching option; initialize that state explicitly.
this.groups.forEach(group=>group.allowSwitchOff=true);
this.optionLabels.forEach((label,i)=>label.string=this.rules.labels[i]);
this.options.forEach((toggle,i)=>{
toggle.isChecked=this.rules.selection[Math.floor(i/2)]===i%2;
const changed=()=>{if(!this.busy&&toggle.isChecked)this.rules.select(Math.floor(i/2) as 0|1,i%2);};
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=()=>context.cancel();button.node.on(Button.EventType.CLICK,cancel);this.releases.push(()=>button.node.off(Button.EventType.CLICK,cancel));});this.setBusy(false);
}
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": "ebbef7d9-7bda-4c61-9b0a-1ee56e7817fb",
"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": "f4582832-23e2-4981-be26-08dfe832aa96",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "TemplateRoom"
}
}
@@ -0,0 +1,39 @@
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. */
@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();}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e0dc1f88-a719-4b19-9827-2cf9e4452be9",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,29 @@
import type {GameEntry,GameHost,GameModule,PlatformGameSnapshot} from '../../framework/sdk/contracts/index.ts';
import {TemplateRoomRules} from './template-room-rules.ts';
export interface TemplateRoomActions {prepare():void;exit():void;}
export interface TemplateRoomPresentation {render(snapshot:PlatformGameSnapshot,actions:TemplateRoomActions):void;close():void;}
/** Template hooks are intentionally empty, matching 02_SubGame_Input.js. No gameplay is inferred. */
export function createTemplateGame(gameId:string|number){
let active:TemplateModule|null=null;
class TemplateModule implements GameModule {
private host:GameHost|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());
}
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();}
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;}
}
const rules=new TemplateRoomRules();
const entry:GameEntry={key:'Game_Surface_3',gameId,route:'Game_Surface_3',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 restoredSnapshot():unknown{return active?.restored;}};
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c6a24a57-c9c3-4eda-9f82-73f1990703fb",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,21 @@
/** 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;
private selected:[number,number]=[0,0];
get selection():readonly number[]{return [...this.selected];}
open(previous:unknown=null):void{
if(previous===null){this.selected=[0,0];return;}
if(!Array.isArray(previous)||!Number.isInteger(previous[0])||!Number.isInteger(previous[1]))throw Error('Invalid template saved roomtype');
this.selected=[previous[0]-1,previous[1]-1];
}
select(group:0|1,index:number):void{
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]];}
seatCount(roomtype:readonly unknown[]):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;
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "0cad77f9-c126-4532-9ffb-70e650008a34",
"files": [],
"subMetas": {},
"userData": {}
}
File diff suppressed because it is too large Load Diff
@@ -52,7 +52,7 @@
},
"autoReleaseAssets": false,
"_globals": {
"__id__": 5
"__id__": 6
},
"_id": "1c10c2ee-adc2-496f-9005-d3e0e6ebbb56"
},
@@ -72,6 +72,9 @@
},
{
"__id__": 4
},
{
"__id__": 5
}
],
"_prefab": null,
@@ -182,8 +185,24 @@
"__uuid__": "e1b53f04-3e2f-4127-9a3f-f629bed6905e",
"__expectedType__": "cc.Prefab"
},
"notices": {
"__uuid__": "b13f487a-451e-41ce-a810-1fab0d52248f",
"redeemCode": {
"__uuid__": "ada34847-93c7-48f2-bb0a-5c1db4b112f6",
"__expectedType__": "cc.Prefab"
},
"qrCode": {
"__uuid__": "95ee9b79-8c30-4f54-a2cf-19d02083cd28",
"__expectedType__": "cc.Prefab"
},
"noticeBoard": {
"__uuid__": "e7a05100-7de4-4935-896e-a59061377b87",
"__expectedType__": "cc.Prefab"
},
"ownedRoomList": {
"__uuid__": "260ea2dc-696c-4a65-a2fd-109dd794bb45",
"__expectedType__": "cc.Prefab"
},
"invite": {
"__uuid__": "a3db6daa-a98e-4a73-b0f7-cd122bdb11c5",
"__expectedType__": "cc.Prefab"
},
"profile": {
@@ -206,6 +225,100 @@
"__uuid__": "f4af0cc2-2514-4bd5-a0de-7a3dbb32438b",
"__expectedType__": "cc.Prefab"
},
"rank": {
"__uuid__": "3eafe9c2-475b-4c81-b71a-37ffa2717c71",
"__expectedType__": "cc.Prefab"
},
"rankItem": {
"__uuid__": "5925d1d2-f4b2-44c1-a37a-fb2bcdcf664e",
"__expectedType__": "cc.Prefab"
},
"rankMedals": [
{
"__uuid__": "fb86ab44-8140-4d8e-82f1-84ae4b8f3216@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "10653735-f46a-4e0f-906f-dacb093dad23@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "87749e95-72aa-435e-93bb-1f41553922f0@f9941",
"__expectedType__": "cc.SpriteFrame"
}
],
"rankTitle": {
"__uuid__": "ecb18612-2066-42d2-8fa0-c0161124cf1e@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"vipRankTitle": {
"__uuid__": "97b4bd7f-8716-497f-a375-d9c841cf34e9@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"ownedRoomDigits": [
{
"__uuid__": "9d49bb70-608e-46d2-8672-5afb78bbd8d7@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "e71157d8-1261-4917-bac1-1b48ab5b6ab9@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "bade804f-d16a-46b6-8746-600ff6b93cce@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "dae7297a-cb1a-49d7-98a9-a3008a8afc7a@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "f1365825-aa47-4efe-936c-81284b0d5e1f@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "b4d68ae8-0799-47c1-8b30-26b2daed2d59@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "be16e577-8add-4c32-a56b-948a52e0180b@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "2fd355d7-493d-474c-b753-676c7ee9e50d@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "3c1709f3-a24f-4a9e-814c-04bc1441da55@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "4dd36de5-fadb-4852-8334-e9b5834a3957@f9941",
"__expectedType__": "cc.SpriteFrame"
}
],
"rankSymbols": [
{
"__uuid__": "6b9ad554-bf4a-4493-82dd-92b4328cb8a4@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "2f1e97ec-1bad-4731-9563-d90ae5b93111@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "a1016858-72af-4a2c-8d8e-7098eb82a310@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "a6986c72-2bef-4f50-b215-86d4de943b68@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "df99cc0a-0b2a-4fdd-b271-7932b2bea237@f9941",
"__expectedType__": "cc.SpriteFrame"
}
],
"digits": [
{
"__uuid__": "fc6e9a8e-0a6d-4555-bc3e-88fd427b9f3a@f9941",
@@ -272,32 +385,56 @@
],
"_id": "abZXhqPblEq57+36N2j8xH"
},
{
"__type__": "ec19eRwzzZA6LQg5DFOX78H",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"createRoom": {
"__uuid__": "584dbb85-4acc-4bea-9d9b-e0cbeb8741da",
"__expectedType__": "cc.Prefab"
},
"room": {
"__uuid__": "f4582832-23e2-4981-be26-08dfe832aa96",
"__expectedType__": "cc.Prefab"
},
"roomScene": {
"__uuid__": "18f06ada-9d36-485c-9f44-556d6980ca7a",
"__expectedType__": "cc.SceneAsset"
},
"_id": "c2sXHnthJJQoYDj00RAeBa"
},
{
"__type__": "cc.SceneGlobals",
"ambient": {
"__id__": 6
},
"shadows": {
"__id__": 7
},
"_skybox": {
"shadows": {
"__id__": 8
},
"fog": {
"_skybox": {
"__id__": 9
},
"octree": {
"fog": {
"__id__": 10
},
"skin": {
"octree": {
"__id__": 11
},
"lightProbeInfo": {
"skin": {
"__id__": 12
},
"postSettings": {
"lightProbeInfo": {
"__id__": 13
},
"postSettings": {
"__id__": 14
},
"bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false
},
@@ -0,0 +1,449 @@
[
{
"__type__": "cc.SceneAsset",
"_name": "TemplateRoom",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"scene": {
"__id__": 1
}
},
{
"__type__": "cc.Scene",
"_name": "TemplateRoom",
"_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": "18f06ada-9d36-485c-9f44-556d6980ca7a"
},
{
"__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": "1cgbnsENBDoLhHJwvyJnKB"
},
{
"__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": "9atEuWMGdBq70ouQQ4tRnO"
},
{
"__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": "4780+rx15BKqkTCexT+Fi8"
},
{
"__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": "9cHcVMc5JHVJoaZqnwhdaW"
},
{
"__type__": "cc.Canvas",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_cameraComponent": {
"__id__": 4
},
"_alignCanvasWithScreen": true,
"_id": "f7IXBbP6ZNjqIM4Q5/I3dI"
},
{
"__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": "9fI6665VZLG5Uuoch5UpgC"
},
{
"__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.2,
"y": 0.5,
"z": 0.8,
"w": 1
},
"_skyColor": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 1
},
"_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
}
]
@@ -0,0 +1,11 @@
{
"ver": "1.1.50",
"importer": "scene",
"imported": true,
"uuid": "18f06ada-9d36-485c-9f44-556d6980ca7a",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,50 @@
import { _decorator, Component, Label, Mask, Node, UITransform, Widget } from 'cc';
import { announcementMotion } from '../../framework/presentation/lobby-model.ts';
import { requireNode } from './node-bindings.ts';
const {ccclass}=_decorator;
function required<T>(value:T|null,name:string):T {if(value===null)throw Error(`Announcement missing ${name}`);return value;}
/** Native clipping and movement of the authored Announcement children. */
@ccclass('AnnouncementView')
export class AnnouncementView extends Component {
private background:Node|null=null;
private textNode:Node|null=null;
private viewport:Node|null=null;
private message='';
private elapsed=0;
private textY=0;
show(message:string):void {
if(!this.viewport){
const bg=requireNode(this.node,'Spirit219'),text=requireNode(this.node,'Spirit220');
const bgUI=required(bg.getComponent(UITransform),'Spirit219 UITransform');
// The text is animated; an authored Widget must not reset its X every frame.
const widget=text.getComponent(Widget);if(widget)widget.enabled=false;
const viewport=new Node('AnnouncementClip');viewport.layer=this.node.layer;viewport.parent=bg;
const clipUI=viewport.addComponent(UITransform);clipUI.setAnchorPoint(0,0);
clipUI.setContentSize(bgUI.contentSize);
viewport.setPosition(-bgUI.width*bgUI.anchorX,-bgUI.height*bgUI.anchorY,0);
viewport.addComponent(Mask);
this.textY=text.position.y-bg.position.y+bgUI.height*bgUI.anchorY;
text.parent=viewport;
const textUI=required(text.getComponent(UITransform),'Spirit220 UITransform');
textUI.setAnchorPoint(0,textUI.anchorY);
const label=required(text.getComponent(Label),'Spirit220 Label');
label.enableWrapText=false;label.overflow=Label.Overflow.NONE;label.horizontalAlign=Label.HorizontalAlign.LEFT;
this.background=bg;this.textNode=text;this.viewport=viewport;
}
this.message=message;this.elapsed=0;
required(required(this.textNode,'text').getComponent(Label),'label').string=message;
this.draw();
}
update(dt:number):void {if(this.viewport){this.elapsed+=dt;this.draw();}}
private draw():void {
const text=required(this.textNode,'text'),bg=required(this.background,'background');
const width=required(bg.getComponent(UITransform),'background UITransform').width;
const state=announcementMotion(this.message,width,this.elapsed);
bg.active=state.visible;text.active=state.visible;
text.setPosition(state.left,this.textY,0);
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4b4591ab-307a-4061-9690-7102b996a3ae",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,10 @@
import {_decorator,Component} from 'cc';
import type {CreateRoomPageContext,CreateRoomPagePort} from '../../framework/sdk/contracts/create-room-page.ts';
const {ccclass}=_decorator;
/** Inherit this component on the subgame prefab root. Platform never inspects its children. */
@ccclass('CreateRoomPage')
export class CreateRoomPage extends Component implements CreateRoomPagePort {
open(_context:CreateRoomPageContext):void{throw Error('Subgame must implement CreateRoomPage.open');}
setBusy(_busy:boolean):void{throw Error('Subgame must implement CreateRoomPage.setBusy');}
close():void{throw Error('Subgame must implement CreateRoomPage.close');}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "956dedf9-3650-418b-af8a-a755a26d991c",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,37 @@
import {_decorator,Button,Component,Label} from 'cc';
import {JoinRoomInput} from '../../framework/presentation/join-room-input.ts';
const {ccclass,property}=_decorator;
export interface JoinRoomViewContext {initialCode:string;submit(code:number):void;cancel():void;paste():Promise<string>;reportError(message:string):void;}
@ccclass('JoinRoomView')
export class JoinRoomView extends Component {
@property([Button]) digitButtons:Button[]=[];
@property([Label]) slots:Label[]=[];
@property(Button) clearButton:Button|null=null;
@property(Button) deleteButton:Button|null=null;
@property(Button) pasteButton:Button|null=null;
@property(Button) submitButton:Button|null=null;
@property([Button]) cancelButtons:Button[]=[];
private input:JoinRoomInput|null=null;
private releases:Array<()=>void>=[];
private generation=0;
private busy=false;
private pasting=false;
open(context:JoinRoomViewContext):void{
this.close();const generation=this.generation;
if(this.digitButtons.length!==10||this.slots.length!==6||!this.clearButton||!this.deleteButton||!this.pasteButton||!this.submitButton||this.cancelButtons.length!==2)throw Error('JoinRoomView requires serialized controls');
const input=this.input=new JoinRoomInput(code=>context.submit(code));input.prefill(context.initialCode);
const bind=(button:Button,action:()=>void)=>{const fn=()=>{if(this.busy||this.pasting)return;try{action();}catch(error){context.reportError(error instanceof Error?error.message:String(error));}finally{this.render();}};
button.node.on(Button.EventType.CLICK,fn);this.releases.push(()=>button.node.off(Button.EventType.CLICK,fn));};
this.digitButtons.forEach((button,digit)=>bind(button,()=>input.digit(digit)));
bind(this.clearButton,()=>input.clear());bind(this.deleteButton,()=>input.remove());bind(this.submitButton,()=>input.confirm());
bind(this.pasteButton,()=>{this.pasting=true;void context.paste().then(text=>{if(this.generation===generation){input.paste(text);this.render();}})
.catch(error=>{if(this.generation===generation)context.reportError(error instanceof Error?error.message:String(error));})
.finally(()=>{if(this.generation===generation)this.pasting=false;});});
this.cancelButtons.forEach(button=>{const fn=()=>context.cancel();button.node.on(Button.EventType.CLICK,fn);this.releases.push(()=>button.node.off(Button.EventType.CLICK,fn));});
this.setBusy(false);this.render();
}
setBusy(busy:boolean):void{this.busy=busy;this.input?.setBusy(busy);for(const b of [...this.digitButtons,this.clearButton,this.deleteButton,this.submitButton,this.pasteButton])if(b)b.interactable=!busy;}
private render():void{const code=this.input?.code;if(code===undefined)return;this.slots.forEach((slot,i)=>{slot.node.active=i<code.length;slot.string=code[i]??'';});}
close():void{this.generation++;this.pasting=false;for(const release of this.releases.splice(0))release();this.input=null;}
onDestroy():void{this.close();}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "033ab9dd-e1a1-48fb-9fd6-5474c1a45eb2",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,17 +1,28 @@
import { _decorator, Component, Prefab, SpriteFrame } from 'cc';
const {ccclass,property}=_decorator;
/** Editor-owned asset references; no runtime UUID/path guessing. */
@ccclass('LobbyAssets')
export class LobbyAssets extends Component {
@property(Prefab) menu:Prefab|null=null;
@property(Prefab) header:Prefab|null=null;
@property(Prefab) noticeMenu:Prefab|null=null;
@property(Prefab) notices:Prefab|null=null;
@property(Prefab) profile:Prefab|null=null;
@property(Prefab) settings:Prefab|null=null;
@property(Prefab) join:Prefab|null=null;
@property(Prefab) tasks:Prefab|null=null;
import { _decorator, Component, Prefab, SpriteFrame } from 'cc';
const {ccclass,property}=_decorator;
/** Editor-owned asset references; no runtime UUID/path guessing. */
@ccclass('LobbyAssets')
export class LobbyAssets extends Component {
@property(Prefab) menu:Prefab|null=null;
@property(Prefab) header:Prefab|null=null;
@property(Prefab) noticeMenu:Prefab|null=null;
@property(Prefab) redeemCode:Prefab|null=null;
@property(Prefab) qrCode:Prefab|null=null;
@property(Prefab) noticeBoard:Prefab|null=null;
@property(Prefab) ownedRoomList:Prefab|null=null;
@property(Prefab) invite:Prefab|null=null;
@property(Prefab) profile:Prefab|null=null;
@property(Prefab) settings:Prefab|null=null;
@property(Prefab) join:Prefab|null=null;
@property(Prefab) tasks:Prefab|null=null;
@property(Prefab) share:Prefab|null=null;
@property([SpriteFrame]) digits:SpriteFrame[]=[];
@property([SpriteFrame]) switches:SpriteFrame[]=[];
}
@property(Prefab) rank:Prefab|null=null;
@property(Prefab) rankItem:Prefab|null=null;
@property([SpriteFrame]) rankMedals:SpriteFrame[]=[];
@property(SpriteFrame) rankTitle:SpriteFrame|null=null;
@property(SpriteFrame) vipRankTitle:SpriteFrame|null=null;
@property([SpriteFrame]) ownedRoomDigits:SpriteFrame[]=[];
@property([SpriteFrame]) rankSymbols:SpriteFrame[]=[];
@property([SpriteFrame]) digits:SpriteFrame[]=[];
@property([SpriteFrame]) switches:SpriteFrame[]=[];
}
@@ -0,0 +1,37 @@
import {_decorator,Component,Layout,Sprite,SpriteFrame,Toggle,ToggleContainer} from 'cc';
const {ccclass,property}=_decorator;
/** Native ToggleContainer owns selection; serialized frames own the tab appearance. */
@ccclass('NoticeTabsView')
export class NoticeTabsView extends Component {
@property([Toggle]) tabs:Toggle[]=[];
@property([Sprite]) backgrounds:Sprite[]=[];
@property([Sprite]) titles:Sprite[]=[];
@property(SpriteFrame) normalBackground:SpriteFrame|null=null;
@property(SpriteFrame) selectedBackground:SpriteFrame|null=null;
@property([SpriteFrame]) normalTitles:SpriteFrame[]=[];
@property([SpriteFrame]) selectedTitles:SpriteFrame[]=[];
private release:(()=>void)|null=null;
bindPages(visible:readonly boolean[],select:(index:number)=>void):()=>void {
this.release?.();
const count=this.tabs.length,container=this.getComponent(ToggleContainer),layout=this.getComponent(Layout);
if(count===0||visible.length!==count||!visible[0]||!container||container.allowSwitchOff||!layout||layout.type!==Layout.Type.VERTICAL)
throw Error('NoticeTabsView requires a vertical single-selection container and a visible first page');
if(!this.normalBackground||!this.selectedBackground||[this.backgrounds,this.titles,this.normalTitles,this.selectedTitles].some(items=>items.length!==count||items.some(item=>!item)))
throw Error('NoticeTabsView requires complete serialized tab artwork');
this.tabs.forEach((tab,index)=>{if(tab.node.parent!==this.node)throw Error('Notice tabs must be direct ToggleContainer children');tab.node.active=visible[index];});
this.tabs[0].setIsCheckedWithoutNotify(true);
this.tabs.slice(1).forEach(tab=>tab.setIsCheckedWithoutNotify(false));
const paint=()=>this.tabs.forEach((tab,index)=>{
this.backgrounds[index].spriteFrame=tab.isChecked?this.selectedBackground:this.normalBackground;
this.titles[index].spriteFrame=tab.isChecked?this.selectedTitles[index]:this.normalTitles[index];
});
const handlers=this.tabs.map((tab,index)=>()=>{paint();if(tab.isChecked)select(index);});
this.tabs.forEach((tab,index)=>tab.node.on(Toggle.EventType.TOGGLE,handlers[index]));
const release=()=>{this.tabs.forEach((tab,index)=>tab.node.off(Toggle.EventType.TOGGLE,handlers[index]));if(this.release===release)this.release=null;};
this.release=release;paint();layout.updateLayout(true);select(0);
return release;
}
onDestroy():void{this.release?.();}
}

Some files were not shown because too many files have changed in this diff Show More