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
+67 -59
View File
@@ -1,61 +1,69 @@
# AGENTS.md # AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## 第一准则:服务器零改动
**任何前端开发都必须完全遵循前后端数据包的协议与数据结构,做到服务器零改动。** 这是本仓库不可逾越的最高准则,优先于一切其它考量:
- 协议信封、`route`/`rpc` 命名、字段名与类型、`roomtype` 配置编码、`deskinfo` 快照结构等,**必须与现有协议逐字节对齐**(依据 `docs/protocol/`),不得为前端方便而改变格式或新增/重命名字段。
- 遇到协议层有疑问时,**先读 `docs/protocol/` 对应章节核对,不要臆测**;宁可前端多做适配,也绝不要求服务器配合改动。
- 新的 Cocos 前端(`YouleNexus`)的目标,就是在不改服务器的前提下复刻该协议契约、实现联调兼容。
## 第二准则:数据源权威、唯一,下游不兜底
**每一类数据只能有一个权威且唯一的来源(single source of truth);下游消费方不得猜测、修补、兜底。**
- **唯一来源**:同一份配置/数据(如服务器地址、渠道身份、协议常量)只在一个地方定义,其它地方一律引用,不得各处复制或并存第二份。
- **下游零兜底**:消费方拿到的数据缺失或非法时,**直接显式报错/抛出,把问题暴露到来源**,不得用默认值「猜一个」、不得静默 `?? 兜底值`、不得 try/catch 后塞个 fallback 蒙混。错误要早暴露、可定位,而不是被下游悄悄掩盖。
- **配置即契约**:来源(如 `profiles.ts` 的服务器地址)必须把该提供的字段显式提供齐全;缺了就是配置错误,应在来源处修正,而不是让下游补。
- 例外只有一种:来源本身明确定义了「可选 + 缺省语义」,且该缺省**写在来源处**(而非散落在各下游)。
> 这条与「服务器零改动」并列:前者保边界契约不变,后者保内部数据流可信、单源、可追溯。新增/修改任何带默认值、回退、容错分支的代码前,先问:这是不是在替上游兜底?若是,改为在来源处修正 + 在下游暴露。
## 仓库总览
这是友乐(Youle)棋牌游戏平台的前端工作区,核心是「**一套前端模板 + 多个子游戏**」的架构,并正在迁移到 Cocos Creator。主要目录:
- **`projects/`** —— 旧版 HTML5 前端。`projects/Game_Surface_3` 是**前端模板(平台外壳)**,也是新前端 UI 迁移的**原工程(迁移源)**;其余目录都是**基于该模板扩展出来的子游戏**,平台层几乎不动,只改子游戏层 + 美术数据。分层结构与关键文件见 `projects/AGENTS.md`。
- **`docs/protocol/`** —— 前端模板框架与服务器收发包的**协议规范**(权威文档,8 篇)。改动任何网络层代码前**必须先读对应章节**,服务器不可改,前端必须严格对齐协议。
- **`cocoscreator_projects/YouleNexus/`** —— 新的 Cocos Creator 3.8+ 前端工程,目标是按 `docs/protocol` 复刻协议契约,做到服务器零改动即可联调。
### 服务器文档与二七王服务端
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. - **`server/development-guide/`**:服务器通用开发文档。查服务端框架、子游戏接入、通信协议和开发约束时,从 [README](server/development-guide/README.md) 开始,按任务阅读对应章节。
- **`server/games/erqiwang/`**:二七王当前改动后的服务器代码。核对实际行为时读取这里的实现,不以 `projects/erqiwang` 的旧前端推断当前服务器规则。
- 二七王规则设计位于 [design.md](server/games/erqiwang/docs/design/design.md),玩法协议位于 [packet_protocol.md](server/games/erqiwang/docs/protocol/packet_protocol.md)。前端接入时同时核对设计、协议与服务端实现;发现差异应明确记录,不把设计要求冒充已实现行为。
- `roomtype` 由目标子游戏定义,可能是数组或字符串;平台通用流程不可擅自转换类型。二七王实际解析以 `class.config.js` 为入口,局数与扣卡检查 `class.export.js`;位索引只在协议文档中维护,不在本文件重复定义。
- 查阅服务器源码用于前端适配,不代表授权修改服务器代码;前端开发继续遵守“服务器零改动”。
## 第一准则:服务器零改动 ### 原工程 `Game_Surface_3` → `YouleNexus` 的 UI 迁移
**任何前端开发都必须完全遵循前后端数据包的协议与数据结构,做到服务器零改动。** 这是本仓库不可逾越的最高准则,优先于一切其它考量: `YouleNexus` 的 UI 资源(`assets/framework/ui/prefabs/`)从**原工程 `projects/Game_Surface_3`** 逐层复刻。数据契约与流程规范:
- 协议信封、`route`/`rpc` 命名、字段名与类型、`roomtype` 配置数组、`deskinfo` 快照结构等,**必须与现有协议逐字节对齐**(依据 `docs/protocol/`),不得为前端方便而改变格式或新增/重命名字段。 - 数据模型(权威):`docs/superpowers/specs/原工程数据模型.md`;迁移流程:`docs/superpowers/specs/UI-手工迁移规范.md`(9 步 funplay MCP + 8 项验证清单)。
- 遇到协议层有疑问时,**先读 `docs/protocol/` 对应章节核对,不要臆测**;宁可前端多做适配,也绝不要求服务器配合改动。 - 已完成的参照真相源:`assets/framework/ui/prefabs/Login_Layer.prefab`(Layer 2)。
- 新的 Cocos 前端(`YouleNexus`)的目标,就是在不改服务器的前提下复刻该协议契约、实现联调兼容。
> ⚠️ **索引与类型(易踩坑,实测修正)**:源数据索引**直接按 ID**——`gameabc_Object.json` 的 `ObjectList[ObjectID]`、`gameabc_Image.json` 的 `ImageFileList[ImageFileID]`(**不是** `ID - 1`,旧文档的 off-by-one 是错的);组件类型看 `ObjectType`(`2`=Sprite、`4`=Text),**不是** XML 的 `SpiritType`(那是另一套枚举)。
## 第二准则:数据源权威、唯一,下游不兜底
## 网络协议(`docs/protocol/`,新旧前端共同契约)
**每一类数据只能有一个权威且唯一的来源(single source of truth);下游消费方不得猜测、修补、兜底。**
`docs/protocol/` 是权威文档,改任何网络层代码前先读对应章节,**不要凭本文件的转述臆测**。
- **唯一来源**:同一份配置/数据(如服务器地址、渠道身份、协议常量)只在一个地方定义,其它地方一律引用,不得各处复制或并存第二份。
- **下游零兜底**:消费方拿到的数据缺失或非法时,**直接显式报错/抛出,把问题暴露到来源**,不得用默认值「猜一个」、不得静默 `?? 兜底值`、不得 try/catch 后塞个 fallback 蒙混。错误要早暴露、可定位,而不是被下游悄悄掩盖。 **两个不可妥协的约束:** ① `roomtype` 房间配置编码的类型与结构必须与目标子游戏**逐字节一致**(服务器据此解析规则);② `deskinfo` 对局快照须可序列化/反序列化以支持 `isbattle==1` 时 `Game_Modify.Reconnect(deskinfo)` 断线重连。
- **配置即契约**:来源(如 `profiles.ts` 的服务器地址)必须把该提供的字段显式提供齐全;缺了就是配置错误,应在来源处修正,而不是让下游补。
- 例外只有一种:来源本身明确定义了「可选 + 缺省语义」,且该缺省**写在来源处**(而非散落在各下游)。 ## 客户端侧适配约束:远程配置 / 原生数据接口 / 原生↔H5 桥接
> 这条与「服务器零改动」并列:前者保边界契约不变,后者保内部数据流可信、单源、可追溯。新增/修改任何带默认值、回退、容错分支的代码前,先问:这是不是在替上游兜底?若是,改为在来源处修正 + 在下游暴露。 继「服务器零改动」之后的**第二类不可妥协约束**:除 WebSocket 协议外,前端还通过「远程配置文件」(`gameserver` → `urlserver`)与「原生接口」(`window.settings` 同步取值、WVJB 异步桥)获取大量数据。新 Cocos 前端必须与原项目**逐字一致**地复刻这些读取/互调/注册方式(接口名、数据格式、回调约定、URL 构造),让原生侧与配置服务**零改动**即可对接。
## 仓库总览 具体机制、handler 清单与源码位置见 `native-bridge-contract` 技能(`.Codex/skills/native-bridge-contract/SKILL.md`)——改这部分前先读它核对,不要臆测接口名或数据格式。
这是友乐(Youle)棋牌游戏平台的前端工作区,核心是「**一套前端模板 + 多个子游戏**」的架构,并正在迁移到 Cocos Creator。三个顶层目录: ## Cocos Creator 新前端:优先使用 funplay-cocos-mcp
- **`projects/`** —— 旧版 HTML5 前端。`projects/Game_Surface_3` 是**前端模板(平台外壳)**,也是新前端 UI 迁移的**原工程(迁移源)**;其余目录都是**基于该模板扩展出来的子游戏**,平台层几乎不动,只改子游戏层 + 美术数据。分层结构与关键文件见 `projects/AGENTS.md`。 新工程 `cocoscreator_projects/YouleNexus`。**任何涉及该工程的编辑器操作**(场景、节点、组件、prefab、资源、动画、预览/截图等),都必须走 `mcp__funplay_cocos__*` 这组 MCP 工具驱动编辑器。
- **`docs/protocol/`** —— 前端模板框架与服务器收发包的**协议规范**(权威文档,8 篇)。改动任何网络层代码前**必须先读对应章节**,服务器不可改,前端必须严格对齐协议。
- **`cocoscreator_projects/YouleNexus/`** —— 新的 Cocos Creator 3.8+ 前端工程,目标是按 `docs/protocol` 复刻协议契约,做到服务器零改动即可联调。 **绝不允许手改** `.scene` / `.prefab` / `.anim` / `.meta`——它们是 UUID 引用的序列化资源,任何文本编辑都会破坏引用结构,导致「资源导入失败」且只能人工恢复。**MCP 连不上时也不例外**:应提示用户去编辑器启动服务,而不是退而求其次去手改文件。
### 原工程 `Game_Surface_3` → `YouleNexus` 的 UI 迁移 具体工具用法不写在这里——MCP server 连上后会自注入完整说明(工具清单、action 速查、批量操作),以其为准,**正常操作不需要先读任何仓库文档**。
`YouleNexus` 的 UI 资源(`assets/framework/ui/prefabs/`)从**原工程 `projects/Game_Surface_3`** 逐层复刻。数据契约与流程规范: `cocos-mcp` 技能(`.Codex/skills/cocos-mcp/SKILL.md`)只记录 MCP 自己给不了的部分:前置条件、探活、排障、已知坑。**工具正常时不必读它;一旦 `cocos_*` 工具没出现、调用报连接失败、或行为与预期不符,就去读。**
- 数据模型(权威):`docs/superpowers/specs/原工程数据模型.md`;迁移流程:`docs/superpowers/specs/UI-手工迁移规范.md`(9 步 funplay MCP + 8 项验证清单)。
- 已完成的参照真相源:`assets/framework/ui/prefabs/Login_Layer.prefab`(Layer 2)。
> ⚠️ **索引与类型(易踩坑,实测修正)**:源数据索引**直接按 ID**——`gameabc_Object.json` 的 `ObjectList[ObjectID]`、`gameabc_Image.json` 的 `ImageFileList[ImageFileID]`(**不是** `ID - 1`,旧文档的 off-by-one 是错的);组件类型看 `ObjectType`(`2`=Sprite、`4`=Text),**不是** XML 的 `SpiritType`(那是另一套枚举)。
## 网络协议(`docs/protocol/`,新旧前端共同契约)
`docs/protocol/` 是权威文档,改任何网络层代码前先读对应章节,**不要凭本文件的转述臆测**。
**两个不可妥协的约束:** ① `roomtype` 房间配置数组结构必须与目标子游戏**逐字节一致**(服务器据此解析规则);② `deskinfo` 对局快照须可序列化/反序列化以支持 `isbattle==1` 时 `Game_Modify.Reconnect(deskinfo)` 断线重连。
## 客户端侧适配约束:远程配置 / 原生数据接口 / 原生↔H5 桥接
继「服务器零改动」之后的**第二类不可妥协约束**:除 WebSocket 协议外,前端还通过「远程配置文件」(`gameserver` → `urlserver`)与「原生接口」(`window.settings` 同步取值、WVJB 异步桥)获取大量数据。新 Cocos 前端必须与原项目**逐字一致**地复刻这些读取/互调/注册方式(接口名、数据格式、回调约定、URL 构造),让原生侧与配置服务**零改动**即可对接。
具体机制、handler 清单与源码位置见 `native-bridge-contract` 技能(`.Codex/skills/native-bridge-contract/SKILL.md`)——改这部分前先读它核对,不要臆测接口名或数据格式。
## Cocos Creator 新前端:优先使用 funplay-cocos-mcp
新工程 `cocoscreator_projects/YouleNexus`。**任何涉及该工程的编辑器操作**(场景、节点、组件、prefab、资源、动画、预览/截图等),都必须走 `mcp__funplay_cocos__*` 这组 MCP 工具驱动编辑器。
**绝不允许手改** `.scene` / `.prefab` / `.anim` / `.meta`——它们是 UUID 引用的序列化资源,任何文本编辑都会破坏引用结构,导致「资源导入失败」且只能人工恢复。**MCP 连不上时也不例外**:应提示用户去编辑器启动服务,而不是退而求其次去手改文件。
具体工具用法不写在这里——MCP server 连上后会自注入完整说明(工具清单、action 速查、批量操作),以其为准,**正常操作不需要先读任何仓库文档**。
`cocos-mcp` 技能(`.Codex/skills/cocos-mcp/SKILL.md`)只记录 MCP 自己给不了的部分:前置条件、探活、排障、已知坑。**工具正常时不必读它;一旦 `cocos_*` 工具没出现、调用报连接失败、或行为与预期不符,就去读。**
@@ -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'; import type { ChannelIdentity } from '../identity.ts';
/** Legacy GameData launchWaitTime (04_Data.js): timer units are milliseconds. */ /** Legacy GameData launchWaitTime (04_Data.js): timer units are milliseconds. */
export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10; export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10;
/** /**
* 构建期默认渠道身份(旧 version.js 等价物)。 * 构建期默认渠道身份(旧 version.js 等价物)。
* gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。 * gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。
*/ */
export const BUILD_IDENTITY: ChannelIdentity = { export const BUILD_IDENTITY: ChannelIdentity = {
// This game's entry in config_test/update_jsonv2.txt belongs to this agent/channel. // This game's entry in config_test/update_jsonv2.txt belongs to this agent/channel.
agentid: '1B2h0ccl205c390Y28m1Ajdplkuu4wgy', agentid: '1B2h0ccl205c390Y28m1Ajdplkuu4wgy',
channelid: 'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG', channelid: 'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG',
gameid: 'J9r4022t80pxlb01zOK76DN1WD4QewFl', // agentid: 'veRa0qrBf0df2K1G4de2tgfmVxB2jxpv',
marketid: 4, // channelid: 'FtJf073aa0d6rI1xD8J1Y42fINTm0ziK',
version: 1, // 数字 versionCode(联调时改成本地服要求的版本号) gameid: 'J9r4022t80pxlb01zOK76DN1WD4QewFl',
}; marketid: 4,
version: 1, // 数字 versionCode(联调时改成本地服要求的版本号)
};
@@ -1,133 +1,181 @@
import type { RuntimeConfig } from '../config/runtime-config.ts'; import type { RuntimeConfig } from '../config/runtime-config.ts';
import { buildFirstSliceEnvelope } from '../protocol/first-slice-routes.ts'; import { buildFirstSliceEnvelope } from '../protocol/first-slice-routes.ts';
import type { OutboundEnvelope } from '../core/types/envelope.ts'; import type { OutboundEnvelope } from '../core/types/envelope.ts';
import { import {
buildExitRoomRequest, buildCreateRoomRequest,
buildJoinRoomRequest, type CreateRoomEnvironment,
buildPrepareRequest, buildExitRoomRequest,
type JoinRoomEnvironment, buildJoinRoomRequest,
type RoomIdentity, buildPrepareRequest,
} from '../protocol/contracts/index.ts'; type JoinRoomEnvironment,
import { PlatformStore } from './stores/platform-store.ts'; type RoomIdentity,
import type { InsideRoomState, PlatformState } from './stores/platform-types.ts'; } from '../protocol/contracts/index.ts';
import { cloneAndFreezeRoomtype, PlatformStore } from './stores/platform-store.ts';
/** The complete-envelope send capability used by platform command adapters. */ import type { InsideRoomState, PlatformState } from './stores/platform-types.ts';
export interface OutboundEnvelopeSendPort {
send(envelope: OutboundEnvelope): void; /** 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; export interface PlatformCommandsOptions {
readonly send: OutboundEnvelopeSendPort; readonly runtimeConfig: RuntimeConfig;
} readonly store: PlatformStore;
readonly send: OutboundEnvelopeSendPort;
export interface JoinRoomCommand extends JoinRoomEnvironment { }
readonly roomcode: string;
} export interface JoinRoomCommand extends JoinRoomEnvironment {
readonly roomcode: string | number;
export class UnsupportedGameHostCommandError extends Error { }
constructor(message: string) {
super(message); export interface CreateRoomCommand extends CreateRoomEnvironment {}
this.name = 'UnsupportedGameHostCommandError';
} export class UnsupportedGameHostCommandError extends Error {
} constructor(message: string) {
super(message);
/** Builds platform-owned room requests exclusively through the first-slice contracts. */ this.name = 'UnsupportedGameHostCommandError';
export class PlatformCommands { }
constructor(private readonly options: PlatformCommandsOptions) {} }
getVipRooms(shortcodeList:readonly (string|number)[]):void { /** Builds platform-owned room requests exclusively through the first-slice contracts. */
this.taskPlayerId(); export class PlatformCommands {
const {agentid,gameid}=this.options.runtimeConfig.identity; constructor(private readonly options: PlatformCommandsOptions) {}
this.options.send.send(buildFirstSliceEnvelope('getInfoByShortCode',{agentid,gameid,shortcodeList:[...shortcodeList]}));
} getPlayerInvitecode():void {
const playerid=this.taskPlayerId(),player=this.options.store.getState().players.entities[playerid];
joinVipRoom(roomcode:string|number,location:unknown):void { const {openid,unionid}=player;
const playerid=this.taskPlayerId(),state=this.options.store.getState(); if(typeof openid!=='string'||!(typeof unionid==='string'||typeof unionid==='number'&&Number.isFinite(unionid)))throw Error('Invite query requires login openid and unionid');
const ip=state.players.entities[playerid].ip; this.options.send.send(buildFirstSliceEnvelope('get_player_invitecode',{
if(typeof ip!=='string')throw Error('VIP join requires player login ip'); agentid:this.options.runtimeConfig.identity.agentid,playerid,unionid,openid,
const {agentid,gameid}=this.options.runtimeConfig.identity; }));
this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:0,location,ip})); }
}
bindInvitecode(invitecode:string):void {
getPlayerTasks(): void { const playerid=this.taskPlayerId();
const playerid=this.taskPlayerId(); if(typeof invitecode!=='string')throw Error('invitecode must be a string');
const {agentid,gameid}=this.options.runtimeConfig.identity; this.options.send.send(buildFirstSliceEnvelope('binding_invitecode',{
this.options.send.send(buildFirstSliceEnvelope('get_player_task',{agentid,playerid,gameid})); agentid:this.options.runtimeConfig.identity.agentid,playerid,invitecode,
} }));
}
claimTask(taskid:number|string):void {
const playerid=this.taskPlayerId(); getVipRooms(shortcodeList:readonly (string|number)[]):void {
const task=this.options.store.getState().tasks?.find(task=>String(task.taskid)===String(taskid)); this.taskPlayerId();
if(!task||task.state!==1)throw Error('Task is not claimable'); const {agentid,gameid}=this.options.runtimeConfig.identity;
// 11_GameUI claim click sends these three fields; Net._SendData adds none. this.options.send.send(buildFirstSliceEnvelope('getInfoByShortCode',{agentid,gameid,shortcodeList:[...shortcodeList]}));
this.options.send.send(buildFirstSliceEnvelope('get_task_award',{ }
agentid:this.options.runtimeConfig.identity.agentid,playerid,taskid:task.taskid,
})); joinVipRoom(roomcode:string|number,location:unknown):void {
} const playerid=this.taskPlayerId(),state=this.options.store.getState();
const ip=state.players.entities[playerid].ip;
private taskPlayerId():number { if(typeof ip!=='string')throw Error('VIP join requires player login ip');
const state=this.options.store.getState(); const {agentid,gameid}=this.options.runtimeConfig.identity;
if(state.app.phase!=='logged-in'||state.room.kind!=='outside')throw Error('Task command requires logged-in lobby'); this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:0,location,ip}));
return this.requireAuthenticatedPlayerId(state); }
}
getPlayerTasks(): void {
prepare(): void { const playerid=this.taskPlayerId();
const state = this.options.store.getState(); const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildPrepareRequest( this.options.send.send(buildFirstSliceEnvelope('get_player_task',{agentid,playerid,gameid}));
this.options.runtimeConfig, }
this.roomIdentity(state, this.requireInsideRoom(state)),
)); 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}));
exitRoom(): void { }
const state = this.options.store.getState(); joinOwnedRoom(roomcode:number,location:unknown):void {
const room = this.requireInsideRoom(state); const playerid=this.taskPlayerId(),state=this.options.store.getState();
if (room.stage !== 0 && room.infinite !== 1) { if(!Number.isSafeInteger(roomcode)||roomcode<=0)throw Error('Invalid owned roomcode');
throw new UnsupportedGameHostCommandError('room.exit requires stage=0 unless infinite=1'); 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(buildExitRoomRequest( this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:1,location,ip}));
this.options.runtimeConfig, }
this.roomIdentity(state, room), getTreasureList():void {
)); this.taskPlayerId();const {agentid,gameid}=this.options.runtimeConfig.identity;
} this.options.send.send(buildFirstSliceEnvelope('get_treasurelist',{agentid,gameid}));
}
joinRoom(command: JoinRoomCommand): void { redeemCard(cardno:string):void {
const state = this.options.store.getState(); const playerid=this.taskPlayerId();
if (state.room.kind !== 'outside') { if(typeof cardno!=='string'||cardno===''||/^[ ]+$/.test(cardno))throw Error('Invalid cardno');
throw new UnsupportedGameHostCommandError('room.join requires player to be outside a room'); this.options.send.send(buildFirstSliceEnvelope('topup_card',{agentid:this.options.runtimeConfig.identity.agentid,playerid,cardno}));
} }
const playerid = this.requireAuthenticatedPlayerId(state);
this.options.send.send(buildJoinRoomRequest( claimTask(taskid:number|string):void {
this.options.runtimeConfig, const playerid=this.taskPlayerId();
{ playerid, roomcode: command.roomcode }, const task=this.options.store.getState().tasks?.find(task=>String(task.taskid)===String(taskid));
command, 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 requireInsideRoom(state: PlatformState): InsideRoomState { }));
if (state.room.kind !== 'inside') { }
throw new UnsupportedGameHostCommandError('room command requires player to be inside a room');
} private taskPlayerId():number {
return state.room; 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);
private requireAuthenticatedPlayerId(state: PlatformState): number { }
const playerid = state.players.selfPlayerId;
if (playerid === null) { prepare(): void {
throw new UnsupportedGameHostCommandError('room command requires an authenticated player'); const state = this.options.store.getState();
} this.options.send.send(buildPrepareRequest(
if (state.players.entities[playerid] === undefined) { this.options.runtimeConfig,
throw new Error('players.entities: missing authenticated player'); this.roomIdentity(state, this.requireInsideRoom(state)),
} ));
return playerid; }
}
exitRoom(): void {
private roomIdentity(state: PlatformState, room: InsideRoomState): RoomIdentity { const state = this.options.store.getState();
return { const room = this.requireInsideRoom(state);
playerid: this.requireAuthenticatedPlayerId(state), if (room.stage !== 0 && room.infinite !== 1) {
roomcode: room.roomcode, 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 type { GameHost } from '../sdk/contracts/index.ts';
import { import {
extractGameSessionOpenFaultErrors, extractGameSessionOpenFaultErrors,
GameSessionHost, GameSessionHost,
} from '../sdk/runtime/game-session-host.ts'; } from '../sdk/runtime/game-session-host.ts';
import { import {
parseLoginResponse, parseCreateRoomResponse,
parseSelfJoinRoomResponse, parseLoginResponse,
type ParsedSelfExitPayload, parseSelfJoinRoomResponse,
type ReconnectSnapshot, type ParsedSelfExitPayload,
} from '../protocol/contracts/index.ts'; type ReconnectSnapshot,
import { PlatformStore } from './stores/platform-store.ts'; } from '../protocol/contracts/index.ts';
import type { PlatformState } from './stores/platform-types.ts'; import { cloneAndFreezeRoomtype, PlatformStore } from './stores/platform-store.ts';
import type { PlatformState } from './stores/platform-types.ts';
export interface GameHostLeasePort { import type {LoginAccountIdentity} from '../protocol/contracts/login-contract.ts';
readonly host: GameHost;
invalidate(): void; export interface GameHostLeasePort {
} readonly host: GameHost;
invalidate(): void;
export type GameHostLeaseFactory = () => GameHostLeasePort; }
/** A valid nonzero server outcome, delivered without changing canonical state. */ export type GameHostLeaseFactory = () => GameHostLeasePort;
export interface ServerDenial {
readonly rpc: 'player_login' | 'self_join_room'; /** A valid nonzero server outcome, delivered without changing canonical state. */
readonly state: number; export interface ServerDenial {
readonly data: Readonly<Record<string, unknown>>; readonly rpc: 'player_login' | 'self_join_room' | 'create_room';
} readonly state: number;
readonly data: Readonly<Record<string, unknown>>;
export interface RuntimeSessionOptions { }
readonly store: PlatformStore;
readonly gameSession: GameSessionHost; export interface RuntimeSessionOptions {
readonly createGameHostLease: GameHostLeaseFactory; readonly roomHistory?: import('../adapters/native/owned-room-history.ts').OwnedRoomHistory;
readonly onServerDenial: (outcome: ServerDenial) => void; readonly store: PlatformStore;
} readonly gameSession: GameSessionHost;
readonly createGameHostLease: GameHostLeaseFactory;
export type RuntimeSessionState = 'operational' | 'faulted' | 'kicked'; readonly onServerDenial: (outcome: ServerDenial) => void;
}
export class RuntimeSessionFault extends Error {
readonly errors: readonly unknown[]; export type RuntimeSessionState = 'operational' | 'faulted' | 'kicked';
constructor(operation: string, errors: readonly unknown[]) { export class RuntimeSessionFault extends Error {
super(`RuntimeSession fault during ${operation}`); readonly errors: readonly unknown[];
this.name = 'RuntimeSessionFault';
this.errors = Object.freeze([...errors]); 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; /** Coordinates the canonical platform snapshot with the one active game session. */
private terminalFault: RuntimeSessionFault | null = null; export class RuntimeSession {
private activeLease: GameHostLeasePort | null = null; private loginIsPending = false;
private lastJoinedRoomcode: string | null = null;
constructor(private readonly options: RuntimeSessionOptions) {} /** Non-device login owns identity in authorization; device login owns it in the response. */
private authorizedIdentity:Pick<LoginAccountIdentity,'openid'|'unionid'>|undefined;
get loginPending(): boolean { return this.loginIsPending; } private wasKicked = false;
private terminalFault: RuntimeSessionFault | null = null;
get kicked(): boolean { return this.wasKicked; } private activeLease: GameHostLeasePort | null = null;
get state(): RuntimeSessionState { constructor(private readonly options: RuntimeSessionOptions) {}
if (this.wasKicked) return 'kicked';
return this.terminalFault !== null ? 'faulted' : 'operational'; get loginPending(): boolean { return this.loginIsPending; }
}
get kicked(): boolean { return this.wasKicked; }
get fault(): Error | null { return this.terminalFault; }
get state(): RuntimeSessionState {
markLoginPending(): void { if (this.wasKicked) return 'kicked';
this.requireOperational(); return this.terminalFault !== null ? 'faulted' : 'operational';
this.loginIsPending = true; }
}
get fault(): Error | null { return this.terminalFault; }
handleLogin(data: unknown): void {
this.requireOperational(); markLoginPending(identity?:Pick<LoginAccountIdentity,'openid'|'unionid'>|null): void {
this.loginIsPending = false; this.requireOperational();
const parsed = parseLoginResponse(data); // No argument is a reconnect of the current account; null selects device response identity.
if (parsed.state !== 0) { if(identity!==undefined)this.authorizedIdentity=identity===null?undefined:identity;
this.options.onServerDenial({ rpc: 'player_login', state: parsed.state, data: parsed.raw }); this.loginIsPending = true;
return; }
}
const committed = this.options.store.applyLoginSuccess(parsed); handleLogin(data: unknown): void {
if (parsed.room === null) { this.requireOperational();
this.releaseOrFault('login lobby replacement'); this.loginIsPending = false;
return; const parsed = parseLoginResponse(data);
} if (parsed.state !== 0) {
this.openCommittedRoom(committed, parsed.reconnect, 'login room open'); this.options.onServerDenial({ rpc: 'player_login', state: parsed.state, data: parsed.raw });
} return;
}
handleSelfJoin(data: unknown): void { this.lastJoinedRoomcode = null;
this.requireOperational(); const committed = this.options.store.applyLoginSuccess(parsed,this.authorizedIdentity);
const parsed = parseSelfJoinRoomResponse(data); if (parsed.room === null) {
if (parsed.state !== 0) { this.releaseOrFault('login lobby replacement');
this.options.onServerDenial({ rpc: 'self_join_room', state: parsed.state, data: parsed.raw }); return;
return; }
} this.openCommittedRoom(committed, parsed.reconnect, 'login room open');
const committed = this.options.store.replaceRoom(parsed); }
this.openCommittedRoom(committed, parsed.reconnect, 'self join room open');
} handleCreateRoom(data: unknown): void {
this.requireOperational();
handleSelfExit(parsed: ParsedSelfExitPayload): void { const parsed = parseCreateRoomResponse(data);
this.requireOperational(); if (parsed.state !== 0) {
const state = this.options.store.getState(); this.options.onServerDenial({rpc: 'create_room', state: parsed.state, data: parsed.raw});
if (state.room.kind !== 'inside') { return;
if (this.options.gameSession.state === 'idle' && this.activeLease === null) return; }
const errors = [ if (parsed.room === null) throw new Error('create_room missing parsed room');
new Error('self_exit_room found an active game without room state'), const current = this.options.store.getState().room;
...this.releaseGame(), if (current.kind === 'inside' && current.roomcode === parsed.room.roomcode
]; && this.options.gameSession.state === 'active') return;
this.throwFault('self exit inconsistent state', errors); // Subgame code receives an owned immutable value, never the server response graph.
} const roomtype = cloneAndFreezeRoomtype(parsed.room.roomtype);
if (parsed.seat !== undefined && parsed.seat !== state.room.selfSeat) { const count = this.options.gameSession.entry.resolveSeatCount(roomtype);
throw new RangeError( const committed = this.options.store.createRoom(parsed, count);
`self_exit_room seat mismatch: expected ${state.room.selfSeat}, received ${parsed.seat}`, this.openCommittedRoom(committed, {present: false}, 'create room open');
); }
}
if (this.options.gameSession.state !== 'active' || this.activeLease === null) { handleSelfJoin(data: unknown): void {
const errors = [ this.requireOperational();
new Error('self_exit_room requires an active game session and host lease'), const parsed = parseSelfJoinRoomResponse(data);
...this.releaseGame(), this.options.roomHistory?.joined(parsed.raw);
]; if (parsed.state !== 0) {
this.throwFault('self exit inconsistent state', errors); this.options.onServerDenial({ rpc: 'self_join_room', state: parsed.state, data: parsed.raw });
} return;
}
try { if (parsed.room !== null && this.lastJoinedRoomcode === parsed.room.roomcode
this.options.store.clearRoom(); && this.options.gameSession.state === 'active') return;
} catch (error) { const committed = this.options.store.replaceRoom(parsed);
this.throwFault('self exit state commit', [error, ...this.releaseGame()]); this.openCommittedRoom(committed, parsed.reconnect, 'self join room open');
} this.lastJoinedRoomcode = parsed.room!.roomcode;
this.releaseOrFault('self exit cleanup'); }
}
handleSelfExit(parsed: ParsedSelfExitPayload): void {
handleServerSwitch(switchConnection: () => void): void { this.requireOperational();
this.requireOperational(); const state = this.options.store.getState();
const errors = this.releaseGame(); if (state.room.kind !== 'inside') {
try { if (this.options.gameSession.state === 'idle' && this.activeLease === null) return;
switchConnection(); const errors = [
} catch (error) { new Error('self_exit_room found an active game without room state'),
errors.push(error); ...this.releaseGame(),
} ];
if (errors.length > 0) this.throwFault('server switch', errors); this.throwFault('self exit inconsistent state', errors);
} }
if (parsed.seat !== undefined && parsed.seat !== state.room.selfSeat) {
handleKick(_data: unknown, stopConnection: () => void): void { throw new RangeError(
if (this.wasKicked) return; `self_exit_room seat mismatch: expected ${state.room.selfSeat}, received ${parsed.seat}`,
this.requireOperational(); );
this.loginIsPending = false; }
this.wasKicked = true; if (this.options.gameSession.state !== 'active' || this.activeLease === null) {
const errors = [
const errors = this.releaseGame(); new Error('self_exit_room requires an active game session and host lease'),
try { ...this.releaseGame(),
stopConnection(); ];
} catch (error) { this.throwFault('self exit inconsistent state', errors);
errors.push(error); }
}
if (errors.length > 0) this.throwFault('kick terminal cleanup', errors); try {
} this.options.roomHistory?.exited(parsed.raw,state.room.infinite);
this.options.store.clearRoom();
private openCommittedRoom( } catch (error) {
state: PlatformState, this.throwFault('self exit state commit', [error, ...this.releaseGame()]);
reconnect: ReconnectSnapshot, }
operation: string, this.releaseOrFault('self exit cleanup');
): void { }
if (state.room.kind !== 'inside') {
this.throwFault(operation, [new Error('Cannot open a game session without committed room state')]); handleServerSwitch(switchConnection: () => void): void {
} this.requireOperational();
const errors = this.releaseGame();
const priorCleanupErrors = this.releaseGame(); try {
if (priorCleanupErrors.length > 0) this.throwFault(operation, priorCleanupErrors); switchConnection();
if (this.gameWasDisposed()) return; } catch (error) {
errors.push(error);
let lease: GameHostLeasePort; }
try { if (errors.length > 0) this.throwFault('server switch', errors);
lease = this.options.createGameHostLease(); }
if (!lease || typeof lease !== 'object' || typeof lease.invalidate !== 'function') {
throw new TypeError('createGameHostLease must return { host, invalidate }'); handleKick(_data: unknown, stopConnection: () => void): void {
} if (this.wasKicked) return;
} catch (error) { this.requireOperational();
this.throwFault(operation, [error]); this.loginIsPending = false;
} this.wasKicked = true;
this.activeLease = lease;
const errors = this.releaseGame();
try { try {
this.options.gameSession.open(lease.host); stopConnection();
if (this.gameWasDisposed()) return; } catch (error) {
this.options.gameSession.publish({ errors.push(error);
type: 'room.entered', }
roomtype: state.room.roomtype, if (errors.length > 0) this.throwFault('kick terminal cleanup', errors);
}); }
if (this.gameWasDisposed()) return;
if (reconnect.present) this.options.gameSession.restore(reconnect.value); private openCommittedRoom(
} catch (error) { state: PlatformState,
const openErrors = extractGameSessionOpenFaultErrors(error); reconnect: ReconnectSnapshot,
const errors = openErrors === null operation: string,
? [error] ): void {
: [openErrors[0], openErrors[1]]; if (state.room.kind !== 'inside') {
errors.push(...this.releaseGame()); this.throwFault(operation, [new Error('Cannot open a game session without committed room state')]);
this.throwFault(operation, errors); }
}
} const priorCleanupErrors = this.releaseGame();
if (priorCleanupErrors.length > 0) this.throwFault(operation, priorCleanupErrors);
private releaseOrFault(operation: string): void { if (this.gameWasDisposed()) return;
const errors = this.releaseGame();
if (errors.length > 0) this.throwFault(operation, errors); let lease: GameHostLeasePort;
} try {
lease = this.options.createGameHostLease();
private gameWasDisposed(): boolean { if (!lease || typeof lease !== 'object' || typeof lease.invalidate !== 'function') {
return this.options.gameSession.state === 'disposed'; throw new TypeError('createGameHostLease must return { host, invalidate }');
} }
} catch (error) {
private releaseGame(): unknown[] { this.throwFault(operation, [error]);
const errors: unknown[] = []; }
const lease = this.activeLease; this.activeLease = lease;
this.activeLease = null;
try {
if (this.options.gameSession.state !== 'idle' && !this.gameWasDisposed()) { this.options.gameSession.open(lease.host);
try { if (this.gameWasDisposed()) return;
this.options.gameSession.close(); this.options.gameSession.publish({
} catch (error) { type: 'room.entered',
errors.push(error); roomtype: state.room.roomtype,
} });
} if (this.gameWasDisposed()) return;
if (lease !== null) { if (reconnect.present) this.options.gameSession.restore(reconnect.value);
try { } catch (error) {
lease.invalidate(); const openErrors = extractGameSessionOpenFaultErrors(error);
} catch (error) { const errors = openErrors === null
errors.push(error); ? [error]
} : [openErrors[0], openErrors[1]];
} errors.push(...this.releaseGame());
return errors; this.throwFault(operation, errors);
} }
}
private throwFault(operation: string, errors: readonly unknown[]): never {
const fault = new RuntimeSessionFault(operation, errors); private releaseOrFault(operation: string): void {
this.loginIsPending = false; const errors = this.releaseGame();
this.terminalFault = fault; if (errors.length > 0) this.throwFault(operation, errors);
throw fault; }
}
private gameWasDisposed(): boolean {
private requireOperational(): void { return this.options.gameSession.state === 'disposed';
if (this.terminalFault !== null) throw this.terminalFault; }
if (this.wasKicked) throw new Error('RuntimeSession is kicked');
} 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'; import type { ServerDenial } from './runtime-session.ts';
/** Semantic scene intents; the headless runtime never imports Cocos. */ /** Semantic scene intents; the headless runtime never imports Cocos. */
export interface ScenePort { export interface ScenePort {
showLoading(): void; showLoading(): void;
showLogin(): void; showLogin(): void;
showLobby(): void; showLobby(): void;
showRoom(): void; showRoom(): void;
showReconnect(): void; showReconnect(): void;
showKicked(data: unknown): void; showKicked(data: unknown): void;
/** Required when the host supports the server's generic message UI. */ /** Required when the host supports the server's generic message UI. */
showTips?(message: string, time: number, source?:'show_message'|'update_roomcard'): void; 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. */ /** Recoverable server result; the consumer interprets the original failure payload. */
showServerDenial(outcome: ServerDenial): void; showServerDenial(outcome: ServerDenial): void;
showFatal(error: Error): 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 { PlatformGameConnectionPhase } from '../../sdk/contracts/snapshots.ts';
import type { TaskData } from '../../core/types/task.ts'; import type { TaskData } from '../../core/types/task.ts';
export type PlatformConnectionPhase = export type PlatformConnectionPhase =
| 'idle' | 'idle'
| 'connecting' | 'connecting'
| PlatformGameConnectionPhase; | PlatformGameConnectionPhase;
export interface PlatformAppState { export interface PlatformAppState {
readonly phase: PlatformConnectionPhase; readonly phase: PlatformConnectionPhase;
} }
export interface PlatformPlayer { export interface PlatformPlayer {
readonly playerid: number; readonly playerid: number;
readonly nickname: string; readonly nickname: string;
readonly avatar: string; readonly avatar: string;
readonly sex: number; readonly sex: number;
readonly bean: number; readonly bean: number;
/** Account-only fields are absent on other room players; never default them in views. */ /** Account-only fields are absent on other room players; never default them in views. */
readonly roomcard?: number; readonly roomcard?: number;
readonly taskstate?: number; readonly openid?: string;
readonly advanced?: number; readonly unionid?: string | number;
readonly score?: number; readonly invitecode?: string | null;
readonly sign?: string | null; readonly taskstate?: number;
readonly ip?: string; readonly advanced?: number;
readonly isprepare?: number; readonly score?: number;
readonly onstate?: number; readonly sign?: string | null;
} readonly ip?: string;
readonly isprepare?: number;
export interface PlayerDirectoryState { readonly onstate?: number;
readonly selfPlayerId: number | null; }
readonly entities: Readonly<Record<number, PlatformPlayer>>;
} export interface PlayerDirectoryState {
readonly selfPlayerId: number | null;
export interface OutsideRoomState { readonly entities: Readonly<Record<number, PlatformPlayer>>;
readonly kind: 'outside'; }
}
export interface OutsideRoomState {
export interface InsideRoomState { readonly kind: 'outside';
readonly kind: 'inside'; }
readonly roomcode: string;
readonly selfSeat: number; export interface InsideRoomState {
readonly seatPlayerIds: readonly (number | null)[]; readonly kind: 'inside';
readonly readySeats: readonly number[]; readonly roomcode: string;
readonly offlineSeats: readonly number[]; readonly selfSeat: number;
readonly roomtype: readonly unknown[]; readonly seatPlayerIds: readonly (number | null)[];
readonly stage: number; readonly readySeats: readonly number[];
readonly needprepare: number; readonly offlineSeats: readonly number[];
readonly infinite: number; readonly roomtype: readonly unknown[];
} readonly stage: number;
readonly needprepare: number;
export type RoomState = OutsideRoomState | InsideRoomState; readonly infinite: number;
}
export type RoomState = OutsideRoomState | InsideRoomState;
export interface PlatformState { export interface PlatformState {
readonly vipRooms?: readonly import('../../core/types/vip-room.ts').VipRoom[]; readonly vipRank?:{readonly entries:readonly import('../../protocol/contracts/rank-list.ts').RankEntry[]|null};
/** Absent until a task list response arrives; cleared by login replacement. */ readonly treasureList?:readonly import('../../protocol/contracts/rank-list.ts').RankEntry[];
readonly tasks?: readonly TaskData[]; /** Receipt identity allows repeated queries with an unchanged code to open the view. */
readonly app: PlatformAppState; readonly inviteResponse?: {readonly kind:'query'|'binding'};
readonly players: PlayerDirectoryState; readonly vipRooms?: readonly import('../../core/types/vip-room.ts').VipRoom[];
readonly room: RoomState; /** 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'; import type { RemoteConfig } from '../config/remote-config.ts';
export interface LobbyTemplate { export interface LobbyTemplate {
isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean; isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean;
} }
/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */ /** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */
export const LOBBY_TEMPLATE:Readonly<LobbyTemplate>=Object.freeze({ export const LOBBY_TEMPLATE:Readonly<LobbyTemplate>=Object.freeze({
isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false, isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false,
}); });
export interface LobbyContext {versionState:0|1;launchMode:0|1;} 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 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({hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1, 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}); ofcShortCode:null as unknown,hideSpid:null as unknown});
function object(value:unknown,name:string):Record<string,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`); if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`);
return value as Record<string,unknown>; return value as Record<string,unknown>;
} }
/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */ /** 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){ export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly<LobbyTemplate>=LOBBY_TEMPLATE){
const rawHall=remote.getValue('hall_config'); const rawHall=remote.getValue('hall_config');
const merged=rawHall==null?{}:object(rawHall,'hall_config'); const merged=rawHall==null?{}:object(rawHall,'hall_config');
const hall={...HALL_DEFAULTS,...merged}; const hall={...HALL_DEFAULTS,...merged};
for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){ 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. // 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData.
if(key==='actData')continue; if(key==='actData')continue;
const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value}); const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value});
} }
for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){ for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){
if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`); if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`);
if(key==='androidPay')hall[key]=Number(hall[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}`); for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`);
const sys={...SYS_DEFAULTS}; const sys={...SYS_DEFAULTS};
for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof 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}); 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.isInteger(Number(sys.vipRankLimit))||Number(sys.vipRankLimit)<=0)throw Error('Invalid vipRankLimit');
if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`); sys.vipRankLimit=Number(sys.vipRankLimit);
if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]); for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){
} if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`);
if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid'); if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]);
const rawAgent=remote.getValue('agentmode'); }
const agentmode=rawAgent==null?1:Number(rawAgent); if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid');
if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode'); const rawAgent=remote.getValue('agentmode');
return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template}); 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 type LobbyMenuConfig=ReturnType<typeof resolveLobbyMenuConfig>;
export interface LobbyMenuState {visible:Record<number,boolean>;left:Record<number,number>;top:Record<number,number>;} /** Original click messages intentionally use different comma characters. */
export function lobbyLockedTip(config:LobbyMenuConfig,action:'world-rooms'|'match'|'official-rooms'):string|null{
/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */ if(action==='world-rooms')return config.locked[568]?'暂未开放,敬请期待':null;
export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{ return config.locked[action==='match'?483:3139]?'暂未开放,敬请期待':null;
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};
} }
export interface LobbyMenuState {visible:Record<number,boolean>;left:Record<number,number>;top:Record<number,number>;}
/** openPallet runs its own visibility pass each time before placing visible entries. */
export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{ /** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */
const visible:Record<number,boolean>={15:false,604:Boolean(config.hall.rankList), export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
235:config.agentmode!==1&&!config.sys.miniPro,3017:Boolean(config.hall.bagUrl),3057:Boolean(config.hall.wareHouse),5:true}; const {hall,sys,template,agentmode}=config;
const left:Record<number,number>={3121:200},top:Record<number,number>={}; const visible:Record<number,boolean>={},left:Record<number,number>={},top:Record<number,number>={};
if(context.versionState===1)return {visible:Object.fromEntries(Object.keys(visible).map(id=>[id,false])),left,top}; const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;};
const shown=[15,604,235,3017,3057,5].filter(id=>visible[id]); // Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X.
shown.forEach((id,index)=>{left[id]=216+155*index;top[id]=540;}); if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;}
return {visible,left,top}; hide([219,220]);
} if(context.versionState===1){
if(context.launchMode===0)hide([234]);
/** IDs remain source traceability only; Cocos owns hit testing and prefab lifetimes. */ hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604,
export const LOBBY_MENU_ACTIONS=Object.freeze({ 286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]);
7:'settings',9:'profile',11:'create-room',12:'join-room',13:'records',14:'share',17:'tasks',679:'vip-rooms', }else{
10:'buy-roomcard',286:'buy-roomcard',287:'buy-roomcard',3161:'buy-roomcard', if(agentmode===1)hide([10,509]);
508:'buy-bean',509:'buy-bean',496:'buy-bean',3162:'buy-bean', visible[235]=false;
483:'match',568:'world-rooms',3016:'store',3018:'activity',99:'my-rooms',76:'qrcode', if(hall.androidPay===1){visible[10]=true;visible[509]=true;}
3139:'official-rooms',234:'more-games',3120:'tray',3233:'rank',3232:'vip-rank',3256:'bind-phone',3264:'cdkey', visible[3018]=!template.isGameHall&&Array.isArray(hall.actData);
5:'notice',3017:'bag',235:'agent',604:'rank',3057:'warehouse',15:'feedback', visible[3016]=Boolean(hall.storeUrl);
} as const); if(template.isGameHall||template.vipInfinite)hide([3139,679]);
export type LobbyAction=typeof LOBBY_MENU_ACTIONS[keyof typeof LOBBY_MENU_ACTIONS]; if(template.mainMenuButton)hide([11,12,568,483]);
export const LOBBY_UNAVAILABLE:Readonly<Partial<Record<LobbyAction,string>>>=Object.freeze({ if(template.hideNotice)hide([16,180,193,194,682,178,179,145]);
'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块', if(sys.hideTaskBtn)hide([17]);
'buy-roomcard':'购买房卡:尚未接入支付模块','buy-bean':'购买星星:尚未接入支付模块', if(sys.miniPro||sys.hideWxShare)hide([14]);
match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口', if(!template.isGameHall){
activity:'活动:尚未接入活动身份参数与原生接口','my-rooms':'我的房间:尚未接入房间记录与进房',qrcode:'二维码:尚未接入分享模块', const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false);
'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口', shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);});
rank:'排行:尚未接入排行榜', 'vip-rank':'VIP 排行:尚未接入排行榜','bind-phone':'绑定手机:尚未接入绑定流程', }
cdkey:'兑换码:尚未接入兑换请求',bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口', visible[3233]=Boolean(hall.rankList);
warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交', 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 { RemoteConfig } from '../config/remote-config.ts';
import type { PlatformState } from '../platform/stores/platform-types.ts'; import type { PlatformState } from '../platform/stores/platform-types.ts';
import { selectSelfPlayer } from '../platform/stores/selectors.ts'; import { selectSelfPlayer } from '../platform/stores/selectors.ts';
// Presentation checks required view inputs without depending on the protocol layer. // 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 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 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 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>;} 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) { export function selectLobbyModel(state:PlatformState) {
const self=selectSelfPlayer(state); const self=selectSelfPlayer(state);
if(!self)throw Error('lobby requires authenticated player'); if(!self)throw Error('lobby requires authenticated player');
return Object.freeze({...self, return Object.freeze({...self,
roomcard:requireNumber(self.roomcard,'players.self.roomcard'), roomcard:requireNumber(self.roomcard,'players.self.roomcard'),
taskstate:requireInteger(self.taskstate,'players.self.taskstate'), taskstate:requireInteger(self.taskstate,'players.self.taskstate'),
score:requireNumber(self.score,'players.self.score'), score:requireNumber(self.score,'players.self.score'),
sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~', sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~',
canAct:state.app.phase==='logged-in'&&state.room.kind==='outside', canAct:state.app.phase==='logged-in'&&state.room.kind==='outside',
}); });
} }
export type LobbyModel=ReturnType<typeof selectLobbyModel>; export type LobbyModel=ReturnType<typeof selectLobbyModel>;
export interface Notice {type:0|1; text:string; image:string; url:string; height:number;} /** Game_Config.Notice: speed 0.1 px/ms, 15 px per gblen byte. */
export interface LobbyPolicy {noticeHours:number; notices:readonly Notice[]; scroll:string; export function announcementMotion(text:string,width:number,elapsedSeconds:number){
starName:string; roomCardName:string; contacts:readonly {label:string;value:string}[];} if(!Number.isFinite(width)||width<=0)throw Error('Invalid announcement width');
if(!Number.isFinite(elapsedSeconds)||elapsedSeconds<0)throw Error('Invalid announcement elapsed time');
/** Source defaults: 04_Data.sysConfig and advertInfo_0/1/2; 12_Logic.get_paravalue. */ const textWidth=text.replace(/[^\x00-\xff]/g,'**').length*15;
export function resolveLobbyPolicy(remote:RemoteConfig):LobbyPolicy { const duration=(width+textWidth)/100;
// 04_Data starts with 房卡; getConfig_Succ replaces only a truthy roomCardName. return {visible:text.length>0,textWidth,duration,left:width-(elapsedSeconds%duration)*100};
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;
} }
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'; import type { RuntimeConfig } from '../config/runtime-config.ts';
/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */ /** Open_ewm uses this exact config key; absent configuration intentionally does nothing. */
export function resolveSharePolicy(config:RuntimeConfig){ export function resolveQrCodePolicy(config:RuntimeConfig){
const remote=config.remoteConfig,raw=remote.getValue('hall_config'); const url=config.remoteConfig.getValue('WechatPublicNo_ewm');
if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config'); if(!url)return null;
const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record<string,unknown>; if(typeof url!=='string')throw Error('Invalid WechatPublicNo_ewm');
for(const key of Object.keys(hall)){const value=remote.getValue(key);if(value!=null)hall[key]=value;} const policy=resolveSharePolicy(config);
const text=(value:unknown,name:string)=>{if(typeof value!=='string')throw Error(`Invalid ${name}`);return value;}; return {url,share:{sharefriend:1,type:3,webpageUrl:url,title:policy.title+policy.description,description:undefined,sharetype:0}};
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};
} }
/** 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 { export {
hasOwn, hasOwn,
optionalField, optionalField,
requireArray, requireArray,
requireBoolean, requireBoolean,
requireInteger, requireInteger,
requireNumber, requireNumber,
requireRecord, requireRecord,
requireString, requireString,
requireStringOrFiniteNumber, requireStringOrFiniteNumber,
type JsonRecord, type JsonRecord,
} from './validation.ts'; } from './validation.ts';
export { export {
buildLoginRequest, buildLoginRequest,
parseLoginResponse, parseLoginResponse,
type DeviceLoginMode, type DeviceLoginMode,
type FirstSliceRuntimeConfig, type FirstSliceRuntimeConfig,
type LoginAccountIdentity, type LoginAccountIdentity,
type LoginDeviceSnapshot, type LoginDeviceSnapshot,
type LoginPlayerIdMode, type LoginPlayerIdMode,
type LoginRequestPayload, type LoginRequestPayload,
type ParsedLoginResponse, type ParsedLoginResponse,
} from './login-contract.ts'; } from './login-contract.ts';
export { export {
buildExitRoomRequest, buildCreateRoomRequest,
buildJoinRoomRequest, parseCreateRoomResponse,
buildPrepareRequest, type ParsedCreateRoomResponse,
parseConnectAgentserverPayload, type CreateRoomEnvironment,
parseConnectRoomserverPayload, buildExitRoomRequest,
parseKickServerPayload, buildJoinRoomRequest,
parseOtherExitRoomPayload, buildPrepareRequest,
parseOtherJoinRoomPayload, parseConnectAgentserverPayload,
parseOtherOfflinePayload, parseConnectRoomserverPayload,
parseOtherOnlinePayload, parseKickServerPayload,
parsePlayerPreparePayload, parseOtherExitRoomPayload,
parseSelfExitRoomPayload, parseOtherJoinRoomPayload,
parseSelfJoinRoomResponse, parseOtherOfflinePayload,
type JoinRoomEnvironment, parseOtherOnlinePayload,
type JoinRoomPayload, parsePlayerPreparePayload,
type ParsedAgentserverPayload, parseSelfExitRoomPayload,
type ParsedKickPayload, parseSelfJoinRoomResponse,
type ParsedOnlinePayload, type JoinRoomEnvironment,
type ParsedRoomEntry, type JoinRoomPayload,
type ParsedRoomResponse, type ParsedAgentserverPayload,
type ParsedRoomserverPayload, type ParsedKickPayload,
type ParsedRoomSnapshot, type ParsedOnlinePayload,
type ParsedSelfExitPayload, type ParsedRoomEntry,
type ParsedSeatPayload, type ParsedRoomResponse,
type ReconnectSnapshot, type ParsedRoomserverPayload,
type RoomActionPayload, type ParsedRoomSnapshot,
type RoomIdentity, type ParsedSelfExitPayload,
} from './room-contracts.ts'; 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 { import {
buildFirstSliceEnvelope, buildFirstSliceEnvelope,
type FirstSliceEnvelope, type FirstSliceEnvelope,
} from '../first-slice-routes.ts'; } from '../first-slice-routes.ts';
import { import {
hasOwn, hasOwn,
optionalField, optionalField,
requireArray, requireArray,
requireInteger, requireInteger,
requireNumber, requireNumber,
requireRecord, requireRecord,
requireString, requireString,
requireStringOrFiniteNumber, requireStringOrFiniteNumber,
type JsonRecord, type JsonRecord,
} from './validation.ts'; } from './validation.ts';
import type { FirstSliceRuntimeConfig } from './login-contract.ts'; import type { FirstSliceRuntimeConfig } from './login-contract.ts';
export type ReconnectSnapshot = export type ReconnectSnapshot =
| { readonly present: false } | { readonly present: false }
| { readonly present: true; readonly value: unknown }; | { readonly present: true; readonly value: unknown };
export interface ParsedRoomSnapshot { export interface ParsedRoomSnapshot {
readonly roomcode: string; readonly roomcode: string;
readonly seat: number; readonly seat: number;
readonly roomtype: readonly unknown[]; readonly roomtype: readonly unknown[];
readonly players: readonly unknown[]; readonly players: readonly unknown[];
} }
export interface ParsedRoomEntry { export interface ParsedRoomEntry {
readonly room: ParsedRoomSnapshot; readonly room: ParsedRoomSnapshot;
readonly reconnect: ReconnectSnapshot; readonly reconnect: ReconnectSnapshot;
readonly raw: JsonRecord; readonly raw: JsonRecord;
} }
export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot { export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot {
if (!Boolean(raw.deskinfo)) return { present: false }; if (!Boolean(raw.deskinfo)) return { present: false };
return { present: true, value: raw.deskinfo }; return { present: true, value: raw.deskinfo };
} }
export function parseRoomEntryFields(raw: JsonRecord): ParsedRoomSnapshot { export function parseRoomEntryFields(raw: JsonRecord): ParsedRoomSnapshot {
const roomcode = requireString(raw.roomcode, '$.data.roomcode'); const roomcode = requireString(raw.roomcode, '$.data.roomcode');
const seat = requireInteger(raw.seat, '$.data.seat'); const seat = requireInteger(raw.seat, '$.data.seat');
const roomtype = requireArray(raw.roomtype, '$.data.roomtype'); const roomtype = requireArray(raw.roomtype, '$.data.roomtype');
const players = requireArray(raw.players, '$.data.players'); const players = requireArray(raw.players, '$.data.players');
if (seat < 0 || seat >= players.length) { if (seat < 0 || seat >= players.length) {
throw new RangeError(`$.data.seat: expected server seat index within players, received number`); throw new RangeError(`$.data.seat: expected server seat index within players, received number`);
} }
return { roomcode, seat, roomtype, players }; return { roomcode, seat, roomtype, players };
} }
export interface RoomIdentity { export interface RoomIdentity {
readonly playerid: number; readonly playerid: number;
readonly roomcode: string; readonly roomcode: string;
} }
export interface RoomActionPayload extends Readonly<Record<string, unknown>> { export interface RoomActionPayload extends Readonly<Record<string, unknown>> {
readonly agentid: string | number; readonly agentid: string | number;
readonly playerid: number; readonly playerid: number;
readonly gameid: string | number; readonly gameid: string | number;
readonly roomcode: string; readonly roomcode: string;
} }
function baseRoomAction( function baseRoomAction(
runtimeConfig: FirstSliceRuntimeConfig, runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity, room: RoomIdentity,
): RoomActionPayload { ): RoomActionPayload {
const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity'); const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity');
const roomRecord = requireRecord(room, '$.room'); const roomRecord = requireRecord(room, '$.room');
return { return {
agentid: requireStringOrFiniteNumber( agentid: requireStringOrFiniteNumber(
identity.agentid, identity.agentid,
'$.runtimeConfig.identity.agentid', '$.runtimeConfig.identity.agentid',
), ),
playerid: requireInteger(roomRecord.playerid, '$.room.playerid'), playerid: requireInteger(roomRecord.playerid, '$.room.playerid'),
gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'), gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'), roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'),
}; };
} }
export function buildPrepareRequest( export function buildPrepareRequest(
runtimeConfig: FirstSliceRuntimeConfig, runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity, room: RoomIdentity,
): FirstSliceEnvelope<'player_prepare', RoomActionPayload> { ): FirstSliceEnvelope<'player_prepare', RoomActionPayload> {
return buildFirstSliceEnvelope('player_prepare', baseRoomAction(runtimeConfig, room)); return buildFirstSliceEnvelope('player_prepare', baseRoomAction(runtimeConfig, room));
} }
export function buildExitRoomRequest( export function buildExitRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig, runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity, room: RoomIdentity,
): FirstSliceEnvelope<'self_exit_room', RoomActionPayload> { ): FirstSliceEnvelope<'self_exit_room', RoomActionPayload> {
return buildFirstSliceEnvelope('self_exit_room', baseRoomAction(runtimeConfig, room)); return buildFirstSliceEnvelope('self_exit_room', baseRoomAction(runtimeConfig, room));
} }
export interface JoinRoomEnvironment { export interface JoinRoomEnvironment {
readonly location: unknown; readonly location: unknown;
readonly ip: string; readonly ip: string;
readonly vipMatch?: 1; readonly vipMatch?: 1;
readonly match_id?: string | number; readonly match_id?: string | number;
} }
export interface JoinRoomPayload extends RoomActionPayload { export interface JoinRoomPayload extends Omit<RoomActionPayload, 'roomcode'> {
readonly location: unknown; readonly roomcode: string | number;
readonly ip: string; readonly location: unknown;
readonly vipMatch?: 1; readonly ip: string;
readonly match_id?: string | number; readonly vipMatch?: 1;
} readonly match_id?: string | number;
}
export function buildJoinRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig, export function buildJoinRoomRequest(
room: RoomIdentity, runtimeConfig: FirstSliceRuntimeConfig,
environment: JoinRoomEnvironment, room: { readonly playerid: number; readonly roomcode: string | number },
): FirstSliceEnvelope<'self_join_room', JoinRoomPayload> { environment: JoinRoomEnvironment,
const environmentRecord = requireRecord(environment, '$.environment'); ): FirstSliceEnvelope<'self_join_room', JoinRoomPayload> {
if (!hasOwn(environmentRecord, 'location') || environmentRecord.location === undefined) { const environmentRecord = requireRecord(environment, '$.environment');
throw new TypeError('$.environment.location: expected present field, received undefined'); 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'); if (environmentRecord.location !== null) {
} requireRecord(environmentRecord.location, '$.environment.location');
const data: Record<string, unknown> = { }
...baseRoomAction(runtimeConfig, room), const data: Record<string, unknown> = {
location: environmentRecord.location, ...baseRoomAction(runtimeConfig, {playerid: room.playerid, roomcode: String(room.roomcode)}),
ip: requireString(environmentRecord.ip, '$.environment.ip'), roomcode: requireStringOrFiniteNumber(room.roomcode, '$.room.roomcode'),
}; location: environmentRecord.location,
const vipMatch = optionalField( ip: requireString(environmentRecord.ip, '$.environment.ip'),
environmentRecord, };
'vipMatch', const vipMatch = optionalField(
'$.environment', environmentRecord,
(value, path): 1 => { 'vipMatch',
const parsed = requireNumber(value, path); '$.environment',
if (parsed !== 1) { (value, path): 1 => {
throw new RangeError(`${path}: expected number 1, received number`); const parsed = requireNumber(value, path);
} if (parsed !== 1) {
return 1; throw new RangeError(`${path}: expected number 1, received number`);
}, }
); return 1;
if (vipMatch !== undefined) data.vipMatch = vipMatch; },
);
const matchId = optionalField( if (vipMatch !== undefined) data.vipMatch = vipMatch;
environmentRecord,
'match_id', const matchId = optionalField(
'$.environment', environmentRecord,
requireStringOrFiniteNumber, 'match_id',
); '$.environment',
if (matchId !== undefined) data.match_id = matchId; requireStringOrFiniteNumber,
return buildFirstSliceEnvelope('self_join_room', data as JoinRoomPayload); );
} 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; export interface ParsedRoomResponse {
readonly reconnect: ReconnectSnapshot; readonly state: number;
readonly stage: 0 | 1; readonly room: ParsedRoomSnapshot | null;
readonly raw: JsonRecord; 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'); export function parseSelfJoinRoomResponse(value: unknown): ParsedRoomResponse {
const room = state === 0 ? parseRoomEntryFields(raw) : null; const raw = requireRecord(value, '$.data');
const parsedDeskwar = optionalField(raw, 'deskwar', '$.data', requireInteger); const state = requireInteger(raw.state, '$.data.state');
if (parsedDeskwar !== undefined && parsedDeskwar !== 0 && parsedDeskwar !== 1) { const room = state === 0 ? parseRoomEntryFields(raw) : null;
throw new RangeError('$.data.deskwar: expected protocol flag 0 or 1'); const parsedDeskwar = optionalField(raw, 'deskwar', '$.data', requireInteger);
} if (parsedDeskwar !== undefined && parsedDeskwar !== 0 && parsedDeskwar !== 1) {
const reconnect = reconnectFrom(raw); throw new RangeError('$.data.deskwar: expected protocol flag 0 or 1');
const stage = reconnect.present ? 1 : parsedDeskwar ?? 0; }
return { state, room, reconnect, stage, raw }; 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; 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'); function parseSeatPayload(value: unknown): ParsedSeatPayload {
if (seat < 0) throw new RangeError('$.data.seat: expected non-negative server seat, received number'); const raw = requireRecord(value, '$.data');
return { seat, raw }; 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 parsePlayerPreparePayload = parseSeatPayload;
export const parseOtherOfflinePayload = parseSeatPayload; export const parseOtherJoinRoomPayload = parseSeatPayload;
export const parseOtherExitRoomPayload = parseSeatPayload;
export interface ParsedSelfExitPayload { export const parseOtherOfflinePayload = parseSeatPayload;
readonly seat?: number;
readonly raw: JsonRecord; 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); export function parseSelfExitRoomPayload(value: unknown): ParsedSelfExitPayload {
if (seat !== undefined && seat < 0) { const raw = requireRecord(value, '$.data');
throw new RangeError('$.data.seat: expected non-negative server seat, received number'); const seat = optionalField(raw, 'seat', '$.data', requireInteger);
} if (seat !== undefined && seat < 0) {
return { ...(seat === undefined ? {} : { seat }), raw }; 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 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 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 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 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; export interface ParsedAgentserverPayload {
readonly raw: JsonRecord; readonly agentserver: string;
} readonly opt: string;
readonly raw: JsonRecord;
export function parseConnectAgentserverPayload(value: unknown): ParsedAgentserverPayload { }
const raw = requireRecord(value, '$.data');
return { export function parseConnectAgentserverPayload(value: unknown): ParsedAgentserverPayload {
agentserver: requireString(raw.agentserver, '$.data.agentserver'), const raw = requireRecord(value, '$.data');
opt: requireString(raw.opt, '$.data.opt'), return {
raw, agentserver: requireString(raw.agentserver, '$.data.agentserver'),
}; opt: requireString(raw.opt, '$.data.opt'),
} raw,
};
export interface ParsedKickPayload { }
readonly msg: string;
readonly raw: JsonRecord; 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 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'; import { APP } from '../core/constants.ts';
/** Legacy 12_Logic dispatches login replies from all three platform routes by rpc. /** Legacy 12_Logic dispatches login replies from all three platform routes by rpc.
* This inbound contract is independent of the outbound agent route below. */ * This inbound contract is independent of the outbound agent route below. */
export function isLoginResponse(message:{route:string;rpc:string}):boolean{ export function isLoginResponse(message:{route:string;rpc:string}):boolean{
return message.rpc==='player_login'&&(message.route==='agent'||message.route==='room'||message.route==='platform'); return message.rpc==='player_login'&&(message.route==='agent'||message.route==='room'||message.route==='platform');
} }
export type FirstSliceRpc = export type FirstSliceRpc =
| 'getInfoByShortCode' | 'getVipRankList'
| 'get_player_task' | 'get_treasurelist'
| 'get_task_award' | 'topup_card'
| 'update_roomcard' | 'get_player_invitecode'
| 'player_finish_task' | 'binding_invitecode'
| 'player_login' | 'getInfoByShortCode'
| 'self_join_room' | 'get_player_task'
| 'player_prepare' | 'get_task_award'
| 'self_exit_room' | 'update_bean'
| 'other_join_room' | 'update_roomcard'
| 'other_exit_room' | 'player_finish_task'
| 'other_offline' | 'player_login'
| 'other_online' | 'create_room'
| 'connect_roomserver' | 'self_join_room'
| 'connect_agentserver' | 'player_prepare'
| 'kick_server' | 'self_exit_room'
| 'kick_offline' | 'other_join_room'
| 'show_message'; | 'other_exit_room'
| 'other_offline'
export type FirstSliceRoute = 'platform' | 'agent' | 'room'; | 'other_online'
| 'connect_roomserver'
/** 第一纵向切片 rpc → route 的唯一权威映射。 */ | 'connect_agentserver'
export const FIRST_SLICE_RPC_ROUTE = { | 'kick_server'
getInfoByShortCode: 'agent', | 'kick_offline'
get_player_task: 'agent', | 'show_message';
get_task_award: 'agent',
update_roomcard: 'agent', export type FirstSliceRoute = 'platform' | 'agent' | 'room';
player_finish_task: 'agent',
player_login: 'agent', /** 第一纵向切片 rpc → route 的唯一权威映射。 */
self_join_room: 'agent', export const FIRST_SLICE_RPC_ROUTE = {
player_prepare: 'room', getVipRankList: 'agent',
self_exit_room: 'room', get_treasurelist: 'agent',
other_join_room: 'room', topup_card: 'agent',
other_exit_room: 'room', get_player_invitecode: 'agent',
other_offline: 'room', binding_invitecode: 'agent',
other_online: 'room', getInfoByShortCode: 'agent',
connect_roomserver: 'room', get_player_task: 'agent',
connect_agentserver: 'agent', get_task_award: 'agent',
kick_server: 'agent', update_bean: 'agent',
kick_offline: 'agent', update_roomcard: 'agent',
show_message: 'agent', player_finish_task: 'agent',
} as const satisfies Record<FirstSliceRpc, 'platform' | 'agent' | 'room'>; player_login: 'agent',
create_room: 'agent',
export function routeForFirstSlice<Rpc extends FirstSliceRpc>( self_join_room: 'agent',
rpc: Rpc, player_prepare: 'room',
): (typeof FIRST_SLICE_RPC_ROUTE)[Rpc] { self_exit_room: 'room',
if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, rpc)) { other_join_room: 'room',
throw new Error(`FIRST_SLICE_RPC_ROUTE 未定义 rpc: ${String(rpc)}`); other_exit_room: 'room',
} other_offline: 'room',
return FIRST_SLICE_RPC_ROUTE[rpc]; other_online: 'room',
} connect_roomserver: 'room',
connect_agentserver: 'agent',
export interface FirstSliceEnvelope< kick_server: 'agent',
Rpc extends FirstSliceRpc, kick_offline: 'agent',
Data extends Readonly<Record<string, unknown>>, show_message: 'agent',
> { } as const satisfies Record<FirstSliceRpc, 'platform' | 'agent' | 'room'>;
readonly app: 'youle';
readonly route: (typeof FIRST_SLICE_RPC_ROUTE)[Rpc]; export function routeForFirstSlice<Rpc extends FirstSliceRpc>(
readonly rpc: Rpc; rpc: Rpc,
readonly data: Data; ): (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)}`);
export function buildFirstSliceEnvelope< }
Rpc extends FirstSliceRpc, return FIRST_SLICE_RPC_ROUTE[rpc];
Data extends Readonly<Record<string, unknown>>, }
>(rpc: Rpc, data: Data): FirstSliceEnvelope<Rpc, Data> {
return { app: APP, route: routeForFirstSlice(rpc), rpc, data }; 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 { InboundMessage } from '../core/types/envelope.ts';
import type { GameSessionHost } from '../sdk/runtime/game-session-host.ts'; import type { GameSessionHost } from '../sdk/runtime/game-session-host.ts';
import type { ConnectionControlPort } from '../platform/connection-intent.ts'; import type { ConnectionControlPort } from '../platform/connection-intent.ts';
import type { RuntimeSession } from '../platform/runtime-session.ts'; import type { RuntimeSession } from '../platform/runtime-session.ts';
import type { PlatformStore } from '../platform/stores/platform-store.ts'; import type { PlatformStore } from '../platform/stores/platform-store.ts';
import { requireRecord, requireString } from './contracts/validation.ts'; import { requireRecord, requireString } from './contracts/validation.ts';
import { import {
parseConnectAgentserverPayload, parseConnectAgentserverPayload,
parseConnectRoomserverPayload, parseConnectRoomserverPayload,
parseKickServerPayload, parseKickServerPayload,
parseOtherExitRoomPayload, parseOtherExitRoomPayload,
parseOtherJoinRoomPayload, parseOtherJoinRoomPayload,
parseOtherOfflinePayload, parseOtherOfflinePayload,
parseOtherOnlinePayload, parseOtherOnlinePayload,
parsePlayerPreparePayload, parsePlayerPreparePayload,
parseSelfExitRoomPayload, parseSelfExitRoomPayload,
} from './contracts/index.ts'; } from './contracts/index.ts';
import { import {
buildFirstSliceEnvelope, buildFirstSliceEnvelope,
isLoginResponse, isLoginResponse,
FIRST_SLICE_RPC_ROUTE, FIRST_SLICE_RPC_ROUTE,
routeForFirstSlice, routeForFirstSlice,
type FirstSliceRpc, type FirstSliceRpc,
} from './first-slice-routes.ts'; } from './first-slice-routes.ts';
export interface PlatformHandlersOptions { export interface PlatformHandlersOptions {
readonly store: PlatformStore; readonly store: PlatformStore;
readonly gameSession: GameSessionHost; readonly gameSession: GameSessionHost;
readonly session: RuntimeSession; readonly session: RuntimeSession;
readonly connection: ConnectionControlPort; readonly connection: ConnectionControlPort;
readonly showMessage?: (message:string,time:number,source:'show_message'|'update_roomcard')=>void; readonly showMessage?: (message:string,time:number,source:'show_message'|'update_roomcard'|'update_bean'|'binding_invitecode')=>void;
} }
export type PlatformDispatchResult = 'handled-platform'; export type PlatformDispatchResult = 'handled-platform';
/** Strict first-slice platform RPC dispatcher. */ /** Strict first-slice platform RPC dispatcher. */
export class PlatformHandlers { export class PlatformHandlers {
constructor(private readonly options: PlatformHandlersOptions) {} constructor(private readonly options: PlatformHandlersOptions) {}
dispatch(message: InboundMessage): PlatformDispatchResult { dispatch(message: InboundMessage): PlatformDispatchResult {
if (!isPlatformRoute(message.route)) { if (!isPlatformRoute(message.route)) {
throw new Error( throw new Error(
`PlatformHandlers received non-platform route: route=${message.route} rpc=${message.rpc}`, `PlatformHandlers received non-platform route: route=${message.route} rpc=${message.rpc}`,
); );
} }
if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, message.rpc)) { if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, message.rpc)) {
throw new Error( throw new Error(
`Unsupported platform rpc: route=${message.route} rpc=${message.rpc}`, `Unsupported platform rpc: route=${message.route} rpc=${message.rpc}`,
); );
} }
const rpc = message.rpc as FirstSliceRpc; const rpc = message.rpc as FirstSliceRpc;
const expectedRoute = routeForFirstSlice(rpc); const expectedRoute = routeForFirstSlice(rpc);
if (message.route !== expectedRoute && !isLoginResponse(message)) { if (message.route !== expectedRoute && !isLoginResponse(message) && !(rpc==='update_bean'&&message.route==='room')) {
throw new Error( throw new Error(
`Platform rpc route mismatch: rpc=${rpc} expected=${expectedRoute} received=${message.route}`, `Platform rpc route mismatch: rpc=${rpc} expected=${expectedRoute} received=${message.route}`,
); );
} }
switch (rpc) { switch (rpc) {
case 'getInfoByShortCode':this.options.store.applyVipRooms(message.data);break; case 'getVipRankList':this.options.store.applyVipRank(message.data);break;
case 'update_roomcard': { case 'get_treasurelist':this.options.store.applyTreasureList(message.data);break;
const raw=requireRecord(message.data,'$.data'); case 'topup_card':break; // Original response is empty; updates arrive separately.
const text=raw.text?requireString(raw.text,'$.data.text'):null; case 'get_player_invitecode':this.options.store.applyInvitecode(message.data);break;
this.options.store.applyRoomcardUpdate(raw); case 'binding_invitecode':{
if(raw.change===undefined&&text){ if(!this.options.showMessage)throw Error('binding_invitecode requires a message UI');
if(!this.options.showMessage)throw Error('update_roomcard requires a message UI'); const text=this.options.store.applyInviteBinding(message.data);
this.options.showMessage(text,0,'update_roomcard'); this.options.showMessage(text,3000,'binding_invitecode');break;
} }
break; case 'getInfoByShortCode':this.options.store.applyVipRooms(message.data);break;
} case 'update_bean': {
case 'get_player_task':this.options.store.applyTaskList(message.data);break; const raw=requireRecord(message.data,'$.data');
case 'get_task_award':this.options.store.applyTaskAward(message.data);break; const text=raw.text?requireString(raw.text,'$.data.text'):null;
case 'player_finish_task':this.options.store.applyTaskFinished(message.data);break; this.options.store.applyBeanUpdate(raw);
case 'player_login': if(raw.change===undefined&&raw.seat===undefined&&text){
this.options.session.handleLogin(message.data); if(!this.options.showMessage)throw Error('update_bean requires a message UI');
break; this.options.showMessage(text,0,'update_bean');
case 'self_join_room': }
this.options.session.handleSelfJoin(message.data); break;
break; }
case 'player_prepare': { case 'update_roomcard': {
const parsed = parsePlayerPreparePayload(message.data); const raw=requireRecord(message.data,'$.data');
this.requireActiveGame(rpc); const text=raw.text?requireString(raw.text,'$.data.text'):null;
this.options.store.playerReady(parsed); this.options.store.applyRoomcardUpdate(raw);
this.options.gameSession.publish({ type: 'room.player-ready', seat: parsed.seat }); if(raw.change===undefined&&text){
break; if(!this.options.showMessage)throw Error('update_roomcard requires a message UI');
} this.options.showMessage(text,0,'update_roomcard');
case 'self_exit_room': { }
const parsed = parseSelfExitRoomPayload(message.data); break;
this.options.session.handleSelfExit(parsed); }
break; case 'get_player_task':this.options.store.applyTaskList(message.data);break;
} case 'get_task_award':this.options.store.applyTaskAward(message.data);break;
case 'other_join_room': { case 'player_finish_task':this.options.store.applyTaskFinished(message.data);break;
const parsed = parseOtherJoinRoomPayload(message.data); case 'player_login':
this.requireActiveGame(rpc); this.options.session.handleLogin(message.data);
this.options.store.playerJoin(parsed); break;
this.options.gameSession.publish({ type: 'room.player-joined', seat: parsed.seat }); case 'create_room':
break; this.options.session.handleCreateRoom(message.data);
} break;
case 'other_exit_room': { case 'self_join_room':
const parsed = parseOtherExitRoomPayload(message.data); this.options.session.handleSelfJoin(message.data);
this.requireActiveGame(rpc); break;
this.options.store.playerExit(parsed); case 'player_prepare': {
this.options.gameSession.publish({ type: 'room.player-left', seat: parsed.seat }); const parsed = parsePlayerPreparePayload(message.data);
break; this.requireActiveGame(rpc);
} this.options.store.playerReady(parsed);
case 'other_offline': { this.options.gameSession.publish({ type: 'room.player-ready', seat: parsed.seat });
const parsed = parseOtherOfflinePayload(message.data); break;
this.requireActiveGame(rpc); }
this.options.store.playerOffline(parsed); case 'self_exit_room': {
this.options.gameSession.publish({ type: 'room.player-offline', seat: parsed.seat }); const parsed = parseSelfExitRoomPayload(message.data);
break; this.options.session.handleSelfExit(parsed);
} break;
case 'other_online': { }
const parsed = parseOtherOnlinePayload(message.data); case 'other_join_room': {
this.requireActiveGame(rpc); const parsed = parseOtherJoinRoomPayload(message.data);
this.options.store.playerOnline(parsed); this.requireActiveGame(rpc);
this.options.gameSession.publish({ type: 'room.player-online', seat: parsed.seat }); this.options.store.playerJoin(parsed);
break; this.options.gameSession.publish({ type: 'room.player-joined', seat: parsed.seat });
} break;
case 'connect_roomserver': { }
const parsed = parseConnectRoomserverPayload(message.data); case 'other_exit_room': {
const target = parseSwitchTarget(parsed.roomserver, '$.data.roomserver'); const parsed = parseOtherExitRoomPayload(message.data);
const resend = buildFirstSliceEnvelope('connect_roomserver', parsed.raw); this.requireActiveGame(rpc);
this.options.session.handleServerSwitch( this.options.store.playerExit(parsed);
() => this.options.connection.switchServer(target, resend), this.options.gameSession.publish({ type: 'room.player-left', seat: parsed.seat });
); break;
break; }
} case 'other_offline': {
case 'connect_agentserver': { const parsed = parseOtherOfflinePayload(message.data);
const parsed = parseConnectAgentserverPayload(message.data); this.requireActiveGame(rpc);
const target = parseSwitchTarget(parsed.agentserver, '$.data.agentserver'); this.options.store.playerOffline(parsed);
const resend = buildFirstSliceEnvelope('connect_agentserver', parsed.raw); this.options.gameSession.publish({ type: 'room.player-offline', seat: parsed.seat });
this.options.session.handleServerSwitch( break;
() => this.options.connection.switchServer(target, resend), }
); case 'other_online': {
break; const parsed = parseOtherOnlinePayload(message.data);
} this.requireActiveGame(rpc);
case 'show_message': { this.options.store.playerOnline(parsed);
const data=requireRecord(message.data,'$.data'); this.options.gameSession.publish({ type: 'room.player-online', seat: parsed.seat });
const text=requireString(data.msg,'$.data.msg'); break;
// Desk.show_message converts the original time with Number; only >0 auto-closes. }
const time=Number(data.time); case 'connect_roomserver': {
if(!this.options.showMessage)throw Error('show_message requires a message UI'); const parsed = parseConnectRoomserverPayload(message.data);
this.options.showMessage(text,time,'show_message'); const target = parseSwitchTarget(parsed.roomserver, '$.data.roomserver');
break; const resend = buildFirstSliceEnvelope('connect_roomserver', parsed.raw);
} this.options.session.handleServerSwitch(
case 'kick_offline': { () => this.options.connection.switchServer(target, resend),
const data=requireRecord(message.data,'$.data'); );
this.options.session.handleKick(data,()=>this.options.connection.stopForKick(data)); break;
break; }
} case 'connect_agentserver': {
case 'kick_server': { const parsed = parseConnectAgentserverPayload(message.data);
const parsed = parseKickServerPayload(message.data); const target = parseSwitchTarget(parsed.agentserver, '$.data.agentserver');
this.options.session.handleKick( const resend = buildFirstSliceEnvelope('connect_agentserver', parsed.raw);
parsed.raw, this.options.session.handleServerSwitch(
() => this.options.connection.stopForKick(parsed.raw), () => this.options.connection.switchServer(target, resend),
); );
break; break;
} }
} case 'show_message': {
return 'handled-platform'; 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.
private requireActiveGame(rpc: string): void { const time=Number(data.time);
if (this.options.gameSession.state !== 'active') { if(!this.options.showMessage)throw Error('show_message requires a message UI');
throw new Error(`${rpc} requires an active game session`); this.options.showMessage(text,time,'show_message');
} break;
} }
} case 'kick_offline': {
const data=requireRecord(message.data,'$.data');
function isPlatformRoute(route: string): route is 'platform' | 'agent' | 'room' { this.options.session.handleKick(data,()=>this.options.connection.stopForKick(data));
return route === 'platform' || route === 'agent' || route === 'room'; break;
} }
case 'kick_server': {
function parseSwitchTarget(address: string, path: string): string { const parsed = parseKickServerPayload(message.data);
if (address.length === 0 || address.trim() !== address) { this.options.session.handleKick(
throw new TypeError(`${path}: expected a non-empty WebSocket server address`); parsed.raw,
} () => this.options.connection.stopForKick(parsed.raw),
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(address); );
const target = scheme ? address : `ws://${address}`; break;
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`); return 'handled-platform';
} }
if (target.includes('#') || authority.includes('@')) {
throw new TypeError(`${path}: expected a WebSocket address without fragment or userinfo`); private requireActiveGame(rpc: string): void {
} if (this.options.gameSession.state !== 'active') {
throw new Error(`${rpc} requires an active game session`);
let parsed: URL; }
try { }
parsed = new URL(target); }
} catch {
throw new TypeError(`${path}: expected a valid WebSocket server address`); function isPlatformRoute(route: string): route is 'platform' | 'agent' | 'room' {
} return route === 'platform' || route === 'agent' || route === 'room';
const protocol = parsed.protocol.toLowerCase(); }
if (
(protocol !== 'ws:' && protocol !== 'wss:') function parseSwitchTarget(address: string, path: string): string {
|| parsed.hostname.length === 0 if (address.length === 0 || address.trim() !== address) {
|| parsed.username.length > 0 throw new TypeError(`${path}: expected a non-empty WebSocket server address`);
|| parsed.password.length > 0 }
|| parsed.hash.length > 0 const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(address);
) { const target = scheme ? address : `ws://${address}`;
throw new TypeError(`${path}: expected a valid WebSocket server address`); const authority = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#]*)/.exec(target)?.[1];
} if (authority === undefined || authority.length === 0) {
return target; 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-entry.ts';
export * from './game-host.ts'; export * from './create-room-page.ts';
export * from './game-module.ts'; export * from './game-host.ts';
export * from './game-seat-mapper.ts'; export * from './game-module.ts';
export * from './platform-events.ts'; export * from './game-seat-mapper.ts';
export * from './snapshots.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", "ver": "1.0.27",
"importer": "image", "importer": "image",
"imported": true, "imported": true,
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6", "uuid": "34daf5f4-3869-4f71-84c4-d09103701be6",
"files": [ "files": [
".json", ".json",
".png" ".png"
], ],
"subMetas": { "subMetas": {
"6c48a": { "6c48a": {
"importer": "texture", "importer": "texture",
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a", "uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a",
"displayName": "00042", "displayName": "00042",
"id": "6c48a", "id": "6c48a",
"name": "texture", "name": "texture",
"userData": { "userData": {
"wrapModeS": "clamp-to-edge", "wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge", "wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6", "imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6",
"isUuid": true, "isUuid": true,
"visible": false, "visible": false,
"minfilter": "linear", "minfilter": "linear",
"magfilter": "linear", "magfilter": "linear",
"mipfilter": "none", "mipfilter": "none",
"anisotropy": 0 "anisotropy": 0
}, },
"ver": "1.0.22", "ver": "1.0.22",
"imported": true, "imported": true,
"files": [ "files": [
".json" ".json"
], ],
"subMetas": {} "subMetas": {}
}, },
"f9941": { "f9941": {
"importer": "sprite-frame", "importer": "sprite-frame",
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@f9941", "uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@f9941",
"displayName": "00042", "displayName": "00042",
"id": "f9941", "id": "f9941",
"name": "spriteFrame", "name": "spriteFrame",
"userData": { "userData": {
"trimThreshold": 1, "trimThreshold": 1,
"rotated": false, "rotated": false,
"offsetX": 0, "offsetX": 0,
"offsetY": 0, "offsetY": 0,
"trimX": 0, "trimX": 0,
"trimY": 0, "trimY": 0,
"width": 108, "width": 108,
"height": 98, "height": 98,
"rawWidth": 108, "rawWidth": 108,
"rawHeight": 98, "rawHeight": 98,
"borderTop": 48, "borderTop": 26,
"borderBottom": 48, "borderBottom": 34,
"borderLeft": 53, "borderLeft": 30,
"borderRight": 53, "borderRight": 31,
"packable": true, "packable": true,
"pixelsToUnit": 100, "pixelsToUnit": 100,
"pivotX": 0.5, "pivotX": 0.5,
"pivotY": 0.5, "pivotY": 0.5,
"meshType": 0, "meshType": 0,
"vertices": { "vertices": {
"rawPosition": [ "rawPosition": [
-54, -54,
-49, -49,
0, 0,
54, 54,
-49, -49,
0, 0,
-54, -54,
49, 49,
0, 0,
54, 54,
49, 49,
0 0
], ],
"indexes": [ "indexes": [
0, 0,
1, 1,
2, 2,
2, 2,
1, 1,
3 3
], ],
"uv": [ "uv": [
0, 0,
98, 98,
108, 108,
98, 98,
0, 0,
0, 0,
108, 108,
0 0
], ],
"nuv": [ "nuv": [
0, 0,
0, 0,
1, 1,
0, 0,
0, 0,
1, 1,
1, 1,
1 1
], ],
"minPos": [ "minPos": [
-54, -54,
-49, -49,
0 0
], ],
"maxPos": [ "maxPos": [
54, 54,
49, 49,
0 0
] ]
}, },
"isUuid": true, "isUuid": true,
"imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a", "imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a",
"atlasUuid": "", "atlasUuid": "",
"trimType": "none" "trimType": "none"
}, },
"ver": "1.0.12", "ver": "1.0.12",
"imported": true, "imported": true,
"files": [ "files": [
".json" ".json"
], ],
"subMetas": {} "subMetas": {}
} }
}, },
"userData": { "userData": {
"type": "sprite-frame", "type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false, "fixAlphaTransparencyArtifacts": false,
"hasAlpha": true, "hasAlpha": true,
"redirect": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a" "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, "offsetY": 0,
"trimX": 0, "trimX": 0,
"trimY": 0, "trimY": 0,
"width": 1600, "width": 1280,
"height": 720, "height": 720,
"rawWidth": 1600, "rawWidth": 1280,
"rawHeight": 720, "rawHeight": 720,
"borderTop": 0, "borderTop": 0,
"borderBottom": 0, "borderBottom": 0,
@@ -60,16 +60,16 @@
"meshType": 0, "meshType": 0,
"vertices": { "vertices": {
"rawPosition": [ "rawPosition": [
-800, -640,
-360, -360,
0, 0,
800, 640,
-360, -360,
0, 0,
-800, -640,
360, 360,
0, 0,
800, 640,
360, 360,
0 0
], ],
@@ -84,11 +84,11 @@
"uv": [ "uv": [
0, 0,
720, 720,
1600, 1280,
720, 720,
0, 0,
0, 0,
1600, 1280,
0 0
], ],
"nuv": [ "nuv": [
@@ -102,12 +102,12 @@
1 1
], ],
"minPos": [ "minPos": [
-800, -640,
-360, -360,
0 0
], ],
"maxPos": [ "maxPos": [
800, 640,
360, 360,
0 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", "ver": "1.1.50",
"importer": "prefab", "importer": "prefab",
"imported": true, "imported": true,
"uuid": "b13f487a-451e-41ce-a810-1fab0d52248f", "uuid": "67117675-9f6d-4def-a67c-db0e9fb19b98",
"files": [ "files": [
".json" ".json"
], ],
"subMetas": {}, "subMetas": {},
"userData": { "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", "ver": "1.1.50",
"importer": "prefab", "importer": "prefab",
"imported": true, "imported": true,
"uuid": "b9f6f400-7b1b-4e51-a841-cc84dcddca82", "uuid": "b9f6f400-7b1b-4e51-a841-cc84dcddca82",
"files": [ "files": [
".json" ".json"
], ],
"subMetas": {}, "subMetas": {},
"userData": { "userData": {
"syncNodeName": "TaskItem" "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", "ver": "1.1.50",
"importer": "prefab", "importer": "prefab",
"imported": true, "imported": true,
"uuid": "fed2e71c-22ac-4e48-a42a-68e48a1929a7", "uuid": "fed2e71c-22ac-4e48-a42a-68e48a1929a7",
"files": [ "files": [
".json" ".json"
], ],
"subMetas": {}, "subMetas": {},
"userData": { "userData": {
"syncNodeName": "VipRoomItem1" "syncNodeName": "VipRoomItem1"
} }
} }
@@ -1,13 +1,13 @@
{ {
"ver": "1.1.50", "ver": "1.1.50",
"importer": "prefab", "importer": "prefab",
"imported": true, "imported": true,
"uuid": "8cb43df4-d8fa-4b3c-ab01-497b23c68435", "uuid": "8cb43df4-d8fa-4b3c-ab01-497b23c68435",
"files": [ "files": [
".json" ".json"
], ],
"subMetas": {}, "subMetas": {},
"userData": { "userData": {
"syncNodeName": "VipRoomItem2" "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, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 24 "__id__": 25
}, },
{ {
"__id__": 26 "__id__": 27
} }
], ],
"_prefab": { "_prefab": {
"__id__": 28 "__id__": 29
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -82,14 +82,14 @@
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 19 "__id__": 20
}, },
{ {
"__id__": 21 "__id__": 22
} }
], ],
"_prefab": { "_prefab": {
"__id__": 23 "__id__": 24
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -186,6 +186,7 @@
"__id__": 5 "__id__": 5
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size",
"width": 1600, "width": 1600,
"height": 720 "height": 720
}, },
@@ -313,10 +314,13 @@
}, },
{ {
"__id__": 16 "__id__": 16
},
{
"__id__": 18
} }
], ],
"_prefab": { "_prefab": {
"__id__": 18 "__id__": 19
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -360,6 +364,7 @@
"__id__": 13 "__id__": 13
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size",
"width": 150, "width": 150,
"height": 150 "height": 150
}, },
@@ -455,6 +460,29 @@
"__type__": "cc.CompPrefabInfo", "__type__": "cc.CompPrefabInfo",
"fileId": "3wlBO4ciKrxeTYsQdlQvBg" "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", "__type__": "cc.PrefabInfo",
"root": { "root": {
@@ -478,9 +506,10 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 20 "__id__": 21
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size",
"width": 1600, "width": 1600,
"height": 720 "height": 720
}, },
@@ -505,7 +534,7 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 22 "__id__": 23
}, },
"_alignFlags": 45, "_alignFlags": 45,
"_target": null, "_target": null,
@@ -554,9 +583,10 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 25 "__id__": 26
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size",
"width": 1600, "width": 1600,
"height": 720 "height": 720
}, },
@@ -581,7 +611,7 @@
}, },
"_enabled": true, "_enabled": true,
"__prefab": { "__prefab": {
"__id__": 27 "__id__": 28
}, },
"_alignFlags": 45, "_alignFlags": 45,
"_target": null, "_target": null,
@@ -1,7 +1,7 @@
[ [
{ {
"__type__": "cc.Prefab", "__type__": "cc.Prefab",
"_name": "Login_Layer", "_name": "Login",
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"_native": "", "_native": "",
@@ -13,7 +13,7 @@
}, },
{ {
"__type__": "cc.Node", "__type__": "cc.Node",
"_name": "Login_Layer", "_name": "Login",
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"_parent": null, "_parent": null,
@@ -1,14 +1,14 @@
{ {
"ver": "1.1.50", "ver": "1.1.50",
"importer": "prefab", "importer": "prefab",
"imported": true, "imported": true,
"uuid": "b209cefb-b381-4c7c-b765-94a5ab75a89c", "uuid": "b209cefb-b381-4c7c-b765-94a5ab75a89c",
"files": [ "files": [
".json" ".json"
], ],
"subMetas": {}, "subMetas": {},
"userData": { "userData": {
"syncNodeName": "Login_Layer", "syncNodeName": "Login",
"hasIcon": false "hasIcon": false
} }
} }
@@ -8,6 +8,6 @@
], ],
"subMetas": {}, "subMetas": {},
"userData": { "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", "__type__": "cc.Prefab",
"_name": "Layer1_Logo", "_name": "ResourceLoading",
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"_native": "", "_native": "",
@@ -13,22 +13,19 @@
}, },
{ {
"__type__": "cc.Node", "__type__": "cc.Node",
"_name": "Layer1_Logo", "_name": "ResourceLoading",
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"_parent": null, "_parent": null,
"_children": [ "_children": [
{ {
"__id__": 2 "__id__": 2
},
{
"__id__": 50
} }
], ],
"_active": true, "_active": true,
"_components": [], "_components": [],
"_prefab": { "_prefab": {
"__id__": 60 "__id__": 50
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -1130,241 +1127,6 @@
"targetOverrides": null, "targetOverrides": null,
"nestedPrefabInstanceRoots": 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", "__type__": "cc.PrefabInfo",
"root": { "root": {
@@ -8,6 +8,6 @@
], ],
"subMetas": {}, "subMetas": {},
"userData": { "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, "autoReleaseAssets": false,
"_globals": { "_globals": {
"__id__": 5 "__id__": 6
}, },
"_id": "1c10c2ee-adc2-496f-9005-d3e0e6ebbb56" "_id": "1c10c2ee-adc2-496f-9005-d3e0e6ebbb56"
}, },
@@ -72,6 +72,9 @@
}, },
{ {
"__id__": 4 "__id__": 4
},
{
"__id__": 5
} }
], ],
"_prefab": null, "_prefab": null,
@@ -182,8 +185,24 @@
"__uuid__": "e1b53f04-3e2f-4127-9a3f-f629bed6905e", "__uuid__": "e1b53f04-3e2f-4127-9a3f-f629bed6905e",
"__expectedType__": "cc.Prefab" "__expectedType__": "cc.Prefab"
}, },
"notices": { "redeemCode": {
"__uuid__": "b13f487a-451e-41ce-a810-1fab0d52248f", "__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" "__expectedType__": "cc.Prefab"
}, },
"profile": { "profile": {
@@ -206,6 +225,100 @@
"__uuid__": "f4af0cc2-2514-4bd5-a0de-7a3dbb32438b", "__uuid__": "f4af0cc2-2514-4bd5-a0de-7a3dbb32438b",
"__expectedType__": "cc.Prefab" "__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": [ "digits": [
{ {
"__uuid__": "fc6e9a8e-0a6d-4555-bc3e-88fd427b9f3a@f9941", "__uuid__": "fc6e9a8e-0a6d-4555-bc3e-88fd427b9f3a@f9941",
@@ -272,32 +385,56 @@
], ],
"_id": "abZXhqPblEq57+36N2j8xH" "_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", "__type__": "cc.SceneGlobals",
"ambient": { "ambient": {
"__id__": 6
},
"shadows": {
"__id__": 7 "__id__": 7
}, },
"_skybox": { "shadows": {
"__id__": 8 "__id__": 8
}, },
"fog": { "_skybox": {
"__id__": 9 "__id__": 9
}, },
"octree": { "fog": {
"__id__": 10 "__id__": 10
}, },
"skin": { "octree": {
"__id__": 11 "__id__": 11
}, },
"lightProbeInfo": { "skin": {
"__id__": 12 "__id__": 12
}, },
"postSettings": { "lightProbeInfo": {
"__id__": 13 "__id__": 13
}, },
"postSettings": {
"__id__": 14
},
"bakedWithStationaryMainLight": false, "bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": 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'; import { _decorator, Component, Prefab, SpriteFrame } from 'cc';
const {ccclass,property}=_decorator; const {ccclass,property}=_decorator;
/** Editor-owned asset references; no runtime UUID/path guessing. */ /** Editor-owned asset references; no runtime UUID/path guessing. */
@ccclass('LobbyAssets') @ccclass('LobbyAssets')
export class LobbyAssets extends Component { export class LobbyAssets extends Component {
@property(Prefab) menu:Prefab|null=null; @property(Prefab) menu:Prefab|null=null;
@property(Prefab) header:Prefab|null=null; @property(Prefab) header:Prefab|null=null;
@property(Prefab) noticeMenu:Prefab|null=null; @property(Prefab) noticeMenu:Prefab|null=null;
@property(Prefab) notices:Prefab|null=null; @property(Prefab) redeemCode:Prefab|null=null;
@property(Prefab) profile:Prefab|null=null; @property(Prefab) qrCode:Prefab|null=null;
@property(Prefab) settings:Prefab|null=null; @property(Prefab) noticeBoard:Prefab|null=null;
@property(Prefab) join:Prefab|null=null; @property(Prefab) ownedRoomList:Prefab|null=null;
@property(Prefab) tasks: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(Prefab) share:Prefab|null=null;
@property([SpriteFrame]) digits:SpriteFrame[]=[]; @property(Prefab) rank:Prefab|null=null;
@property([SpriteFrame]) switches:SpriteFrame[]=[]; @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[]=[];
}

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