diff --git a/AGENTS.md b/AGENTS.md index f7ddd68..acffb2f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`;位索引只在协议文档中维护,不在本文件重复定义。 +- 查阅服务器源码用于前端适配,不代表授权修改服务器代码;前端开发继续遵守“服务器零改动”。 -## 第一准则:服务器零改动 - -**任何前端开发都必须完全遵循前后端数据包的协议与数据结构,做到服务器零改动。** 这是本仓库不可逾越的最高准则,优先于一切其它考量: - -- 协议信封、`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` 复刻协议契约,做到服务器零改动即可联调。 - -### 原工程 `Game_Surface_3` → `YouleNexus` 的 UI 迁移 - -`YouleNexus` 的 UI 资源(`assets/framework/ui/prefabs/`)从**原工程 `projects/Game_Surface_3`** 逐层复刻。数据契约与流程规范: - -- 数据模型(权威):`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_*` 工具没出现、调用报连接失败、或行为与预期不符,就去读。** +### 原工程 `Game_Surface_3` → `YouleNexus` 的 UI 迁移 + +`YouleNexus` 的 UI 资源(`assets/framework/ui/prefabs/`)从**原工程 `projects/Game_Surface_3`** 逐层复刻。数据契约与流程规范: + +- 数据模型(权威):`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_*` 工具没出现、调用报连接失败、或行为与预期不符,就去读。** diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/invite-clipboard.ts b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/invite-clipboard.ts new file mode 100644 index 0000000..72b0770 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/invite-clipboard.ts @@ -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}; + +/** 05_Func: settings takes no arguments; WVJB takes "" and returns through its callback. */ +export async function readClipboardText(host:ClipboardHost):Promise{ + const value=host.kind==='bridge'?await new Promise((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{ + return (await readClipboardText(host)).replace(/[^0-9]/g,''); +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/invite-clipboard.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/invite-clipboard.ts.meta new file mode 100644 index 0000000..274ebcf --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/invite-clipboard.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "4aeb3de8-f982-4246-95be-7dca058bc99d", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/owned-room-history.ts b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/owned-room-history.ts new file mode 100644 index 0000000..debf180 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/owned-room-history.ts @@ -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,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>):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>,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]); + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/owned-room-history.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/owned-room-history.ts.meta new file mode 100644 index 0000000..48a8de6 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/owned-room-history.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "ca074aba-afc9-428f-93ea-199d30dba6ef", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts index 56e2e53..52969d0 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts @@ -1,17 +1,19 @@ -import type { ChannelIdentity } from '../identity.ts'; - -/** Legacy GameData launchWaitTime (04_Data.js): timer units are milliseconds. */ -export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10; - -/** - * 构建期默认渠道身份(旧 version.js 等价物)。 - * gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。 - */ -export const BUILD_IDENTITY: ChannelIdentity = { - // This game's entry in config_test/update_jsonv2.txt belongs to this agent/channel. - agentid: '1B2h0ccl205c390Y28m1Ajdplkuu4wgy', - channelid: 'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG', - gameid: 'J9r4022t80pxlb01zOK76DN1WD4QewFl', - marketid: 4, - version: 1, // 数字 versionCode(联调时改成本地服要求的版本号) -}; +import type { ChannelIdentity } from '../identity.ts'; + +/** Legacy GameData launchWaitTime (04_Data.js): timer units are milliseconds. */ +export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10; + +/** + * 构建期默认渠道身份(旧 version.js 等价物)。 + * gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。 + */ +export const BUILD_IDENTITY: ChannelIdentity = { + // This game's entry in config_test/update_jsonv2.txt belongs to this agent/channel. + agentid: '1B2h0ccl205c390Y28m1Ajdplkuu4wgy', + channelid: 'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG', + // agentid: 'veRa0qrBf0df2K1G4de2tgfmVxB2jxpv', + // channelid: 'FtJf073aa0d6rI1xD8J1Y42fINTm0ziK', + gameid: 'J9r4022t80pxlb01zOK76DN1WD4QewFl', + marketid: 4, + version: 1, // 数字 versionCode(联调时改成本地服要求的版本号) +}; diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts b/cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts index 7b404d7..e937748 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts @@ -1,133 +1,181 @@ -import type { RuntimeConfig } from '../config/runtime-config.ts'; -import { buildFirstSliceEnvelope } from '../protocol/first-slice-routes.ts'; -import type { OutboundEnvelope } from '../core/types/envelope.ts'; -import { - buildExitRoomRequest, - buildJoinRoomRequest, - buildPrepareRequest, - type JoinRoomEnvironment, - type RoomIdentity, -} from '../protocol/contracts/index.ts'; -import { PlatformStore } from './stores/platform-store.ts'; -import type { InsideRoomState, PlatformState } from './stores/platform-types.ts'; - -/** The complete-envelope send capability used by platform command adapters. */ -export interface OutboundEnvelopeSendPort { - send(envelope: OutboundEnvelope): void; -} - -export interface PlatformCommandsOptions { - readonly runtimeConfig: RuntimeConfig; - readonly store: PlatformStore; - readonly send: OutboundEnvelopeSendPort; -} - -export interface JoinRoomCommand extends JoinRoomEnvironment { - readonly roomcode: string; -} - -export class UnsupportedGameHostCommandError extends Error { - constructor(message: string) { - super(message); - this.name = 'UnsupportedGameHostCommandError'; - } -} - -/** Builds platform-owned room requests exclusively through the first-slice contracts. */ -export class PlatformCommands { - constructor(private readonly options: PlatformCommandsOptions) {} - - getVipRooms(shortcodeList:readonly (string|number)[]):void { - this.taskPlayerId(); - const {agentid,gameid}=this.options.runtimeConfig.identity; - this.options.send.send(buildFirstSliceEnvelope('getInfoByShortCode',{agentid,gameid,shortcodeList:[...shortcodeList]})); - } - - joinVipRoom(roomcode:string|number,location:unknown):void { - const playerid=this.taskPlayerId(),state=this.options.store.getState(); - const ip=state.players.entities[playerid].ip; - if(typeof ip!=='string')throw Error('VIP join requires player login ip'); - const {agentid,gameid}=this.options.runtimeConfig.identity; - this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:0,location,ip})); - } - - getPlayerTasks(): void { - const playerid=this.taskPlayerId(); - const {agentid,gameid}=this.options.runtimeConfig.identity; - this.options.send.send(buildFirstSliceEnvelope('get_player_task',{agentid,playerid,gameid})); - } - - claimTask(taskid:number|string):void { - const playerid=this.taskPlayerId(); - const task=this.options.store.getState().tasks?.find(task=>String(task.taskid)===String(taskid)); - if(!task||task.state!==1)throw Error('Task is not claimable'); - // 11_GameUI claim click sends these three fields; Net._SendData adds none. - this.options.send.send(buildFirstSliceEnvelope('get_task_award',{ - agentid:this.options.runtimeConfig.identity.agentid,playerid,taskid:task.taskid, - })); - } - - private taskPlayerId():number { - const state=this.options.store.getState(); - if(state.app.phase!=='logged-in'||state.room.kind!=='outside')throw Error('Task command requires logged-in lobby'); - return this.requireAuthenticatedPlayerId(state); - } - - prepare(): void { - const state = this.options.store.getState(); - this.options.send.send(buildPrepareRequest( - this.options.runtimeConfig, - this.roomIdentity(state, this.requireInsideRoom(state)), - )); - } - - exitRoom(): void { - const state = this.options.store.getState(); - const room = this.requireInsideRoom(state); - if (room.stage !== 0 && room.infinite !== 1) { - throw new UnsupportedGameHostCommandError('room.exit requires stage=0 unless infinite=1'); - } - this.options.send.send(buildExitRoomRequest( - this.options.runtimeConfig, - this.roomIdentity(state, room), - )); - } - - joinRoom(command: JoinRoomCommand): void { - const state = this.options.store.getState(); - if (state.room.kind !== 'outside') { - throw new UnsupportedGameHostCommandError('room.join requires player to be outside a room'); - } - const playerid = this.requireAuthenticatedPlayerId(state); - this.options.send.send(buildJoinRoomRequest( - this.options.runtimeConfig, - { playerid, roomcode: command.roomcode }, - command, - )); - } - - private requireInsideRoom(state: PlatformState): InsideRoomState { - if (state.room.kind !== 'inside') { - throw new UnsupportedGameHostCommandError('room command requires player to be inside a room'); - } - return state.room; - } - - private requireAuthenticatedPlayerId(state: PlatformState): number { - const playerid = state.players.selfPlayerId; - if (playerid === null) { - throw new UnsupportedGameHostCommandError('room command requires an authenticated player'); - } - if (state.players.entities[playerid] === undefined) { - throw new Error('players.entities: missing authenticated player'); - } - return playerid; - } - - private roomIdentity(state: PlatformState, room: InsideRoomState): RoomIdentity { - return { - playerid: this.requireAuthenticatedPlayerId(state), - roomcode: room.roomcode, - }; - } -} +import type { RuntimeConfig } from '../config/runtime-config.ts'; +import { buildFirstSliceEnvelope } from '../protocol/first-slice-routes.ts'; +import type { OutboundEnvelope } from '../core/types/envelope.ts'; +import { + buildCreateRoomRequest, + type CreateRoomEnvironment, + buildExitRoomRequest, + buildJoinRoomRequest, + buildPrepareRequest, + type JoinRoomEnvironment, + type RoomIdentity, +} from '../protocol/contracts/index.ts'; +import { cloneAndFreezeRoomtype, PlatformStore } from './stores/platform-store.ts'; +import type { InsideRoomState, PlatformState } from './stores/platform-types.ts'; + +/** The complete-envelope send capability used by platform command adapters. */ +export interface OutboundEnvelopeSendPort { + send(envelope: OutboundEnvelope): void; +} + +export interface PlatformCommandsOptions { + readonly runtimeConfig: RuntimeConfig; + readonly store: PlatformStore; + readonly send: OutboundEnvelopeSendPort; +} + +export interface JoinRoomCommand extends JoinRoomEnvironment { + readonly roomcode: string | number; +} + +export interface CreateRoomCommand extends CreateRoomEnvironment {} + +export class UnsupportedGameHostCommandError extends Error { + constructor(message: string) { + super(message); + this.name = 'UnsupportedGameHostCommandError'; + } +} + +/** Builds platform-owned room requests exclusively through the first-slice contracts. */ +export class PlatformCommands { + constructor(private readonly options: PlatformCommandsOptions) {} + + getPlayerInvitecode():void { + const playerid=this.taskPlayerId(),player=this.options.store.getState().players.entities[playerid]; + const {openid,unionid}=player; + if(typeof openid!=='string'||!(typeof unionid==='string'||typeof unionid==='number'&&Number.isFinite(unionid)))throw Error('Invite query requires login openid and unionid'); + this.options.send.send(buildFirstSliceEnvelope('get_player_invitecode',{ + agentid:this.options.runtimeConfig.identity.agentid,playerid,unionid,openid, + })); + } + + bindInvitecode(invitecode:string):void { + const playerid=this.taskPlayerId(); + if(typeof invitecode!=='string')throw Error('invitecode must be a string'); + this.options.send.send(buildFirstSliceEnvelope('binding_invitecode',{ + agentid:this.options.runtimeConfig.identity.agentid,playerid,invitecode, + })); + } + + getVipRooms(shortcodeList:readonly (string|number)[]):void { + this.taskPlayerId(); + const {agentid,gameid}=this.options.runtimeConfig.identity; + this.options.send.send(buildFirstSliceEnvelope('getInfoByShortCode',{agentid,gameid,shortcodeList:[...shortcodeList]})); + } + + joinVipRoom(roomcode:string|number,location:unknown):void { + const playerid=this.taskPlayerId(),state=this.options.store.getState(); + const ip=state.players.entities[playerid].ip; + if(typeof ip!=='string')throw Error('VIP join requires player login ip'); + const {agentid,gameid}=this.options.runtimeConfig.identity; + this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:0,location,ip})); + } + + getPlayerTasks(): void { + const playerid=this.taskPlayerId(); + const {agentid,gameid}=this.options.runtimeConfig.identity; + this.options.send.send(buildFirstSliceEnvelope('get_player_task',{agentid,playerid,gameid})); + } + + getVipRankList(limit:number):void { + this.taskPlayerId();if(!Number.isInteger(limit)||limit<=0)throw Error('Invalid vipRankLimit'); + this.options.send.send(buildFirstSliceEnvelope('getVipRankList',{agentid:this.options.runtimeConfig.identity.agentid,limit})); + } + joinOwnedRoom(roomcode:number,location:unknown):void { + const playerid=this.taskPlayerId(),state=this.options.store.getState(); + if(!Number.isSafeInteger(roomcode)||roomcode<=0)throw Error('Invalid owned roomcode'); + const ip=state.players.entities[playerid].ip;if(typeof ip!=='string')throw Error('Owned room join requires player login ip'); + const {agentid,gameid}=this.options.runtimeConfig.identity; + this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:1,location,ip})); + } + getTreasureList():void { + this.taskPlayerId();const {agentid,gameid}=this.options.runtimeConfig.identity; + this.options.send.send(buildFirstSliceEnvelope('get_treasurelist',{agentid,gameid})); + } + redeemCard(cardno:string):void { + const playerid=this.taskPlayerId(); + if(typeof cardno!=='string'||cardno===''||/^[ ]+$/.test(cardno))throw Error('Invalid cardno'); + this.options.send.send(buildFirstSliceEnvelope('topup_card',{agentid:this.options.runtimeConfig.identity.agentid,playerid,cardno})); + } + + claimTask(taskid:number|string):void { + const playerid=this.taskPlayerId(); + const task=this.options.store.getState().tasks?.find(task=>String(task.taskid)===String(taskid)); + if(!task||task.state!==1)throw Error('Task is not claimable'); + // 11_GameUI claim click sends these three fields; Net._SendData adds none. + this.options.send.send(buildFirstSliceEnvelope('get_task_award',{ + agentid:this.options.runtimeConfig.identity.agentid,playerid,taskid:task.taskid, + })); + } + + private taskPlayerId():number { + const state=this.options.store.getState(); + if(state.app.phase!=='logged-in'||state.room.kind!=='outside')throw Error('Task command requires logged-in lobby'); + return this.requireAuthenticatedPlayerId(state); + } + + prepare(): void { + const state = this.options.store.getState(); + this.options.send.send(buildPrepareRequest( + this.options.runtimeConfig, + this.roomIdentity(state, this.requireInsideRoom(state)), + )); + } + + exitRoom(): void { + const state = this.options.store.getState(); + const room = this.requireInsideRoom(state); + if (room.stage !== 0 && room.infinite !== 1) { + throw new UnsupportedGameHostCommandError('room.exit requires stage=0 unless infinite=1'); + } + this.options.send.send(buildExitRoomRequest( + this.options.runtimeConfig, + this.roomIdentity(state, room), + )); + } + + createRoom(command: CreateRoomCommand): void { + const playerid = this.taskPlayerId(); + const roomtype = cloneAndFreezeRoomtype(command.roomtype); + this.options.send.send(buildCreateRoomRequest(this.options.runtimeConfig, playerid, {...command, roomtype})); + } + + joinRoom(command: JoinRoomCommand): void { + const state = this.options.store.getState(); + if (state.room.kind !== 'outside') { + throw new UnsupportedGameHostCommandError('room.join requires player to be outside a room'); + } + const playerid = this.requireAuthenticatedPlayerId(state); + this.options.send.send(buildJoinRoomRequest( + this.options.runtimeConfig, + { playerid, roomcode: command.roomcode }, + command, + )); + } + + private requireInsideRoom(state: PlatformState): InsideRoomState { + if (state.room.kind !== 'inside') { + throw new UnsupportedGameHostCommandError('room command requires player to be inside a room'); + } + return state.room; + } + + private requireAuthenticatedPlayerId(state: PlatformState): number { + const playerid = state.players.selfPlayerId; + if (playerid === null) { + throw new UnsupportedGameHostCommandError('room command requires an authenticated player'); + } + if (state.players.entities[playerid] === undefined) { + throw new Error('players.entities: missing authenticated player'); + } + return playerid; + } + + private roomIdentity(state: PlatformState, room: InsideRoomState): RoomIdentity { + return { + playerid: this.requireAuthenticatedPlayerId(state), + roomcode: room.roomcode, + }; + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime-session.ts b/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime-session.ts index 7d03a51..1912ece 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime-session.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime-session.ts @@ -1,243 +1,276 @@ -import type { GameHost } from '../sdk/contracts/index.ts'; -import { - extractGameSessionOpenFaultErrors, - GameSessionHost, -} from '../sdk/runtime/game-session-host.ts'; -import { - parseLoginResponse, - parseSelfJoinRoomResponse, - type ParsedSelfExitPayload, - type ReconnectSnapshot, -} from '../protocol/contracts/index.ts'; -import { PlatformStore } from './stores/platform-store.ts'; -import type { PlatformState } from './stores/platform-types.ts'; - -export interface GameHostLeasePort { - readonly host: GameHost; - invalidate(): void; -} - -export type GameHostLeaseFactory = () => GameHostLeasePort; - -/** A valid nonzero server outcome, delivered without changing canonical state. */ -export interface ServerDenial { - readonly rpc: 'player_login' | 'self_join_room'; - readonly state: number; - readonly data: Readonly>; -} - -export interface RuntimeSessionOptions { - readonly store: PlatformStore; - readonly gameSession: GameSessionHost; - readonly createGameHostLease: GameHostLeaseFactory; - readonly onServerDenial: (outcome: ServerDenial) => void; -} - -export type RuntimeSessionState = 'operational' | 'faulted' | 'kicked'; - -export class RuntimeSessionFault extends Error { - readonly errors: readonly unknown[]; - - constructor(operation: string, errors: readonly unknown[]) { - super(`RuntimeSession fault during ${operation}`); - this.name = 'RuntimeSessionFault'; - this.errors = Object.freeze([...errors]); - } -} - -/** Coordinates the canonical platform snapshot with the one active game session. */ -export class RuntimeSession { - private loginIsPending = false; - private wasKicked = false; - private terminalFault: RuntimeSessionFault | null = null; - private activeLease: GameHostLeasePort | null = null; - - constructor(private readonly options: RuntimeSessionOptions) {} - - get loginPending(): boolean { return this.loginIsPending; } - - get kicked(): boolean { return this.wasKicked; } - - get state(): RuntimeSessionState { - if (this.wasKicked) return 'kicked'; - return this.terminalFault !== null ? 'faulted' : 'operational'; - } - - get fault(): Error | null { return this.terminalFault; } - - markLoginPending(): void { - this.requireOperational(); - this.loginIsPending = true; - } - - handleLogin(data: unknown): void { - this.requireOperational(); - this.loginIsPending = false; - const parsed = parseLoginResponse(data); - if (parsed.state !== 0) { - this.options.onServerDenial({ rpc: 'player_login', state: parsed.state, data: parsed.raw }); - return; - } - const committed = this.options.store.applyLoginSuccess(parsed); - if (parsed.room === null) { - this.releaseOrFault('login lobby replacement'); - return; - } - this.openCommittedRoom(committed, parsed.reconnect, 'login room open'); - } - - handleSelfJoin(data: unknown): void { - this.requireOperational(); - const parsed = parseSelfJoinRoomResponse(data); - if (parsed.state !== 0) { - this.options.onServerDenial({ rpc: 'self_join_room', state: parsed.state, data: parsed.raw }); - return; - } - const committed = this.options.store.replaceRoom(parsed); - this.openCommittedRoom(committed, parsed.reconnect, 'self join room open'); - } - - handleSelfExit(parsed: ParsedSelfExitPayload): void { - this.requireOperational(); - const state = this.options.store.getState(); - if (state.room.kind !== 'inside') { - if (this.options.gameSession.state === 'idle' && this.activeLease === null) return; - const errors = [ - new Error('self_exit_room found an active game without room state'), - ...this.releaseGame(), - ]; - this.throwFault('self exit inconsistent state', errors); - } - if (parsed.seat !== undefined && parsed.seat !== state.room.selfSeat) { - throw new RangeError( - `self_exit_room seat mismatch: expected ${state.room.selfSeat}, received ${parsed.seat}`, - ); - } - if (this.options.gameSession.state !== 'active' || this.activeLease === null) { - const errors = [ - new Error('self_exit_room requires an active game session and host lease'), - ...this.releaseGame(), - ]; - this.throwFault('self exit inconsistent state', errors); - } - - try { - this.options.store.clearRoom(); - } catch (error) { - this.throwFault('self exit state commit', [error, ...this.releaseGame()]); - } - this.releaseOrFault('self exit cleanup'); - } - - handleServerSwitch(switchConnection: () => void): void { - this.requireOperational(); - const errors = this.releaseGame(); - try { - switchConnection(); - } catch (error) { - errors.push(error); - } - if (errors.length > 0) this.throwFault('server switch', errors); - } - - handleKick(_data: unknown, stopConnection: () => void): void { - if (this.wasKicked) return; - this.requireOperational(); - this.loginIsPending = false; - this.wasKicked = true; - - const errors = this.releaseGame(); - try { - stopConnection(); - } catch (error) { - errors.push(error); - } - if (errors.length > 0) this.throwFault('kick terminal cleanup', errors); - } - - private openCommittedRoom( - state: PlatformState, - reconnect: ReconnectSnapshot, - operation: string, - ): void { - if (state.room.kind !== 'inside') { - this.throwFault(operation, [new Error('Cannot open a game session without committed room state')]); - } - - const priorCleanupErrors = this.releaseGame(); - if (priorCleanupErrors.length > 0) this.throwFault(operation, priorCleanupErrors); - if (this.gameWasDisposed()) return; - - let lease: GameHostLeasePort; - try { - lease = this.options.createGameHostLease(); - if (!lease || typeof lease !== 'object' || typeof lease.invalidate !== 'function') { - throw new TypeError('createGameHostLease must return { host, invalidate }'); - } - } catch (error) { - this.throwFault(operation, [error]); - } - this.activeLease = lease; - - try { - this.options.gameSession.open(lease.host); - if (this.gameWasDisposed()) return; - this.options.gameSession.publish({ - type: 'room.entered', - roomtype: state.room.roomtype, - }); - if (this.gameWasDisposed()) return; - if (reconnect.present) this.options.gameSession.restore(reconnect.value); - } catch (error) { - const openErrors = extractGameSessionOpenFaultErrors(error); - const errors = openErrors === null - ? [error] - : [openErrors[0], openErrors[1]]; - errors.push(...this.releaseGame()); - this.throwFault(operation, errors); - } - } - - private releaseOrFault(operation: string): void { - const errors = this.releaseGame(); - if (errors.length > 0) this.throwFault(operation, errors); - } - - private gameWasDisposed(): boolean { - return this.options.gameSession.state === 'disposed'; - } - - private releaseGame(): unknown[] { - const errors: unknown[] = []; - const lease = this.activeLease; - this.activeLease = null; - - if (this.options.gameSession.state !== 'idle' && !this.gameWasDisposed()) { - try { - this.options.gameSession.close(); - } catch (error) { - errors.push(error); - } - } - if (lease !== null) { - try { - lease.invalidate(); - } catch (error) { - errors.push(error); - } - } - return errors; - } - - private throwFault(operation: string, errors: readonly unknown[]): never { - const fault = new RuntimeSessionFault(operation, errors); - this.loginIsPending = false; - this.terminalFault = fault; - throw fault; - } - - private requireOperational(): void { - if (this.terminalFault !== null) throw this.terminalFault; - if (this.wasKicked) throw new Error('RuntimeSession is kicked'); - } -} +import type { GameHost } from '../sdk/contracts/index.ts'; +import { + extractGameSessionOpenFaultErrors, + GameSessionHost, +} from '../sdk/runtime/game-session-host.ts'; +import { + parseCreateRoomResponse, + parseLoginResponse, + parseSelfJoinRoomResponse, + type ParsedSelfExitPayload, + type ReconnectSnapshot, +} from '../protocol/contracts/index.ts'; +import { cloneAndFreezeRoomtype, PlatformStore } from './stores/platform-store.ts'; +import type { PlatformState } from './stores/platform-types.ts'; +import type {LoginAccountIdentity} from '../protocol/contracts/login-contract.ts'; + +export interface GameHostLeasePort { + readonly host: GameHost; + invalidate(): void; +} + +export type GameHostLeaseFactory = () => GameHostLeasePort; + +/** A valid nonzero server outcome, delivered without changing canonical state. */ +export interface ServerDenial { + readonly rpc: 'player_login' | 'self_join_room' | 'create_room'; + readonly state: number; + readonly data: Readonly>; +} + +export interface RuntimeSessionOptions { + readonly roomHistory?: import('../adapters/native/owned-room-history.ts').OwnedRoomHistory; + readonly store: PlatformStore; + readonly gameSession: GameSessionHost; + readonly createGameHostLease: GameHostLeaseFactory; + readonly onServerDenial: (outcome: ServerDenial) => void; +} + +export type RuntimeSessionState = 'operational' | 'faulted' | 'kicked'; + +export class RuntimeSessionFault extends Error { + readonly errors: readonly unknown[]; + + constructor(operation: string, errors: readonly unknown[]) { + super(`RuntimeSession fault during ${operation}`); + this.name = 'RuntimeSessionFault'; + this.errors = Object.freeze([...errors]); + } +} + +/** Coordinates the canonical platform snapshot with the one active game session. */ +export class RuntimeSession { + private loginIsPending = false; + private lastJoinedRoomcode: string | null = null; + /** Non-device login owns identity in authorization; device login owns it in the response. */ + private authorizedIdentity:Pick|undefined; + private wasKicked = false; + private terminalFault: RuntimeSessionFault | null = null; + private activeLease: GameHostLeasePort | null = null; + + constructor(private readonly options: RuntimeSessionOptions) {} + + get loginPending(): boolean { return this.loginIsPending; } + + get kicked(): boolean { return this.wasKicked; } + + get state(): RuntimeSessionState { + if (this.wasKicked) return 'kicked'; + return this.terminalFault !== null ? 'faulted' : 'operational'; + } + + get fault(): Error | null { return this.terminalFault; } + + markLoginPending(identity?:Pick|null): void { + this.requireOperational(); + // No argument is a reconnect of the current account; null selects device response identity. + if(identity!==undefined)this.authorizedIdentity=identity===null?undefined:identity; + this.loginIsPending = true; + } + + handleLogin(data: unknown): void { + this.requireOperational(); + this.loginIsPending = false; + const parsed = parseLoginResponse(data); + if (parsed.state !== 0) { + this.options.onServerDenial({ rpc: 'player_login', state: parsed.state, data: parsed.raw }); + return; + } + this.lastJoinedRoomcode = null; + const committed = this.options.store.applyLoginSuccess(parsed,this.authorizedIdentity); + if (parsed.room === null) { + this.releaseOrFault('login lobby replacement'); + return; + } + this.openCommittedRoom(committed, parsed.reconnect, 'login room open'); + } + + handleCreateRoom(data: unknown): void { + this.requireOperational(); + const parsed = parseCreateRoomResponse(data); + if (parsed.state !== 0) { + this.options.onServerDenial({rpc: 'create_room', state: parsed.state, data: parsed.raw}); + return; + } + if (parsed.room === null) throw new Error('create_room missing parsed room'); + const current = this.options.store.getState().room; + if (current.kind === 'inside' && current.roomcode === parsed.room.roomcode + && this.options.gameSession.state === 'active') return; + // Subgame code receives an owned immutable value, never the server response graph. + const roomtype = cloneAndFreezeRoomtype(parsed.room.roomtype); + const count = this.options.gameSession.entry.resolveSeatCount(roomtype); + const committed = this.options.store.createRoom(parsed, count); + this.openCommittedRoom(committed, {present: false}, 'create room open'); + } + + handleSelfJoin(data: unknown): void { + this.requireOperational(); + const parsed = parseSelfJoinRoomResponse(data); + this.options.roomHistory?.joined(parsed.raw); + if (parsed.state !== 0) { + this.options.onServerDenial({ rpc: 'self_join_room', state: parsed.state, data: parsed.raw }); + return; + } + if (parsed.room !== null && this.lastJoinedRoomcode === parsed.room.roomcode + && this.options.gameSession.state === 'active') return; + const committed = this.options.store.replaceRoom(parsed); + this.openCommittedRoom(committed, parsed.reconnect, 'self join room open'); + this.lastJoinedRoomcode = parsed.room!.roomcode; + } + + handleSelfExit(parsed: ParsedSelfExitPayload): void { + this.requireOperational(); + const state = this.options.store.getState(); + if (state.room.kind !== 'inside') { + if (this.options.gameSession.state === 'idle' && this.activeLease === null) return; + const errors = [ + new Error('self_exit_room found an active game without room state'), + ...this.releaseGame(), + ]; + this.throwFault('self exit inconsistent state', errors); + } + if (parsed.seat !== undefined && parsed.seat !== state.room.selfSeat) { + throw new RangeError( + `self_exit_room seat mismatch: expected ${state.room.selfSeat}, received ${parsed.seat}`, + ); + } + if (this.options.gameSession.state !== 'active' || this.activeLease === null) { + const errors = [ + new Error('self_exit_room requires an active game session and host lease'), + ...this.releaseGame(), + ]; + this.throwFault('self exit inconsistent state', errors); + } + + try { + this.options.roomHistory?.exited(parsed.raw,state.room.infinite); + this.options.store.clearRoom(); + } catch (error) { + this.throwFault('self exit state commit', [error, ...this.releaseGame()]); + } + this.releaseOrFault('self exit cleanup'); + } + + handleServerSwitch(switchConnection: () => void): void { + this.requireOperational(); + const errors = this.releaseGame(); + try { + switchConnection(); + } catch (error) { + errors.push(error); + } + if (errors.length > 0) this.throwFault('server switch', errors); + } + + handleKick(_data: unknown, stopConnection: () => void): void { + if (this.wasKicked) return; + this.requireOperational(); + this.loginIsPending = false; + this.wasKicked = true; + + const errors = this.releaseGame(); + try { + stopConnection(); + } catch (error) { + errors.push(error); + } + if (errors.length > 0) this.throwFault('kick terminal cleanup', errors); + } + + private openCommittedRoom( + state: PlatformState, + reconnect: ReconnectSnapshot, + operation: string, + ): void { + if (state.room.kind !== 'inside') { + this.throwFault(operation, [new Error('Cannot open a game session without committed room state')]); + } + + const priorCleanupErrors = this.releaseGame(); + if (priorCleanupErrors.length > 0) this.throwFault(operation, priorCleanupErrors); + if (this.gameWasDisposed()) return; + + let lease: GameHostLeasePort; + try { + lease = this.options.createGameHostLease(); + if (!lease || typeof lease !== 'object' || typeof lease.invalidate !== 'function') { + throw new TypeError('createGameHostLease must return { host, invalidate }'); + } + } catch (error) { + this.throwFault(operation, [error]); + } + this.activeLease = lease; + + try { + this.options.gameSession.open(lease.host); + if (this.gameWasDisposed()) return; + this.options.gameSession.publish({ + type: 'room.entered', + roomtype: state.room.roomtype, + }); + if (this.gameWasDisposed()) return; + if (reconnect.present) this.options.gameSession.restore(reconnect.value); + } catch (error) { + const openErrors = extractGameSessionOpenFaultErrors(error); + const errors = openErrors === null + ? [error] + : [openErrors[0], openErrors[1]]; + errors.push(...this.releaseGame()); + this.throwFault(operation, errors); + } + } + + private releaseOrFault(operation: string): void { + const errors = this.releaseGame(); + if (errors.length > 0) this.throwFault(operation, errors); + } + + private gameWasDisposed(): boolean { + return this.options.gameSession.state === 'disposed'; + } + + private releaseGame(): unknown[] { + this.lastJoinedRoomcode = null; + const errors: unknown[] = []; + const lease = this.activeLease; + this.activeLease = null; + + if (this.options.gameSession.state !== 'idle' && !this.gameWasDisposed()) { + try { + this.options.gameSession.close(); + } catch (error) { + errors.push(error); + } + } + if (lease !== null) { + try { + lease.invalidate(); + } catch (error) { + errors.push(error); + } + } + return errors; + } + + private throwFault(operation: string, errors: readonly unknown[]): never { + const fault = new RuntimeSessionFault(operation, errors); + this.loginIsPending = false; + this.terminalFault = fault; + throw fault; + } + + private requireOperational(): void { + if (this.terminalFault !== null) throw this.terminalFault; + if (this.wasKicked) throw new Error('RuntimeSession is kicked'); + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts b/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts index ae93026..5e8af18 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts @@ -1,757 +1,770 @@ -import type { RuntimeConfig } from '../config/runtime-config.ts'; -import { LOGIN_GUARD_MS } from '../core/constants.ts'; -import { STARTUP_CONFIG } from '../config/profiles.ts'; -import type { OutboundEnvelope } from '../core/types/envelope.ts'; -import { realClock, type Clock } from '../net/heartbeat.ts'; -import type { WireClient, WireEvent } from '../net/wire-client.ts'; -import { - buildLoginRequest, - type LoginAccountIdentity, - type LoginDeviceSnapshot, -} from '../protocol/contracts/index.ts'; -import { PlatformHandlers } from '../protocol/platform-handlers.ts'; -import { Router } from '../protocol/router.ts'; -import { isLoginResponse } from '../protocol/first-slice-routes.ts'; -import type { GameEntry, GameHost } from '../sdk/contracts/index.ts'; -import { GameSessionHost } from '../sdk/runtime/game-session-host.ts'; -import { PlatformCommands, type JoinRoomCommand } from './commands.ts'; -import type { ConnectionIntent } from './connection-intent.ts'; -import { - createGameHostAdapter, - type GameHostLease, -} from './game-host-adapter.ts'; -import { RuntimeSession } from './runtime-session.ts'; -import type { ScenePort } from './scene-port.ts'; -import { PlatformStore } from './stores/platform-store.ts'; -import type { PlatformState } from './stores/platform-types.ts'; -import { ReadyGate, type ReadyGateName } from './ready-gate.ts'; - -export type PlatformWireClient = Pick< - WireClient, - 'subscribe' | 'start' | 'send' | 'switchServer' | 'reconnectCurrent' | 'stop' ->; - -export interface PlatformRuntimeOptions { - readonly gameEntry: GameEntry; - readonly resolveRuntimeConfig: () => Promise; - readonly createWireClient: (config: RuntimeConfig) => PlatformWireClient; - readonly scene: ScenePort; - readonly loadResources: () => Promise; - readonly waitForMinimumDisplay: () => Promise; - readonly getLoginDeviceSnapshot: () => LoginDeviceSnapshot; - readonly clock?: Clock; -} - -type RuntimeLifecycle = 'idle' | 'starting' | 'running' | 'kicked' | 'fatal' | 'stopped'; - -/** Keeps the scene transition between room activation and optional snapshot restore. */ -class SceneOrderedGameSessionHost extends GameSessionHost { - private roomSceneShown = false; - - constructor(entry: GameEntry, private readonly scene: ScenePort) { - super(entry); - } - - open(host: GameHost): void { - this.roomSceneShown = false; - super.open(host); - } - - restore(deskinfo: unknown): void { - this.showRoomAfterOpen(); - if (this.state === 'disposed') return; - super.restore(deskinfo); - } - - showRoomAfterOpen(): void { - if (this.roomSceneShown) return; - if (this.state !== 'active') { - throw new Error('Cannot show the room without an active game session'); - } - this.roomSceneShown = true; - this.scene.showRoom(); - } - - close(): void { - try { - super.close(); - } finally { - this.roomSceneShown = false; - } - } - - dispose(): void { - try { - super.dispose(); - } finally { - this.roomSceneShown = false; - } - } -} - -class PlatformRuntimeCleanupError extends Error { - readonly errors: readonly unknown[]; - - constructor(operation: string, errors: readonly unknown[]) { - super(`PlatformRuntime cleanup failed during ${operation}`); - this.name = 'PlatformRuntimeCleanupError'; - this.errors = Object.freeze([...errors]); - } -} - -/** Owns the sole headless config, wire, router, store, and game-session composition. */ -export class PlatformRuntime { - private readonly store = new PlatformStore(); - private readonly gate = new ReadyGate(); - private readonly clock: Clock; - private lifecycle: RuntimeLifecycle = 'idle'; - private config: RuntimeConfig | null = null; - private wire: PlatformWireClient | null = null; - private unsubscribeWire: (() => void) | null = null; - private session: RuntimeSession | null = null; - private router: Router | null = null; - private gameSession: SceneOrderedGameSessionHost | null = null; - private commands: PlatformCommands | null = null; - private activeGameHostLease: GameHostLease | null = null; - private connectionIntent: ConnectionIntent = { type: 'none' }; - private loginEnvelope: OutboundEnvelope | null = null; - private loginGuard: unknown = null; - private wireIsOpen = false; - private loginSceneShown = false; - private fatalError: Error | null = null; - private wireStopped = false; - private startupGeneration = 0; - private readonly stateObservers = new Set<() => void>(); - private observerDispatchError: Error | null = null; - - constructor(private readonly options: PlatformRuntimeOptions) { - this.clock = options.clock ?? realClock; - } - - get state(): PlatformState { - return this.store.getState(); - } - - get ready(): boolean { - return this.gate.ready; - } - - subscribeState( - listener: (state: PlatformState) => void, - onError: (error: unknown) => void, - ): () => void { - if ( - this.lifecycle === 'kicked' - || this.lifecycle === 'fatal' - || this.lifecycle === 'stopped' - ) { - throw new Error(`PlatformRuntime cannot subscribe while ${this.lifecycle}`); - } - - let active = true; - const unsubscribeStore = this.store.subscribe((state) => { - if (!active) return; - try { - listener(state); - } catch (error) { - off(); - try { - onError(error); - } catch (callbackError) { - const failure = this.failureError( - 'UI observer', - error, - [callbackError], - ); - if (this.isStopped()) { - this.observerDispatchError = failure; - } else { - this.showFatalOnce(failure); - } - } - } - }); - const off = (): void => { - if (!active) return; - active = false; - try { - unsubscribeStore(); - } finally { - this.stateObservers.delete(off); - } - }; - this.stateObservers.add(off); - return off; - } - - async start(): Promise { - if (this.lifecycle !== 'idle') { - throw new Error(`PlatformRuntime cannot start while ${this.lifecycle}`); - } - const generation = this.startupGeneration + 1; - this.startupGeneration = generation; - this.lifecycle = 'starting'; - - try { - this.options.scene.showLoading(); - this.requireActiveStartup(generation); - const resourcesReady = this.beginReadyTask( - 'resources', - this.options.loadResources, - generation, - ); - const minimumDisplayReady = this.beginReadyTask( - 'minimum-display', - this.options.waitForMinimumDisplay, - generation, - ); - this.requireActiveStartup(generation); - const config = await this.options.resolveRuntimeConfig(); - this.requireActiveStartup(generation); - if (config.identity.gameid !== this.options.gameEntry.gameId) { - throw new Error( - 'RuntimeConfig identity.gameid must strictly equal GameEntry.gameId: ' - + `config=${String(config.identity.gameid)} (${typeof config.identity.gameid}), ` - + `entry=${String(this.options.gameEntry.gameId)} (${typeof this.options.gameEntry.gameId})`, - ); - } - this.config = config; - - this.requireActiveStartup(generation); - const gameSession = new SceneOrderedGameSessionHost( - this.options.gameEntry, - this.options.scene, - ); - const send = Object.freeze({ - send: (envelope: OutboundEnvelope): void => { - this.requireOpenWire().send(envelope); - }, - }); - const commands = new PlatformCommands({ - runtimeConfig: config, - store: this.store, - send, - }); - const session = new RuntimeSession({ - store: this.store, - gameSession, - createGameHostLease: () => this.createGameHostLease(commands, send), - onServerDenial: outcome => { - // Legacy onopen retries only an authenticated session. A rejected first login - // must wait for another user submission, including after a socket reconnect. - if(outcome.rpc==='player_login'&&this.store.getState().players.selfPlayerId===null){ - this.connectionIntent={type:'none'};this.loginEnvelope=null; - } - this.options.scene.showServerDenial(outcome); - }, - }); - const handlers = new PlatformHandlers({ - store: this.store, - gameSession, - session, - connection: { - switchServer: (target, resend) => { this.beginServerSwitch(target, resend); }, - stopForKick: (data) => { this.stopForKick(data); }, - }, - showMessage:(message,time,source)=>{ - if(!this.options.scene.showTips)throw Error('show_message requires ScenePort.showTips'); - this.options.scene.showTips(message,time,source); - }, - }); - const router = new Router({ session, platform: handlers, gameSession }); - this.requireActiveStartup(generation); - const wire = this.options.createWireClient(config); - - this.requireActiveStartup(generation); - this.gameSession = gameSession; - this.commands = commands; - this.session = session; - this.router = router; - this.wire = wire; - this.requireActiveStartup(generation); - this.subscribeToWire(wire, generation); - this.requireActiveStartup(generation); - this.markReady('config'); - this.lifecycle = 'running'; - this.requireActiveStartup(generation); - wire.start(); - await Promise.all([resourcesReady, minimumDisplayReady]); - this.requireActiveStartup(generation); - } catch (error) { - if (!this.isActiveStartup(generation)) { - throw this.fatalError ?? error; - } - const fatal = this.showFatalOnce(error); - throw fatal; - } - } - - login(account: LoginAccountIdentity): void { - const config = this.requireConfig(); - const wire = this.requireOpenWire(); - const session = this.requireSession(); - const envelope = buildLoginRequest( - config, - account, - this.options.getLoginDeviceSnapshot(), - ); - - wire.send(envelope); - this.loginEnvelope = envelope; - this.connectionIntent = { type: 'login', envelope }; - session.markLoginPending(); - this.startLoginGuard(); - } - - joinRoom(command: JoinRoomCommand): void { - this.requireCommands().joinRoom(command); - } - - prepare(): void { - this.requireCommands().prepare(); - } - - getVipRooms(codes:readonly (string|number)[]):void {this.requireCommands().getVipRooms(codes);} - joinVipRoom(code:string|number,location:unknown):void {this.requireCommands().joinVipRoom(code,location);} - getPlayerTasks():void {this.requireCommands().getPlayerTasks();} - claimTask(taskid:number|string):void {this.requireCommands().claimTask(taskid);} - - exitRoom(): void { - this.requireCommands().exitRoom(); - } - - stop(): void { - if (this.lifecycle === 'stopped') return; - this.startupGeneration += 1; - this.lifecycle = 'stopped'; - this.connectionIntent = { type: 'none' }; - this.wireIsOpen = false; - - const errors: unknown[] = []; - for (const off of [...this.stateObservers]) { - this.captureCleanupError(errors, off); - } - this.captureCleanupError(errors, () => { this.clearLoginGuard(); }); - const unsubscribe = this.unsubscribeWire; - this.unsubscribeWire = null; - if (unsubscribe !== null) this.captureCleanupError(errors, unsubscribe); - - const gameSession = this.gameSession; - if (gameSession !== null) { - this.captureCleanupError(errors, () => { gameSession.dispose(); }); - } - - const lease = this.activeGameHostLease; - this.activeGameHostLease = null; - if (lease !== null) { - this.captureCleanupError(errors, () => { lease.invalidate(); }); - } - - this.captureCleanupError(errors, () => { this.stopWireOnce(); }); - this.throwCleanupErrors('stop', errors); - } - - private beginReadyTask( - name: ReadyGateName, - task: () => Promise, - generation: number, - ): Promise { - return Promise.resolve() - .then(() => { - this.requireActiveStartup(generation); - return task(); - }) - .then( - () => { - if (this.isActiveStartup(generation)) this.markReady(name); - }, - (error: unknown) => { - if (this.isActiveStartup(generation)) this.showFatalOnce(error); - }, - ); - } - - private markReady(name: ReadyGateName): void { - if ( - this.lifecycle === 'fatal' - || this.lifecycle === 'kicked' - || this.lifecycle === 'stopped' - ) return; - this.gate.mark(name); - if (this.gate.ready && !this.loginSceneShown) { - this.loginSceneShown = true; - this.options.scene.showLogin(); - } - } - - private onWireEvent(event: WireEvent): void { - if ( - this.lifecycle === 'fatal' - || this.lifecycle === 'kicked' - || this.lifecycle === 'stopped' - ) return; - try { - switch (event.type) { - case 'open': - this.wireIsOpen = true; - this.store.setConnectionPhase('connected'); - this.throwObserverDispatchError(); - this.markReady('socket'); - if (this.connectionIntent.type === 'switch') { - const loginEnvelope = this.loginEnvelope; - if (loginEnvelope === null) { - throw new Error('server switch requires a previously sent player_login envelope'); - } - this.requireOpenWire().send(this.connectionIntent.resend); - this.connectionIntent = { type: 'login', envelope: loginEnvelope }; - } else if (this.connectionIntent.type === 'login') { - this.sendLoginEnvelope(this.connectionIntent.envelope); - } - break; - case 'message': - if (isLoginResponse(event.message)) { - this.clearLoginGuard(); - } - const stateBeforeDispatch = this.store.getState(); - const dispatchResult = this.requireRouter().dispatch(event.message); - this.throwObserverDispatchError(); - if ( - dispatchResult === 'handled-platform' - && this.lifecycle === 'running' - && this.store.getState() !== stateBeforeDispatch - ) { - this.updateSceneAfterMessage(event.message.route, event.message.rpc); - } - break; - case 'slow': - case 'reconnecting': - this.wireIsOpen = false; - this.store.setConnectionPhase(event.type); - this.throwObserverDispatchError(); - if (this.isTerminal()) return; - this.options.scene.showReconnect(); - break; - case 'close': - this.wireIsOpen = false; - break; - } - } catch (error) { - const failure = this.withObserverDispatchError(error); - // A lifecycle callback may stop and then throw. Preserve that error without - // replacing the requested terminal stop with a fatal lifecycle transition. - if (this.isStopped()) throw failure; - this.showFatalOnce(failure); - } - } - - private isStopped(): boolean { - return this.lifecycle === 'stopped'; - } - - private isTerminal(): boolean { - return this.lifecycle === 'fatal' - || this.lifecycle === 'kicked' - || this.lifecycle === 'stopped'; - } - - private throwObserverDispatchError(): void { - const failure = this.takeObserverDispatchError(); - if (failure === null) return; - throw failure; - } - - private takeObserverDispatchError(): Error | null { - const failure = this.observerDispatchError; - this.observerDispatchError = null; - return failure; - } - - private withObserverDispatchError(error: unknown): unknown { - const observerFailure = this.takeObserverDispatchError(); - return observerFailure === null - ? error - : this.failureError('UI observer dispatch', observerFailure, [error]); - } - - private sendLoginEnvelope(envelope: OutboundEnvelope): void { - const wire = this.requireOpenWire(); - const session = this.requireSession(); - wire.send(envelope); - session.markLoginPending(); - this.startLoginGuard(); - } - - private beginServerSwitch(target: string, resend: OutboundEnvelope): void { - if (this.loginEnvelope === null) { - throw new Error('server switch requires a previously sent player_login envelope'); - } - const wire = this.requireWire(); - this.clearLoginGuard(); - this.connectionIntent = { type: 'switch', target, resend }; - this.wireIsOpen = false; - wire.switchServer(target); - } - - private createGameHostLease( - commands: PlatformCommands, - send: { send(envelope: OutboundEnvelope): void }, - ): GameHostLease { - const lease = createGameHostAdapter({ - gameEntry: this.options.gameEntry, - store: this.store, - commands, - send, - }); - let invalidated = false; - const trackedLease: GameHostLease = Object.freeze({ - host: lease.host, - invalidate: (): void => { - if (invalidated) return; - invalidated = true; - try { - lease.invalidate(); - } finally { - if (this.activeGameHostLease === trackedLease) { - this.activeGameHostLease = null; - } - } - }, - }); - this.activeGameHostLease = trackedLease; - return trackedLease; - } - - private updateSceneAfterMessage(route: string, rpc: string): void { - if (route !== 'platform' && route !== 'agent' && route !== 'room') return; - if (rpc === 'player_login') { - if (this.store.getState().room.kind === 'outside') { - this.options.scene.showLobby(); - } else { - this.requireGameSession().showRoomAfterOpen(); - } - return; - } - if (rpc === 'self_join_room') { - this.requireGameSession().showRoomAfterOpen(); - return; - } - if (rpc === 'self_exit_room') this.options.scene.showLobby(); - } - - private stopForKick(data: unknown): void { - this.lifecycle = 'kicked'; - this.connectionIntent = { type: 'none' }; - this.wireIsOpen = false; - const errors: unknown[] = []; - for (const off of [...this.stateObservers]) { - this.captureCleanupError(errors, off); - } - this.captureCleanupError(errors, () => { this.store.setConnectionPhase('kicked'); }); - this.captureCleanupError(errors, () => { this.clearLoginGuard(); }); - this.captureCleanupError(errors, () => { this.options.scene.showKicked(data); }); - this.captureCleanupError(errors, () => { this.stopWireOnce(); }); - this.throwCleanupErrors('kick', errors); - } - - private stopWireOnce(): void { - if (this.wireStopped) return; - const wire = this.wire; - if (wire === null) return; - this.wireStopped = true; - wire.stop(); - } - - private subscribeToWire(wire: PlatformWireClient, generation: number): void { - let actualUnsubscribe: (() => void) | null = null; - let releaseRequested = false; - let released = false; - const temporaryOwner = (): void => { - if (released) return; - if (actualUnsubscribe === null) { - releaseRequested = true; - return; - } - released = true; - const unsubscribe = actualUnsubscribe; - actualUnsubscribe = null; - unsubscribe(); - }; - - this.unsubscribeWire = temporaryOwner; - const returnedUnsubscribe: unknown = wire.subscribe( - (event) => { this.onWireEvent(event); }, - ); - if (typeof returnedUnsubscribe !== 'function') { - throw new TypeError( - 'WireClient.subscribe must return an unsubscribe function', - ); - } - actualUnsubscribe = returnedUnsubscribe as () => void; - if (!releaseRequested) return; - - const cleanupErrors: unknown[] = []; - this.captureCleanupError(cleanupErrors, temporaryOwner); - if (cleanupErrors.length === 0) this.requireActiveStartup(generation); - - const primary = this.fatalError - ?? new Error(`PlatformRuntime startup interrupted while ${this.lifecycle}`); - const failure = this.failureError( - 'subscription handoff', - primary, - cleanupErrors, - ); - if (this.fatalError !== null) this.fatalError = failure; - throw failure; - } - - private captureCleanupError(errors: unknown[], operation: () => void): void { - try { - operation(); - } catch (error) { - errors.push(error); - } - } - - private throwCleanupErrors(operation: string, errors: readonly unknown[]): void { - if (errors.length === 1) throw errors[0]; - if (errors.length > 1) throw new PlatformRuntimeCleanupError(operation, errors); - } - - private startLoginGuard(): void { - this.clearLoginGuard(); - let handle: unknown = null; - handle = this.clock.setTimeout(() => { - if (this.loginGuard !== handle) return; - this.loginGuard = null; - try { - if(STARTUP_CONFIG.debugIO)console.log('[连接状态] 登录回包超时,请求重连',{reason:'login-response-timeout',timeoutMs:LOGIN_GUARD_MS,time:new Date().toISOString()}); - this.requireWire().reconnectCurrent(); - } catch (error) { - this.showFatalOnce(error); - } - }, LOGIN_GUARD_MS); - this.loginGuard = handle; - } - - private clearLoginGuard(): void { - if (this.loginGuard === null) return; - const handle = this.loginGuard; - this.loginGuard = null; - this.clock.clearTimeout(handle); - } - - private requireConfig(): RuntimeConfig { - this.requireRunning(); - if (this.config === null) throw new Error('PlatformRuntime has no RuntimeConfig'); - return this.config; - } - - private requireOpenWire(): PlatformWireClient { - const wire = this.requireWire(); - if (!this.wireIsOpen) throw new Error('PlatformRuntime connection is not open'); - return wire; - } - - private requireWire(): PlatformWireClient { - this.requireRunning(); - if (this.wire === null) throw new Error('PlatformRuntime has no WireClient'); - return this.wire; - } - - private requireSession(): RuntimeSession { - this.requireRunning(); - if (this.session === null) throw new Error('PlatformRuntime has no RuntimeSession'); - return this.session; - } - - private requireRouter(): Router { - this.requireRunning(); - if (this.router === null) throw new Error('PlatformRuntime has no Router'); - return this.router; - } - - private requireGameSession(): SceneOrderedGameSessionHost { - this.requireRunning(); - if (this.gameSession === null) throw new Error('PlatformRuntime has no game session'); - return this.gameSession; - } - - private requireCommands(): PlatformCommands { - this.requireRunning(); - if (this.commands === null) throw new Error('PlatformRuntime has no platform commands'); - return this.commands; - } - - private requireRunning(): void { - if (this.lifecycle !== 'running') { - throw this.fatalError ?? new Error(`PlatformRuntime is ${this.lifecycle}`); - } - } - - private isActiveStartup(generation: number): boolean { - return this.startupGeneration === generation - && (this.lifecycle === 'starting' || this.lifecycle === 'running'); - } - - private requireActiveStartup(generation: number): void { - if (this.isActiveStartup(generation)) return; - throw this.fatalError - ?? new Error(`PlatformRuntime startup interrupted while ${this.lifecycle}`); - } - - private showFatalOnce(value: unknown): Error { - if (this.fatalError !== null) return this.fatalError; - const primary = value; - this.fatalError = this.failureError('fatal', primary, []); - this.startupGeneration += 1; - this.lifecycle = 'fatal'; - this.connectionIntent = { type: 'none' }; - this.wireIsOpen = false; - - const cleanupErrors: unknown[] = []; - for (const off of [...this.stateObservers]) { - this.captureCleanupError(cleanupErrors, off); - } - this.captureCleanupError(cleanupErrors, () => { this.clearLoginGuard(); }); - - const unsubscribe = this.unsubscribeWire; - this.unsubscribeWire = null; - if (unsubscribe !== null) this.captureCleanupError(cleanupErrors, unsubscribe); - - const gameSession = this.gameSession; - if (gameSession !== null) { - this.captureCleanupError(cleanupErrors, () => { gameSession.dispose(); }); - } - - const lease = this.activeGameHostLease; - this.activeGameHostLease = null; - if (lease !== null) { - this.captureCleanupError(cleanupErrors, () => { lease.invalidate(); }); - } - - this.captureCleanupError(cleanupErrors, () => { this.stopWireOnce(); }); - const fatal = this.failureError('fatal', primary, cleanupErrors); - this.fatalError = fatal; - try { - this.options.scene.showFatal(fatal); - } catch (sceneError) { - const withSceneError = this.failureError( - 'fatal', - primary, - [...cleanupErrors, sceneError], - ); - this.fatalError = withSceneError; - return withSceneError; - } - return fatal; - } - - private failureError( - operation: string, - primary: unknown, - cleanupErrors: readonly unknown[], - ): Error { - if (cleanupErrors.length === 0 && primary instanceof Error) return primary; - return new PlatformRuntimeCleanupError(operation, [primary, ...cleanupErrors]); - } -} +import type { RuntimeConfig } from '../config/runtime-config.ts'; +import { LOGIN_GUARD_MS } from '../core/constants.ts'; +import { STARTUP_CONFIG } from '../config/profiles.ts'; +import type { OutboundEnvelope } from '../core/types/envelope.ts'; +import { realClock, type Clock } from '../net/heartbeat.ts'; +import type { WireClient, WireEvent } from '../net/wire-client.ts'; +import { + buildLoginRequest, + type LoginAccountIdentity, + type LoginDeviceSnapshot, +} from '../protocol/contracts/index.ts'; +import { PlatformHandlers } from '../protocol/platform-handlers.ts'; +import { Router } from '../protocol/router.ts'; +import { isLoginResponse } from '../protocol/first-slice-routes.ts'; +import type { GameEntry, GameHost } from '../sdk/contracts/index.ts'; +import { GameSessionHost } from '../sdk/runtime/game-session-host.ts'; +import { PlatformCommands, type JoinRoomCommand, type CreateRoomCommand } from './commands.ts'; +import type { ConnectionIntent } from './connection-intent.ts'; +import { + createGameHostAdapter, + type GameHostLease, +} from './game-host-adapter.ts'; +import { RuntimeSession } from './runtime-session.ts'; +import type { ScenePort } from './scene-port.ts'; +import { PlatformStore } from './stores/platform-store.ts'; +import type { PlatformState } from './stores/platform-types.ts'; +import { ReadyGate, type ReadyGateName } from './ready-gate.ts'; + +export type PlatformWireClient = Pick< + WireClient, + 'subscribe' | 'start' | 'send' | 'switchServer' | 'reconnectCurrent' | 'stop' +>; + +export interface PlatformRuntimeOptions { + readonly gameEntry: GameEntry; + readonly resolveRuntimeConfig: () => Promise; + readonly createWireClient: (config: RuntimeConfig) => PlatformWireClient; + readonly scene: ScenePort; + readonly loadResources: () => Promise; + readonly waitForMinimumDisplay: () => Promise; + readonly getLoginDeviceSnapshot: () => LoginDeviceSnapshot; + readonly roomHistory?: import('../adapters/native/owned-room-history.ts').OwnedRoomHistory; + readonly clock?: Clock; +} + +type RuntimeLifecycle = 'idle' | 'starting' | 'running' | 'kicked' | 'fatal' | 'stopped'; + +/** Keeps the scene transition between room activation and optional snapshot restore. */ +class SceneOrderedGameSessionHost extends GameSessionHost { + private roomSceneShown = false; + + constructor(entry: GameEntry, private readonly scene: ScenePort) { + super(entry); + } + + open(host: GameHost): void { + this.roomSceneShown = false; + super.open(host); + } + + restore(deskinfo: unknown): void { + this.showRoomAfterOpen(); + if (this.state === 'disposed') return; + super.restore(deskinfo); + } + + showRoomAfterOpen(): void { + if (this.roomSceneShown) return; + if (this.state !== 'active') { + throw new Error('Cannot show the room without an active game session'); + } + this.roomSceneShown = true; + this.scene.showRoom(); + } + + close(): void { + try { + super.close(); + } finally { + this.roomSceneShown = false; + } + } + + dispose(): void { + try { + super.dispose(); + } finally { + this.roomSceneShown = false; + } + } +} + +class PlatformRuntimeCleanupError extends Error { + readonly errors: readonly unknown[]; + + constructor(operation: string, errors: readonly unknown[]) { + super(`PlatformRuntime cleanup failed during ${operation}`); + this.name = 'PlatformRuntimeCleanupError'; + this.errors = Object.freeze([...errors]); + } +} + +/** Owns the sole headless config, wire, router, store, and game-session composition. */ +export class PlatformRuntime { + private readonly store = new PlatformStore(); + private readonly gate = new ReadyGate(); + private readonly clock: Clock; + private lifecycle: RuntimeLifecycle = 'idle'; + private config: RuntimeConfig | null = null; + private wire: PlatformWireClient | null = null; + private unsubscribeWire: (() => void) | null = null; + private session: RuntimeSession | null = null; + private router: Router | null = null; + private gameSession: SceneOrderedGameSessionHost | null = null; + private commands: PlatformCommands | null = null; + private activeGameHostLease: GameHostLease | null = null; + private connectionIntent: ConnectionIntent = { type: 'none' }; + private loginEnvelope: OutboundEnvelope | null = null; + private loginGuard: unknown = null; + private wireIsOpen = false; + private loginSceneShown = false; + private fatalError: Error | null = null; + private wireStopped = false; + private startupGeneration = 0; + private readonly stateObservers = new Set<() => void>(); + private observerDispatchError: Error | null = null; + + constructor(private readonly options: PlatformRuntimeOptions) { + this.clock = options.clock ?? realClock; + } + + get state(): PlatformState { + return this.store.getState(); + } + + get ready(): boolean { + return this.gate.ready; + } + + subscribeState( + listener: (state: PlatformState) => void, + onError: (error: unknown) => void, + ): () => void { + if ( + this.lifecycle === 'kicked' + || this.lifecycle === 'fatal' + || this.lifecycle === 'stopped' + ) { + throw new Error(`PlatformRuntime cannot subscribe while ${this.lifecycle}`); + } + + let active = true; + const unsubscribeStore = this.store.subscribe((state) => { + if (!active) return; + try { + listener(state); + } catch (error) { + off(); + try { + onError(error); + } catch (callbackError) { + const failure = this.failureError( + 'UI observer', + error, + [callbackError], + ); + if (this.isStopped()) { + this.observerDispatchError = failure; + } else { + this.showFatalOnce(failure); + } + } + } + }); + const off = (): void => { + if (!active) return; + active = false; + try { + unsubscribeStore(); + } finally { + this.stateObservers.delete(off); + } + }; + this.stateObservers.add(off); + return off; + } + + async start(): Promise { + if (this.lifecycle !== 'idle') { + throw new Error(`PlatformRuntime cannot start while ${this.lifecycle}`); + } + const generation = this.startupGeneration + 1; + this.startupGeneration = generation; + this.lifecycle = 'starting'; + + try { + this.options.scene.showLoading(); + this.requireActiveStartup(generation); + const resourcesReady = this.beginReadyTask( + 'resources', + this.options.loadResources, + generation, + ); + const minimumDisplayReady = this.beginReadyTask( + 'minimum-display', + this.options.waitForMinimumDisplay, + generation, + ); + this.requireActiveStartup(generation); + const config = await this.options.resolveRuntimeConfig(); + this.requireActiveStartup(generation); + if (config.identity.gameid !== this.options.gameEntry.gameId) { + throw new Error( + 'RuntimeConfig identity.gameid must strictly equal GameEntry.gameId: ' + + `config=${String(config.identity.gameid)} (${typeof config.identity.gameid}), ` + + `entry=${String(this.options.gameEntry.gameId)} (${typeof this.options.gameEntry.gameId})`, + ); + } + this.config = config; + + this.requireActiveStartup(generation); + const gameSession = new SceneOrderedGameSessionHost( + this.options.gameEntry, + this.options.scene, + ); + const send = Object.freeze({ + send: (envelope: OutboundEnvelope): void => { + this.requireOpenWire().send(envelope); + }, + }); + const commands = new PlatformCommands({ + runtimeConfig: config, + store: this.store, + send, + }); + const session = new RuntimeSession({ + roomHistory:this.options.roomHistory, + store: this.store, + gameSession, + createGameHostLease: () => this.createGameHostLease(commands, send), + onServerDenial: outcome => { + // Legacy onopen retries only an authenticated session. A rejected first login + // must wait for another user submission, including after a socket reconnect. + if(outcome.rpc==='player_login'&&this.store.getState().players.selfPlayerId===null){ + this.connectionIntent={type:'none'};this.loginEnvelope=null; + } + this.options.scene.showServerDenial(outcome); + }, + }); + const handlers = new PlatformHandlers({ + store: this.store, + gameSession, + session, + connection: { + switchServer: (target, resend) => { this.beginServerSwitch(target, resend); }, + stopForKick: (data) => { this.stopForKick(data); }, + }, + showMessage:(message,time,source)=>{ + if(!this.options.scene.showTips)throw Error('show_message requires ScenePort.showTips'); + this.options.scene.showTips(message,time,source); + }, + }); + const router = new Router({ session, platform: handlers, gameSession }); + this.requireActiveStartup(generation); + const wire = this.options.createWireClient(config); + + this.requireActiveStartup(generation); + this.gameSession = gameSession; + this.commands = commands; + this.session = session; + this.router = router; + this.wire = wire; + this.requireActiveStartup(generation); + this.subscribeToWire(wire, generation); + this.requireActiveStartup(generation); + this.markReady('config'); + this.lifecycle = 'running'; + this.requireActiveStartup(generation); + wire.start(); + await Promise.all([resourcesReady, minimumDisplayReady]); + this.requireActiveStartup(generation); + } catch (error) { + if (!this.isActiveStartup(generation)) { + throw this.fatalError ?? error; + } + const fatal = this.showFatalOnce(error); + throw fatal; + } + } + + login(account: LoginAccountIdentity): void { + const config = this.requireConfig(); + const wire = this.requireOpenWire(); + const session = this.requireSession(); + const device=this.options.getLoginDeviceSnapshot(); + const envelope = buildLoginRequest( + config, + account, + device, + ); + + wire.send(envelope); + this.loginEnvelope = envelope; + this.connectionIntent = { type: 'login', envelope }; + session.markLoginPending(device.deviceLogin.enabled?null:Object.freeze({openid:envelope.data.openid,unionid:envelope.data.unionid})); + this.startLoginGuard(); + } + + createRoom(command: CreateRoomCommand): void { + this.requireCommands().createRoom(command); + } + + joinRoom(command: JoinRoomCommand): void { + this.requireCommands().joinRoom(command); + } + + prepare(): void { + this.requireCommands().prepare(); + } + + getVipRooms(codes:readonly (string|number)[]):void {this.requireCommands().getVipRooms(codes);} + getVipRankList(limit:number):void {this.requireCommands().getVipRankList(limit);} + joinOwnedRoom(code:number,location:unknown):void {this.requireCommands().joinOwnedRoom(code,location);} + getTreasureList():void {this.requireCommands().getTreasureList();} + redeemCard(cardno:string):void {this.requireCommands().redeemCard(cardno);} + joinVipRoom(code:string|number,location:unknown):void {this.requireCommands().joinVipRoom(code,location);} + getPlayerTasks():void {this.requireCommands().getPlayerTasks();} + getPlayerInvitecode():void {this.requireCommands().getPlayerInvitecode();} + bindInvitecode(code:string):void {this.requireCommands().bindInvitecode(code);} + claimTask(taskid:number|string):void {this.requireCommands().claimTask(taskid);} + + exitRoom(): void { + this.requireCommands().exitRoom(); + } + + stop(): void { + if (this.lifecycle === 'stopped') return; + this.startupGeneration += 1; + this.lifecycle = 'stopped'; + this.connectionIntent = { type: 'none' }; + this.wireIsOpen = false; + + const errors: unknown[] = []; + for (const off of [...this.stateObservers]) { + this.captureCleanupError(errors, off); + } + this.captureCleanupError(errors, () => { this.clearLoginGuard(); }); + const unsubscribe = this.unsubscribeWire; + this.unsubscribeWire = null; + if (unsubscribe !== null) this.captureCleanupError(errors, unsubscribe); + + const gameSession = this.gameSession; + if (gameSession !== null) { + this.captureCleanupError(errors, () => { gameSession.dispose(); }); + } + + const lease = this.activeGameHostLease; + this.activeGameHostLease = null; + if (lease !== null) { + this.captureCleanupError(errors, () => { lease.invalidate(); }); + } + + this.captureCleanupError(errors, () => { this.stopWireOnce(); }); + this.throwCleanupErrors('stop', errors); + } + + private beginReadyTask( + name: ReadyGateName, + task: () => Promise, + generation: number, + ): Promise { + return Promise.resolve() + .then(() => { + this.requireActiveStartup(generation); + return task(); + }) + .then( + () => { + if (this.isActiveStartup(generation)) this.markReady(name); + }, + (error: unknown) => { + if (this.isActiveStartup(generation)) this.showFatalOnce(error); + }, + ); + } + + private markReady(name: ReadyGateName): void { + if ( + this.lifecycle === 'fatal' + || this.lifecycle === 'kicked' + || this.lifecycle === 'stopped' + ) return; + this.gate.mark(name); + if (this.gate.ready && !this.loginSceneShown) { + this.loginSceneShown = true; + this.options.scene.showLogin(); + } + } + + private onWireEvent(event: WireEvent): void { + if ( + this.lifecycle === 'fatal' + || this.lifecycle === 'kicked' + || this.lifecycle === 'stopped' + ) return; + try { + switch (event.type) { + case 'open': + this.wireIsOpen = true; + this.store.setConnectionPhase('connected'); + this.throwObserverDispatchError(); + this.markReady('socket'); + if (this.connectionIntent.type === 'switch') { + const loginEnvelope = this.loginEnvelope; + if (loginEnvelope === null) { + throw new Error('server switch requires a previously sent player_login envelope'); + } + this.requireOpenWire().send(this.connectionIntent.resend); + this.connectionIntent = { type: 'login', envelope: loginEnvelope }; + } else if (this.connectionIntent.type === 'login') { + this.sendLoginEnvelope(this.connectionIntent.envelope); + } + break; + case 'message': + if (isLoginResponse(event.message)) { + this.clearLoginGuard(); + } + const stateBeforeDispatch = this.store.getState(); + const dispatchResult = this.requireRouter().dispatch(event.message); + this.throwObserverDispatchError(); + if ( + dispatchResult === 'handled-platform' + && this.lifecycle === 'running' + && this.store.getState() !== stateBeforeDispatch + ) { + this.updateSceneAfterMessage(event.message.route, event.message.rpc); + } + break; + case 'slow': + case 'reconnecting': + this.wireIsOpen = false; + this.store.setConnectionPhase(event.type); + this.throwObserverDispatchError(); + if (this.isTerminal()) return; + this.options.scene.showReconnect(); + break; + case 'close': + this.wireIsOpen = false; + break; + } + } catch (error) { + const failure = this.withObserverDispatchError(error); + // A lifecycle callback may stop and then throw. Preserve that error without + // replacing the requested terminal stop with a fatal lifecycle transition. + if (this.isStopped()) throw failure; + this.showFatalOnce(failure); + } + } + + private isStopped(): boolean { + return this.lifecycle === 'stopped'; + } + + private isTerminal(): boolean { + return this.lifecycle === 'fatal' + || this.lifecycle === 'kicked' + || this.lifecycle === 'stopped'; + } + + private throwObserverDispatchError(): void { + const failure = this.takeObserverDispatchError(); + if (failure === null) return; + throw failure; + } + + private takeObserverDispatchError(): Error | null { + const failure = this.observerDispatchError; + this.observerDispatchError = null; + return failure; + } + + private withObserverDispatchError(error: unknown): unknown { + const observerFailure = this.takeObserverDispatchError(); + return observerFailure === null + ? error + : this.failureError('UI observer dispatch', observerFailure, [error]); + } + + private sendLoginEnvelope(envelope: OutboundEnvelope): void { + const wire = this.requireOpenWire(); + const session = this.requireSession(); + wire.send(envelope); + session.markLoginPending(); + this.startLoginGuard(); + } + + private beginServerSwitch(target: string, resend: OutboundEnvelope): void { + if (this.loginEnvelope === null) { + throw new Error('server switch requires a previously sent player_login envelope'); + } + const wire = this.requireWire(); + this.clearLoginGuard(); + this.connectionIntent = { type: 'switch', target, resend }; + this.wireIsOpen = false; + wire.switchServer(target); + } + + private createGameHostLease( + commands: PlatformCommands, + send: { send(envelope: OutboundEnvelope): void }, + ): GameHostLease { + const lease = createGameHostAdapter({ + gameEntry: this.options.gameEntry, + store: this.store, + commands, + send, + }); + let invalidated = false; + const trackedLease: GameHostLease = Object.freeze({ + host: lease.host, + invalidate: (): void => { + if (invalidated) return; + invalidated = true; + try { + lease.invalidate(); + } finally { + if (this.activeGameHostLease === trackedLease) { + this.activeGameHostLease = null; + } + } + }, + }); + this.activeGameHostLease = trackedLease; + return trackedLease; + } + + private updateSceneAfterMessage(route: string, rpc: string): void { + if (route !== 'platform' && route !== 'agent' && route !== 'room') return; + if (rpc === 'player_login') { + if (this.store.getState().room.kind === 'outside') { + this.options.scene.showLobby(); + } else { + this.requireGameSession().showRoomAfterOpen(); + } + return; + } + if (rpc === 'self_join_room' || rpc === 'create_room') { + if (this.store.getState().room.kind === 'inside') this.requireGameSession().showRoomAfterOpen(); + return; + } + if (rpc === 'self_exit_room') this.options.scene.showLobby(); + } + + private stopForKick(data: unknown): void { + this.lifecycle = 'kicked'; + this.connectionIntent = { type: 'none' }; + this.wireIsOpen = false; + const errors: unknown[] = []; + for (const off of [...this.stateObservers]) { + this.captureCleanupError(errors, off); + } + this.captureCleanupError(errors, () => { this.store.setConnectionPhase('kicked'); }); + this.captureCleanupError(errors, () => { this.clearLoginGuard(); }); + this.captureCleanupError(errors, () => { this.options.scene.showKicked(data); }); + this.captureCleanupError(errors, () => { this.stopWireOnce(); }); + this.throwCleanupErrors('kick', errors); + } + + private stopWireOnce(): void { + if (this.wireStopped) return; + const wire = this.wire; + if (wire === null) return; + this.wireStopped = true; + wire.stop(); + } + + private subscribeToWire(wire: PlatformWireClient, generation: number): void { + let actualUnsubscribe: (() => void) | null = null; + let releaseRequested = false; + let released = false; + const temporaryOwner = (): void => { + if (released) return; + if (actualUnsubscribe === null) { + releaseRequested = true; + return; + } + released = true; + const unsubscribe = actualUnsubscribe; + actualUnsubscribe = null; + unsubscribe(); + }; + + this.unsubscribeWire = temporaryOwner; + const returnedUnsubscribe: unknown = wire.subscribe( + (event) => { this.onWireEvent(event); }, + ); + if (typeof returnedUnsubscribe !== 'function') { + throw new TypeError( + 'WireClient.subscribe must return an unsubscribe function', + ); + } + actualUnsubscribe = returnedUnsubscribe as () => void; + if (!releaseRequested) return; + + const cleanupErrors: unknown[] = []; + this.captureCleanupError(cleanupErrors, temporaryOwner); + if (cleanupErrors.length === 0) this.requireActiveStartup(generation); + + const primary = this.fatalError + ?? new Error(`PlatformRuntime startup interrupted while ${this.lifecycle}`); + const failure = this.failureError( + 'subscription handoff', + primary, + cleanupErrors, + ); + if (this.fatalError !== null) this.fatalError = failure; + throw failure; + } + + private captureCleanupError(errors: unknown[], operation: () => void): void { + try { + operation(); + } catch (error) { + errors.push(error); + } + } + + private throwCleanupErrors(operation: string, errors: readonly unknown[]): void { + if (errors.length === 1) throw errors[0]; + if (errors.length > 1) throw new PlatformRuntimeCleanupError(operation, errors); + } + + private startLoginGuard(): void { + this.clearLoginGuard(); + let handle: unknown = null; + handle = this.clock.setTimeout(() => { + if (this.loginGuard !== handle) return; + this.loginGuard = null; + try { + if(STARTUP_CONFIG.debugIO)console.log('[连接状态] 登录回包超时,请求重连',{reason:'login-response-timeout',timeoutMs:LOGIN_GUARD_MS,time:new Date().toISOString()}); + this.requireWire().reconnectCurrent(); + } catch (error) { + this.showFatalOnce(error); + } + }, LOGIN_GUARD_MS); + this.loginGuard = handle; + } + + private clearLoginGuard(): void { + if (this.loginGuard === null) return; + const handle = this.loginGuard; + this.loginGuard = null; + this.clock.clearTimeout(handle); + } + + private requireConfig(): RuntimeConfig { + this.requireRunning(); + if (this.config === null) throw new Error('PlatformRuntime has no RuntimeConfig'); + return this.config; + } + + private requireOpenWire(): PlatformWireClient { + const wire = this.requireWire(); + if (!this.wireIsOpen) throw new Error('PlatformRuntime connection is not open'); + return wire; + } + + private requireWire(): PlatformWireClient { + this.requireRunning(); + if (this.wire === null) throw new Error('PlatformRuntime has no WireClient'); + return this.wire; + } + + private requireSession(): RuntimeSession { + this.requireRunning(); + if (this.session === null) throw new Error('PlatformRuntime has no RuntimeSession'); + return this.session; + } + + private requireRouter(): Router { + this.requireRunning(); + if (this.router === null) throw new Error('PlatformRuntime has no Router'); + return this.router; + } + + private requireGameSession(): SceneOrderedGameSessionHost { + this.requireRunning(); + if (this.gameSession === null) throw new Error('PlatformRuntime has no game session'); + return this.gameSession; + } + + private requireCommands(): PlatformCommands { + this.requireRunning(); + if (this.commands === null) throw new Error('PlatformRuntime has no platform commands'); + return this.commands; + } + + private requireRunning(): void { + if (this.lifecycle !== 'running') { + throw this.fatalError ?? new Error(`PlatformRuntime is ${this.lifecycle}`); + } + } + + private isActiveStartup(generation: number): boolean { + return this.startupGeneration === generation + && (this.lifecycle === 'starting' || this.lifecycle === 'running'); + } + + private requireActiveStartup(generation: number): void { + if (this.isActiveStartup(generation)) return; + throw this.fatalError + ?? new Error(`PlatformRuntime startup interrupted while ${this.lifecycle}`); + } + + private showFatalOnce(value: unknown): Error { + if (this.fatalError !== null) return this.fatalError; + const primary = value; + this.fatalError = this.failureError('fatal', primary, []); + this.startupGeneration += 1; + this.lifecycle = 'fatal'; + this.connectionIntent = { type: 'none' }; + this.wireIsOpen = false; + + const cleanupErrors: unknown[] = []; + for (const off of [...this.stateObservers]) { + this.captureCleanupError(cleanupErrors, off); + } + this.captureCleanupError(cleanupErrors, () => { this.clearLoginGuard(); }); + + const unsubscribe = this.unsubscribeWire; + this.unsubscribeWire = null; + if (unsubscribe !== null) this.captureCleanupError(cleanupErrors, unsubscribe); + + const gameSession = this.gameSession; + if (gameSession !== null) { + this.captureCleanupError(cleanupErrors, () => { gameSession.dispose(); }); + } + + const lease = this.activeGameHostLease; + this.activeGameHostLease = null; + if (lease !== null) { + this.captureCleanupError(cleanupErrors, () => { lease.invalidate(); }); + } + + this.captureCleanupError(cleanupErrors, () => { this.stopWireOnce(); }); + const fatal = this.failureError('fatal', primary, cleanupErrors); + this.fatalError = fatal; + try { + this.options.scene.showFatal(fatal); + } catch (sceneError) { + const withSceneError = this.failureError( + 'fatal', + primary, + [...cleanupErrors, sceneError], + ); + this.fatalError = withSceneError; + return withSceneError; + } + return fatal; + } + + private failureError( + operation: string, + primary: unknown, + cleanupErrors: readonly unknown[], + ): Error { + if (cleanupErrors.length === 0 && primary instanceof Error) return primary; + return new PlatformRuntimeCleanupError(operation, [primary, ...cleanupErrors]); + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts b/cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts index 1528ebc..a7d64ab 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts @@ -1,16 +1,16 @@ -import type { ServerDenial } from './runtime-session.ts'; - -/** Semantic scene intents; the headless runtime never imports Cocos. */ -export interface ScenePort { - showLoading(): void; - showLogin(): void; - showLobby(): void; - showRoom(): void; - showReconnect(): void; - showKicked(data: unknown): void; - /** Required when the host supports the server's generic message UI. */ - showTips?(message: string, time: number, source?:'show_message'|'update_roomcard'): void; - /** Recoverable server result; the consumer interprets the original failure payload. */ - showServerDenial(outcome: ServerDenial): void; - showFatal(error: Error): void; -} +import type { ServerDenial } from './runtime-session.ts'; + +/** Semantic scene intents; the headless runtime never imports Cocos. */ +export interface ScenePort { + showLoading(): void; + showLogin(): void; + showLobby(): void; + showRoom(): void; + showReconnect(): void; + showKicked(data: unknown): void; + /** Required when the host supports the server's generic message UI. */ + showTips?(message: string, time: number, source?:'show_message'|'update_roomcard'|'update_bean'|'binding_invitecode'): void; + /** Recoverable server result; the consumer interprets the original failure payload. */ + showServerDenial(outcome: ServerDenial): void; + showFatal(error: Error): void; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts index ebba44f..9491ad7 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts @@ -1,836 +1,919 @@ -import type { Unsubscribe } from '../../core/reactive.ts'; -import {parseVipRooms} from '../../protocol/contracts/vip-room.ts'; -import type { TaskData } from '../../core/types/task.ts'; -import { - hasOwn, - requireArray, - requireBoolean, - requireInteger, - requireNumber, - requireRecord, - requireString, - type ParsedLoginResponse, - type ParsedOnlinePayload, - type ParsedRoomResponse, - type ParsedSeatPayload, -} from '../../protocol/contracts/index.ts'; -import type { - InsideRoomState, - PlatformConnectionPhase, - PlatformPlayer, - PlatformState, -} from './platform-types.ts'; - -interface RoomBuildResult { - readonly players: PlatformState['players']; - readonly room: Extract; -} - -const LOGIN_ROOM_FIELDS = ['roomcode', 'seat', 'roomtype', 'players'] as const; - -/** Task service may encode numeric fields as decimal strings. Normalize once, - * preserving legacy numeric comparisons/arithmetic without coercing empty/null values. */ -function requireTaskNumber(value:unknown,path:string):number { - if(typeof value==='string'){ - if(!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(value.trim())){ - throw new TypeError(`${path}: expected a finite number or decimal numeric string`); - } - return requireNumber(Number(value),path); - } - return requireNumber(value,path); -} - -function requirePositiveInteger(value: unknown, path: string): number { - const parsed = requireInteger(value, path); - if (parsed <= 0) throw new RangeError(`${path}: expected positive integer`); - return parsed; -} - -function requireProtocolFlag(value: unknown, path: string): number { - const parsed = requireInteger(value, path); - if (parsed !== 0 && parsed !== 1) { - throw new RangeError(`${path}: expected protocol flag 0 or 1`); - } - return parsed; -} - -function requireOnlineState(value: unknown, path: string): number { - const parsed = requireInteger(value, path); - if (parsed < 0 || parsed > 2) { - throw new RangeError(`${path}: expected onstate 0, 1, or 2`); - } - return parsed; -} - -function parseRoomPlayer(value: unknown, path: string): { - readonly player: PlatformPlayer; - readonly isprepare: number; - readonly onstate: number; -} { - const raw = requireRecord(value, path); - const isprepare = requireProtocolFlag(raw.isprepare, `${path}.isprepare`); - const onstate = requireOnlineState(raw.onstate, `${path}.onstate`); - const player: PlatformPlayer = { - playerid: requirePositiveInteger(raw.playerid, `${path}.playerid`), - nickname: requireString(raw.nickname, `${path}.nickname`), - avatar: requireString(raw.avatar, `${path}.avatar`), - sex: requireInteger(raw.sex, `${path}.sex`), - bean: requireNumber(raw.bean, `${path}.bean`), - ...(hasOwn(raw, 'ip') ? { ip: requireString(raw.ip, `${path}.ip`) } : {}), - isprepare, - onstate, - }; - return { player, isprepare, onstate }; -} - -function roomtypePropertyPath(path: string, key: string, isArray: boolean): string { - if (isArray && /^(0|[1-9]\d*)$/.test(key)) return `${path}[${key}]`; - if (/^[A-Za-z_$][\w$]*$/.test(key)) return `${path}.${key}`; - return `${path}[${JSON.stringify(key)}]`; -} - -interface RoomtypePropertySnapshot { - readonly key: PropertyKey; - readonly path: string; - readonly descriptor: PropertyDescriptor; -} - -interface RoomtypeObjectSnapshot { - readonly source: object; - readonly path: string; - readonly isArray: boolean; - readonly prototype: object | null; - readonly keys: readonly PropertyKey[]; - readonly properties: readonly RoomtypePropertySnapshot[]; -} - -function samePropertyDescriptor( - first: PropertyDescriptor, - second: PropertyDescriptor, -): boolean { - const firstIsData = 'value' in first; - const secondIsData = 'value' in second; - if (firstIsData !== secondIsData) return false; - if ( - first.enumerable !== second.enumerable - || first.configurable !== second.configurable - ) return false; - if (firstIsData && secondIsData) { - return first.writable === second.writable && Object.is(first.value, second.value); - } - return Object.is(first.get, second.get) && Object.is(first.set, second.set); -} - -function verifyRoomtypeSourceGraph(graph: readonly RoomtypeObjectSnapshot[]): void { - for (const snapshot of graph) { - if (Array.isArray(snapshot.source) !== snapshot.isArray) { - throw new TypeError(`${snapshot.path}: unstable roomtype container kind`); - } - if (!Object.is(Object.getPrototypeOf(snapshot.source), snapshot.prototype)) { - throw new TypeError(`${snapshot.path}: unstable roomtype prototype`); - } - const keys = Reflect.ownKeys(snapshot.source); - if ( - keys.length !== snapshot.keys.length - || keys.some((key, index) => !Object.is(key, snapshot.keys[index])) - ) { - throw new TypeError(`${snapshot.path}: unstable roomtype own keys`); - } - for (const property of snapshot.properties) { - const descriptor = Reflect.getOwnPropertyDescriptor(snapshot.source, property.key); - if ( - descriptor === undefined - || !samePropertyDescriptor(property.descriptor, descriptor) - ) { - throw new TypeError(`${property.path}: unstable roomtype property descriptor`); - } - } - } -} - -function cloneAndFreezeRoomtype(value: readonly unknown[]): readonly unknown[] { - const copies = new WeakMap(); - const active = new WeakSet(); - const ownedGraph: object[] = []; - const sourceGraph: RoomtypeObjectSnapshot[] = []; - - const copy = (current: unknown, path: string): unknown => { - if (current === null) return null; - if (typeof current === 'string' || typeof current === 'boolean') return current; - if (typeof current === 'number') { - if (!Number.isFinite(current)) { - throw new TypeError(`${path}: expected finite JSON number`); - } - return current; - } - if (typeof current !== 'object') { - throw new TypeError(`${path}: expected JSON value`); - } - if (active.has(current)) { - throw new TypeError(`${path}: cyclic roomtype reference`); - } - const priorCopy = copies.get(current); - if (priorCopy !== undefined) return priorCopy; - - const sourceIsArray = Array.isArray(current); - const prototype = Object.getPrototypeOf(current); - if (!sourceIsArray && prototype !== Object.prototype && prototype !== null) { - throw new TypeError(`${path}: expected JSON array or plain object container`); - } - - const keys = Reflect.ownKeys(current); - const properties: RoomtypePropertySnapshot[] = []; - for (const key of keys) { - if (typeof key !== 'string') { - throw new TypeError(`${path}: symbol properties are not valid roomtype data`); - } - const propertyPath = roomtypePropertyPath(path, key, sourceIsArray); - const descriptor = Reflect.getOwnPropertyDescriptor(current, key); - if (descriptor === undefined) { - throw new TypeError(`${propertyPath}: unstable property structure`); - } - properties.push({ key, path: propertyPath, descriptor }); - } - - const lengthProperty = properties.find((property) => property.key === 'length'); - let target: unknown[] | Record; - let arrayLength = 0; - if (sourceIsArray) { - if (lengthProperty === undefined || !('value' in lengthProperty.descriptor)) { - throw new TypeError(`${path}.length: expected stable data property`); - } - arrayLength = requireInteger(lengthProperty.descriptor.value, `${path}.length`); - if (arrayLength < 0) throw new RangeError(`${path}.length: expected non-negative integer`); - target = new Array(arrayLength); - } else { - target = prototype === null ? Object.create(null) as Record : {}; - } - - sourceGraph.push({ source: current, path, isArray: sourceIsArray, prototype, keys, properties }); - copies.set(current, target); - active.add(current); - ownedGraph.push(target); - for (const property of properties) { - const { key, path: propertyPath, descriptor } = property; - if (sourceIsArray && key === 'length') continue; - if (sourceIsArray) { - const index = Number(key); - if (!Number.isInteger(index) || index < 0 || index >= arrayLength || String(index) !== key) { - throw new TypeError(`${propertyPath}: expected array index`); - } - } - if (!('value' in descriptor)) { - throw new TypeError(`${propertyPath}: accessor properties are not allowed in roomtype`); - } - if (!descriptor.enumerable) { - throw new TypeError(`${propertyPath}: expected enumerable JSON data property`); - } - const copiedValue = copy(descriptor.value, propertyPath); - Object.defineProperty(target, key, { - configurable: true, - enumerable: true, - writable: true, - value: copiedValue, - }); - } - active.delete(current); - return target; - }; - - const canonical = copy(value, 'roomtype') as readonly unknown[]; - verifyRoomtypeSourceGraph(sourceGraph); - for (let index = ownedGraph.length - 1; index >= 0; index -= 1) { - Object.freeze(ownedGraph[index]); - } - return canonical; -} - -function assertSelfProfileMatches( - loginPlayer: PlatformPlayer, - roomPlayer: PlatformPlayer, - path: string, -): void { - for (const field of ['playerid', 'nickname', 'avatar', 'sex', 'bean'] as const) { - if (roomPlayer[field] !== loginPlayer[field]) { - throw new RangeError(`${path}.${field}: conflicts with authoritative login profile`); - } - } - if (roomPlayer.ip !== undefined && roomPlayer.ip !== loginPlayer.ip) { - throw new RangeError(`${path}.ip: conflicts with authoritative login profile`); - } -} - -function requireMatchingState( - parsedState: unknown, - raw: Readonly>, - path: string, -): number { - const state = requireInteger(parsedState, `${path}.state`); - const rawState = requireInteger(raw.state, `${path}.raw.state`); - if (state !== rawState) { - throw new RangeError(`${path}.state: expected parsed state to equal raw.state`); - } - return state; -} - -function buildRoom( - roomValue: unknown, - rawValue: unknown, - selfPlayer: PlatformPlayer, - stage: number, -): RoomBuildResult { - const room = requireRecord(roomValue, '$.room'); - const raw = requireRecord(rawValue, '$.raw'); - const roomcode = requireString(room.roomcode, '$.room.roomcode'); - const selfSeat = requireInteger(room.seat, '$.room.seat'); - const roomtype = requireArray(room.roomtype, '$.room.roomtype'); - const sourcePlayers = requireArray(room.players, '$.room.players'); - if (requireString(raw.roomcode, '$.raw.roomcode') !== roomcode) { - throw new RangeError('$.room.roomcode: expected value parsed from raw roomcode'); - } - if (requireInteger(raw.seat, '$.raw.seat') !== selfSeat) { - throw new RangeError('$.room.seat: expected value parsed from raw seat'); - } - if (raw.roomtype !== roomtype) { - throw new RangeError('$.room.roomtype: expected canonical raw roomtype reference'); - } - if (raw.players !== sourcePlayers) { - throw new RangeError('$.room.players: expected canonical raw players reference'); - } - if (selfSeat < 0 || selfSeat >= sourcePlayers.length) { - throw new RangeError('$.room.seat: expected server seat index within players'); - } - - const needprepare = requireProtocolFlag(raw.needprepare, '$.raw.needprepare'); - const infinite = requireProtocolFlag(raw.infinite, '$.raw.infinite'); - - const seatPlayerIds: (number | null)[] = []; - const readySeats: number[] = []; - const offlineSeats: number[] = []; - const parsedPlayers = new Map(); - - for (let seat = 0; seat < sourcePlayers.length; seat += 1) { - const value = sourcePlayers[seat]; - if (value === null) { - seatPlayerIds.push(null); - continue; - } - const parsed = parseRoomPlayer(value, `$.room.players[${seat}]`); - if (parsedPlayers.has(parsed.player.playerid)) { - throw new RangeError(`$.room.players[${seat}].playerid: duplicate player seat`); - } - parsedPlayers.set(parsed.player.playerid, parsed.player); - seatPlayerIds.push(parsed.player.playerid); - if (parsed.isprepare === 1) readySeats.push(seat); - if (parsed.onstate === 1) offlineSeats.push(seat); - } - - const selfRoomPlayer = parsedPlayers.get(selfPlayer.playerid); - if (seatPlayerIds[selfSeat] !== selfPlayer.playerid || selfRoomPlayer === undefined) { - throw new RangeError(`$.room.players[${selfSeat}].playerid: conflicts with authenticated player`); - } - assertSelfProfileMatches(selfPlayer, selfRoomPlayer, `$.room.players[${selfSeat}]`); - - const canonicalRoomtype = cloneAndFreezeRoomtype(roomtype); - const entities: Record = {}; - for (const [playerid, parsedPlayer] of parsedPlayers) { - const player = playerid === selfPlayer.playerid - ? { - ...selfPlayer, - isprepare: parsedPlayer.isprepare, - onstate: parsedPlayer.onstate, - } - : parsedPlayer; - entities[playerid] = Object.freeze(player); - } - - return { - players: Object.freeze({ - selfPlayerId: selfPlayer.playerid, - entities: Object.freeze(entities), - }), - room: Object.freeze({ - kind: 'inside', - roomcode, - selfSeat, - seatPlayerIds: Object.freeze(seatPlayerIds), - readySeats: Object.freeze(readySeats), - offlineSeats: Object.freeze(offlineSeats), - roomtype: canonicalRoomtype, - stage, - needprepare, - infinite, - }), - }; -} - -function requireInsideRoom(state: PlatformState): InsideRoomState { - if (state.room.kind !== 'inside') { - throw new Error('room: expected player to be inside a room'); - } - return state.room; -} - -function parseSeatAction(value: ParsedSeatPayload): { - readonly seat: number; - readonly raw: Readonly>; -} { - const input = requireRecord(value, '$.event'); - const seat = requireInteger(input.seat, '$.event.seat'); - const raw = requireRecord(input.raw, '$.event.raw'); - if (requireInteger(raw.seat, '$.event.raw.seat') !== seat) { - throw new RangeError('$.event.raw.seat: expected the parsed seat'); - } - return { seat, raw }; -} - -function stageAfterOptionalDeskwar( - raw: Readonly>, - currentStage: number, -): number { - if (!hasOwn(raw, 'deskwar')) return currentStage; - return requireProtocolFlag(raw.deskwar, '$.event.raw.deskwar') === 1 - ? 1 - : currentStage; -} - -function requireOccupiedSeat(room: InsideRoomState, seat: number): number { - if (seat < 0 || seat >= room.seatPlayerIds.length) { - throw new RangeError('$.event.seat: expected room seat range'); - } - const playerid = room.seatPlayerIds[seat]; - if (playerid === null) { - throw new RangeError('$.event.seat: expected an occupied seat'); - } - return playerid; -} - -const INITIAL_STATE: PlatformState = Object.freeze({ - app: Object.freeze({ phase: 'idle' }), - players: Object.freeze({ - selfPlayerId: null, - entities: Object.freeze({}), - }), - room: Object.freeze({ kind: 'outside' }), -}); - -export type PlatformStateListener = (state: PlatformState, previous: PlatformState) => void; - -interface PlatformTransition { - readonly state: PlatformState; - readonly previous: PlatformState; - readonly listeners: readonly PlatformStateListener[]; -} - -export class PlatformStore { - private state: PlatformState = INITIAL_STATE; - private readonly listeners = new Set(); - private readonly transitions: PlatformTransition[] = []; - private publishing = false; - - getState(): PlatformState { - return this.state; - } - - applyTaskList(payload:unknown):void { - const raw=requireRecord(payload,'$.data'); - const ids=new Set(); - const tasks=requireArray(raw.tasks,'$.data.tasks').map((value,index):TaskData=>{ - const path=`$.data.tasks[${index}]`,task=requireRecord(value,path); - const taskid=this.taskId(task.taskid); - if(ids.has(String(taskid)))throw Error(`${path}.taskid: duplicate task`); - ids.add(String(taskid)); - const title=requireString(task.title,`${path}.title`); - const award=requireTaskNumber(task.award,`${path}.award`),finish=requireTaskNumber(task.finish,`${path}.finish`); - const total=requireTaskNumber(task.total,`${path}.total`),state=requireInteger(requireTaskNumber(task.state,`${path}.state`),`${path}.state`); - if(award<0||finish<0||total<0||state<0||(state===0&&total===0))throw Error(`${path}: invalid task progress or state`); - return Object.freeze({taskid,title,award,finish,total,state}); - }); - this.taskPlayer(); - this.commit(Object.freeze({...this.state,tasks:Object.freeze(tasks)})); - } - - applyTaskAward(payload:unknown):void { - const raw=requireRecord(payload,'$.data'),taskid=this.taskId(raw.taskid); - const taskstate=requireInteger(raw.taskstate,'$.data.taskstate'); - const tasks=this.state.tasks; - if(!tasks||!tasks.some(task=>String(task.taskid)===String(taskid)))throw Error('Task award references an unloaded task'); - const player=this.taskPlayer(); - this.commit(Object.freeze({...this.state, - tasks:Object.freeze(tasks.map(task=>String(task.taskid)===String(taskid)?Object.freeze({...task,state:2}):task)), - players:Object.freeze({...this.state.players,entities:Object.freeze({...this.state.players.entities,[player.playerid]:Object.freeze({...player,taskstate})})}), - })); - } - - applyTaskFinished(payload:unknown):void { - const state=requireInteger(requireRecord(payload,'$.data').state,'$.data.state'); - const player=this.taskPlayer(); - const taskstate=requireInteger(player.taskstate,'players.self.taskstate'); - if(state===1&&taskstate===0)this.commit(Object.freeze({...this.state,players:Object.freeze({...this.state.players, - entities:Object.freeze({...this.state.players.entities,[player.playerid]:Object.freeze({...player,taskstate:1})}), - })})); - } - - applyRoomcardUpdate(payload:unknown):void { - const raw=requireRecord(payload,'$.data'); - // Net.update_roomcard only applies absolute balances when change is absent. - if(raw.change!==undefined)return; - const roomcard=requireNumber(raw.roomcard,'$.data.roomcard'),player=this.taskPlayer(); - this.commit(Object.freeze({...this.state,players:Object.freeze({...this.state.players, - entities:Object.freeze({...this.state.players.entities,[player.playerid]:Object.freeze({...player,roomcard})}), - })})); - } - - private taskPlayer():PlatformPlayer { - const id=this.state.players.selfPlayerId; - if(id===null||!this.state.players.entities[id])throw Error('Task response requires authenticated player'); - return this.state.players.entities[id]; - } - private taskId(value:unknown):number|string { - if(typeof value==='number'&&Number.isFinite(value))return value; - if(typeof value==='string'&&value.length>0)return value; - throw Error('Invalid taskid'); - } - - subscribe(listener: PlatformStateListener): Unsubscribe { - this.listeners.add(listener); - return (): void => { this.listeners.delete(listener); }; - } - - setConnectionPhase(phase: Exclude): void { - if (phase !== 'connected' && phase !== 'reconnecting' && phase !== 'slow' && phase !== 'kicked') { - throw new TypeError('app.phase: expected a wire connection phase'); - } - const previous = this.getState(); - if (previous.app.phase === phase) return; - this.commit(Object.freeze({ ...previous, app: Object.freeze({ phase }) })); - } - - applyVipRooms(data:unknown):void { - this.commit(Object.freeze({...this.state,vipRooms:parseVipRooms(data)})); - } - - private commit(state: PlatformState): PlatformState { - if (Object.is(state, this.state)) return state; - const previous = this.state; - this.state = state; - this.transitions.push({ - state, - previous, - listeners: [...this.listeners], - }); - if (this.publishing) return state; - - this.publishing = true; - try { - while (this.transitions.length > 0) { - const transition = this.transitions.shift(); - if (transition === undefined) continue; - for (const listener of transition.listeners) { - try { - listener(transition.state, transition.previous); - } catch (error) { - console.error('[reactive] subscriber error:', error); - } - } - } - } finally { - this.publishing = false; - } - return state; - } - - applyLoginSuccess(parsed: ParsedLoginResponse): PlatformState { - const input = requireRecord(parsed, '$.login'); - const raw = requireRecord(input.raw, '$.login.raw'); - const state = requireMatchingState(input.state, raw, '$.login'); - const rawHasRoom = LOGIN_ROOM_FIELDS.some((field) => hasOwn(raw, field)); - if (input.room === undefined || (input.room !== null) !== rawHasRoom) { - throw new TypeError('$.login.room: parsed room presence conflicts with raw room fields'); - } - if (state !== 0) { - throw new RangeError('$.login.state: expected successful state 0'); - } - const playerid = requireInteger(input.playerid, '$.login.playerid'); - if (playerid <= 0) { - throw new RangeError('$.login.playerid: expected positive integer'); - } - if (requireInteger(raw.playerid, '$.login.raw.playerid') !== playerid) { - throw new RangeError('$.login.raw.playerid: expected the parsed playerid'); - } - const player: PlatformPlayer = Object.freeze({ - playerid, - nickname: requireString(raw.nickname, '$.login.raw.nickname'), - avatar: requireString(raw.avatar, '$.login.raw.avatar'), - sex: requireInteger(raw.sex, '$.login.raw.sex'), - bean: requireNumber(raw.bean, '$.login.raw.bean'), - ip: requireString(raw.ip, '$.login.raw.ip'), - ...(hasOwn(raw, 'roomcard') ? {roomcard: requireNumber(raw.roomcard, '$.login.raw.roomcard')} : {}), - ...(hasOwn(raw, 'taskstate') ? {taskstate: requireInteger(raw.taskstate, '$.login.raw.taskstate')} : {}), - ...(hasOwn(raw, 'advanced') ? {advanced: requireInteger(raw.advanced, '$.login.raw.advanced')} : {}), - ...(hasOwn(raw, 'score') ? {score: requireNumber(raw.score, '$.login.raw.score')} : {}), - ...(hasOwn(raw, 'sign') ? {sign: raw.sign === null ? null : requireString(raw.sign, '$.login.raw.sign')} : {}), - }); - const roomResult = input.room === null - ? { - players: Object.freeze({ - selfPlayerId: playerid, - entities: Object.freeze({ [playerid]: player }), - }), - room: Object.freeze({ kind: 'outside' as const }), - } - : buildRoom( - input.room, - raw, - player, - requireProtocolFlag(raw.isbattle, '$.login.raw.isbattle'), - ); - return this.commit(Object.freeze({ - app: Object.freeze({ phase: 'logged-in' }), - players: roomResult.players, - room: roomResult.room, - })); - } - - replaceRoom(parsed: ParsedRoomResponse): PlatformState { - const input = requireRecord(parsed, '$.response'); - const raw = requireRecord(input.raw, '$.response.raw'); - const state = requireMatchingState(input.state, raw, '$.response'); - if (input.room === undefined || (input.room !== null) !== (state === 0)) { - throw new TypeError('$.response.room: parsed room presence conflicts with raw state'); - } - if (state !== 0) { - throw new RangeError('$.response.state: expected successful state 0'); - } - if (input.room === null) { - throw new TypeError('$.response.room: expected parsed room snapshot'); - } - const previous = this.getState(); - const selfPlayerId = previous.players.selfPlayerId; - if (selfPlayerId === null) { - throw new Error('players.selfPlayerId: expected authenticated player'); - } - const selfPlayer = previous.players.entities[selfPlayerId]; - if (selfPlayer === undefined) { - throw new Error('players.entities: missing authenticated player'); - } - const reconnect = requireRecord(input.reconnect, '$.response.reconnect'); - const reconnectPresent = requireBoolean( - reconnect.present, - '$.response.reconnect.present', - ); - const rawReconnectPresent = Boolean(raw.deskinfo); - if (reconnectPresent !== rawReconnectPresent) { - throw new RangeError( - '$.response.reconnect.present: expected deskinfo truthiness from raw response', - ); - } - if (reconnectPresent && reconnect.value !== raw.deskinfo) { - throw new RangeError( - '$.response.reconnect.value: expected canonical raw deskinfo reference', - ); - } - const stage = requireProtocolFlag(input.stage, '$.response.stage'); - if (reconnectPresent && stage !== 1) { - throw new RangeError('$.response.stage: reconnect room must be in battle stage'); - } - const roomResult = buildRoom(input.room, raw, selfPlayer, stage); - return this.commit(Object.freeze({ - // A switch authenticates with connect_*server, then self_join_room instead of player_login. - app: previous.app.phase === 'logged-in' - ? previous.app - : Object.freeze({ phase: 'logged-in' }), - players: roomResult.players, - room: roomResult.room, - })); - } - - clearRoom(): void { - const previous = this.getState(); - requireInsideRoom(previous); - const selfPlayerId = previous.players.selfPlayerId; - if (selfPlayerId === null) { - throw new Error('players.selfPlayerId: expected authenticated player'); - } - const selfPlayer = previous.players.entities[selfPlayerId]; - if (selfPlayer === undefined) { - throw new Error('players.entities: missing authenticated player'); - } - this.commit(Object.freeze({ - app: previous.app, - players: Object.freeze({ - selfPlayerId, - entities: Object.freeze({ [selfPlayerId]: selfPlayer }), - }), - room: Object.freeze({ kind: 'outside' }), - })); - } - - playerJoin(parsed: ParsedSeatPayload): void { - const event = parseSeatAction(parsed); - const previous = this.getState(); - const room = requireInsideRoom(previous); - if (event.seat < 0 || event.seat >= room.seatPlayerIds.length) { - throw new RangeError('$.event.seat: expected occupied room seat range'); - } - if (room.seatPlayerIds[event.seat] !== null) { - throw new RangeError('$.event.seat: expected an empty seat'); - } - const joined = parseRoomPlayer(event.raw, '$.event.raw'); - if (room.seatPlayerIds.includes(joined.player.playerid)) { - throw new RangeError('$.event.raw.playerid: duplicate player seat'); - } - const stage = stageAfterOptionalDeskwar(event.raw, room.stage); - - const player = Object.freeze(joined.player); - const entities = Object.freeze({ - ...previous.players.entities, - [player.playerid]: player, - }); - const seatPlayerIds = [...room.seatPlayerIds]; - seatPlayerIds[event.seat] = player.playerid; - const readySeats = joined.isprepare === 1 - ? [...room.readySeats, event.seat] - : [...room.readySeats]; - const offlineSeats = joined.onstate === 1 - ? [...room.offlineSeats, event.seat] - : [...room.offlineSeats]; - - this.commit(Object.freeze({ - app: previous.app, - players: Object.freeze({ - selfPlayerId: previous.players.selfPlayerId, - entities, - }), - room: Object.freeze({ - ...room, - seatPlayerIds: Object.freeze(seatPlayerIds), - readySeats: Object.freeze(readySeats), - offlineSeats: Object.freeze(offlineSeats), - stage, - }), - })); - } - - playerReady(parsed: ParsedSeatPayload): void { - const event = parseSeatAction(parsed); - const previous = this.getState(); - const room = requireInsideRoom(previous); - const playerid = requireOccupiedSeat(room, event.seat); - if (room.readySeats.includes(event.seat)) { - throw new RangeError('$.event.seat: player is already ready'); - } - const player = previous.players.entities[playerid]; - if (player === undefined) { - throw new RangeError('players.entities: occupied seat has no canonical player'); - } - const stage = stageAfterOptionalDeskwar(event.raw, room.stage); - const entities = Object.freeze({ - ...previous.players.entities, - [playerid]: Object.freeze({ ...player, isprepare: 1 }), - }); - const readySeats = Object.freeze([...room.readySeats, event.seat]); - this.commit(Object.freeze({ - app: previous.app, - players: Object.freeze({ - selfPlayerId: previous.players.selfPlayerId, - entities, - }), - room: Object.freeze({ ...room, readySeats, stage }), - })); - } - - playerOffline(parsed: ParsedSeatPayload): void { - const event = parseSeatAction(parsed); - const previous = this.getState(); - const room = requireInsideRoom(previous); - const playerid = requireOccupiedSeat(room, event.seat); - if (room.offlineSeats.includes(event.seat)) { - throw new RangeError('$.event.seat: player is already offline'); - } - const player = previous.players.entities[playerid]; - if (player === undefined) { - throw new RangeError('players.entities: occupied seat has no canonical player'); - } - const entities = Object.freeze({ - ...previous.players.entities, - [playerid]: Object.freeze({ ...player, onstate: 1 }), - }); - const offlineSeats = Object.freeze([...room.offlineSeats, event.seat]); - this.commit(Object.freeze({ - app: previous.app, - players: Object.freeze({ - selfPlayerId: previous.players.selfPlayerId, - entities, - }), - room: Object.freeze({ ...room, offlineSeats }), - })); - } - - playerOnline(parsed: ParsedOnlinePayload): void { - const event = parseSeatAction(parsed); - const input = requireRecord(parsed, '$.event'); - const ip = requireString(input.ip, '$.event.ip'); - if (requireString(event.raw.ip, '$.event.raw.ip') !== ip) { - throw new RangeError('$.event.raw.ip: expected the parsed ip'); - } - const previous = this.getState(); - const room = requireInsideRoom(previous); - const playerid = requireOccupiedSeat(room, event.seat); - if (!room.offlineSeats.includes(event.seat)) { - throw new RangeError('$.event.seat: player is already online'); - } - const player = previous.players.entities[playerid]; - if (player === undefined) { - throw new RangeError('players.entities: occupied seat has no canonical player'); - } - const entities = Object.freeze({ - ...previous.players.entities, - [playerid]: Object.freeze({ ...player, ip, onstate: 0 }), - }); - const offlineSeats = Object.freeze( - room.offlineSeats.filter((seat) => seat !== event.seat), - ); - this.commit(Object.freeze({ - app: previous.app, - players: Object.freeze({ - selfPlayerId: previous.players.selfPlayerId, - entities, - }), - room: Object.freeze({ ...room, offlineSeats }), - })); - } - - playerExit(parsed: ParsedSeatPayload): void { - const event = parseSeatAction(parsed); - const previous = this.getState(); - const room = requireInsideRoom(previous); - const playerid = requireOccupiedSeat(room, event.seat); - if (event.seat === room.selfSeat) { - throw new RangeError('$.event.seat: self exit must clear the room'); - } - - const entities: Record = { - ...previous.players.entities, - }; - delete entities[playerid]; - const seatPlayerIds = [...room.seatPlayerIds]; - seatPlayerIds[event.seat] = null; - const readySeats = room.readySeats.filter((seat) => seat !== event.seat); - const offlineSeats = room.offlineSeats.filter((seat) => seat !== event.seat); - - this.commit(Object.freeze({ - app: previous.app, - players: Object.freeze({ - selfPlayerId: previous.players.selfPlayerId, - entities: Object.freeze(entities), - }), - room: Object.freeze({ - ...room, - seatPlayerIds: Object.freeze(seatPlayerIds), - readySeats: Object.freeze(readySeats), - offlineSeats: Object.freeze(offlineSeats), - }), - })); - } -} +import type { Unsubscribe } from '../../core/reactive.ts'; +import {parseVipRooms} from '../../protocol/contracts/vip-room.ts'; +import {parseRankList,parseVipRankList} from '../../protocol/contracts/rank-list.ts'; +import type { TaskData } from '../../core/types/task.ts'; +import type {LoginAccountIdentity} from '../../protocol/contracts/login-contract.ts'; +import {requireStringOrFiniteNumber} from '../../protocol/contracts/validation.ts'; +import { + hasOwn, + requireArray, + requireBoolean, + requireInteger, + requireNumber, + requireRecord, + requireString, + type ParsedLoginResponse, + type ParsedOnlinePayload, + type ParsedRoomResponse, + type ParsedCreateRoomResponse, + type ParsedSeatPayload, +} from '../../protocol/contracts/index.ts'; +import type { + InsideRoomState, + PlatformConnectionPhase, + PlatformPlayer, + PlatformState, +} from './platform-types.ts'; + +interface RoomBuildResult { + readonly players: PlatformState['players']; + readonly room: Extract; +} + +const LOGIN_ROOM_FIELDS = ['roomcode', 'seat', 'roomtype', 'players'] as const; + +/** Task service may encode numeric fields as decimal strings. Normalize once, + * preserving legacy numeric comparisons/arithmetic without coercing empty/null values. */ +function requireTaskNumber(value:unknown,path:string):number { + if(typeof value==='string'){ + if(!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(value.trim())){ + throw new TypeError(`${path}: expected a finite number or decimal numeric string`); + } + return requireNumber(Number(value),path); + } + return requireNumber(value,path); +} + +function requirePositiveInteger(value: unknown, path: string): number { + const parsed = requireInteger(value, path); + if (parsed <= 0) throw new RangeError(`${path}: expected positive integer`); + return parsed; +} + +function requireProtocolFlag(value: unknown, path: string): number { + const parsed = requireInteger(value, path); + if (parsed !== 0 && parsed !== 1) { + throw new RangeError(`${path}: expected protocol flag 0 or 1`); + } + return parsed; +} + +function requireOnlineState(value: unknown, path: string): number { + const parsed = requireInteger(value, path); + if (parsed < 0 || parsed > 2) { + throw new RangeError(`${path}: expected onstate 0, 1, or 2`); + } + return parsed; +} + +function parseRoomPlayer(value: unknown, path: string): { + readonly player: PlatformPlayer; + readonly isprepare: number; + readonly onstate: number; +} { + const raw = requireRecord(value, path); + const isprepare = requireProtocolFlag(raw.isprepare, `${path}.isprepare`); + const onstate = requireOnlineState(raw.onstate, `${path}.onstate`); + const player: PlatformPlayer = { + playerid: requirePositiveInteger(raw.playerid, `${path}.playerid`), + nickname: requireString(raw.nickname, `${path}.nickname`), + avatar: requireString(raw.avatar, `${path}.avatar`), + sex: requireInteger(raw.sex, `${path}.sex`), + bean: requireNumber(raw.bean, `${path}.bean`), + ...(hasOwn(raw, 'ip') ? { ip: requireString(raw.ip, `${path}.ip`) } : {}), + isprepare, + onstate, + }; + return { player, isprepare, onstate }; +} + +function roomtypePropertyPath(path: string, key: string, isArray: boolean): string { + if (isArray && /^(0|[1-9]\d*)$/.test(key)) return `${path}[${key}]`; + if (/^[A-Za-z_$][\w$]*$/.test(key)) return `${path}.${key}`; + return `${path}[${JSON.stringify(key)}]`; +} + +interface RoomtypePropertySnapshot { + readonly key: PropertyKey; + readonly path: string; + readonly descriptor: PropertyDescriptor; +} + +interface RoomtypeObjectSnapshot { + readonly source: object; + readonly path: string; + readonly isArray: boolean; + readonly prototype: object | null; + readonly keys: readonly PropertyKey[]; + readonly properties: readonly RoomtypePropertySnapshot[]; +} + +function samePropertyDescriptor( + first: PropertyDescriptor, + second: PropertyDescriptor, +): boolean { + const firstIsData = 'value' in first; + const secondIsData = 'value' in second; + if (firstIsData !== secondIsData) return false; + if ( + first.enumerable !== second.enumerable + || first.configurable !== second.configurable + ) return false; + if (firstIsData && secondIsData) { + return first.writable === second.writable && Object.is(first.value, second.value); + } + return Object.is(first.get, second.get) && Object.is(first.set, second.set); +} + +function verifyRoomtypeSourceGraph(graph: readonly RoomtypeObjectSnapshot[]): void { + for (const snapshot of graph) { + if (Array.isArray(snapshot.source) !== snapshot.isArray) { + throw new TypeError(`${snapshot.path}: unstable roomtype container kind`); + } + if (!Object.is(Object.getPrototypeOf(snapshot.source), snapshot.prototype)) { + throw new TypeError(`${snapshot.path}: unstable roomtype prototype`); + } + const keys = Reflect.ownKeys(snapshot.source); + if ( + keys.length !== snapshot.keys.length + || keys.some((key, index) => !Object.is(key, snapshot.keys[index])) + ) { + throw new TypeError(`${snapshot.path}: unstable roomtype own keys`); + } + for (const property of snapshot.properties) { + const descriptor = Reflect.getOwnPropertyDescriptor(snapshot.source, property.key); + if ( + descriptor === undefined + || !samePropertyDescriptor(property.descriptor, descriptor) + ) { + throw new TypeError(`${property.path}: unstable roomtype property descriptor`); + } + } + } +} + +export function cloneAndFreezeRoomtype(value: readonly unknown[]): readonly unknown[] { + const copies = new WeakMap(); + const active = new WeakSet(); + const ownedGraph: object[] = []; + const sourceGraph: RoomtypeObjectSnapshot[] = []; + + const copy = (current: unknown, path: string): unknown => { + if (current === null) return null; + if (typeof current === 'string' || typeof current === 'boolean') return current; + if (typeof current === 'number') { + if (!Number.isFinite(current)) { + throw new TypeError(`${path}: expected finite JSON number`); + } + return current; + } + if (typeof current !== 'object') { + throw new TypeError(`${path}: expected JSON value`); + } + if (active.has(current)) { + throw new TypeError(`${path}: cyclic roomtype reference`); + } + const priorCopy = copies.get(current); + if (priorCopy !== undefined) return priorCopy; + + const sourceIsArray = Array.isArray(current); + const prototype = Object.getPrototypeOf(current); + if (!sourceIsArray && prototype !== Object.prototype && prototype !== null) { + throw new TypeError(`${path}: expected JSON array or plain object container`); + } + + const keys = Reflect.ownKeys(current); + const properties: RoomtypePropertySnapshot[] = []; + for (const key of keys) { + if (typeof key !== 'string') { + throw new TypeError(`${path}: symbol properties are not valid roomtype data`); + } + const propertyPath = roomtypePropertyPath(path, key, sourceIsArray); + const descriptor = Reflect.getOwnPropertyDescriptor(current, key); + if (descriptor === undefined) { + throw new TypeError(`${propertyPath}: unstable property structure`); + } + properties.push({ key, path: propertyPath, descriptor }); + } + + const lengthProperty = properties.find((property) => property.key === 'length'); + let target: unknown[] | Record; + let arrayLength = 0; + if (sourceIsArray) { + if (lengthProperty === undefined || !('value' in lengthProperty.descriptor)) { + throw new TypeError(`${path}.length: expected stable data property`); + } + arrayLength = requireInteger(lengthProperty.descriptor.value, `${path}.length`); + if (arrayLength < 0) throw new RangeError(`${path}.length: expected non-negative integer`); + target = new Array(arrayLength); + } else { + target = prototype === null ? Object.create(null) as Record : {}; + } + + sourceGraph.push({ source: current, path, isArray: sourceIsArray, prototype, keys, properties }); + copies.set(current, target); + active.add(current); + ownedGraph.push(target); + for (const property of properties) { + const { key, path: propertyPath, descriptor } = property; + if (sourceIsArray && key === 'length') continue; + if (sourceIsArray) { + const index = Number(key); + if (!Number.isInteger(index) || index < 0 || index >= arrayLength || String(index) !== key) { + throw new TypeError(`${propertyPath}: expected array index`); + } + } + if (!('value' in descriptor)) { + throw new TypeError(`${propertyPath}: accessor properties are not allowed in roomtype`); + } + if (!descriptor.enumerable) { + throw new TypeError(`${propertyPath}: expected enumerable JSON data property`); + } + const copiedValue = copy(descriptor.value, propertyPath); + Object.defineProperty(target, key, { + configurable: true, + enumerable: true, + writable: true, + value: copiedValue, + }); + } + active.delete(current); + return target; + }; + + const canonical = copy(value, 'roomtype') as readonly unknown[]; + verifyRoomtypeSourceGraph(sourceGraph); + for (let index = ownedGraph.length - 1; index >= 0; index -= 1) { + Object.freeze(ownedGraph[index]); + } + return canonical; +} + +function assertSelfProfileMatches( + loginPlayer: PlatformPlayer, + roomPlayer: PlatformPlayer, + path: string, +): void { + for (const field of ['playerid', 'nickname', 'avatar', 'sex', 'bean'] as const) { + if (roomPlayer[field] !== loginPlayer[field]) { + throw new RangeError(`${path}.${field}: conflicts with authoritative login profile`); + } + } + if (roomPlayer.ip !== undefined && roomPlayer.ip !== loginPlayer.ip) { + throw new RangeError(`${path}.ip: conflicts with authoritative login profile`); + } +} + +function requireMatchingState( + parsedState: unknown, + raw: Readonly>, + path: string, +): number { + const state = requireInteger(parsedState, `${path}.state`); + const rawState = requireInteger(raw.state, `${path}.raw.state`); + if (state !== rawState) { + throw new RangeError(`${path}.state: expected parsed state to equal raw.state`); + } + return state; +} + +function buildRoom( + roomValue: unknown, + rawValue: unknown, + selfPlayer: PlatformPlayer, + stage: number, +): RoomBuildResult { + const room = requireRecord(roomValue, '$.room'); + const raw = requireRecord(rawValue, '$.raw'); + const roomcode = requireString(room.roomcode, '$.room.roomcode'); + const selfSeat = requireInteger(room.seat, '$.room.seat'); + const roomtype = requireArray(room.roomtype, '$.room.roomtype'); + const sourcePlayers = requireArray(room.players, '$.room.players'); + if (requireString(raw.roomcode, '$.raw.roomcode') !== roomcode) { + throw new RangeError('$.room.roomcode: expected value parsed from raw roomcode'); + } + if (requireInteger(raw.seat, '$.raw.seat') !== selfSeat) { + throw new RangeError('$.room.seat: expected value parsed from raw seat'); + } + if (raw.roomtype !== roomtype) { + throw new RangeError('$.room.roomtype: expected canonical raw roomtype reference'); + } + if (raw.players !== sourcePlayers) { + throw new RangeError('$.room.players: expected canonical raw players reference'); + } + if (selfSeat < 0 || selfSeat >= sourcePlayers.length) { + throw new RangeError('$.room.seat: expected server seat index within players'); + } + + const needprepare = requireProtocolFlag(raw.needprepare, '$.raw.needprepare'); + const infinite = requireProtocolFlag(raw.infinite, '$.raw.infinite'); + + const seatPlayerIds: (number | null)[] = []; + const readySeats: number[] = []; + const offlineSeats: number[] = []; + const parsedPlayers = new Map(); + + for (let seat = 0; seat < sourcePlayers.length; seat += 1) { + const value = sourcePlayers[seat]; + if (value === null) { + seatPlayerIds.push(null); + continue; + } + const parsed = parseRoomPlayer(value, `$.room.players[${seat}]`); + if (parsedPlayers.has(parsed.player.playerid)) { + throw new RangeError(`$.room.players[${seat}].playerid: duplicate player seat`); + } + parsedPlayers.set(parsed.player.playerid, parsed.player); + seatPlayerIds.push(parsed.player.playerid); + if (parsed.isprepare === 1) readySeats.push(seat); + if (parsed.onstate === 1) offlineSeats.push(seat); + } + + const selfRoomPlayer = parsedPlayers.get(selfPlayer.playerid); + if (seatPlayerIds[selfSeat] !== selfPlayer.playerid || selfRoomPlayer === undefined) { + throw new RangeError(`$.room.players[${selfSeat}].playerid: conflicts with authenticated player`); + } + assertSelfProfileMatches(selfPlayer, selfRoomPlayer, `$.room.players[${selfSeat}]`); + + const canonicalRoomtype = cloneAndFreezeRoomtype(roomtype); + const entities: Record = {}; + for (const [playerid, parsedPlayer] of parsedPlayers) { + const player = playerid === selfPlayer.playerid + ? { + ...selfPlayer, + isprepare: parsedPlayer.isprepare, + onstate: parsedPlayer.onstate, + } + : parsedPlayer; + entities[playerid] = Object.freeze(player); + } + + return { + players: Object.freeze({ + selfPlayerId: selfPlayer.playerid, + entities: Object.freeze(entities), + }), + room: Object.freeze({ + kind: 'inside', + roomcode, + selfSeat, + seatPlayerIds: Object.freeze(seatPlayerIds), + readySeats: Object.freeze(readySeats), + offlineSeats: Object.freeze(offlineSeats), + roomtype: canonicalRoomtype, + stage, + needprepare, + infinite, + }), + }; +} + +function requireInsideRoom(state: PlatformState): InsideRoomState { + if (state.room.kind !== 'inside') { + throw new Error('room: expected player to be inside a room'); + } + return state.room; +} + +function parseSeatAction(value: ParsedSeatPayload): { + readonly seat: number; + readonly raw: Readonly>; +} { + const input = requireRecord(value, '$.event'); + const seat = requireInteger(input.seat, '$.event.seat'); + const raw = requireRecord(input.raw, '$.event.raw'); + if (requireInteger(raw.seat, '$.event.raw.seat') !== seat) { + throw new RangeError('$.event.raw.seat: expected the parsed seat'); + } + return { seat, raw }; +} + +function stageAfterOptionalDeskwar( + raw: Readonly>, + currentStage: number, +): number { + if (!hasOwn(raw, 'deskwar')) return currentStage; + return requireProtocolFlag(raw.deskwar, '$.event.raw.deskwar') === 1 + ? 1 + : currentStage; +} + +function requireOccupiedSeat(room: InsideRoomState, seat: number): number { + if (seat < 0 || seat >= room.seatPlayerIds.length) { + throw new RangeError('$.event.seat: expected room seat range'); + } + const playerid = room.seatPlayerIds[seat]; + if (playerid === null) { + throw new RangeError('$.event.seat: expected an occupied seat'); + } + return playerid; +} + +const INITIAL_STATE: PlatformState = Object.freeze({ + app: Object.freeze({ phase: 'idle' }), + players: Object.freeze({ + selfPlayerId: null, + entities: Object.freeze({}), + }), + room: Object.freeze({ kind: 'outside' }), +}); + +export type PlatformStateListener = (state: PlatformState, previous: PlatformState) => void; + +interface PlatformTransition { + readonly state: PlatformState; + readonly previous: PlatformState; + readonly listeners: readonly PlatformStateListener[]; +} + +export class PlatformStore { + private state: PlatformState = INITIAL_STATE; + private readonly listeners = new Set(); + private readonly transitions: PlatformTransition[] = []; + private publishing = false; + + getState(): PlatformState { + return this.state; + } + + applyTaskList(payload:unknown):void { + const raw=requireRecord(payload,'$.data'); + const ids=new Set(); + const tasks=requireArray(raw.tasks,'$.data.tasks').map((value,index):TaskData=>{ + const path=`$.data.tasks[${index}]`,task=requireRecord(value,path); + const taskid=this.taskId(task.taskid); + if(ids.has(String(taskid)))throw Error(`${path}.taskid: duplicate task`); + ids.add(String(taskid)); + const title=requireString(task.title,`${path}.title`); + const award=requireTaskNumber(task.award,`${path}.award`),finish=requireTaskNumber(task.finish,`${path}.finish`); + const total=requireTaskNumber(task.total,`${path}.total`),state=requireInteger(requireTaskNumber(task.state,`${path}.state`),`${path}.state`); + if(award<0||finish<0||total<0||state<0||(state===0&&total===0))throw Error(`${path}: invalid task progress or state`); + return Object.freeze({taskid,title,award,finish,total,state}); + }); + this.taskPlayer(); + this.commit(Object.freeze({...this.state,tasks:Object.freeze(tasks)})); + } + + applyTaskAward(payload:unknown):void { + const raw=requireRecord(payload,'$.data'),taskid=this.taskId(raw.taskid); + const taskstate=requireInteger(raw.taskstate,'$.data.taskstate'); + const tasks=this.state.tasks; + if(!tasks||!tasks.some(task=>String(task.taskid)===String(taskid)))throw Error('Task award references an unloaded task'); + const player=this.taskPlayer(); + this.commit(Object.freeze({...this.state, + tasks:Object.freeze(tasks.map(task=>String(task.taskid)===String(taskid)?Object.freeze({...task,state:2}):task)), + players:Object.freeze({...this.state.players,entities:Object.freeze({...this.state.players.entities,[player.playerid]:Object.freeze({...player,taskstate})})}), + })); + } + + applyTaskFinished(payload:unknown):void { + const state=requireInteger(requireRecord(payload,'$.data').state,'$.data.state'); + const player=this.taskPlayer(); + const taskstate=requireInteger(player.taskstate,'players.self.taskstate'); + if(state===1&&taskstate===0)this.commit(Object.freeze({...this.state,players:Object.freeze({...this.state.players, + entities:Object.freeze({...this.state.players.entities,[player.playerid]:Object.freeze({...player,taskstate:1})}), + })})); + } + + applyRoomcardUpdate(payload:unknown):void { + const raw=requireRecord(payload,'$.data'); + // Net.update_roomcard only applies absolute balances when change is absent. + if(raw.change!==undefined)return; + const roomcard=requireNumber(raw.roomcard,'$.data.roomcard'),player=this.taskPlayer(); + this.commit(Object.freeze({...this.state,players:Object.freeze({...this.state.players, + entities:Object.freeze({...this.state.players.entities,[player.playerid]:Object.freeze({...player,roomcard})}), + })})); + } + + private taskPlayer():PlatformPlayer { + const id=this.state.players.selfPlayerId; + if(id===null||!this.state.players.entities[id])throw Error('Task response requires authenticated player'); + return this.state.players.entities[id]; + } + private taskId(value:unknown):number|string { + if(typeof value==='number'&&Number.isFinite(value))return value; + if(typeof value==='string'&&value.length>0)return value; + throw Error('Invalid taskid'); + } + + subscribe(listener: PlatformStateListener): Unsubscribe { + this.listeners.add(listener); + return (): void => { this.listeners.delete(listener); }; + } + + setConnectionPhase(phase: Exclude): void { + if (phase !== 'connected' && phase !== 'reconnecting' && phase !== 'slow' && phase !== 'kicked') { + throw new TypeError('app.phase: expected a wire connection phase'); + } + const previous = this.getState(); + if (previous.app.phase === phase) return; + this.commit(Object.freeze({ ...previous, app: Object.freeze({ phase }) })); + } + + applyVipRooms(data:unknown):void { + this.commit(Object.freeze({...this.state,vipRooms:parseVipRooms(data)})); + } + /** Absolute server balances; change-only purchase notices never overwrite money. */ + applyBeanUpdate(data:unknown):void { + const raw=requireRecord(data,'$.data');if(raw.change!==undefined)return; + let player=this.taskPlayer(); + if(raw.seat!==undefined){ + const seat=requireInteger(raw.seat,'$.data.seat'),room=this.state.room; + if(room.kind!=='inside')throw Error('update_bean seat requires a room'); + const id=room.seatPlayerIds[seat]; + if(id===undefined||id===null||!this.state.players.entities[id])throw Error('update_bean seat has no player'); + player=this.state.players.entities[id]; + } + const bean=requireNumber(raw.bean,'$.data.bean'); + this.commit(Object.freeze({...this.state,players:Object.freeze({...this.state.players, + entities:Object.freeze({...this.state.players.entities,[player.playerid]:Object.freeze({...player,bean})})})})); + } + applyVipRank(data:unknown):void { + const raw=requireRecord(data,'$.data'); + // Desk.getVipRankList ignores a missing/falsy list. A receipt still completes this request. + this.commit(Object.freeze({...this.state,vipRank:Object.freeze({entries:raw.list?parseVipRankList(raw.list):null})})); + } + applyTreasureList(data:unknown):void { + const raw=requireRecord(data,'$.data'); + this.commit(Object.freeze({...this.state,treasureList:parseRankList(raw.list)})); + } + + applyInvitecode(data:unknown):void { + const raw=requireRecord(data,'$.data'); + const invitecode=raw.invitecode===null?null:requireString(raw.invitecode,'$.data.invitecode'); + this.commitInvite('query',invitecode); + } + + applyInviteBinding(data:unknown):string { + const raw=requireRecord(data,'$.data'); + // Legacy binding_invitecode compares state == 0; the service also sends integer strings. + // Normalize at this response boundary, without changing the shared integer validator. + const stateValue=typeof raw.state==='string'&&/^[+-]?\d+$/.test(raw.state)?Number(raw.state):raw.state; + const state=requireInteger(stateValue,'$.data.state'),error=requireString(raw.error,'$.data.error'); + const invitecode=state===0?requireString(raw.invitecode,'$.data.invitecode'):this.taskPlayer().invitecode; + this.commitInvite('binding',invitecode); + return error; + } + + private commitInvite(kind:'query'|'binding',invitecode:string|null|undefined):void { + const player=this.taskPlayer(); + this.commit(Object.freeze({...this.state,inviteResponse:Object.freeze({kind}),players:Object.freeze({ + ...this.state.players,entities:Object.freeze({...this.state.players.entities,[player.playerid]:Object.freeze({...player,invitecode})}), + })})); + } + + private commit(state: PlatformState): PlatformState { + if (Object.is(state, this.state)) return state; + const previous = this.state; + this.state = state; + this.transitions.push({ + state, + previous, + listeners: [...this.listeners], + }); + if (this.publishing) return state; + + this.publishing = true; + try { + while (this.transitions.length > 0) { + const transition = this.transitions.shift(); + if (transition === undefined) continue; + for (const listener of transition.listeners) { + try { + listener(transition.state, transition.previous); + } catch (error) { + console.error('[reactive] subscriber error:', error); + } + } + } + } finally { + this.publishing = false; + } + return state; + } + + applyLoginSuccess(parsed: ParsedLoginResponse,authorizedIdentity?:Pick): PlatformState { + const input = requireRecord(parsed, '$.login'); + const raw = requireRecord(input.raw, '$.login.raw'); + // Source selection is explicit: authorization for non-device login, response otherwise. + const identity=authorizedIdentity===undefined?raw:authorizedIdentity; + const state = requireMatchingState(input.state, raw, '$.login'); + const rawHasRoom = LOGIN_ROOM_FIELDS.some((field) => hasOwn(raw, field)); + if (input.room === undefined || (input.room !== null) !== rawHasRoom) { + throw new TypeError('$.login.room: parsed room presence conflicts with raw room fields'); + } + if (state !== 0) { + throw new RangeError('$.login.state: expected successful state 0'); + } + const playerid = requireInteger(input.playerid, '$.login.playerid'); + if (playerid <= 0) { + throw new RangeError('$.login.playerid: expected positive integer'); + } + if (requireInteger(raw.playerid, '$.login.raw.playerid') !== playerid) { + throw new RangeError('$.login.raw.playerid: expected the parsed playerid'); + } + const player: PlatformPlayer = Object.freeze({ + playerid, + nickname: requireString(raw.nickname, '$.login.raw.nickname'), + ...(hasOwn(identity,'openid')?{openid:requireString(identity.openid,'$.login.identity.openid')}:{}), + ...(hasOwn(identity,'unionid')?{unionid:requireStringOrFiniteNumber(identity.unionid,'$.login.identity.unionid')}:{}), + ...(hasOwn(raw,'invitecode')?{invitecode:raw.invitecode===null?null:requireString(raw.invitecode,'$.login.raw.invitecode')}:{}), + avatar: requireString(raw.avatar, '$.login.raw.avatar'), + sex: requireInteger(raw.sex, '$.login.raw.sex'), + bean: requireNumber(raw.bean, '$.login.raw.bean'), + ip: requireString(raw.ip, '$.login.raw.ip'), + ...(hasOwn(raw, 'roomcard') ? {roomcard: requireNumber(raw.roomcard, '$.login.raw.roomcard')} : {}), + ...(hasOwn(raw, 'taskstate') ? {taskstate: requireInteger(raw.taskstate, '$.login.raw.taskstate')} : {}), + ...(hasOwn(raw, 'advanced') ? {advanced: requireInteger(raw.advanced, '$.login.raw.advanced')} : {}), + ...(hasOwn(raw, 'score') ? {score: requireNumber(raw.score, '$.login.raw.score')} : {}), + ...(hasOwn(raw, 'sign') ? {sign: raw.sign === null ? null : requireString(raw.sign, '$.login.raw.sign')} : {}), + }); + const roomResult = input.room === null + ? { + players: Object.freeze({ + selfPlayerId: playerid, + entities: Object.freeze({ [playerid]: player }), + }), + room: Object.freeze({ kind: 'outside' as const }), + } + : buildRoom( + input.room, + raw, + player, + requireProtocolFlag(raw.isbattle, '$.login.raw.isbattle'), + ); + return this.commit(Object.freeze({ + app: Object.freeze({ phase: 'logged-in' }), + players: roomResult.players, + room: roomResult.room, + })); + } + + createRoom(parsed: ParsedCreateRoomResponse, seatCount: number): PlatformState { + if (parsed.state !== 0 || parsed.room === null) throw new Error('createRoom requires success'); + const count = requirePositiveInteger(seatCount, 'GameEntry.resolveSeatCount'); + const {roomcode, seat, roomtype} = parsed.room; + if (seat < 0 || seat >= count) throw new RangeError('create_room seat outside GameEntry seat count'); + const previous = this.getState(); + const selfPlayerId = previous.players.selfPlayerId; + if (selfPlayerId === null) throw new Error('create_room requires authenticated player'); + const selfPlayer = previous.players.entities[selfPlayerId]; + if (selfPlayer === undefined) throw new Error('create_room missing login player'); + const seatPlayerIds: (number | null)[] = Array(count).fill(null); + seatPlayerIds[seat] = selfPlayerId; + // Desk.create_room seats C_Player and resets preparation. No response.players is fabricated. + return this.commit(Object.freeze({ + app: Object.freeze({phase: 'logged-in'}), + players: Object.freeze({selfPlayerId, entities: Object.freeze({ + [selfPlayerId]: Object.freeze({...selfPlayer, isprepare: 0, onstate: 0}), + })}), + room: Object.freeze({kind: 'inside', roomcode, selfSeat: seat, + seatPlayerIds: Object.freeze(seatPlayerIds), readySeats: Object.freeze([]), + offlineSeats: Object.freeze([]), roomtype: cloneAndFreezeRoomtype(roomtype), + stage: 0, needprepare: parsed.needprepare, infinite: parsed.infinite, + }), + })); + } + + replaceRoom(parsed: ParsedRoomResponse): PlatformState { + const input = requireRecord(parsed, '$.response'); + const raw = requireRecord(input.raw, '$.response.raw'); + const state = requireMatchingState(input.state, raw, '$.response'); + if (input.room === undefined || (input.room !== null) !== (state === 0)) { + throw new TypeError('$.response.room: parsed room presence conflicts with raw state'); + } + if (state !== 0) { + throw new RangeError('$.response.state: expected successful state 0'); + } + if (input.room === null) { + throw new TypeError('$.response.room: expected parsed room snapshot'); + } + const previous = this.getState(); + const selfPlayerId = previous.players.selfPlayerId; + if (selfPlayerId === null) { + throw new Error('players.selfPlayerId: expected authenticated player'); + } + const selfPlayer = previous.players.entities[selfPlayerId]; + if (selfPlayer === undefined) { + throw new Error('players.entities: missing authenticated player'); + } + const reconnect = requireRecord(input.reconnect, '$.response.reconnect'); + const reconnectPresent = requireBoolean( + reconnect.present, + '$.response.reconnect.present', + ); + const rawReconnectPresent = Boolean(raw.deskinfo); + if (reconnectPresent !== rawReconnectPresent) { + throw new RangeError( + '$.response.reconnect.present: expected deskinfo truthiness from raw response', + ); + } + if (reconnectPresent && reconnect.value !== raw.deskinfo) { + throw new RangeError( + '$.response.reconnect.value: expected canonical raw deskinfo reference', + ); + } + const stage = requireProtocolFlag(input.stage, '$.response.stage'); + if (reconnectPresent && stage !== 1) { + throw new RangeError('$.response.stage: reconnect room must be in battle stage'); + } + const roomResult = buildRoom(input.room, raw, selfPlayer, stage); + return this.commit(Object.freeze({ + // A switch authenticates with connect_*server, then self_join_room instead of player_login. + app: previous.app.phase === 'logged-in' + ? previous.app + : Object.freeze({ phase: 'logged-in' }), + players: roomResult.players, + room: roomResult.room, + })); + } + + clearRoom(): void { + const previous = this.getState(); + requireInsideRoom(previous); + const selfPlayerId = previous.players.selfPlayerId; + if (selfPlayerId === null) { + throw new Error('players.selfPlayerId: expected authenticated player'); + } + const selfPlayer = previous.players.entities[selfPlayerId]; + if (selfPlayer === undefined) { + throw new Error('players.entities: missing authenticated player'); + } + this.commit(Object.freeze({ + app: previous.app, + players: Object.freeze({ + selfPlayerId, + entities: Object.freeze({ [selfPlayerId]: selfPlayer }), + }), + room: Object.freeze({ kind: 'outside' }), + })); + } + + playerJoin(parsed: ParsedSeatPayload): void { + const event = parseSeatAction(parsed); + const previous = this.getState(); + const room = requireInsideRoom(previous); + if (event.seat < 0 || event.seat >= room.seatPlayerIds.length) { + throw new RangeError('$.event.seat: expected occupied room seat range'); + } + if (room.seatPlayerIds[event.seat] !== null) { + throw new RangeError('$.event.seat: expected an empty seat'); + } + const joined = parseRoomPlayer(event.raw, '$.event.raw'); + if (room.seatPlayerIds.includes(joined.player.playerid)) { + throw new RangeError('$.event.raw.playerid: duplicate player seat'); + } + const stage = stageAfterOptionalDeskwar(event.raw, room.stage); + + const player = Object.freeze(joined.player); + const entities = Object.freeze({ + ...previous.players.entities, + [player.playerid]: player, + }); + const seatPlayerIds = [...room.seatPlayerIds]; + seatPlayerIds[event.seat] = player.playerid; + const readySeats = joined.isprepare === 1 + ? [...room.readySeats, event.seat] + : [...room.readySeats]; + const offlineSeats = joined.onstate === 1 + ? [...room.offlineSeats, event.seat] + : [...room.offlineSeats]; + + this.commit(Object.freeze({ + app: previous.app, + players: Object.freeze({ + selfPlayerId: previous.players.selfPlayerId, + entities, + }), + room: Object.freeze({ + ...room, + seatPlayerIds: Object.freeze(seatPlayerIds), + readySeats: Object.freeze(readySeats), + offlineSeats: Object.freeze(offlineSeats), + stage, + }), + })); + } + + playerReady(parsed: ParsedSeatPayload): void { + const event = parseSeatAction(parsed); + const previous = this.getState(); + const room = requireInsideRoom(previous); + const playerid = requireOccupiedSeat(room, event.seat); + if (room.readySeats.includes(event.seat)) { + throw new RangeError('$.event.seat: player is already ready'); + } + const player = previous.players.entities[playerid]; + if (player === undefined) { + throw new RangeError('players.entities: occupied seat has no canonical player'); + } + const stage = stageAfterOptionalDeskwar(event.raw, room.stage); + const entities = Object.freeze({ + ...previous.players.entities, + [playerid]: Object.freeze({ ...player, isprepare: 1 }), + }); + const readySeats = Object.freeze([...room.readySeats, event.seat]); + this.commit(Object.freeze({ + app: previous.app, + players: Object.freeze({ + selfPlayerId: previous.players.selfPlayerId, + entities, + }), + room: Object.freeze({ ...room, readySeats, stage }), + })); + } + + playerOffline(parsed: ParsedSeatPayload): void { + const event = parseSeatAction(parsed); + const previous = this.getState(); + const room = requireInsideRoom(previous); + const playerid = requireOccupiedSeat(room, event.seat); + if (room.offlineSeats.includes(event.seat)) { + throw new RangeError('$.event.seat: player is already offline'); + } + const player = previous.players.entities[playerid]; + if (player === undefined) { + throw new RangeError('players.entities: occupied seat has no canonical player'); + } + const entities = Object.freeze({ + ...previous.players.entities, + [playerid]: Object.freeze({ ...player, onstate: 1 }), + }); + const offlineSeats = Object.freeze([...room.offlineSeats, event.seat]); + this.commit(Object.freeze({ + app: previous.app, + players: Object.freeze({ + selfPlayerId: previous.players.selfPlayerId, + entities, + }), + room: Object.freeze({ ...room, offlineSeats }), + })); + } + + playerOnline(parsed: ParsedOnlinePayload): void { + const event = parseSeatAction(parsed); + const input = requireRecord(parsed, '$.event'); + const ip = requireString(input.ip, '$.event.ip'); + if (requireString(event.raw.ip, '$.event.raw.ip') !== ip) { + throw new RangeError('$.event.raw.ip: expected the parsed ip'); + } + const previous = this.getState(); + const room = requireInsideRoom(previous); + const playerid = requireOccupiedSeat(room, event.seat); + if (!room.offlineSeats.includes(event.seat)) { + throw new RangeError('$.event.seat: player is already online'); + } + const player = previous.players.entities[playerid]; + if (player === undefined) { + throw new RangeError('players.entities: occupied seat has no canonical player'); + } + const entities = Object.freeze({ + ...previous.players.entities, + [playerid]: Object.freeze({ ...player, ip, onstate: 0 }), + }); + const offlineSeats = Object.freeze( + room.offlineSeats.filter((seat) => seat !== event.seat), + ); + this.commit(Object.freeze({ + app: previous.app, + players: Object.freeze({ + selfPlayerId: previous.players.selfPlayerId, + entities, + }), + room: Object.freeze({ ...room, offlineSeats }), + })); + } + + playerExit(parsed: ParsedSeatPayload): void { + const event = parseSeatAction(parsed); + const previous = this.getState(); + const room = requireInsideRoom(previous); + const playerid = requireOccupiedSeat(room, event.seat); + if (event.seat === room.selfSeat) { + throw new RangeError('$.event.seat: self exit must clear the room'); + } + + const entities: Record = { + ...previous.players.entities, + }; + delete entities[playerid]; + const seatPlayerIds = [...room.seatPlayerIds]; + seatPlayerIds[event.seat] = null; + const readySeats = room.readySeats.filter((seat) => seat !== event.seat); + const offlineSeats = room.offlineSeats.filter((seat) => seat !== event.seat); + + this.commit(Object.freeze({ + app: previous.app, + players: Object.freeze({ + selfPlayerId: previous.players.selfPlayerId, + entities: Object.freeze(entities), + }), + room: Object.freeze({ + ...room, + seatPlayerIds: Object.freeze(seatPlayerIds), + readySeats: Object.freeze(readySeats), + offlineSeats: Object.freeze(offlineSeats), + }), + })); + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts index 12af4d2..21bddd7 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts @@ -1,61 +1,68 @@ -import type { PlatformGameConnectionPhase } from '../../sdk/contracts/snapshots.ts'; -import type { TaskData } from '../../core/types/task.ts'; - -export type PlatformConnectionPhase = - | 'idle' - | 'connecting' - | PlatformGameConnectionPhase; - -export interface PlatformAppState { - readonly phase: PlatformConnectionPhase; -} - -export interface PlatformPlayer { - readonly playerid: number; - readonly nickname: string; - readonly avatar: string; - readonly sex: number; - readonly bean: number; - /** Account-only fields are absent on other room players; never default them in views. */ +import type { PlatformGameConnectionPhase } from '../../sdk/contracts/snapshots.ts'; +import type { TaskData } from '../../core/types/task.ts'; + +export type PlatformConnectionPhase = + | 'idle' + | 'connecting' + | PlatformGameConnectionPhase; + +export interface PlatformAppState { + readonly phase: PlatformConnectionPhase; +} + +export interface PlatformPlayer { + readonly playerid: number; + readonly nickname: string; + readonly avatar: string; + readonly sex: number; + readonly bean: number; + /** Account-only fields are absent on other room players; never default them in views. */ readonly roomcard?: number; - readonly taskstate?: number; - readonly advanced?: number; - readonly score?: number; - readonly sign?: string | null; - readonly ip?: string; - readonly isprepare?: number; - readonly onstate?: number; -} - -export interface PlayerDirectoryState { - readonly selfPlayerId: number | null; - readonly entities: Readonly>; -} - -export interface OutsideRoomState { - readonly kind: 'outside'; -} - -export interface InsideRoomState { - readonly kind: 'inside'; - readonly roomcode: string; - readonly selfSeat: number; - readonly seatPlayerIds: readonly (number | null)[]; - readonly readySeats: readonly number[]; - readonly offlineSeats: readonly number[]; - readonly roomtype: readonly unknown[]; - readonly stage: number; - readonly needprepare: number; - readonly infinite: number; -} - -export type RoomState = OutsideRoomState | InsideRoomState; - + readonly openid?: string; + readonly unionid?: string | number; + readonly invitecode?: string | null; + readonly taskstate?: number; + readonly advanced?: number; + readonly score?: number; + readonly sign?: string | null; + readonly ip?: string; + readonly isprepare?: number; + readonly onstate?: number; +} + +export interface PlayerDirectoryState { + readonly selfPlayerId: number | null; + readonly entities: Readonly>; +} + +export interface OutsideRoomState { + readonly kind: 'outside'; +} + +export interface InsideRoomState { + readonly kind: 'inside'; + readonly roomcode: string; + readonly selfSeat: number; + readonly seatPlayerIds: readonly (number | null)[]; + readonly readySeats: readonly number[]; + readonly offlineSeats: readonly number[]; + readonly roomtype: readonly unknown[]; + readonly stage: number; + readonly needprepare: number; + readonly infinite: number; +} + +export type RoomState = OutsideRoomState | InsideRoomState; + export interface PlatformState { - readonly vipRooms?: readonly import('../../core/types/vip-room.ts').VipRoom[]; - /** Absent until a task list response arrives; cleared by login replacement. */ - readonly tasks?: readonly TaskData[]; - readonly app: PlatformAppState; - readonly players: PlayerDirectoryState; - readonly room: RoomState; -} + readonly vipRank?:{readonly entries:readonly import('../../protocol/contracts/rank-list.ts').RankEntry[]|null}; + readonly treasureList?:readonly import('../../protocol/contracts/rank-list.ts').RankEntry[]; + /** Receipt identity allows repeated queries with an unchanged code to open the view. */ + readonly inviteResponse?: {readonly kind:'query'|'binding'}; + readonly vipRooms?: readonly import('../../core/types/vip-room.ts').VipRoom[]; + /** Absent until a task list response arrives; cleared by login replacement. */ + readonly tasks?: readonly TaskData[]; + readonly app: PlatformAppState; + readonly players: PlayerDirectoryState; + readonly room: RoomState; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/join-room-input.ts b/cocoscreator_projects/YouleNexus/assets/framework/presentation/join-room-input.ts new file mode 100644 index 0000000..62d642d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/join-room-input.ts @@ -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));} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/join-room-input.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/presentation/join-room-input.ts.meta new file mode 100644 index 0000000..1011558 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/join-room-input.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "9301c0f4-8779-44f8-8d19-5f540b499e82", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/legacy-auth-districts.ts b/cocoscreator_projects/YouleNexus/assets/framework/presentation/legacy-auth-districts.ts new file mode 100644 index 0000000..8799c6f --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/legacy-auth-districts.ts @@ -0,0 +1,2 @@ +/** Exact migration source: Game_Surface_3/js/00_Surface/03_Banwords.js GB2260. */ +export const LEGACY_AUTH_DISTRICTS:Readonly> = {"110000":"北京市","110100":"北京市市辖区","110101":"北京市东城区","110102":"北京市西城区","110103":"北京市崇文区","110104":"北京市宣武区","110105":"北京市朝阳区","110106":"北京市丰台区","110107":"北京市石景山区","110108":"北京市海淀区","110109":"北京市门头沟区","110111":"北京市房山区","110112":"北京市通州区","110113":"北京市顺义区","110200":"北京市县","110221":"北京市昌平县","110224":"北京市大兴县","110226":"北京市平谷县","110227":"北京市怀柔县","110228":"北京市密云县","110229":"北京市延庆县","120000":"天津市","120100":"天津市市辖区","120101":"天津市和平区","120102":"天津市河东区","120103":"天津市河西区","120104":"天津市南开区","120105":"天津市河北区","120106":"天津市红桥区","120107":"天津市塘沽区","120108":"天津市汉沽区","120109":"天津市大港区","120110":"天津市东丽区","120111":"天津市西青区","120112":"天津市津南区","120113":"天津市北辰区","120200":"天津市县","120221":"天津市宁河县","120222":"天津市武清县","120223":"天津市静海县","120224":"天津市宝坻县","120225":"天津市蓟县","130000":"河北省","130100":"河北省石家庄市","130101":"河北省石家庄市市辖区","130102":"河北省石家庄市长安区","130103":"河北省石家庄市桥东区","130104":"河北省石家庄市桥西区","130105":"河北省石家庄市新华区","130106":"河北省石家庄市郊区","130107":"河北省石家庄市井陉矿区","130121":"河北省石家庄市井陉县","130123":"河北省石家庄市正定县","130124":"河北省石家庄市栾城县","130125":"河北省石家庄市行唐县","130126":"河北省石家庄市灵寿县","130127":"河北省石家庄市高邑县","130128":"河北省石家庄市深泽县","130129":"河北省石家庄市赞皇县","130130":"河北省石家庄市无极县","130131":"河北省石家庄市平山县","130132":"河北省石家庄市元氏县","130133":"河北省石家庄市赵县","130181":"河北省石家庄市辛集市","130182":"河北省石家庄市藁城市","130183":"河北省石家庄市晋州市","130184":"河北省石家庄市新乐市","130185":"河北省石家庄市鹿泉市","130200":"河北省唐山市","130201":"河北省唐山市市辖区","130202":"河北省唐山市路南区","130203":"河北省唐山市路北区","130204":"河北省唐山市古冶区","130205":"河北省唐山市开平区","130206":"河北省唐山市新区","130221":"河北省唐山市丰润县","130223":"河北省唐山市滦县","130224":"河北省唐山市滦南县","130225":"河北省唐山市乐亭县","130227":"河北省唐山市迁西县","130229":"河北省唐山市玉田县","130230":"河北省唐山市唐海县","130281":"河北省唐山市遵化市","130282":"河北省唐山市丰南市","130283":"河北省唐山市迁安市","130300":"河北省秦皇岛市秦皇岛市","130301":"河北省秦皇岛市市辖区","130302":"河北省秦皇岛市海港区","130303":"河北省秦皇岛市山海关区","130304":"河北省秦皇岛市北戴河区","130321":"河北省秦皇岛市青龙满族自治县","130322":"河北省秦皇岛市昌黎县","130323":"河北省秦皇岛市抚宁县","130324":"河北省秦皇岛市卢龙县","130400":"河北省邯郸市邯郸市","130401":"河北省邯郸市市辖区","130402":"河北省邯郸市邯山区","130403":"河北省邯郸市丛台区","130404":"河北省邯郸市复兴区","130406":"河北省邯郸市峰峰矿区","130421":"河北省邯郸市邯郸县","130423":"河北省邯郸市临漳县","130424":"河北省邯郸市成安县","130425":"河北省邯郸市大名县","130426":"河北省邯郸市涉县","130427":"河北省邯郸市磁县","130428":"河北省邯郸市肥乡县","130429":"河北省邯郸市永年县","130430":"河北省邯郸市邱县","130431":"河北省邯郸市鸡泽县","130432":"河北省邯郸市广平县","130433":"河北省邯郸市馆陶县","130434":"河北省邯郸市魏县","130435":"河北省邯郸市曲周县","130481":"河北省邯郸市武安市","130500":"河北省邢台市","130501":"河北省邢台市市辖区","130502":"河北省邢台市桥东区","130503":"河北省邢台市桥西区","130521":"河北省邢台市邢台县","130522":"河北省邢台市临城县","130523":"河北省邢台市内丘县","130524":"河北省邢台市柏乡县","130525":"河北省邢台市隆尧县","130526":"河北省邢台市任县","130527":"河北省邢台市南和县","130528":"河北省邢台市宁晋县","130529":"河北省邢台市巨鹿县","130530":"河北省邢台市新河县","130531":"河北省邢台市广宗县","130532":"河北省邢台市平乡县","130533":"河北省邢台市威县","130534":"河北省邢台市清河县","130535":"河北省邢台市临西县","130581":"河北省邢台市南宫市","130582":"河北省邢台市沙河市","130600":"河北省保定市","130601":"河北省保定市市辖区","130602":"河北省保定市新市区","130603":"河北省保定市北市区","130604":"河北省保定市南市区","130621":"河北省保定市满城县","130622":"河北省保定市清苑县","130623":"河北省保定市涞水县","130624":"河北省保定市阜平县","130625":"河北省保定市徐水县","130626":"河北省保定市定兴县","130627":"河北省保定市唐县","130628":"河北省保定市高阳县","130629":"河北省保定市容城县","130630":"河北省保定市涞源县","130631":"河北省保定市望都县","130632":"河北省保定市安新县","130633":"河北省保定市易县","130634":"河北省保定市曲阳县","130635":"河北省保定市蠡县","130636":"河北省保定市顺平县","130637":"河北省保定市博野县","130638":"河北省保定市雄县","130681":"河北省保定市涿州市","130682":"河北省保定市定州市","130683":"河北省保定市安国市","130684":"河北省保定市高碑店市","130700":"河北省张家口市","130701":"河北省张家口市市辖区","130702":"河北省张家口市桥东区","130703":"河北省张家口市桥西区","130705":"河北省张家口市宣化区","130706":"河北省张家口市下花园区","130721":"河北省张家口市宣化县","130722":"河北省张家口市张北县","130723":"河北省张家口市康保县","130724":"河北省张家口市沽源县","130725":"河北省张家口市尚义县","130726":"河北省张家口市蔚县","130727":"河北省张家口市阳原县","130728":"河北省张家口市怀安县","130729":"河北省张家口市万全县","130730":"河北省张家口市怀来县","130731":"河北省张家口市涿鹿县","130732":"河北省张家口市赤城县","130733":"河北省张家口市崇礼县","130800":"河北省承德市","130801":"河北省承德市市辖区","130802":"河北省承德市双桥区","130803":"河北省承德市双滦区","130804":"河北省承德市鹰手营子矿区","130821":"河北省承德市承德县","130822":"河北省承德市兴隆县","130823":"河北省承德市平泉县","130824":"河北省承德市滦平县","130825":"河北省承德市隆化县","130826":"河北省承德市丰宁满族自治县","130827":"河北省承德市宽城满族自治县","130828":"河北省承德市围场满族蒙古族自治县","130900":"河北省沧州市","130901":"河北省沧州市市辖区","130902":"河北省沧州市新华区","130903":"河北省沧州市运河区","130921":"河北省沧州市沧县","130922":"河北省沧州市青县","130923":"河北省沧州市东光县","130924":"河北省沧州市海兴县","130925":"河北省沧州市盐山县","130926":"河北省沧州市肃宁县","130927":"河北省沧州市南皮县","130928":"河北省沧州市吴桥县","130929":"河北省沧州市献县","130930":"河北省沧州市孟村回族自治县","130981":"河北省沧州市泊头市","130982":"河北省沧州市任丘市","130983":"河北省沧州市黄骅市","130984":"河北省沧州市河间市","131000":"河北省廊坊市","131001":"河北省廊坊市市辖区","131002":"河北省廊坊市安次区","131022":"河北省廊坊市固安县","131023":"河北省廊坊市永清县","131024":"河北省廊坊市香河县","131025":"河北省廊坊市大城县","131026":"河北省廊坊市文安县","131028":"河北省廊坊市大厂回族自治县","131081":"河北省廊坊市霸州市","131082":"河北省廊坊市三河市","131100":"河北省衡水市","131101":"河北省衡水市市辖区","131102":"河北省衡水市桃城区","131121":"河北省衡水市枣强县","131122":"河北省衡水市武邑县","131123":"河北省衡水市武强县","131124":"河北省衡水市饶阳县","131125":"河北省衡水市安平县","131126":"河北省衡水市故城县","131127":"河北省衡水市景县","131128":"河北省衡水市阜城县","131181":"河北省衡水市冀州市","131182":"河北省衡水市深州市","140000":"山西省","140100":"山西省太原市","140101":"山西省太原市市辖区","140105":"山西省太原市小店区","140106":"山西省太原市迎泽区","140107":"山西省太原市杏花岭区","140108":"山西省太原市尖草坪区","140109":"山西省太原市万柏林区","140110":"山西省太原市晋源区","140121":"山西省太原市清徐县","140122":"山西省太原市阳曲县","140123":"山西省太原市娄烦县","140181":"山西省太原市古交市","140200":"山西省大同市","140201":"山西省大同市市辖区","140202":"山西省大同市城区","140203":"山西省大同市矿区","140211":"山西省大同市南郊区","140212":"山西省大同市新荣区","140221":"山西省大同市阳高县","140222":"山西省大同市天镇县","140223":"山西省大同市广灵县","140224":"山西省大同市灵丘县","140225":"山西省大同市浑源县","140226":"山西省大同市左云县","140227":"山西省大同市大同县","140300":"山西省阳泉市","140301":"山西省阳泉市市辖区","140302":"山西省阳泉市城区","140303":"山西省阳泉市矿区","140311":"山西省阳泉市郊区","140321":"山西省阳泉市平定县","140322":"山西省阳泉市盂县","140400":"山西省长治市","140401":"山西省长治市市辖区","140402":"山西省长治市城区","140411":"山西省长治市郊区","140421":"山西省长治市长治县","140423":"山西省长治市襄垣县","140424":"山西省长治市屯留县","140425":"山西省长治市平顺县","140426":"山西省长治市黎城县","140427":"山西省长治市壶关县","140428":"山西省长治市长子县","140429":"山西省长治市武乡县","140430":"山西省长治市沁县","140431":"山西省长治市沁源县","140481":"山西省长治市潞城市","140500":"山西省晋城市","140501":"山西省晋城市市辖区","140502":"山西省晋城市城区","140521":"山西省晋城市沁水县","140522":"山西省晋城市阳城县","140524":"山西省晋城市陵川县","140525":"山西省晋城市泽州县","140581":"山西省晋城市高平市","140600":"山西省晋城市朔州市","140601":"山西省晋城市市辖区","140602":"山西省晋城市朔城区","140603":"山西省晋城市平鲁区","140621":"山西省晋城市山阴县","140622":"山西省晋城市应县","140623":"山西省晋城市右玉县","140624":"山西省晋城市怀仁县","142200":"山西省忻州地区","142201":"山西省忻州地区忻州市","142202":"山西省忻州地区原平市","142222":"山西省忻州地区定襄县","142223":"山西省忻州地区五台县","142225":"山西省忻州地区代县","142226":"山西省忻州地区繁峙县","142227":"山西省忻州地区宁武县","142228":"山西省忻州地区静乐县","142229":"山西省忻州地区神池县","142230":"山西省忻州地区五寨县","142231":"山西省忻州地区岢岚县","142232":"山西省忻州地区河曲县","142233":"山西省忻州地区保德县","142234":"山西省忻州地区偏关县","142300":"山西省忻州地区吕梁地区","142301":"山西省忻州地区孝义市","142302":"山西省忻州地区离石市","142303":"山西省忻州地区汾阳市","142322":"山西省忻州地区文水县","142323":"山西省忻州地区交城县","142325":"山西省忻州地区兴县","142326":"山西省忻州地区临县","142327":"山西省忻州地区柳林县","142328":"山西省忻州地区石楼县","142329":"山西省忻州地区岚县","142330":"山西省忻州地区方山县","142332":"山西省忻州地区中阳县","142333":"山西省忻州地区交口县","142400":"山西省晋中地区","142401":"山西省晋中地区榆次市","142402":"山西省晋中地区介休市","142421":"山西省晋中地区榆社县","142422":"山西省晋中地区左权县","142423":"山西省晋中地区和顺县","142424":"山西省晋中地区昔阳县","142427":"山西省晋中地区寿阳县","142429":"山西省晋中地区太谷县","142430":"山西省晋中地区祁县","142431":"山西省晋中地区平遥县","142433":"山西省晋中地区灵石县","142600":"山西省临汾地区","142601":"山西省临汾地区临汾市","142602":"山西省临汾地区侯马市","142603":"山西省临汾地区霍州市","142621":"山西省临汾地区曲沃县","142622":"山西省临汾地区翼城县","142623":"山西省临汾地区襄汾县","142625":"山西省临汾地区洪洞县","142627":"山西省临汾地区古县","142628":"山西省临汾地区安泽县","142629":"山西省临汾地区浮山县","142630":"山西省临汾地区吉县","142631":"山西省临汾地区乡宁县","142632":"山西省临汾地区蒲县","142633":"山西省临汾地区大宁县","142634":"山西省临汾地区永和县","142635":"山西省临汾地区隰县","142636":"山西省临汾地区汾西县","142700":"山西省运城地区","142701":"山西省运城地区运城市","142702":"山西省运城地区永济市","142703":"山西省运城地区河津市","142723":"山西省运城地区芮城县","142724":"山西省运城地区临猗县","142725":"山西省运城地区万荣县","142726":"山西省运城地区新绛县","142727":"山西省运城地区稷山县","142729":"山西省运城地区闻喜县","142730":"山西省运城地区夏县","142731":"山西省运城地区绛县","142732":"山西省运城地区平陆县","142733":"山西省运城地区垣曲县","150000":"内蒙古自治区","150100":"内蒙古自治区呼和浩特市","150101":"内蒙古自治区呼和浩特市市辖区","150102":"内蒙古自治区呼和浩特市新城区","150103":"内蒙古自治区呼和浩特市回民区","150104":"内蒙古自治区呼和浩特市玉泉区","150105":"内蒙古自治区呼和浩特市郊区","150121":"内蒙古自治区呼和浩特市土默特左旗","150122":"内蒙古自治区呼和浩特市托克托县","150123":"内蒙古自治区呼和浩特市和林格尔县","150124":"内蒙古自治区呼和浩特市清水河县","150125":"内蒙古自治区呼和浩特市武川县","150200":"内蒙古自治区包头市","150201":"内蒙古自治区包头市市辖区","150202":"内蒙古自治区包头市东河区","150203":"内蒙古自治区包头市昆都伦区","150204":"内蒙古自治区包头市青山区","150205":"内蒙古自治区包头市石拐矿区","150206":"内蒙古自治区包头市白云矿区","150207":"内蒙古自治区包头市郊区","150221":"内蒙古自治区包头市土默特右旗","150222":"内蒙古自治区包头市固阳县","150223":"内蒙古自治区包头市达尔罕茂明安联合旗","150300":"内蒙古自治区乌海市","150301":"内蒙古自治区乌海市市辖区","150302":"内蒙古自治区乌海市海勃湾区","150303":"内蒙古自治区乌海市海南区","150304":"内蒙古自治区乌海市乌达区","150400":"内蒙古自治区赤峰市","150401":"内蒙古自治区赤峰市市辖区","150402":"内蒙古自治区赤峰市红山区","150403":"内蒙古自治区赤峰市元宝山区","150404":"内蒙古自治区赤峰市松山区","150421":"内蒙古自治区赤峰市阿鲁科尔沁旗","150422":"内蒙古自治区赤峰市巴林左旗","150423":"内蒙古自治区赤峰市巴林右旗","150424":"内蒙古自治区赤峰市林西县","150425":"内蒙古自治区赤峰市克什克腾旗","150426":"内蒙古自治区赤峰市翁牛特旗","150428":"内蒙古自治区赤峰市喀喇沁旗","150429":"内蒙古自治区赤峰市宁城县","150430":"内蒙古自治区赤峰市敖汉旗","152100":"内蒙古自治区呼伦贝尔盟","152101":"内蒙古自治区呼伦贝尔盟海拉尔市","152102":"内蒙古自治区呼伦贝尔盟满洲里市","152103":"内蒙古自治区呼伦贝尔盟扎兰屯市","152104":"内蒙古自治区呼伦贝尔盟牙克石市","152105":"内蒙古自治区呼伦贝尔盟根河市","152106":"内蒙古自治区呼伦贝尔盟额尔古纳市","152122":"内蒙古自治区呼伦贝尔盟阿荣旗","152123":"内蒙古自治区呼伦贝尔盟莫力达瓦达斡尔族自治旗","152127":"内蒙古自治区呼伦贝尔盟鄂伦春自治旗","152128":"内蒙古自治区呼伦贝尔盟鄂温克族自治旗","152129":"内蒙古自治区呼伦贝尔盟新巴尔虎右旗","152130":"内蒙古自治区呼伦贝尔盟新巴尔虎左旗","152131":"内蒙古自治区呼伦贝尔盟陈巴尔虎旗","152200":"内蒙古自治区兴安盟","152201":"内蒙古自治区兴安盟乌兰浩特市","152202":"内蒙古自治区兴安盟阿尔山市","152221":"内蒙古自治区兴安盟科尔沁右翼前旗","152222":"内蒙古自治区兴安盟科尔沁右翼中旗","152223":"内蒙古自治区兴安盟扎赉特旗","152224":"内蒙古自治区兴安盟突泉县","152300":"内蒙古自治区哲里木盟","152301":"内蒙古自治区哲里木盟通辽市","152302":"内蒙古自治区哲里木盟霍林郭勒市","152322":"内蒙古自治区哲里木盟科尔沁左翼中旗","152323":"内蒙古自治区哲里木盟科尔沁左翼后旗","152324":"内蒙古自治区哲里木盟开鲁县","152325":"内蒙古自治区哲里木盟库伦旗","152326":"内蒙古自治区哲里木盟奈曼旗","152327":"内蒙古自治区哲里木盟扎鲁特旗","152500":"内蒙古自治区锡林郭勒盟","152501":"内蒙古自治区锡林郭勒盟二连浩特市","152502":"内蒙古自治区锡林郭勒盟锡林浩特市","152522":"内蒙古自治区锡林郭勒盟阿巴嘎旗","152523":"内蒙古自治区锡林郭勒盟苏尼特左旗","152524":"内蒙古自治区锡林郭勒盟苏尼特右旗","152525":"内蒙古自治区锡林郭勒盟东乌珠穆沁旗","152526":"内蒙古自治区锡林郭勒盟西乌珠穆沁旗","152527":"内蒙古自治区锡林郭勒盟太仆寺旗","152528":"内蒙古自治区锡林郭勒盟镶黄旗","152529":"内蒙古自治区锡林郭勒盟正镶白旗","152530":"内蒙古自治区锡林郭勒盟正蓝旗","152531":"内蒙古自治区锡林郭勒盟多伦县","152600":"内蒙古自治区乌兰察布盟","152601":"内蒙古自治区乌兰察布盟集宁市","152602":"内蒙古自治区乌兰察布盟丰镇市","152624":"内蒙古自治区乌兰察布盟卓资县","152625":"内蒙古自治区乌兰察布盟化德县","152626":"内蒙古自治区乌兰察布盟商都县","152627":"内蒙古自治区乌兰察布盟兴和县","152629":"内蒙古自治区乌兰察布盟凉城县","152630":"内蒙古自治区乌兰察布盟察哈尔右翼前旗","152631":"内蒙古自治区乌兰察布盟察哈尔右翼中旗","152632":"内蒙古自治区乌兰察布盟察哈尔右翼后旗","152634":"内蒙古自治区乌兰察布盟四子王旗","152700":"内蒙古自治区伊克昭盟","152701":"内蒙古自治区伊克昭盟东胜市","152722":"内蒙古自治区伊克昭盟达拉特旗","152723":"内蒙古自治区伊克昭盟准格尔旗","152724":"内蒙古自治区伊克昭盟鄂托克前旗","152725":"内蒙古自治区伊克昭盟鄂托克旗","152726":"内蒙古自治区伊克昭盟杭锦旗","152727":"内蒙古自治区伊克昭盟乌审旗","152728":"内蒙古自治区伊克昭盟伊金霍洛旗","152800":"内蒙古自治区巴彦淖尔盟","152801":"内蒙古自治区巴彦淖尔盟临河市","152822":"内蒙古自治区巴彦淖尔盟五原县","152823":"内蒙古自治区巴彦淖尔盟磴口县","152824":"内蒙古自治区巴彦淖尔盟乌拉特前旗","152825":"内蒙古自治区巴彦淖尔盟乌拉特中旗","152826":"内蒙古自治区巴彦淖尔盟乌拉特后旗","152827":"内蒙古自治区巴彦淖尔盟杭锦后旗","152900":"内蒙古自治区阿拉善盟","152921":"内蒙古自治区阿拉善盟阿拉善左旗","152922":"内蒙古自治区阿拉善盟阿拉善右旗","152923":"内蒙古自治区阿拉善盟额济纳旗","210000":"辽宁省","210100":"辽宁省沈阳市","210101":"辽宁省沈阳市市辖区","210102":"辽宁省沈阳市和平区","210103":"辽宁省沈阳市沈河区","210104":"辽宁省沈阳市大东区","210105":"辽宁省沈阳市皇姑区","210106":"辽宁省沈阳市铁西区","210111":"辽宁省沈阳市苏家屯区","210112":"辽宁省沈阳市东陵区","210113":"辽宁省沈阳市新城子区","210114":"辽宁省沈阳市于洪区","210122":"辽宁省沈阳市辽中县","210123":"辽宁省沈阳市康平县","210124":"辽宁省沈阳市法库县","210181":"辽宁省沈阳市新民市","210200":"辽宁省大连市","210201":"辽宁省大连市市辖区","210202":"辽宁省大连市中山区","210203":"辽宁省大连市西岗区","210204":"辽宁省大连市沙河口区","210211":"辽宁省大连市甘井子区","210212":"辽宁省大连市旅顺口区","210213":"辽宁省大连市金州区","210224":"辽宁省大连市长海县","210281":"辽宁省大连市瓦房店市","210282":"辽宁省大连市普兰店市","210283":"辽宁省大连市庄河市","210300":"辽宁省鞍山市","210301":"辽宁省鞍山市市辖区","210302":"辽宁省鞍山市铁东区","210303":"辽宁省鞍山市铁西区","210304":"辽宁省鞍山市立山区","210311":"辽宁省鞍山市千山区","210321":"辽宁省鞍山市台安县","210323":"辽宁省鞍山市岫岩满族自治县","210381":"辽宁省鞍山市海城市","210400":"辽宁省抚顺市","210401":"辽宁省抚顺市市辖区","210402":"辽宁省抚顺市新抚区","210403":"辽宁省抚顺市露天区","210404":"辽宁省抚顺市望花区","210411":"辽宁省抚顺市顺城区","210421":"辽宁省抚顺市抚顺县","210422":"辽宁省抚顺市新宾满族自治县","210423":"辽宁省抚顺市清原满族自治县","210500":"辽宁省本溪市","210501":"辽宁省本溪市市辖区","210502":"辽宁省本溪市平山区","210503":"辽宁省本溪市溪湖区","210504":"辽宁省本溪市明山区","210505":"辽宁省本溪市南芬区","210521":"辽宁省本溪市本溪满族自治县","210522":"辽宁省本溪市桓仁满族自治县","210600":"辽宁省丹东市","210601":"辽宁省丹东市市辖区","210602":"辽宁省丹东市元宝区","210603":"辽宁省丹东市振兴区","210604":"辽宁省丹东市振安区","210624":"辽宁省丹东市宽甸满族自治县","210681":"辽宁省丹东市东港市","210682":"辽宁省丹东市凤城市","210700":"辽宁省锦州市","210701":"辽宁省锦州市市辖区","210702":"辽宁省锦州市古塔区","210703":"辽宁省锦州市凌河区","210711":"辽宁省锦州市太和区","210726":"辽宁省锦州市黑山县","210727":"辽宁省锦州市义县","210781":"辽宁省锦州市凌海市","210782":"辽宁省锦州市北宁市","210800":"辽宁省营口市","210801":"辽宁省营口市市辖区","210802":"辽宁省营口市站前区","210803":"辽宁省营口市西市区","210804":"辽宁省营口市鲅鱼圈区","210811":"辽宁省营口市老边区","210881":"辽宁省营口市盖州市","210882":"辽宁省营口市大石桥市","210900":"辽宁省阜新市","210901":"辽宁省阜新市市辖区","210902":"辽宁省阜新市海州区","210903":"辽宁省阜新市新邱区","210904":"辽宁省阜新市太平区","210905":"辽宁省阜新市清河门区","210911":"辽宁省阜新市细河区","210921":"辽宁省阜新市阜新蒙古族自治县","210922":"辽宁省阜新市彰武县","211000":"辽宁省辽阳市","211001":"辽宁省辽阳市市辖区","211002":"辽宁省辽阳市白塔区","211003":"辽宁省辽阳市文圣区","211004":"辽宁省辽阳市宏伟区","211005":"辽宁省辽阳市弓长岭区","211011":"辽宁省辽阳市太子河区","211021":"辽宁省辽阳市辽阳县","211081":"辽宁省辽阳市灯塔市","211100":"辽宁省盘锦市","211101":"辽宁省盘锦市市辖区","211102":"辽宁省盘锦市双台子区","211103":"辽宁省盘锦市兴隆台区","211121":"辽宁省盘锦市大洼县","211122":"辽宁省盘锦市盘山县","211200":"辽宁省铁岭市","211201":"辽宁省铁岭市市辖区","211202":"辽宁省铁岭市银州区","211204":"辽宁省铁岭市清河区","211221":"辽宁省铁岭市铁岭县","211223":"辽宁省铁岭市西丰县","211224":"辽宁省铁岭市昌图县","211281":"辽宁省铁岭市铁法市","211282":"辽宁省铁岭市开原市","211300":"辽宁省朝阳市","211301":"辽宁省朝阳市市辖区","211302":"辽宁省朝阳市双塔区","211303":"辽宁省朝阳市龙城区","211321":"辽宁省朝阳市朝阳县","211322":"辽宁省朝阳市建平县","211324":"辽宁省朝阳市喀喇沁左翼蒙古族自治县","211381":"辽宁省朝阳市北票市","211382":"辽宁省朝阳市凌源市","211400":"辽宁省葫芦岛市","211401":"辽宁省葫芦岛市市辖区","211402":"辽宁省葫芦岛市连山区","211403":"辽宁省葫芦岛市龙港区","211404":"辽宁省葫芦岛市南票区","211421":"辽宁省葫芦岛市绥中县","211422":"辽宁省葫芦岛市建昌县","211481":"辽宁省葫芦岛市兴城市","220000":"吉林省","220100":"吉林省长春市","220101":"吉林省长春市市辖区","220102":"吉林省长春市南关区","220103":"吉林省长春市宽城区","220104":"吉林省长春市朝阳区","220105":"吉林省长春市二道区","220106":"吉林省长春市绿园区","220112":"吉林省长春市双阳区","220122":"吉林省长春市农安县","220181":"吉林省长春市九台市","220182":"吉林省长春市榆树市","220183":"吉林省长春市德惠市","220200":"吉林省吉林市","220201":"吉林省吉林市市辖区","220202":"吉林省吉林市昌邑区","220203":"吉林省吉林市龙潭区","220204":"吉林省吉林市船营区","220211":"吉林省吉林市丰满区","220221":"吉林省吉林市永吉县","220281":"吉林省吉林市蛟河市","220282":"吉林省吉林市桦甸市","220283":"吉林省吉林市舒兰市","220284":"吉林省吉林市磐石市","220300":"吉林省四平市","220301":"吉林省四平市市辖区","220302":"吉林省四平市铁西区","220303":"吉林省四平市铁东区","220322":"吉林省四平市梨树县","220323":"吉林省四平市伊通满族自治县","220381":"吉林省四平市公主岭市","220382":"吉林省四平市双辽市","220400":"吉林省辽源市","220401":"吉林省辽源市市辖区","220402":"吉林省辽源市龙山区","220403":"吉林省辽源市西安区","220421":"吉林省辽源市东丰县","220422":"吉林省辽源市东辽县","220500":"吉林省通化市","220501":"吉林省通化市市辖区","220502":"吉林省通化市东昌区","220503":"吉林省通化市二道江区","220521":"吉林省通化市通化县","220523":"吉林省通化市辉南县","220524":"吉林省通化市柳河县","220581":"吉林省通化市梅河口市","220582":"吉林省通化市集安市","220600":"吉林省白山市","220601":"吉林省白山市市辖区","220602":"吉林省白山市八道江区","220621":"吉林省白山市抚松县","220622":"吉林省白山市靖宇县","220623":"吉林省白山市长白朝鲜族自治县","220625":"吉林省白山市江源县","220681":"吉林省白山市临江市","220700":"吉林省松原市","220701":"吉林省松原市市辖区","220702":"吉林省松原市宁江区","220721":"吉林省松原市前郭尔罗斯蒙古族自治县","220722":"吉林省松原市长岭县","220723":"吉林省松原市乾安县","220724":"吉林省松原市扶余县","220800":"吉林省白城市","220801":"吉林省白城市市辖区","220802":"吉林省白城市洮北区","220821":"吉林省白城市镇赉县","220822":"吉林省白城市通榆县","220881":"吉林省白城市洮南市","220882":"吉林省白城市大安市","222400":"吉林省延边朝鲜族自治州","222401":"吉林省延边朝鲜族自治州延吉市","222402":"吉林省延边朝鲜族自治州图们市","222403":"吉林省延边朝鲜族自治州敦化市","222404":"吉林省延边朝鲜族自治州珲春市","222405":"吉林省延边朝鲜族自治州龙井市","222406":"吉林省延边朝鲜族自治州和龙市","222424":"吉林省延边朝鲜族自治州汪清县","222426":"吉林省延边朝鲜族自治州安图县","230000":"黑龙江省","230100":"黑龙江省哈尔滨市","230101":"黑龙江省哈尔滨市市辖区","230102":"黑龙江省哈尔滨市道里区","230103":"黑龙江省哈尔滨市南岗区","230104":"黑龙江省哈尔滨市道外区","230105":"黑龙江省哈尔滨市太平区","230106":"黑龙江省哈尔滨市香坊区","230107":"黑龙江省哈尔滨市动力区","230108":"黑龙江省哈尔滨市平房区","230121":"黑龙江省哈尔滨市呼兰县","230123":"黑龙江省哈尔滨市依兰县","230124":"黑龙江省哈尔滨市方正县","230125":"黑龙江省哈尔滨市宾县","230126":"黑龙江省哈尔滨市巴彦县","230127":"黑龙江省哈尔滨市木兰县","230128":"黑龙江省哈尔滨市通河县","230129":"黑龙江省哈尔滨市延寿县","230181":"黑龙江省哈尔滨市阿城市","230182":"黑龙江省哈尔滨市双城市","230183":"黑龙江省哈尔滨市尚志市","230184":"黑龙江省哈尔滨市五常市","230200":"黑龙江省齐齐哈尔市","230201":"黑龙江省齐齐哈尔市市辖区","230202":"黑龙江省齐齐哈尔市龙沙区","230203":"黑龙江省齐齐哈尔市建华区","230204":"黑龙江省齐齐哈尔市铁锋区","230205":"黑龙江省齐齐哈尔市昂昂溪区","230206":"黑龙江省齐齐哈尔市富拉尔基区","230207":"黑龙江省齐齐哈尔市碾子山区","230208":"黑龙江省齐齐哈尔市梅里斯达斡尔族区","230221":"黑龙江省齐齐哈尔市龙江县","230223":"黑龙江省齐齐哈尔市依安县","230224":"黑龙江省齐齐哈尔市泰来县","230225":"黑龙江省齐齐哈尔市甘南县","230227":"黑龙江省齐齐哈尔市富裕县","230229":"黑龙江省齐齐哈尔市克山县","230230":"黑龙江省齐齐哈尔市克东县","230231":"黑龙江省齐齐哈尔市拜泉县","230281":"黑龙江省齐齐哈尔市讷河市","230300":"黑龙江省鸡西市","230301":"黑龙江省鸡西市市辖区","230302":"黑龙江省鸡西市鸡冠区","230303":"黑龙江省鸡西市恒山区","230304":"黑龙江省鸡西市滴道区","230305":"黑龙江省鸡西市梨树区","230306":"黑龙江省鸡西市城子河区","230307":"黑龙江省鸡西市麻山区","230321":"黑龙江省鸡西市鸡东县","230381":"黑龙江省鸡西市虎林市","230382":"黑龙江省鸡西市密山市","230400":"黑龙江省鹤岗市","230401":"黑龙江省鹤岗市市辖区","230402":"黑龙江省鹤岗市向阳区","230403":"黑龙江省鹤岗市工农区","230404":"黑龙江省鹤岗市南山区","230405":"黑龙江省鹤岗市兴安区","230406":"黑龙江省鹤岗市东山区","230407":"黑龙江省鹤岗市兴山区","230421":"黑龙江省鹤岗市萝北县","230422":"黑龙江省鹤岗市绥滨县","230500":"黑龙江省双鸭山市","230501":"黑龙江省双鸭山市市辖区","230502":"黑龙江省双鸭山市尖山区","230503":"黑龙江省双鸭山市岭东区","230505":"黑龙江省双鸭山市四方台区","230506":"黑龙江省双鸭山市宝山区","230521":"黑龙江省双鸭山市集贤县","230522":"黑龙江省双鸭山市友谊县","230523":"黑龙江省双鸭山市宝清县","230524":"黑龙江省双鸭山市饶河县","230600":"黑龙江省大庆市","230601":"黑龙江省大庆市市辖区","230602":"黑龙江省大庆市萨尔图区","230603":"黑龙江省大庆市龙凤区","230604":"黑龙江省大庆市让胡路区","230605":"黑龙江省大庆市红岗区","230606":"黑龙江省大庆市大同区","230621":"黑龙江省大庆市肇州县","230622":"黑龙江省大庆市肇源县","230623":"黑龙江省大庆市林甸县","230624":"黑龙江省大庆市杜尔伯特蒙古族自治县","230700":"黑龙江省伊春市","230701":"黑龙江省伊春市市辖区","230702":"黑龙江省伊春市伊春区","230703":"黑龙江省伊春市南岔区","230704":"黑龙江省伊春市友好区","230705":"黑龙江省伊春市西林区","230706":"黑龙江省伊春市翠峦区","230707":"黑龙江省伊春市新青区","230708":"黑龙江省伊春市美溪区","230709":"黑龙江省伊春市金山屯区","230710":"黑龙江省伊春市五营区","230711":"黑龙江省伊春市乌马河区","230712":"黑龙江省伊春市汤旺河区","230713":"黑龙江省伊春市带岭区","230714":"黑龙江省伊春市乌伊岭区","230715":"黑龙江省伊春市红星区","230716":"黑龙江省伊春市上甘岭区","230722":"黑龙江省伊春市嘉荫县","230781":"黑龙江省伊春市铁力市","230800":"黑龙江省佳木斯市","230801":"黑龙江省佳木斯市市辖区","230802":"黑龙江省佳木斯市永红区","230803":"黑龙江省佳木斯市向阳区","230804":"黑龙江省佳木斯市前进区","230805":"黑龙江省佳木斯市东风区","230811":"黑龙江省佳木斯市郊区","230822":"黑龙江省佳木斯市桦南县","230826":"黑龙江省佳木斯市桦川县","230828":"黑龙江省佳木斯市汤原县","230833":"黑龙江省佳木斯市抚远县","230881":"黑龙江省佳木斯市同江市","230882":"黑龙江省佳木斯市富锦市","230900":"黑龙江省七台河市","230901":"黑龙江省七台河市市辖区","230902":"黑龙江省七台河市新兴区","230903":"黑龙江省七台河市桃山区","230904":"黑龙江省七台河市茄子河区","230921":"黑龙江省七台河市勃利县","231000":"黑龙江省牡丹江市","231001":"黑龙江省牡丹江市市辖区","231002":"黑龙江省牡丹江市东安区","231003":"黑龙江省牡丹江市阳明区","231004":"黑龙江省牡丹江市爱民区","231005":"黑龙江省牡丹江市西安区","231024":"黑龙江省牡丹江市东宁县","231025":"黑龙江省牡丹江市林口县","231081":"黑龙江省牡丹江市绥芬河市","231083":"黑龙江省牡丹江市海林市","231084":"黑龙江省牡丹江市宁安市","231085":"黑龙江省牡丹江市穆棱市","231100":"黑龙江省黑河市","231101":"黑龙江省黑河市市辖区","231102":"黑龙江省黑河市爱辉区","231121":"黑龙江省黑河市嫩江县","231123":"黑龙江省黑河市逊克县","231124":"黑龙江省黑河市孙吴县","231181":"黑龙江省黑河市北安市","231182":"黑龙江省黑河市五大连池市","232300":"黑龙江省绥化地区","232301":"黑龙江省绥化地区绥化市","232302":"黑龙江省绥化地区安达市","232303":"黑龙江省绥化地区肇东市","232304":"黑龙江省绥化地区海伦市","232324":"黑龙江省绥化地区望奎县","232325":"黑龙江省绥化地区兰西县","232326":"黑龙江省绥化地区青冈县","232330":"黑龙江省绥化地区庆安县","232331":"黑龙江省绥化地区明水县","232332":"黑龙江省绥化地区绥棱县","232700":"黑龙江省大兴安岭地区","232721":"黑龙江省大兴安岭地区呼玛县","232722":"黑龙江省大兴安岭地区塔河县","232723":"黑龙江省大兴安岭地区漠河县","310000":"上海市","310100":"上海市市辖区","310101":"上海市黄浦区","310102":"上海市南市区","310103":"上海市卢湾区","310104":"上海市徐汇区","310105":"上海市长宁区","310106":"上海市静安区","310107":"上海市普陀区","310108":"上海市闸北区","310109":"上海市虹口区","310110":"上海市杨浦区","310112":"上海市闵行区","310113":"上海市宝山区","310114":"上海市嘉定区","310115":"上海市浦东新区","310116":"上海市金山区","310117":"上海市松江区","310200":"上海市县","310225":"上海市南汇县","310226":"上海市奉贤县","310229":"上海市青浦县","310230":"上海市崇明县","320000":"江苏省","320100":"江苏省南京市","320101":"江苏省南京市市辖区","320102":"江苏省南京市玄武区","320103":"江苏省南京市白下区","320104":"江苏省南京市秦淮区","320105":"江苏省南京市建邺区","320106":"江苏省南京市鼓楼区","320107":"江苏省南京市下关区","320111":"江苏省南京市浦口区","320112":"江苏省南京市大厂区","320113":"江苏省南京市栖霞区","320114":"江苏省南京市雨花台区","320121":"江苏省南京市江宁县","320122":"江苏省南京市江浦县","320123":"江苏省南京市六合县","320124":"江苏省南京市溧水县","320125":"江苏省南京市高淳县","320200":"江苏省无锡市","320201":"江苏省无锡市市辖区","320202":"江苏省无锡市崇安区","320203":"江苏省无锡市南长区","320204":"江苏省无锡市北塘区","320211":"江苏省无锡市郊区","320281":"江苏省无锡市江阴市","320282":"江苏省无锡市宜兴市","320283":"江苏省无锡市锡山市","320300":"江苏省徐州市","320301":"江苏省徐州市市辖区","320302":"江苏省徐州市鼓楼区","320303":"江苏省徐州市云龙区","320304":"江苏省徐州市九里区","320305":"江苏省徐州市贾汪区","320311":"江苏省徐州市泉山区","320321":"江苏省徐州市丰县","320322":"江苏省徐州市沛县","320323":"江苏省徐州市铜山县","320324":"江苏省徐州市睢宁县","320381":"江苏省徐州市新沂市","320382":"江苏省徐州市邳州市","320400":"江苏省常州市","320401":"江苏省常州市市辖区","320402":"江苏省常州市天宁区","320404":"江苏省常州市钟楼区","320405":"江苏省常州市戚墅堰区","320411":"江苏省常州市郊区","320481":"江苏省常州市溧阳市","320482":"江苏省常州市金坛市","320483":"江苏省常州市武进市","320500":"江苏省苏州市","320501":"江苏省苏州市市辖区","320502":"江苏省苏州市沧浪区","320503":"江苏省苏州市平江区","320504":"江苏省苏州市金阊区","320511":"江苏省苏州市郊区","320581":"江苏省苏州市常熟市","320582":"江苏省苏州市张家港市","320583":"江苏省苏州市昆山市","320584":"江苏省苏州市吴江市","320585":"江苏省苏州市太仓市","320586":"江苏省苏州市吴县市","320600":"江苏省南通市","320601":"江苏省南通市市辖区","320602":"江苏省南通市崇川区","320611":"江苏省南通市港闸区","320621":"江苏省南通市海安县","320623":"江苏省南通市如东县","320681":"江苏省南通市启东市","320682":"江苏省南通市如皋市","320683":"江苏省南通市通州市","320684":"江苏省南通市海门市","320700":"江苏省连云港市","320701":"江苏省连云港市市辖区","320703":"江苏省连云港市连云区","320704":"江苏省连云港市云台区","320705":"江苏省连云港市新浦区","320706":"江苏省连云港市海州区","320721":"江苏省连云港市赣榆县","320722":"江苏省连云港市东海县","320723":"江苏省连云港市灌云县","320724":"江苏省连云港市灌南县","320800":"江苏省淮阴市","320801":"江苏省淮阴市市辖区","320802":"江苏省淮阴市清河区","320811":"江苏省淮阴市清浦区","320821":"江苏省淮阴市淮阴县","320826":"江苏省淮阴市涟水县","320829":"江苏省淮阴市洪泽县","320830":"江苏省淮阴市盱眙县","320831":"江苏省淮阴市金湖县","320882":"江苏省淮阴市淮安市","320900":"江苏省盐城市","320901":"江苏省盐城市市辖区","320902":"江苏省盐城市城区","320921":"江苏省盐城市响水县","320922":"江苏省盐城市滨海县","320923":"江苏省盐城市阜宁县","320924":"江苏省盐城市射阳县","320925":"江苏省盐城市建湖县","320928":"江苏省盐城市盐都县","320981":"江苏省盐城市东台市","320982":"江苏省盐城市大丰市","321000":"江苏省扬州市","321001":"江苏省扬州市市辖区","321002":"江苏省扬州市广陵区","321011":"江苏省扬州市郊区","321023":"江苏省扬州市宝应县","321027":"江苏省扬州市邗江县","321081":"江苏省扬州市仪征市","321084":"江苏省扬州市高邮市","321088":"江苏省扬州市江都市","321100":"江苏省镇江市","321101":"江苏省镇江市市辖区","321102":"江苏省镇江市京口区","321111":"江苏省镇江市润州区","321121":"江苏省镇江市丹徒县","321181":"江苏省镇江市丹阳市","321182":"江苏省镇江市扬中市","321183":"江苏省镇江市句容市","321200":"江苏省泰州市","321201":"江苏省泰州市市辖区","321202":"江苏省泰州市海陵区","321203":"江苏省泰州市高港区","321281":"江苏省泰州市兴化市","321282":"江苏省泰州市靖江市","321283":"江苏省泰州市泰兴市","321284":"江苏省泰州市姜堰市","321300":"江苏省宿迁市","321301":"江苏省宿迁市市辖区","321302":"江苏省宿迁市宿城区","321321":"江苏省宿迁市宿豫县","321322":"江苏省宿迁市沭阳县","321323":"江苏省宿迁市泗阳县","321324":"江苏省宿迁市泗洪县","330000":"浙江省","330100":"浙江省杭州市","330101":"浙江省杭州市市辖区","330102":"浙江省杭州市上城区","330103":"浙江省杭州市下城区","330104":"浙江省杭州市江干区","330105":"浙江省杭州市拱墅区","330106":"浙江省杭州市西湖区","330108":"浙江省杭州市滨江区","330122":"浙江省杭州市桐庐县","330127":"浙江省杭州市淳安县","330181":"浙江省杭州市萧山市","330182":"浙江省杭州市建德市","330183":"浙江省杭州市富阳市","330184":"浙江省杭州市余杭市","330185":"浙江省杭州市临安市","330200":"浙江省宁波市","330201":"浙江省宁波市市辖区","330203":"浙江省宁波市海曙区","330204":"浙江省宁波市江东区","330205":"浙江省宁波市江北区","330206":"浙江省宁波市北仑区","330211":"浙江省宁波市镇海区","330225":"浙江省宁波市象山县","330226":"浙江省宁波市宁海县","330227":"浙江省宁波市鄞县","330281":"浙江省宁波市余姚市","330282":"浙江省宁波市慈溪市","330283":"浙江省宁波市奉化市","330300":"浙江省温州市","330301":"浙江省温州市市辖区","330302":"浙江省温州市鹿城区","330303":"浙江省温州市龙湾区","330304":"浙江省温州市瓯海区","330322":"浙江省温州市洞头县","330324":"浙江省温州市永嘉县","330326":"浙江省温州市平阳县","330327":"浙江省温州市苍南县","330328":"浙江省温州市文成县","330329":"浙江省温州市泰顺县","330381":"浙江省温州市瑞安市","330382":"浙江省温州市乐清市","330400":"浙江省嘉兴市","330401":"浙江省嘉兴市市辖区","330402":"浙江省嘉兴市秀城区","330411":"浙江省嘉兴市郊区","330421":"浙江省嘉兴市嘉善县","330424":"浙江省嘉兴市海盐县","330481":"浙江省嘉兴市海宁市","330482":"浙江省嘉兴市平湖市","330483":"浙江省嘉兴市桐乡市","330500":"浙江省湖州市","330501":"浙江省湖州市市辖区","330521":"浙江省湖州市德清县","330522":"浙江省湖州市长兴县","330523":"浙江省湖州市安吉县","330600":"浙江省绍兴市","330601":"浙江省绍兴市市辖区","330602":"浙江省绍兴市越城区","330621":"浙江省绍兴市绍兴县","330624":"浙江省绍兴市新昌县","330681":"浙江省绍兴市诸暨市","330682":"浙江省绍兴市上虞市","330683":"浙江省绍兴市嵊州市","330700":"浙江省金华市","330701":"浙江省金华市市辖区","330702":"浙江省金华市婺城区","330721":"浙江省金华市金华县","330723":"浙江省金华市武义县","330726":"浙江省金华市浦江县","330727":"浙江省金华市磐安县","330781":"浙江省金华市兰溪市","330782":"浙江省金华市义乌市","330783":"浙江省金华市东阳市","330784":"浙江省金华市永康市","330800":"浙江省衢州市","330801":"浙江省衢州市市辖区","330802":"浙江省衢州市柯城区","330821":"浙江省衢州市衢县","330822":"浙江省衢州市常山县","330824":"浙江省衢州市开化县","330825":"浙江省衢州市龙游县","330881":"浙江省衢州市江山市","330900":"浙江省舟山市","330901":"浙江省舟山市市辖区","330902":"浙江省舟山市定海区","330903":"浙江省舟山市普陀区","330921":"浙江省舟山市岱山县","330922":"浙江省舟山市嵊泗县","331000":"浙江省台州市","331001":"浙江省台州市市辖区","331002":"浙江省台州市椒江区","331003":"浙江省台州市黄岩区","331004":"浙江省台州市路桥区","331021":"浙江省台州市玉环县","331022":"浙江省台州市三门县","331023":"浙江省台州市天台县","331024":"浙江省台州市仙居县","331081":"浙江省台州市温岭市","331082":"浙江省台州市临海市","332500":"浙江省丽水地区","332501":"浙江省丽水地区丽水市","332502":"浙江省丽水地区龙泉市","332522":"浙江省丽水地区青田县","332523":"浙江省丽水地区云和县","332525":"浙江省丽水地区庆元县","332526":"浙江省丽水地区缙云县","332527":"浙江省丽水地区遂昌县","332528":"浙江省丽水地区松阳县","332529":"浙江省丽水地区景宁畲族自治县","340000":"安徽省","340100":"安徽省合肥市","340101":"安徽省合肥市市辖区","340102":"安徽省合肥市东市区","340103":"安徽省合肥市中市区","340104":"安徽省合肥市西市区","340111":"安徽省合肥市郊区","340121":"安徽省合肥市长丰县","340122":"安徽省合肥市肥东县","340123":"安徽省合肥市肥西县","340200":"安徽省芜湖市","340201":"安徽省芜湖市市辖区","340202":"安徽省芜湖市镜湖区","340203":"安徽省芜湖市马塘区","340204":"安徽省芜湖市新芜区","340207":"安徽省芜湖市鸠江区","340221":"安徽省芜湖市芜湖县","340222":"安徽省芜湖市繁昌县","340223":"安徽省芜湖市南陵县","340300":"安徽省蚌埠市","340301":"安徽省蚌埠市市辖区","340302":"安徽省蚌埠市东市区","340303":"安徽省蚌埠市中市区","340304":"安徽省蚌埠市西市区","340311":"安徽省蚌埠市郊区","340321":"安徽省蚌埠市怀远县","340322":"安徽省蚌埠市五河县","340323":"安徽省蚌埠市固镇县","340400":"安徽省淮南市","340401":"安徽省淮南市市辖区","340402":"安徽省淮南市大通区","340403":"安徽省淮南市田家庵区","340404":"安徽省淮南市谢家集区","340405":"安徽省淮南市八公山区","340406":"安徽省淮南市潘集区","340421":"安徽省淮南市凤台县","340500":"安徽省马鞍山市","340501":"安徽省马鞍山市市辖区","340502":"安徽省马鞍山市金家庄区","340503":"安徽省马鞍山市花山区","340504":"安徽省马鞍山市雨山区","340505":"安徽省马鞍山市向山区","340521":"安徽省马鞍山市当涂县","340600":"安徽省淮北市","340601":"安徽省淮北市市辖区","340602":"安徽省淮北市杜集区","340603":"安徽省淮北市相山区","340604":"安徽省淮北市烈山区","340621":"安徽省淮北市濉溪县","340700":"安徽省铜陵市","340701":"安徽省铜陵市市辖区","340702":"安徽省铜陵市铜官山区","340703":"安徽省铜陵市狮子山区","340711":"安徽省铜陵市郊区","340721":"安徽省铜陵市铜陵县","340800":"安徽省安庆市","340801":"安徽省安庆市市辖区","340802":"安徽省安庆市迎江区","340803":"安徽省安庆市大观区","340811":"安徽省安庆市郊区","340822":"安徽省安庆市怀宁县","340823":"安徽省安庆市枞阳县","340824":"安徽省安庆市潜山县","340825":"安徽省安庆市太湖县","340826":"安徽省安庆市宿松县","340827":"安徽省安庆市望江县","340828":"安徽省安庆市岳西县","340881":"安徽省安庆市桐城市","341000":"安徽省黄山市","341001":"安徽省黄山市市辖区","341002":"安徽省黄山市屯溪区","341003":"安徽省黄山市黄山区","341004":"安徽省黄山市徽州区","341021":"安徽省黄山市歙县","341022":"安徽省黄山市休宁县","341023":"安徽省黄山市黟县","341024":"安徽省黄山市祁门县","341100":"安徽省滁州市","341101":"安徽省滁州市市辖区","341102":"安徽省滁州市琅琊区","341103":"安徽省滁州市南谯区","341122":"安徽省滁州市来安县","341124":"安徽省滁州市全椒县","341125":"安徽省滁州市定远县","341126":"安徽省滁州市凤阳县","341181":"安徽省滁州市天长市","341182":"安徽省滁州市明光市","341200":"安徽省阜阳市","341201":"安徽省阜阳市市辖区","341202":"安徽省阜阳市颍州区","341203":"安徽省阜阳市颍东区","341204":"安徽省阜阳市颍泉区","341221":"安徽省阜阳市临泉县","341222":"安徽省阜阳市太和县","341223":"安徽省阜阳市涡阳县","341224":"安徽省阜阳市蒙城县","341225":"安徽省阜阳市阜南县","341226":"安徽省阜阳市颍上县","341227":"安徽省阜阳市利辛县","341281":"安徽省阜阳市亳州市","341282":"安徽省阜阳市界首市","341300":"安徽省宿州市","341301":"安徽省宿州市市辖区","341302":"安徽省宿州市甬桥区","341321":"安徽省宿州市砀山县","341322":"安徽省宿州市萧县","341323":"安徽省宿州市灵璧县","341324":"安徽省宿州市泗县","342400":"安徽省六安地区","342401":"安徽省六安地区六安市","342422":"安徽省六安地区寿县","342423":"安徽省六安地区霍邱县","342425":"安徽省六安地区舒城县","342426":"安徽省六安地区金寨县","342427":"安徽省六安地区霍山县","342500":"安徽省宣城地区","342501":"安徽省宣城地区宣州市","342502":"安徽省宣城地区宁国市","342522":"安徽省宣城地区郎溪县","342523":"安徽省宣城地区广德县","342529":"安徽省宣城地区泾县","342530":"安徽省宣城地区旌德县","342531":"安徽省宣城地区绩溪县","342600":"安徽省巢湖地区","342601":"安徽省巢湖地区巢湖市","342622":"安徽省巢湖地区庐江县","342623":"安徽省巢湖地区无为县","342625":"安徽省巢湖地区含山县","342626":"安徽省巢湖地区和县","342900":"安徽省池州地区","342901":"安徽省池州地区贵池市","342921":"安徽省池州地区东至县","342922":"安徽省池州地区石台县","342923":"安徽省池州地区青阳县","350000":"福建省","350100":"福建省福州市","350101":"福建省福州市市辖区","350102":"福建省福州市鼓楼区","350103":"福建省福州市台江区","350104":"福建省福州市仓山区","350105":"福建省福州市马尾区","350111":"福建省福州市晋安区","350121":"福建省福州市闽侯县","350122":"福建省福州市连江县","350123":"福建省福州市罗源县","350124":"福建省福州市闽清县","350125":"福建省福州市永泰县","350128":"福建省福州市平潭县","350181":"福建省福州市福清市","350182":"福建省福州市长乐市","350200":"福建省厦门市","350201":"福建省厦门市市辖区","350202":"福建省厦门市鼓浪屿区","350203":"福建省厦门市思明区","350204":"福建省厦门市开元区","350205":"福建省厦门市杏林区","350206":"福建省厦门市湖里区","350211":"福建省厦门市集美区","350212":"福建省厦门市同安区","350300":"福建省莆田市","350301":"福建省莆田市市辖区","350302":"福建省莆田市城厢区","350303":"福建省莆田市涵江区","350321":"福建省莆田市莆田县","350322":"福建省莆田市仙游县","350400":"福建省三明市","350401":"福建省三明市市辖区","350402":"福建省三明市梅列区","350403":"福建省三明市三元区","350421":"福建省三明市明溪县","350423":"福建省三明市清流县","350424":"福建省三明市宁化县","350425":"福建省三明市大田县","350426":"福建省三明市尤溪县","350427":"福建省三明市沙县","350428":"福建省三明市将乐县","350429":"福建省三明市泰宁县","350430":"福建省三明市建宁县","350481":"福建省三明市永安市","350500":"福建省泉州市","350501":"福建省泉州市市辖区","350502":"福建省泉州市鲤城区","350503":"福建省泉州市丰泽区","350504":"福建省泉州市洛江区","350521":"福建省泉州市惠安县","350524":"福建省泉州市安溪县","350525":"福建省泉州市永春县","350526":"福建省泉州市德化县","350527":"福建省泉州市金门县","350581":"福建省泉州市石狮市","350582":"福建省泉州市晋江市","350583":"福建省泉州市南安市","350600":"福建省漳州市","350601":"福建省漳州市市辖区","350602":"福建省漳州市芗城区","350603":"福建省漳州市龙文区","350622":"福建省漳州市云霄县","350623":"福建省漳州市漳浦县","350624":"福建省漳州市诏安县","350625":"福建省漳州市长泰县","350626":"福建省漳州市东山县","350627":"福建省漳州市南靖县","350628":"福建省漳州市平和县","350629":"福建省漳州市华安县","350681":"福建省漳州市龙海市","350700":"福建省南平市","350701":"福建省南平市市辖区","350702":"福建省南平市延平区","350721":"福建省南平市顺昌县","350722":"福建省南平市浦城县","350723":"福建省南平市光泽县","350724":"福建省南平市松溪县","350725":"福建省南平市政和县","350781":"福建省南平市邵武市","350782":"福建省南平市武夷山市","350783":"福建省南平市建瓯市","350784":"福建省南平市建阳市","350800":"福建省龙岩市","350801":"福建省龙岩市市辖区","350802":"福建省龙岩市新罗区","350821":"福建省龙岩市长汀县","350822":"福建省龙岩市永定县","350823":"福建省龙岩市上杭县","350824":"福建省龙岩市武平县","350825":"福建省龙岩市连城县","350881":"福建省龙岩市漳平市","352200":"福建省宁德地区","352201":"福建省宁德地区宁德市","352202":"福建省宁德地区福安市","352203":"福建省宁德地区福鼎市","352225":"福建省宁德地区霞浦县","352227":"福建省宁德地区古田县","352228":"福建省宁德地区屏南县","352229":"福建省宁德地区寿宁县","352230":"福建省宁德地区周宁县","352231":"福建省宁德地区柘荣县","360000":"江西省","360100":"江西省南昌市","360101":"江西省南昌市市辖区","360102":"江西省南昌市东湖区","360103":"江西省南昌市西湖区","360104":"江西省南昌市青云谱区","360105":"江西省南昌市湾里区","360111":"江西省南昌市郊区","360121":"江西省南昌市南昌县","360122":"江西省南昌市新建县","360123":"江西省南昌市安义县","360124":"江西省南昌市进贤县","360200":"江西省景德镇市","360201":"江西省景德镇市市辖区","360202":"江西省景德镇市昌江区","360203":"江西省景德镇市珠山区","360222":"江西省景德镇市浮梁县","360281":"江西省景德镇市乐平市","360300":"江西省萍乡市","360301":"江西省萍乡市市辖区","360302":"江西省萍乡市安源区","360313":"江西省萍乡市湘东区","360321":"江西省萍乡市莲花县","360322":"江西省萍乡市上栗县","360323":"江西省萍乡市芦溪县","360400":"江西省九江市","360401":"江西省九江市市辖区","360402":"江西省九江市庐山区","360403":"江西省九江市浔阳区","360421":"江西省九江市九江县","360423":"江西省九江市武宁县","360424":"江西省九江市修水县","360425":"江西省九江市永修县","360426":"江西省九江市德安县","360427":"江西省九江市星子县","360428":"江西省九江市都昌县","360429":"江西省九江市湖口县","360430":"江西省九江市彭泽县","360481":"江西省九江市瑞昌市","360500":"江西省新余市","360501":"江西省新余市市辖区","360502":"江西省新余市渝水区","360521":"江西省新余市分宜县","360600":"江西省鹰潭市","360601":"江西省鹰潭市市辖区","360602":"江西省鹰潭市月湖区","360622":"江西省鹰潭市余江县","360681":"江西省鹰潭市贵溪市","360700":"江西省赣州市","360701":"江西省赣州市市辖区","360702":"江西省赣州市章贡区","360721":"江西省赣州市赣县","360722":"江西省赣州市信丰县","360723":"江西省赣州市大余县","360724":"江西省赣州市上犹县","360725":"江西省赣州市崇义县","360726":"江西省赣州市安远县","360727":"江西省赣州市龙南县","360728":"江西省赣州市定南县","360729":"江西省赣州市全南县","360730":"江西省赣州市宁都县","360731":"江西省赣州市于都县","360732":"江西省赣州市兴国县","360733":"江西省赣州市会昌县","360734":"江西省赣州市寻乌县","360735":"江西省赣州市石城县","360781":"江西省赣州市瑞金市","360782":"江西省赣州市南康市","362200":"江西省宜春地区","362201":"江西省宜春地区宜春市","362202":"江西省宜春地区丰城市","362203":"江西省宜春地区樟树市","362204":"江西省宜春地区高安市","362226":"江西省宜春地区奉新县","362227":"江西省宜春地区万载县","362228":"江西省宜春地区上高县","362229":"江西省宜春地区宜丰县","362232":"江西省宜春地区靖安县","362233":"江西省宜春地区铜鼓县","362300":"江西省上饶地区","362301":"江西省上饶地区上饶市","362302":"江西省上饶地区德兴市","362321":"江西省上饶地区上饶县","362322":"江西省上饶地区广丰县","362323":"江西省上饶地区玉山县","362324":"江西省上饶地区铅山县","362325":"江西省上饶地区横峰县","362326":"江西省上饶地区弋阳县","362329":"江西省上饶地区余干县","362330":"江西省上饶地区波阳县","362331":"江西省上饶地区万年县","362334":"江西省上饶地区婺源县","362400":"江西省吉安地区","362401":"江西省吉安地区吉安市","362402":"江西省吉安地区井冈山市","362421":"江西省吉安地区吉安县","362422":"江西省吉安地区吉水县","362423":"江西省吉安地区峡江县","362424":"江西省吉安地区新干县","362425":"江西省吉安地区永丰县","362426":"江西省吉安地区泰和县","362427":"江西省吉安地区遂川县","362428":"江西省吉安地区万安县","362429":"江西省吉安地区安福县","362430":"江西省吉安地区永新县","362432":"江西省吉安地区宁冈县","362500":"江西省抚州地区","362502":"江西省抚州地区临川市","362522":"江西省抚州地区南城县","362523":"江西省抚州地区黎川县","362524":"江西省抚州地区南丰县","362525":"江西省抚州地区崇仁县","362526":"江西省抚州地区乐安县","362527":"江西省抚州地区宜黄县","362528":"江西省抚州地区金溪县","362529":"江西省抚州地区资溪县","362531":"江西省抚州地区东乡县","362532":"江西省抚州地区广昌县","370000":"山东省","370100":"山东省济南市","370101":"山东省济南市市辖区","370102":"山东省济南市历下区","370103":"山东省济南市市中区","370104":"山东省济南市槐荫区","370105":"山东省济南市天桥区","370112":"山东省济南市历城区","370123":"山东省济南市长清县","370124":"山东省济南市平阴县","370125":"山东省济南市济阳县","370126":"山东省济南市商河县","370181":"山东省济南市章丘市","370200":"山东省青岛市","370201":"山东省青岛市市辖区","370202":"山东省青岛市市南区","370203":"山东省青岛市市北区","370205":"山东省青岛市四方区","370211":"山东省青岛市黄岛区","370212":"山东省青岛市崂山区","370213":"山东省青岛市李沧区","370214":"山东省青岛市城阳区","370281":"山东省青岛市胶州市","370282":"山东省青岛市即墨市","370283":"山东省青岛市平度市","370284":"山东省青岛市胶南市","370285":"山东省青岛市莱西市","370300":"山东省淄博市","370301":"山东省淄博市市辖区","370302":"山东省淄博市淄川区","370303":"山东省淄博市张店区","370304":"山东省淄博市博山区","370305":"山东省淄博市临淄区","370306":"山东省淄博市周村区","370321":"山东省淄博市桓台县","370322":"山东省淄博市高青县","370323":"山东省淄博市沂源县","370400":"山东省枣庄市","370401":"山东省枣庄市市辖区","370402":"山东省枣庄市市中区","370403":"山东省枣庄市薛城区","370404":"山东省枣庄市峄城区","370405":"山东省枣庄市台儿庄区","370406":"山东省枣庄市山亭区","370481":"山东省枣庄市滕州市","370500":"山东省东营市","370501":"山东省东营市市辖区","370502":"山东省东营市东营区","370503":"山东省东营市河口区","370521":"山东省东营市垦利县","370522":"山东省东营市利津县","370523":"山东省东营市广饶县","370600":"山东省烟台市","370601":"山东省烟台市市辖区","370602":"山东省烟台市芝罘区","370611":"山东省烟台市福山区","370612":"山东省烟台市牟平区","370613":"山东省烟台市莱山区","370634":"山东省烟台市长岛县","370681":"山东省烟台市龙口市","370682":"山东省烟台市莱阳市","370683":"山东省烟台市莱州市","370684":"山东省烟台市蓬莱市","370685":"山东省烟台市招远市","370686":"山东省烟台市栖霞市","370687":"山东省烟台市海阳市","370700":"山东省潍坊市","370701":"山东省潍坊市市辖区","370702":"山东省潍坊市潍城区","370703":"山东省潍坊市寒亭区","370704":"山东省潍坊市坊子区","370705":"山东省潍坊市奎文区","370724":"山东省潍坊市临朐县","370725":"山东省潍坊市昌乐县","370781":"山东省潍坊市青州市","370782":"山东省潍坊市诸城市","370783":"山东省潍坊市寿光市","370784":"山东省潍坊市安丘市","370785":"山东省潍坊市高密市","370786":"山东省潍坊市昌邑市","370800":"山东省济宁市","370801":"山东省济宁市市辖区","370802":"山东省济宁市市中区","370811":"山东省济宁市任城区","370826":"山东省济宁市微山县","370827":"山东省济宁市鱼台县","370828":"山东省济宁市金乡县","370829":"山东省济宁市嘉祥县","370830":"山东省济宁市汶上县","370831":"山东省济宁市泗水县","370832":"山东省济宁市梁山县","370881":"山东省济宁市曲阜市","370882":"山东省济宁市兖州市","370883":"山东省济宁市邹城市","370900":"山东省泰安市","370901":"山东省泰安市市辖区","370902":"山东省泰安市泰山区","370911":"山东省泰安市郊区","370921":"山东省泰安市宁阳县","370923":"山东省泰安市东平县","370982":"山东省泰安市新泰市","370983":"山东省泰安市肥城市","371000":"山东省威海市","371001":"山东省威海市市辖区","371002":"山东省威海市环翠区","371081":"山东省威海市文登市","371082":"山东省威海市荣成市","371083":"山东省威海市乳山市","371100":"山东省日照市","371101":"山东省日照市市辖区","371102":"山东省日照市东港区","371121":"山东省日照市五莲县","371122":"山东省日照市莒县","371200":"山东省莱芜市","371201":"山东省莱芜市市辖区","371202":"山东省莱芜市莱城区","371203":"山东省莱芜市钢城区","371300":"山东省临沂市","371301":"山东省临沂市市辖区","371302":"山东省临沂市兰山区","371311":"山东省临沂市罗庄区","371312":"山东省临沂市河东区","371321":"山东省临沂市沂南县","371322":"山东省临沂市郯城县","371323":"山东省临沂市沂水县","371324":"山东省临沂市苍山县","371325":"山东省临沂市费县","371326":"山东省临沂市平邑县","371327":"山东省临沂市莒南县","371328":"山东省临沂市蒙阴县","371329":"山东省临沂市临沭县","371400":"山东省德州市","371401":"山东省德州市市辖区","371402":"山东省德州市德城区","371421":"山东省德州市陵县","371422":"山东省德州市宁津县","371423":"山东省德州市庆云县","371424":"山东省德州市临邑县","371425":"山东省德州市齐河县","371426":"山东省德州市平原县","371427":"山东省德州市夏津县","371428":"山东省德州市武城县","371481":"山东省德州市乐陵市","371482":"山东省德州市禹城市","371500":"山东省聊城市","371501":"山东省聊城市市辖区","371502":"山东省聊城市东昌府区","371521":"山东省聊城市阳谷县","371522":"山东省聊城市莘县","371523":"山东省聊城市茌平县","371524":"山东省聊城市东阿县","371525":"山东省聊城市冠县","371526":"山东省聊城市高唐县","371581":"山东省聊城市临清市","372300":"山东省滨州地区","372301":"山东省滨州地区滨州市","372321":"山东省滨州地区惠民县","372323":"山东省滨州地区阳信县","372324":"山东省滨州地区无棣县","372325":"山东省滨州地区沾化县","372328":"山东省滨州地区博兴县","372330":"山东省滨州地区邹平县","372900":"山东省菏泽地区","372901":"山东省菏泽地区菏泽市","372922":"山东省菏泽地区曹县","372923":"山东省菏泽地区定陶县","372924":"山东省菏泽地区成武县","372925":"山东省菏泽地区单县","372926":"山东省菏泽地区巨野县","372928":"山东省菏泽地区郓城县","372929":"山东省菏泽地区鄄城县","372930":"山东省菏泽地区东明县","410000":"河南省","410100":"河南省郑州市","410101":"河南省郑州市市辖区","410102":"河南省郑州市中原区","410103":"河南省郑州市二七区","410104":"河南省郑州市管城回族区","410105":"河南省郑州市金水区","410106":"河南省郑州市上街区","410108":"河南省郑州市邙山区","410122":"河南省郑州市中牟县","410181":"河南省郑州市巩义市","410182":"河南省郑州市荥阳市","410183":"河南省郑州市新密市","410184":"河南省郑州市新郑市","410185":"河南省郑州市登封市","410200":"河南省开封市","410201":"河南省开封市市辖区","410202":"河南省开封市龙亭区","410203":"河南省开封市顺河回族区","410204":"河南省开封市鼓楼区","410205":"河南省开封市南关区","410211":"河南省开封市郊区","410221":"河南省开封市杞县","410222":"河南省开封市通许县","410223":"河南省开封市尉氏县","410224":"河南省开封市开封县","410225":"河南省开封市兰考县","410300":"河南省洛阳市","410301":"河南省洛阳市市辖区","410302":"河南省洛阳市老城区","410303":"河南省洛阳市西工区","410304":"河南省洛阳市廛河回族区","410305":"河南省洛阳市涧西区","410306":"河南省洛阳市吉利区","410311":"河南省洛阳市郊区","410322":"河南省洛阳市孟津县","410323":"河南省洛阳市新安县","410324":"河南省洛阳市栾川县","410325":"河南省洛阳市嵩县","410326":"河南省洛阳市汝阳县","410327":"河南省洛阳市宜阳县","410328":"河南省洛阳市洛宁县","410329":"河南省洛阳市伊川县","410381":"河南省洛阳市偃师市","410400":"河南省平顶山市","410401":"河南省平顶山市市辖区","410402":"河南省平顶山市新华区","410403":"河南省平顶山市卫东区","410404":"河南省平顶山市石龙区","410411":"河南省平顶山市湛河区","410421":"河南省平顶山市宝丰县","410422":"河南省平顶山市叶县","410423":"河南省平顶山市鲁山县","410425":"河南省平顶山市郏县","410481":"河南省平顶山市舞钢市","410482":"河南省平顶山市汝州市","410500":"河南省安阳市","410501":"河南省安阳市市辖区","410502":"河南省安阳市文峰区","410503":"河南省安阳市北关区","410504":"河南省安阳市铁西区","410511":"河南省安阳市郊区","410522":"河南省安阳市安阳县","410523":"河南省安阳市汤阴县","410526":"河南省安阳市滑县","410527":"河南省安阳市内黄县","410581":"河南省安阳市林州市","410600":"河南省鹤壁市","410601":"河南省鹤壁市市辖区","410602":"河南省鹤壁市鹤山区","410603":"河南省鹤壁市山城区","410611":"河南省鹤壁市郊区","410621":"河南省鹤壁市浚县","410622":"河南省鹤壁市淇县","410700":"河南省新乡市","410701":"河南省新乡市市辖区","410702":"河南省新乡市红旗区","410703":"河南省新乡市新华区","410704":"河南省新乡市北站区","410711":"河南省新乡市郊区","410721":"河南省新乡市新乡县","410724":"河南省新乡市获嘉县","410725":"河南省新乡市原阳县","410726":"河南省新乡市延津县","410727":"河南省新乡市封丘县","410728":"河南省新乡市长垣县","410781":"河南省新乡市卫辉市","410782":"河南省新乡市辉县市","410800":"河南省焦作市","410801":"河南省焦作市市辖区","410802":"河南省焦作市解放区","410803":"河南省焦作市中站区","410804":"河南省焦作市马村区","410811":"河南省焦作市山阳区","410821":"河南省焦作市修武县","410822":"河南省焦作市博爱县","410823":"河南省焦作市武陟县","410825":"河南省焦作市温县","410881":"河南省焦作市济源市","410882":"河南省焦作市沁阳市","410883":"河南省焦作市孟州市","410900":"河南省濮阳市","410901":"河南省濮阳市市辖区","410902":"河南省濮阳市市区","410922":"河南省濮阳市清丰县","410923":"河南省濮阳市南乐县","410926":"河南省濮阳市范县","410927":"河南省濮阳市台前县","410928":"河南省濮阳市濮阳县","411000":"河南省许昌市","411001":"河南省许昌市市辖区","411002":"河南省许昌市魏都区","411023":"河南省许昌市许昌县","411024":"河南省许昌市鄢陵县","411025":"河南省许昌市襄城县","411081":"河南省许昌市禹州市","411082":"河南省许昌市长葛市","411100":"河南省漯河市","411101":"河南省漯河市市辖区","411102":"河南省漯河市源汇区","411121":"河南省漯河市舞阳县","411122":"河南省漯河市临颍县","411123":"河南省漯河市郾城县","411200":"河南省三门峡市","411201":"河南省三门峡市市辖区","411202":"河南省三门峡市湖滨区","411221":"河南省三门峡市渑池县","411222":"河南省三门峡市陕县","411224":"河南省三门峡市卢氏县","411281":"河南省三门峡市义马市","411282":"河南省三门峡市灵宝市","411300":"河南省南阳市","411301":"河南省南阳市市辖区","411302":"河南省南阳市宛城区","411303":"河南省南阳市卧龙区","411321":"河南省南阳市南召县","411322":"河南省南阳市方城县","411323":"河南省南阳市西峡县","411324":"河南省南阳市镇平县","411325":"河南省南阳市内乡县","411326":"河南省南阳市淅川县","411327":"河南省南阳市社旗县","411328":"河南省南阳市唐河县","411329":"河南省南阳市新野县","411330":"河南省南阳市桐柏县","411381":"河南省南阳市邓州市","411400":"河南省商丘市","411401":"河南省商丘市市辖区","411402":"河南省商丘市梁园区","411403":"河南省商丘市睢阳区","411421":"河南省商丘市民权县","411422":"河南省商丘市睢县","411423":"河南省商丘市宁陵县","411424":"河南省商丘市柘城县","411425":"河南省商丘市虞城县","411426":"河南省商丘市夏邑县","411481":"河南省商丘市永城市","411500":"河南省信阳市","411501":"河南省信阳市市辖区","411502":"河南省信阳市师河区","411503":"河南省信阳市平桥区","411521":"河南省信阳市罗山县","411522":"河南省信阳市光山县","411523":"河南省信阳市新县","411524":"河南省信阳市商城县","411525":"河南省信阳市固始县","411526":"河南省信阳市潢川县","411527":"河南省信阳市淮滨县","411528":"河南省信阳市息县","412700":"河南省周口地区","412701":"河南省周口地区周口市","412702":"河南省周口地区项城市","412721":"河南省周口地区扶沟县","412722":"河南省周口地区西华县","412723":"河南省周口地区商水县","412724":"河南省周口地区太康县","412725":"河南省周口地区鹿邑县","412726":"河南省周口地区郸城县","412727":"河南省周口地区淮阳县","412728":"河南省周口地区沈丘县","412800":"河南省驻马店地区","412801":"河南省驻马店地区驻马店市","412821":"河南省驻马店地区确山县","412822":"河南省驻马店地区泌阳县","412823":"河南省驻马店地区遂平县","412824":"河南省驻马店地区西平县","412825":"河南省驻马店地区上蔡县","412826":"河南省驻马店地区汝南县","412827":"河南省驻马店地区平舆县","412828":"河南省驻马店地区新蔡县","412829":"河南省驻马店地区正阳县","420000":"湖北省","420100":"湖北省武汉市","420101":"湖北省武汉市市辖区","420102":"湖北省武汉市江岸区","420103":"湖北省武汉市江汉区","420104":"湖北省武汉市乔口区","420105":"湖北省武汉市汉阳区","420106":"湖北省武汉市武昌区","420107":"湖北省武汉市青山区","420111":"湖北省武汉市洪山区","420112":"湖北省武汉市东西湖区","420113":"湖北省武汉市汉南区","420114":"湖北省武汉市蔡甸区","420115":"湖北省武汉市江夏区","420116":"湖北省武汉市黄陂区","420117":"湖北省武汉市新洲区","420200":"湖北省黄石市","420201":"湖北省黄石市市辖区","420202":"湖北省黄石市黄石港区","420203":"湖北省黄石市石灰窑区","420204":"湖北省黄石市下陆区","420205":"湖北省黄石市铁山区","420222":"湖北省黄石市阳新县","420281":"湖北省黄石市大冶市","420300":"湖北省十堰市","420301":"湖北省十堰市市辖区","420302":"湖北省十堰市茅箭区","420303":"湖北省十堰市张湾区","420321":"湖北省十堰市郧县","420322":"湖北省十堰市郧西县","420323":"湖北省十堰市竹山县","420324":"湖北省十堰市竹溪县","420325":"湖北省十堰市房县","420381":"湖北省十堰市丹江口市","420500":"湖北省宜昌市","420501":"湖北省宜昌市市辖区","420502":"湖北省宜昌市西陵区","420503":"湖北省宜昌市伍家岗区","420504":"湖北省宜昌市点军区","420505":"湖北省宜昌市虎亭区","420521":"湖北省宜昌市宜昌县","420525":"湖北省宜昌市远安县","420526":"湖北省宜昌市兴山县","420527":"湖北省宜昌市秭归县","420528":"湖北省宜昌市长阳土家族自治县","420529":"湖北省宜昌市五峰土家族自治县","420581":"湖北省宜昌市宜都市","420582":"湖北省宜昌市当阳市","420583":"湖北省宜昌市枝江市","420600":"湖北省襄樊市","420601":"湖北省襄樊市市辖区","420602":"湖北省襄樊市襄城区","420606":"湖北省襄樊市樊城区","420621":"湖北省襄樊市襄阳县","420624":"湖北省襄樊市南漳县","420625":"湖北省襄樊市谷城县","420626":"湖北省襄樊市保康县","420682":"湖北省襄樊市老河口市","420683":"湖北省襄樊市枣阳市","420684":"湖北省襄樊市宜城市","420700":"湖北省鄂州市","420701":"湖北省鄂州市市辖区","420702":"湖北省鄂州市梁子湖区","420703":"湖北省鄂州市华容区","420704":"湖北省鄂州市鄂城区","420800":"湖北省荆门市","420801":"湖北省荆门市市辖区","420802":"湖北省荆门市东宝区","420821":"湖北省荆门市京山县","420822":"湖北省荆门市沙洋县","420881":"湖北省荆门市钟祥市","420900":"湖北省孝感市","420901":"湖北省孝感市市辖区","420902":"湖北省孝感市孝南区","420921":"湖北省孝感市孝昌县","420922":"湖北省孝感市大悟县","420923":"湖北省孝感市云梦县","420981":"湖北省孝感市应城市","420982":"湖北省孝感市安陆市","420983":"湖北省孝感市广水市","420984":"湖北省孝感市汉川市","421000":"湖北省荆州市","421001":"湖北省荆州市市辖区","421002":"湖北省荆州市沙市区","421003":"湖北省荆州市荆州区","421022":"湖北省荆州市公安县","421023":"湖北省荆州市监利县","421024":"湖北省荆州市江陵县","421081":"湖北省荆州市石首市","421083":"湖北省荆州市洪湖市","421087":"湖北省荆州市松滋市","421100":"湖北省黄冈市","421101":"湖北省黄冈市市辖区","421102":"湖北省黄冈市黄州区","421121":"湖北省黄冈市团风县","421122":"湖北省黄冈市红安县","421123":"湖北省黄冈市罗田县","421124":"湖北省黄冈市英山县","421125":"湖北省黄冈市浠水县","421126":"湖北省黄冈市蕲春县","421127":"湖北省黄冈市黄梅县","421181":"湖北省黄冈市麻城市","421182":"湖北省黄冈市武穴市","421200":"湖北省咸宁市","421201":"湖北省咸宁市市辖区","421202":"湖北省咸宁市咸安区","421221":"湖北省咸宁市嘉鱼县","421222":"湖北省咸宁市通城县","421223":"湖北省咸宁市崇阳县","421224":"湖北省咸宁市通山县","422800":"湖北省施土家族苗族自治州","422801":"湖北省恩施土家族苗族自治州恩施县","422802":"湖北省恩施土家族苗族自治州利川市","422822":"湖北省恩施土家族苗族自治州建始县","422823":"湖北省恩施土家族苗族自治州巴东县","422825":"湖北省恩施土家族苗族自治州宣恩县","422826":"湖北省恩施土家族苗族自治州咸丰县","422827":"湖北省恩施土家族苗族自治州来凤县","422828":"湖北省恩施土家族苗族自治州鹤峰县","429000":"湖北省省直辖县级行政单位","429001":"湖北省随州市","429004":"湖北省仙桃市","429005":"湖北省潜江市","429006":"湖北省天门市","429021":"湖北省神农架林区","430000":"湖南省","430100":"湖南省长沙市","430101":"湖南省长沙市市辖区","430102":"湖南省长沙市芙蓉区","430103":"湖南省长沙市天心区","430104":"湖南省长沙市岳麓区","430105":"湖南省长沙市开福区","430111":"湖南省长沙市雨花区","430121":"湖南省长沙市长沙县","430122":"湖南省长沙市望城县","430124":"湖南省长沙市宁乡县","430181":"湖南省长沙市浏阳市","430200":"湖南省株洲市","430201":"湖南省株洲市市辖区","430202":"湖南省株洲市荷塘区","430203":"湖南省株洲市芦淞区","430204":"湖南省株洲市石峰区","430211":"湖南省株洲市天元区","430221":"湖南省株洲市株洲县","430223":"湖南省株洲市攸县","430224":"湖南省株洲市茶陵县","430225":"湖南省株洲市炎陵县","430281":"湖南省株洲市醴陵市","430300":"湖南省湘潭市","430301":"湖南省湘潭市市辖区","430302":"湖南省湘潭市雨湖区","430304":"湖南省湘潭市岳塘区","430321":"湖南省湘潭市湘潭县","430381":"湖南省湘潭市湘乡市","430382":"湖南省湘潭市韶山市","430400":"湖南省衡阳市","430401":"湖南省衡阳市市辖区","430402":"湖南省衡阳市江东区","430403":"湖南省衡阳市城南区","430404":"湖南省衡阳市城北区","430411":"湖南省衡阳市郊区","430412":"湖南省衡阳市南岳区","430421":"湖南省衡阳市衡阳县","430422":"湖南省衡阳市衡南县","430423":"湖南省衡阳市衡山县","430424":"湖南省衡阳市衡东县","430426":"湖南省衡阳市祁东县","430481":"湖南省衡阳市耒阳市","430482":"湖南省衡阳市常宁市","430500":"湖南省邵阳市","430501":"湖南省邵阳市市辖区","430502":"湖南省邵阳市双清区","430503":"湖南省邵阳市大祥区","430511":"湖南省邵阳市北塔区","430521":"湖南省邵阳市邵东县","430522":"湖南省邵阳市新邵县","430523":"湖南省邵阳市邵阳县","430524":"湖南省邵阳市隆回县","430525":"湖南省邵阳市洞口县","430527":"湖南省邵阳市绥宁县","430528":"湖南省邵阳市新宁县","430529":"湖南省邵阳市城步苗族自治县","430581":"湖南省邵阳市武冈市","430600":"湖南省岳阳市","430601":"湖南省岳阳市市辖区","430602":"湖南省岳阳市岳阳楼区","430603":"湖南省岳阳市云溪区","430611":"湖南省岳阳市君山区","430621":"湖南省岳阳市岳阳县","430623":"湖南省岳阳市华容县","430624":"湖南省岳阳市湘阴县","430626":"湖南省岳阳市平江县","430681":"湖南省岳阳市汨罗市","430682":"湖南省岳阳市临湘市","430700":"湖南省常德市","430701":"湖南省常德市市辖区","430702":"湖南省常德市武陵区","430703":"湖南省常德市鼎城区","430721":"湖南省常德市安乡县","430722":"湖南省常德市汉寿县","430723":"湖南省常德市澧县","430724":"湖南省常德市临澧县","430725":"湖南省常德市桃源县","430726":"湖南省常德市石门县","430781":"湖南省常德市津市市","430800":"湖南省张家界市","430801":"湖南省张家界市市辖区","430802":"湖南省张家界市永定区","430811":"湖南省张家界市武陵源区","430821":"湖南省张家界市慈利县","430822":"湖南省张家界市桑植县","430900":"湖南省益阳市","430901":"湖南省益阳市市辖区","430902":"湖南省益阳市资阳区","430903":"湖南省益阳市赫山区","430921":"湖南省益阳市南县","430922":"湖南省益阳市桃江县","430923":"湖南省益阳市安化县","430981":"湖南省益阳市沅江市","431000":"湖南省郴州市","431001":"湖南省郴州市市辖区","431002":"湖南省郴州市北湖区","431003":"湖南省郴州市苏仙区","431021":"湖南省郴州市桂阳县","431022":"湖南省郴州市宜章县","431023":"湖南省郴州市永兴县","431024":"湖南省郴州市嘉禾县","431025":"湖南省郴州市临武县","431026":"湖南省郴州市汝城县","431027":"湖南省郴州市桂东县","431028":"湖南省郴州市安仁县","431081":"湖南省郴州市资兴市","431100":"湖南省永州市","431101":"湖南省永州市市辖区","431102":"湖南省永州市芝山区","431103":"湖南省永州市冷水滩区","431121":"湖南省永州市祁阳县","431122":"湖南省永州市东安县","431123":"湖南省永州市双牌县","431124":"湖南省永州市道县","431125":"湖南省永州市江永县","431126":"湖南省永州市宁远县","431127":"湖南省永州市蓝山县","431128":"湖南省永州市新田县","431129":"湖南省永州市江华瑶族自治县","431200":"湖南省怀化市","431201":"湖南省怀化市市辖区","431202":"湖南省怀化市鹤城区","431221":"湖南省怀化市中方县","431222":"湖南省怀化市沅陵县","431223":"湖南省怀化市辰溪县","431224":"湖南省怀化市溆浦县","431225":"湖南省怀化市会同县","431226":"湖南省怀化市麻阳苗族自治县","431227":"湖南省怀化市新晃侗族自治县","431228":"湖南省怀化市芷江侗族自治县","431229":"湖南省怀化市靖州苗族侗族自治县","431230":"湖南省怀化市通道侗族自治县","431281":"湖南省怀化市洪江市","432500":"湖南省娄底地区","432501":"湖南省娄底地区娄底市","432502":"湖南省娄底地区冷水江市","432503":"湖南省娄底地区涟源市","432522":"湖南省娄底地区双峰县","432524":"湖南省娄底地区新化县","433000":"湖南省怀化市","433001":"湖南省怀化市","433100":"湖南省湘西土家族苗族自治州","433101":"湖南省湘西土家族苗族自治州吉首市","433122":"湖南省湘西土家族苗族自治州泸溪县","433123":"湖南省湘西土家族苗族自治州凤凰县","433124":"湖南省湘西土家族苗族自治州花垣县","433125":"湖南省湘西土家族苗族自治州保靖县","433126":"湖南省湘西土家族苗族自治州古丈县","433127":"湖南省湘西土家族苗族自治州永顺县","433130":"湖南省湘西土家族苗族自治州龙山县","440000":"广东省","440100":"广东省广州市","440101":"广东省广州市市辖区","440102":"广东省广州市东山区","440103":"广东省广州市荔湾区","440104":"广东省广州市越秀区","440105":"广东省广州市海珠区","440106":"广东省广州市天河区","440107":"广东省广州市芳村区","440111":"广东省广州市白云区","440112":"广东省广州市黄埔区","440181":"广东省广州市番禺市","440182":"广东省广州市花都市","440183":"广东省广州市增城市","440184":"广东省广州市从化市","440200":"广东省韶关市","440201":"广东省韶关市市辖区","440202":"广东省韶关市北江区","440203":"广东省韶关市武江区","440204":"广东省韶关市浈江区","440221":"广东省韶关市曲江县","440222":"广东省韶关市始兴县","440224":"广东省韶关市仁化县","440229":"广东省韶关市翁源县","440232":"广东省韶关市乳源瑶族自治县","440233":"广东省韶关市新丰县","440281":"广东省韶关市乐昌市","440282":"广东省韶关市南雄市","440300":"广东省深圳市","440301":"广东省深圳市市辖区","440303":"广东省深圳市罗湖区","440304":"广东省深圳市福田区","440305":"广东省深圳市南山区","440306":"广东省深圳市宝安区","440307":"广东省深圳市龙岗区","440308":"广东省深圳市盐田区","440400":"广东省珠海市","440401":"广东省珠海市市辖区","440402":"广东省珠海市香洲区","440421":"广东省珠海市斗门县","440500":"广东省汕头市","440501":"广东省汕头市市辖区","440506":"广东省汕头市达濠区","440507":"广东省汕头市龙湖区","440508":"广东省汕头市金园区","440509":"广东省汕头市升平区","440510":"广东省汕头市河浦区","440523":"广东省汕头市南澳县","440582":"广东省汕头市潮阳市","440583":"广东省汕头市澄海市","440600":"广东省佛山市","440601":"广东省佛山市市辖区","440602":"广东省佛山市城区","440603":"广东省佛山市石湾区","440681":"广东省佛山市顺德市","440682":"广东省佛山市南海市","440683":"广东省佛山市三水市","440684":"广东省佛山市高明市","440700":"广东省江门市","440701":"广东省江门市市辖区","440703":"广东省江门市蓬江区","440704":"广东省江门市江海区","440781":"广东省江门市台山市","440782":"广东省江门市新会市","440783":"广东省江门市开平市","440784":"广东省江门市鹤山市","440785":"广东省江门市恩平市","440800":"广东省湛江市","440801":"广东省湛江市市辖区","440802":"广东省湛江市赤坎区","440803":"广东省湛江市霞山区","440804":"广东省湛江市坡头区","440811":"广东省湛江市麻章区","440823":"广东省湛江市遂溪县","440825":"广东省湛江市徐闻县","440881":"广东省湛江市廉江市","440882":"广东省湛江市雷州市","440883":"广东省湛江市吴川市","440900":"广东省茂名市","440901":"广东省茂名市市辖区","440902":"广东省茂名市茂南区","440923":"广东省茂名市电白县","440981":"广东省茂名市高州市","440982":"广东省茂名市化州市","440983":"广东省茂名市信宜市","441200":"广东省肇庆市","441201":"广东省肇庆市市辖区","441202":"广东省肇庆市端州区","441203":"广东省肇庆市鼎湖区","441223":"广东省肇庆市广宁县","441224":"广东省肇庆市怀集县","441225":"广东省肇庆市封开县","441226":"广东省肇庆市德庆县","441283":"广东省肇庆市高要市","441284":"广东省肇庆市四会市","441300":"广东省惠州市","441301":"广东省惠州市市辖区","441302":"广东省惠州市惠城区","441322":"广东省惠州市博罗县","441323":"广东省惠州市惠东县","441324":"广东省惠州市龙门县","441381":"广东省惠州市惠阳市","441400":"广东省梅州市","441401":"广东省梅州市市辖区","441402":"广东省梅州市梅江区","441421":"广东省梅州市梅县","441422":"广东省梅州市大埔县","441423":"广东省梅州市丰顺县","441424":"广东省梅州市五华县","441426":"广东省梅州市平远县","441427":"广东省梅州市蕉岭县","441481":"广东省梅州市兴宁市","441500":"广东省汕尾市","441501":"广东省汕尾市市辖区","441502":"广东省汕尾市城区","441521":"广东省汕尾市海丰县","441523":"广东省汕尾市陆河县","441581":"广东省汕尾市陆丰市","441600":"广东省河源市","441601":"广东省河源市市辖区","441602":"广东省河源市源城区","441621":"广东省河源市紫金县","441622":"广东省河源市龙川县","441623":"广东省河源市连平县","441624":"广东省河源市和平县","441625":"广东省河源市东源县","441700":"广东省阳江市","441701":"广东省阳江市市辖区","441702":"广东省阳江市江城区","441721":"广东省阳江市阳西县","441723":"广东省阳江市阳东县","441781":"广东省阳江市阳春市","441800":"广东省清远市","441801":"广东省清远市市辖区","441802":"广东省清远市清城区","441821":"广东省清远市佛冈县","441823":"广东省清远市阳山县","441825":"广东省清远市连山壮族瑶族自治县","441826":"广东省清远市连南瑶族自治县","441827":"广东省清远市清新县","441881":"广东省清远市英德市","441882":"广东省清远市连州市","441900":"广东省东莞市","441901":"广东省东莞市市辖区","442000":"广东省中山市","442001":"广东省中山市市辖区","445100":"广东省潮州市","445101":"广东省潮州市市辖区","445102":"广东省潮州市湘桥区","445121":"广东省潮州市潮安县","445122":"广东省潮州市饶平县","445200":"广东省揭阳市","445201":"广东省揭阳市市辖区","445202":"广东省揭阳市榕城区","445221":"广东省揭阳市揭东县","445222":"广东省揭阳市揭西县","445224":"广东省揭阳市惠来县","445281":"广东省揭阳市普宁市","445300":"广东省云浮市","445301":"广东省云浮市市辖区","445302":"广东省云浮市云城区","445321":"广东省云浮市新兴县","445322":"广东省云浮市郁南县","445323":"广东省云浮市云安县","445381":"广东省云浮市罗定市","450000":"广西壮族自治区","450100":"广西壮族自治区南宁市","450101":"广西壮族自治区南宁市市辖区","450102":"广西壮族自治区南宁市兴宁区","450103":"广西壮族自治区南宁市新城区","450104":"广西壮族自治区南宁市城北区","450105":"广西壮族自治区南宁市江南区","450106":"广西壮族自治区南宁市永新区","450111":"广西壮族自治区南宁市市郊区","450121":"广西壮族自治区南宁市邕宁县","450122":"广西壮族自治区南宁市武鸣县","450200":"广西壮族自治区柳州市","450201":"广西壮族自治区柳州市市辖区","450202":"广西壮族自治区柳州市城中区","450203":"广西壮族自治区柳州市鱼峰区","450204":"广西壮族自治区柳州市柳南区","450205":"广西壮族自治区柳州市柳北区","450211":"广西壮族自治区柳州市市郊区","450221":"广西壮族自治区柳州市柳江县","450222":"广西壮族自治区柳州市柳城县","450300":"广西壮族自治区桂林市","450301":"广西壮族自治区桂林市市辖区","450302":"广西壮族自治区桂林市秀峰区","450303":"广西壮族自治区桂林市叠彩区","450304":"广西壮族自治区桂林市象山区","450305":"广西壮族自治区桂林市七星区","450311":"广西壮族自治区桂林市雁山区","450321":"广西壮族自治区桂林市阳朔县","450322":"广西壮族自治区桂林市临桂县","450323":"广西壮族自治区桂林市灵川县","450324":"广西壮族自治区桂林市全州县","450325":"广西壮族自治区桂林市兴安县","450326":"广西壮族自治区桂林市永福县","450327":"广西壮族自治区桂林市灌阳县","450328":"广西壮族自治区桂林市龙胜各族自治县","450329":"广西壮族自治区桂林市资源县","450330":"广西壮族自治区桂林市平乐县","450331":"广西壮族自治区桂林市荔浦县","450332":"广西壮族自治区桂林市恭城瑶族自治县","450400":"广西壮族自治区梧州市","450401":"广西壮族自治区梧州市市辖区","450403":"广西壮族自治区梧州市万秀区","450404":"广西壮族自治区梧州市蝶山区","450411":"广西壮族自治区梧州市市郊区","450421":"广西壮族自治区梧州市苍梧县","450422":"广西壮族自治区梧州市藤县","450423":"广西壮族自治区梧州市蒙山县","450481":"广西壮族自治区梧州市岑溪市","450500":"广西壮族自治区北海市","450501":"广西壮族自治区北海市市辖区","450502":"广西壮族自治区北海市海城区","450503":"广西壮族自治区北海市银海区","450512":"广西壮族自治区北海市铁山港区","450521":"广西壮族自治区北海市合浦县","450600":"广西壮族自治区防城港市","450601":"广西壮族自治区防城港市市辖区","450602":"广西壮族自治区防城港市港口区","450603":"广西壮族自治区防城港市防城区","450621":"广西壮族自治区防城港市上思县","450681":"广西壮族自治区防城港市东兴市","450700":"广西壮族自治区钦州市","450701":"广西壮族自治区钦州市市辖区","450702":"广西壮族自治区钦州市钦南区","450703":"广西壮族自治区钦州市钦北区","450721":"广西壮族自治区钦州市灵山县","450722":"广西壮族自治区钦州市浦北县","450800":"广西壮族自治区贵港市","450801":"广西壮族自治区贵港市市辖区","450802":"广西壮族自治区贵港市港北区","450803":"广西壮族自治区贵港市港南区","450821":"广西壮族自治区贵港市平南县","450881":"广西壮族自治区贵港市桂平市","450900":"广西壮族自治区玉林市","450901":"广西壮族自治区玉林市市辖区","450902":"广西壮族自治区玉林市玉州区","450921":"广西壮族自治区玉林市容县","450922":"广西壮族自治区玉林市陆川县","450923":"广西壮族自治区玉林市博白县","450924":"广西壮族自治区玉林市兴业县","450981":"广西壮族自治区玉林市北流市","452100":"广西壮族自治区南宁地区","452101":"广西壮族自治区南宁地区凭祥市","452122":"广西壮族自治区南宁地区横县","452123":"广西壮族自治区南宁地区宾阳县","452124":"广西壮族自治区南宁地区上林县","452126":"广西壮族自治区南宁地区隆安县","452127":"广西壮族自治区南宁地区马山县","452128":"广西壮族自治区南宁地区扶绥县","452129":"广西壮族自治区南宁地区崇左县","452130":"广西壮族自治区南宁地区大新县","452131":"广西壮族自治区南宁地区天等县","452132":"广西壮族自治区南宁地区宁明县","452133":"广西壮族自治区南宁地区龙州县","452200":"广西壮族自治区柳州地区","452201":"广西壮族自治区柳州地区合山市","452223":"广西壮族自治区柳州地区鹿寨县","452224":"广西壮族自治区柳州地区象州县","452225":"广西壮族自治区柳州地区武宣县","452226":"广西壮族自治区柳州地区来宾县","452227":"广西壮族自治区柳州地区融安县","452228":"广西壮族自治区柳州地区三江侗族自治县","452229":"广西壮族自治区柳州地区融水苗族自治县","452230":"广西壮族自治区柳州地区金秀瑶族自治县","452231":"广西壮族自治区柳州地区忻城县","452400":"广西壮族自治区贺州地区","452402":"广西壮族自治区贺州地区贺州市","452424":"广西壮族自治区贺州地区昭平县","452427":"广西壮族自治区贺州地区钟山县","452428":"广西壮族自治区贺州地区富川瑶族自治县","452600":"广西壮族自治区百色地区","452601":"广西壮族自治区百色地区百色市","452622":"广西壮族自治区百色地区田阳县","452623":"广西壮族自治区百色地区田东县","452624":"广西壮族自治区百色地区平果县","452625":"广西壮族自治区百色地区德保县","452626":"广西壮族自治区百色地区靖西县","452627":"广西壮族自治区百色地区那坡县","452628":"广西壮族自治区百色地区凌云县","452629":"广西壮族自治区百色地区乐业县","452630":"广西壮族自治区百色地区田林县","452631":"广西壮族自治区百色地区隆林各族自治县","452632":"广西壮族自治区百色地区西林县","452700":"广西壮族自治区河池地区","452701":"广西壮族自治区河池地区河池市","452702":"广西壮族自治区河池地区宜州市","452723":"广西壮族自治区河池地区罗城仫佬族自治县","452724":"广西壮族自治区河池地区环江毛南族自治县","452725":"广西壮族自治区河池地区南丹县","452726":"广西壮族自治区河池地区天峨县","452727":"广西壮族自治区河池地区凤山县","452728":"广西壮族自治区河池地区东兰县","452729":"广西壮族自治区河池地区巴马瑶族自治县","452730":"广西壮族自治区河池地区都安瑶族自治县","452731":"广西壮族自治区河池地区大化瑶族自治县","460000":"海南省","460001":"海南省三亚市通什市","460002":"海南省三亚市琼海市","460003":"海南省三亚市儋州市","460004":"海南省三亚市琼山市","460005":"海南省三亚市文昌市","460006":"海南省三亚市万宁市","460007":"海南省三亚市东方市","460025":"海南省三亚市定安县","460026":"海南省三亚市屯昌县","460027":"海南省三亚市澄迈县","460028":"海南省三亚市临高县","460030":"海南省三亚市白沙黎族自治县","460031":"海南省三亚市昌江黎族自治县","460033":"海南省三亚市乐东黎族自治县","460034":"海南省三亚市陵水黎族自治县","460035":"海南省三亚市保亭黎族苗族自治县","460036":"海南省三亚市琼中黎族苗族自治县","460037":"海南省西沙群岛","460038":"海南省南沙群岛","460039":"海南省中沙群岛的岛礁及其海域","460100":"海南省海口市","460101":"海南省海口市市辖区","460102":"海南省海口市振东区","460103":"海南省海口市新华区","460104":"海南省海口市秀英区","460200":"海南省三亚市","460201":"海南省三亚市市辖区","500000":"重庆市","500100":"重庆市市辖区","500101":"重庆市万州区","500102":"重庆市涪陵区","500103":"重庆市渝中区","500104":"重庆市大渡口区","500105":"重庆市江北区","500106":"重庆市沙坪坝区","500107":"重庆市九龙坡区","500108":"重庆市南岸区","500109":"重庆市北碚区","500110":"重庆市万盛区","500111":"重庆市双桥区","500112":"重庆市渝北区","500113":"重庆市巴南区","500200":"重庆市县","500221":"重庆市长寿县","500222":"重庆市綦江县","500223":"重庆市潼南县","500224":"重庆市铜梁县","500225":"重庆市大足县","500226":"重庆市荣昌县","500227":"重庆市璧山县","500228":"重庆市梁平县","500229":"重庆市城口县","500230":"重庆市丰都县","500231":"重庆市垫江县","500232":"重庆市武隆县","500233":"重庆市忠县","500234":"重庆市开县","500235":"重庆市云阳县","500236":"重庆市奉节县","500237":"重庆市巫山县","500238":"重庆市巫溪县","500239":"重庆市黔江土家族苗族自治县","500240":"重庆市石柱土家族自治县","500241":"重庆市秀山土家族苗族自治县","500242":"重庆市酉阳土家族苗族自治县","500243":"重庆市彭水苗族土家族自治县","500300":"重庆市(市)","500381":"重庆市江津市","500382":"重庆市合川市","500383":"重庆市永川市","500384":"重庆市南川市","510000":"四川省","510100":"四川省成都市","510101":"四川省成都市市辖区","510104":"四川省成都市锦江区","510105":"四川省成都市青羊区","510106":"四川省成都市金牛区","510107":"四川省成都市武侯区","510108":"四川省成都市成华区","510112":"四川省成都市龙泉驿区","510113":"四川省成都市青白江区","510121":"四川省成都市金堂县","510122":"四川省成都市双流县","510123":"四川省成都市温江县","510124":"四川省成都市郫县","510125":"四川省成都市新都县","510129":"四川省成都市大邑县","510131":"四川省成都市蒲江县","510132":"四川省成都市新津县","510181":"四川省成都市都江堰市","510182":"四川省成都市彭州市","510183":"四川省成都市邛崃市","510184":"四川省成都市崇州市","510300":"四川省自贡市","510301":"四川省自贡市市辖区","510302":"四川省自贡市自流井区","510303":"四川省自贡市贡井区","510304":"四川省自贡市大安区","510311":"四川省自贡市沿滩区","510321":"四川省自贡市荣县","510322":"四川省自贡市富顺县","510400":"四川省攀枝花市","510401":"四川省攀枝花市市辖区","510402":"四川省攀枝花市东区","510403":"四川省攀枝花市西区","510411":"四川省攀枝花市仁和区","510421":"四川省攀枝花市米易县","510422":"四川省攀枝花市盐边县","510500":"四川省泸州市","510501":"四川省泸州市市辖区","510502":"四川省泸州市江阳区","510503":"四川省泸州市纳溪区","510504":"四川省泸州市龙马潭区","510521":"四川省泸州市泸县","510522":"四川省泸州市合江县","510524":"四川省泸州市叙永县","510525":"四川省泸州市古蔺县","510600":"四川省德阳市","510601":"四川省德阳市市辖区","510603":"四川省德阳市旌阳区","510623":"四川省德阳市中江县","510626":"四川省德阳市罗江县","510681":"四川省德阳市广汉市","510682":"四川省德阳市什邡市","510683":"四川省德阳市绵竹市","510700":"四川省绵阳市","510701":"四川省绵阳市市辖区","510703":"四川省绵阳市涪城区","510704":"四川省绵阳市游仙区","510722":"四川省绵阳市三台县","510723":"四川省绵阳市盐亭县","510724":"四川省绵阳市安县","510725":"四川省绵阳市梓潼县","510726":"四川省绵阳市北川县","510727":"四川省绵阳市平武县","510781":"四川省绵阳市江油市","510800":"四川省广元市","510801":"四川省广元市市辖区","510802":"四川省广元市市中区","510811":"四川省广元市元坝区","510812":"四川省广元市朝天区","510821":"四川省广元市旺苍县","510822":"四川省广元市青川县","510823":"四川省广元市剑阁县","510824":"四川省广元市苍溪县","510900":"四川省遂宁市","510901":"四川省遂宁市市辖区","510902":"四川省遂宁市市中区","510921":"四川省遂宁市蓬溪县","510922":"四川省遂宁市射洪县","510923":"四川省遂宁市大英县","511000":"四川省内江市","511001":"四川省内江市市辖区","511002":"四川省内江市市中区","511011":"四川省内江市东兴区","511024":"四川省内江市威远县","511025":"四川省内江市资中县","511028":"四川省内江市隆昌县","511100":"四川省乐山市","511101":"四川省乐山市市辖区","511102":"四川省乐山市市中区","511111":"四川省乐山市沙湾区","511112":"四川省乐山市五通桥区","511113":"四川省乐山市金口河区","511123":"四川省乐山市犍为县","511124":"四川省乐山市井研县","511126":"四川省乐山市夹江县","511129":"四川省乐山市沐川县","511132":"四川省乐山市峨边彝族自治县","511133":"四川省乐山市马边彝族自治县","511181":"四川省乐山市峨眉山市","511300":"四川省南充市","511301":"四川省南充市市辖区","511302":"四川省南充市顺庆区","511303":"四川省南充市高坪区","511304":"四川省南充市嘉陵区","511321":"四川省南充市南部县","511322":"四川省南充市营山县","511323":"四川省南充市蓬安县","511324":"四川省南充市仪陇县","511325":"四川省南充市西充县","511381":"四川省南充市阆中市","511500":"四川省宜宾市","511501":"四川省宜宾市市辖区","511502":"四川省宜宾市翠屏区","511521":"四川省宜宾市宜宾县","511522":"四川省宜宾市南溪县","511523":"四川省宜宾市江安县","511524":"四川省宜宾市长宁县","511525":"四川省宜宾市高县","511526":"四川省宜宾市珙县","511527":"四川省宜宾市筠连县","511528":"四川省宜宾市兴文县","511529":"四川省宜宾市屏山县","511600":"四川省广安市","511601":"四川省广安市市辖区","511602":"四川省广安市广安区","511621":"四川省广安市岳池县","511622":"四川省广安市武胜县","511623":"四川省广安市邻水县","511681":"四川省广安市华蓥市","513000":"四川省达川地区","513001":"四川省达川地区达川市","513002":"四川省达川地区万源市","513021":"四川省达川地区达县","513022":"四川省达川地区宣汉县","513023":"四川省达川地区开江县","513029":"四川省达川地区大竹县","513030":"四川省达川地区渠县","513100":"四川省雅安地区","513101":"四川省雅安地区雅安市","513122":"四川省雅安地区名山县","513123":"四川省雅安地区荥经县","513124":"四川省雅安地区汉源县","513125":"四川省雅安地区石棉县","513126":"四川省雅安地区天全县","513127":"四川省雅安地区芦山县","513128":"四川省雅安地区宝兴县","513200":"四川省阿坝藏族羌族自治州","513221":"四川省阿坝藏族羌族自治州汶川县","513222":"四川省阿坝藏族羌族自治州理县","513223":"四川省阿坝藏族羌族自治州茂县","513224":"四川省阿坝藏族羌族自治州松潘县","513225":"四川省阿坝藏族羌族自治州九寨沟县","513226":"四川省阿坝藏族羌族自治州金川县","513227":"四川省阿坝藏族羌族自治州小金县","513228":"四川省阿坝藏族羌族自治州黑水县","513229":"四川省阿坝藏族羌族自治州马尔康县","513230":"四川省阿坝藏族羌族自治州壤塘县","513231":"四川省阿坝藏族羌族自治州阿坝县","513232":"四川省阿坝藏族羌族自治州若尔盖县","513233":"四川省阿坝藏族羌族自治州红原县","513300":"四川省甘孜藏族自治州","513321":"四川省甘孜藏族自治州康定县","513322":"四川省甘孜藏族自治州泸定县","513323":"四川省甘孜藏族自治州丹巴县","513324":"四川省甘孜藏族自治州九龙县","513325":"四川省甘孜藏族自治州雅江县","513326":"四川省甘孜藏族自治州道孚县","513327":"四川省甘孜藏族自治州炉霍县","513328":"四川省甘孜藏族自治州甘孜县","513329":"四川省甘孜藏族自治州新龙县","513330":"四川省甘孜藏族自治州德格县","513331":"四川省甘孜藏族自治州白玉县","513332":"四川省甘孜藏族自治州石渠县","513333":"四川省甘孜藏族自治州色达县","513334":"四川省甘孜藏族自治州理塘县","513335":"四川省甘孜藏族自治州巴塘县","513336":"四川省甘孜藏族自治州乡城县","513337":"四川省甘孜藏族自治州稻城县","513338":"四川省甘孜藏族自治州得荣县","513400":"四川省凉山彝族自治州","513401":"四川省凉山彝族自治州西昌市","513422":"四川省凉山彝族自治州木里藏族自治县","513423":"四川省凉山彝族自治州盐源县","513424":"四川省凉山彝族自治州德昌县","513425":"四川省凉山彝族自治州会理县","513426":"四川省凉山彝族自治州会东县","513427":"四川省凉山彝族自治州宁南县","513428":"四川省凉山彝族自治州普格县","513429":"四川省凉山彝族自治州布拖县","513430":"四川省凉山彝族自治州金阳县","513431":"四川省凉山彝族自治州昭觉县","513432":"四川省凉山彝族自治州喜德县","513433":"四川省凉山彝族自治州冕宁县","513434":"四川省凉山彝族自治州越西县","513435":"四川省凉山彝族自治州甘洛县","513436":"四川省凉山彝族自治州美姑县","513437":"四川省凉山彝族自治州雷波县","513700":"四川省巴中地区","513701":"四川省巴中地区巴中市","513721":"四川省巴中地区通江县","513722":"四川省巴中地区南江县","513723":"四川省巴中地区平昌县","513800":"四川省眉山地区","513821":"四川省眉山地区眉山县","513822":"四川省眉山地区仁寿县","513823":"四川省眉山地区彭山县","513824":"四川省眉山地区洪雅县","513825":"四川省眉山地区丹棱县","513826":"四川省眉山地区青神县","513900":"四川省眉山地区资阳地区","513901":"四川省眉山地区资阳市","513902":"四川省眉山地区简阳市","513921":"四川省眉山地区安岳县","513922":"四川省眉山地区乐至县","520000":"贵州省","520100":"贵州省贵阳市","520101":"贵州省贵阳市市辖区","520102":"贵州省贵阳市南明区","520103":"贵州省贵阳市云岩区","520111":"贵州省贵阳市花溪区","520112":"贵州省贵阳市乌当区","520113":"贵州省贵阳市白云区","520121":"贵州省贵阳市开阳县","520122":"贵州省贵阳市息烽县","520123":"贵州省贵阳市修文县","520181":"贵州省贵阳市清镇市","520200":"贵州省六盘水市","520201":"贵州省六盘水市钟山区","520202":"贵州省六盘水市盘县特区","520203":"贵州省六盘水市六枝特区","520221":"贵州省六盘水市水城县","520300":"贵州省遵义市","520301":"贵州省遵义市市辖区","520302":"贵州省遵义市红花岗区","520321":"贵州省遵义市遵义县","520322":"贵州省遵义市桐梓县","520323":"贵州省遵义市绥阳县","520324":"贵州省遵义市正安县","520325":"贵州省遵义市道真仡佬族苗族自治县","520326":"贵州省遵义市务川仡佬族苗族自治县","520327":"贵州省遵义市凤冈县","520328":"贵州省遵义市湄潭县","520329":"贵州省遵义市余庆县","520330":"贵州省遵义市习水县","520381":"贵州省遵义市赤水市","520382":"贵州省遵义市仁怀市","522200":"贵州省铜仁地区","522201":"贵州省铜仁地区铜仁市","522222":"贵州省铜仁地区江口县","522223":"贵州省铜仁地区玉屏侗族自治县","522224":"贵州省铜仁地区石阡县","522225":"贵州省铜仁地区思南县","522226":"贵州省铜仁地区印江土家族苗族自治县","522227":"贵州省铜仁地区德江县","522228":"贵州省铜仁地区沿河土家族自治县","522229":"贵州省铜仁地区松桃苗族自治县","522230":"贵州省铜仁地区万山特区","522300":"贵州省黔西南布依族苗族自治州","522301":"贵州省黔西南布依族苗族自治州兴义市","522322":"贵州省黔西南布依族苗族自治州兴仁县","522323":"贵州省黔西南布依族苗族自治州普安县","522324":"贵州省黔西南布依族苗族自治州晴隆县","522325":"贵州省黔西南布依族苗族自治州贞丰县","522326":"贵州省黔西南布依族苗族自治州望谟县","522327":"贵州省黔西南布依族苗族自治州册亨县","522328":"贵州省黔西南布依族苗族自治州安龙县","522400":"贵州省毕节地区","522401":"贵州省毕节地区毕节市","522422":"贵州省毕节地区大方县","522423":"贵州省毕节地区黔西县","522424":"贵州省毕节地区金沙县","522425":"贵州省毕节地区织金县","522426":"贵州省毕节地区纳雍县","522427":"贵州省毕节地区威宁彝族回族苗族自治县","522428":"贵州省毕节地区赫章县","522500":"贵州省安顺地区","522501":"贵州省安顺地区安顺市","522526":"贵州省安顺地区平坝县","522527":"贵州省安顺地区普定县","522528":"贵州省安顺地区关岭布依族苗族自治县","522529":"贵州省安顺地区镇宁布依族苗族自治县","522530":"贵州省安顺地区紫云苗族布依族自治县","522600":"贵州省黔东南苗族侗族自治州","522601":"贵州省黔东南苗族侗族自治州凯里市","522622":"贵州省黔东南苗族侗族自治州黄平县","522623":"贵州省黔东南苗族侗族自治州施秉县","522624":"贵州省黔东南苗族侗族自治州三穗县","522625":"贵州省黔东南苗族侗族自治州镇远县","522626":"贵州省黔东南苗族侗族自治州岑巩县","522627":"贵州省黔东南苗族侗族自治州天柱县","522628":"贵州省黔东南苗族侗族自治州锦屏县","522629":"贵州省黔东南苗族侗族自治州剑河县","522630":"贵州省黔东南苗族侗族自治州台江县","522631":"贵州省黔东南苗族侗族自治州黎平县","522632":"贵州省黔东南苗族侗族自治州榕江县","522633":"贵州省黔东南苗族侗族自治州从江县","522634":"贵州省黔东南苗族侗族自治州雷山县","522635":"贵州省黔东南苗族侗族自治州麻江县","522636":"贵州省黔东南苗族侗族自治州丹寨县","522700":"贵州省黔南布依族苗族自治州","522701":"贵州省黔南布依族苗族自治州都匀市","522702":"贵州省黔南布依族苗族自治州福泉市","522722":"贵州省黔南布依族苗族自治州荔波县","522723":"贵州省黔南布依族苗族自治州贵定县","522725":"贵州省黔南布依族苗族自治州瓮安县","522726":"贵州省黔南布依族苗族自治州独山县","522727":"贵州省黔南布依族苗族自治州平塘县","522728":"贵州省黔南布依族苗族自治州罗甸县","522729":"贵州省黔南布依族苗族自治州长顺县","522730":"贵州省黔南布依族苗族自治州龙里县","522731":"贵州省黔南布依族苗族自治州惠水县","522732":"贵州省黔南布依族苗族自治州三都水族自治县","530000":"云南省","530100":"云南省昆明市","530101":"云南省昆明市市辖区","530102":"云南省昆明市五华区","530103":"云南省昆明市盘龙区","530111":"云南省昆明市官渡区","530112":"云南省昆明市西山区","530113":"云南省昆明市东川区","530121":"云南省昆明市呈贡县","530122":"云南省昆明市晋宁县","530124":"云南省昆明市富民县","530125":"云南省昆明市宜良县","530126":"云南省昆明市石林彝族自治县","530127":"云南省昆明市嵩明县","530128":"云南省昆明市禄劝彝族苗族自治县","530129":"云南省昆明市寻甸回族彝族自治县","530181":"云南省昆明市安宁市","530300":"云南省曲靖市","530301":"云南省曲靖市市辖区","530302":"云南省曲靖市麒麟区","530321":"云南省曲靖市马龙县","530322":"云南省曲靖市陆良县","530323":"云南省曲靖市师宗县","530324":"云南省曲靖市罗平县","530325":"云南省曲靖市富源县","530326":"云南省曲靖市会泽县","530328":"云南省曲靖市沾益县","530381":"云南省曲靖市宣威市","530400":"云南省玉溪市","530401":"云南省玉溪市市辖区","530402":"云南省玉溪市红塔区","530421":"云南省玉溪市江川县","530422":"云南省玉溪市澄江县","530423":"云南省玉溪市通海县","530424":"云南省玉溪市华宁县","530425":"云南省玉溪市易门县","530426":"云南省玉溪市峨山彝族自治县","530427":"云南省玉溪市新平彝族傣族自治县","530428":"云南省玉溪市元江哈尼族彝族傣族自治县","532100":"云南省昭通地区","532101":"云南省昭通地区昭通市","532122":"云南省昭通地区鲁甸县","532123":"云南省昭通地区巧家县","532124":"云南省昭通地区盐津县","532125":"云南省昭通地区大关县","532126":"云南省昭通地区永善县","532127":"云南省昭通地区绥江县","532128":"云南省昭通地区镇雄县","532129":"云南省昭通地区彝良县","532130":"云南省昭通地区威信县","532131":"云南省昭通地区水富县","532300":"云南省楚雄彝族自治州","532301":"云南省楚雄彝族自治州楚雄市","532322":"云南省楚雄彝族自治州双柏县","532323":"云南省楚雄彝族自治州牟定县","532324":"云南省楚雄彝族自治州南华县","532325":"云南省楚雄彝族自治州姚安县","532326":"云南省楚雄彝族自治州大姚县","532327":"云南省楚雄彝族自治州永仁县","532328":"云南省楚雄彝族自治州元谋县","532329":"云南省楚雄彝族自治州武定县","532331":"云南省楚雄彝族自治州禄丰县","532500":"云南省红河哈尼族彝族自治州","532501":"云南省红河哈尼族彝族自治州个旧市","532502":"云南省红河哈尼族彝族自治州开远市","532522":"云南省红河哈尼族彝族自治州蒙自县","532523":"云南省红河哈尼族彝族自治州屏边苗族自治县","532524":"云南省红河哈尼族彝族自治州建水县","532525":"云南省红河哈尼族彝族自治州石屏县","532526":"云南省红河哈尼族彝族自治州弥勒县","532527":"云南省红河哈尼族彝族自治州泸西县","532528":"云南省红河哈尼族彝族自治州元阳县","532529":"云南省红河哈尼族彝族自治州红河县","532530":"云南省红河哈尼族彝族自治州金平苗族瑶族傣族自治县","532531":"云南省红河哈尼族彝族自治州绿春县","532532":"云南省红河哈尼族彝族自治州河口瑶族自治县","532600":"云南省文山壮族苗族自治州","532621":"云南省文山壮族苗族自治州文山县","532622":"云南省文山壮族苗族自治州砚山县","532623":"云南省文山壮族苗族自治州西畴县","532624":"云南省文山壮族苗族自治州麻栗坡县","532625":"云南省文山壮族苗族自治州马关县","532626":"云南省文山壮族苗族自治州丘北县","532627":"云南省文山壮族苗族自治州广南县","532628":"云南省文山壮族苗族自治州富宁县","532700":"云南省思茅地区","532701":"云南省思茅地区思茅市","532722":"云南省思茅地区普洱哈尼族彝族自治县","532723":"云南省思茅地区墨江哈尼族自治县","532724":"云南省思茅地区景东彝族自治县","532725":"云南省思茅地区景谷傣族彝族自治县","532726":"云南省思茅地区镇沅彝族哈尼族拉祜族自治县","532727":"云南省思茅地区江城哈尼族彝族自治县","532728":"云南省思茅地区孟连傣族拉祜族佤族自治县","532729":"云南省思茅地区澜沧拉祜族自治县","532730":"云南省思茅地区西盟佤族自治县","532800":"云南省西双版纳傣族自治州","532801":"云南省西双版纳傣族自治州景洪市","532822":"云南省西双版纳傣族自治州勐海县","532823":"云南省西双版纳傣族自治州勐腊县","532900":"云南省大理白族自治州","532901":"云南省大理白族自治州大理市","532922":"云南省大理白族自治州漾濞彝族自治县","532923":"云南省大理白族自治州祥云县","532924":"云南省大理白族自治州宾川县","532925":"云南省大理白族自治州弥渡县","532926":"云南省大理白族自治州南涧彝族自治县","532927":"云南省大理白族自治州巍山彝族回族自治县","532928":"云南省大理白族自治州永平县","532929":"云南省大理白族自治州云龙县","532930":"云南省大理白族自治州洱源县","532931":"云南省大理白族自治州剑川县","532932":"云南省大理白族自治州鹤庆县","533000":"云南省保山地区","533001":"云南省保山地区保山市","533022":"云南省保山地区施甸县","533023":"云南省保山地区腾冲县","533024":"云南省保山地区龙陵县","533025":"云南省保山地区昌宁县","533100":"云南省德宏傣族景颇族自治州","533101":"云南省德宏傣族景颇族自治州畹町市","533102":"云南省德宏傣族景颇族自治州瑞丽市","533103":"云南省德宏傣族景颇族自治州潞西市","533122":"云南省德宏傣族景颇族自治州梁河县","533123":"云南省德宏傣族景颇族自治州盈江县","533124":"云南省德宏傣族景颇族自治州陇川县","533200":"云南省丽江地区","533221":"云南省丽江地区丽江纳西族自治县","533222":"云南省丽江地区永胜县","533223":"云南省丽江地区华坪县","533224":"云南省丽江地区宁蒗彝族自治县","533300":"云南省怒江傈僳族自治州","533321":"云南省怒江傈僳族自治州泸水县","533323":"云南省怒江傈僳族自治州福贡县","533324":"云南省怒江傈僳族自治州贡山独龙族怒族自治县","533325":"云南省怒江傈僳族自治州兰坪白族普米族自治县","533400":"云南省迪庆藏族自治州","533421":"云南省迪庆藏族自治州中甸县","533422":"云南省迪庆藏族自治州德钦县","533423":"云南省迪庆藏族自治州维西傈僳族自治县","533500":"云南省临沧地区","533521":"云南省临沧地区临沧县","533522":"云南省临沧地区凤庆县","533523":"云南省临沧地区云县","533524":"云南省临沧地区永德县","533525":"云南省临沧地区镇康县","533526":"云南省临沧地区双江拉祜族佤族布朗族傣族自治县","533527":"云南省临沧地区耿马傣族佤族自治县","533528":"云南省临沧地区沧源佤族自治县","540000":"西藏自治区","540100":"西藏自治区拉萨市","540101":"西藏自治区拉萨市市辖区","540102":"西藏自治区拉萨市城关区","540121":"西藏自治区拉萨市林周县","540122":"西藏自治区拉萨市当雄县","540123":"西藏自治区拉萨市尼木县","540124":"西藏自治区拉萨市曲水县","540125":"西藏自治区拉萨市堆龙德庆县","540126":"西藏自治区拉萨市达孜县","540127":"西藏自治区拉萨市墨竹工卡县","542100":"西藏自治区昌都地区","542121":"西藏自治区昌都地区昌都县","542122":"西藏自治区昌都地区江达县","542123":"西藏自治区昌都地区贡觉县","542124":"西藏自治区昌都地区类乌齐县","542125":"西藏自治区昌都地区丁青县","542126":"西藏自治区昌都地区察雅县","542127":"西藏自治区昌都地区八宿县","542128":"西藏自治区昌都地区左贡县","542129":"西藏自治区昌都地区芒康县","542132":"西藏自治区昌都地区洛隆县","542133":"西藏自治区昌都地区边坝县","542134":"西藏自治区昌都地区盐井县","542135":"西藏自治区昌都地区碧土县","542136":"西藏自治区昌都地区妥坝县","542137":"西藏自治区昌都地区生达县","542200":"西藏自治区山南地区","542221":"西藏自治区山南地区乃东县","542222":"西藏自治区山南地区扎囊县","542223":"西藏自治区山南地区贡嘎县","542224":"西藏自治区山南地区桑日县","542225":"西藏自治区山南地区琼结县","542226":"西藏自治区山南地区曲松县","542227":"西藏自治区山南地区措美县","542228":"西藏自治区山南地区洛扎县","542229":"西藏自治区山南地区加查县","542231":"西藏自治区山南地区隆子县","542232":"西藏自治区山南地区错那县","542233":"西藏自治区山南地区浪卡子县","542300":"西藏自治区日喀则地区","542301":"西藏自治区日喀则地区日喀则市","542322":"西藏自治区日喀则地区南木林县","542323":"西藏自治区日喀则地区江孜县","542324":"西藏自治区日喀则地区定日县","542325":"西藏自治区日喀则地区萨迦县","542326":"西藏自治区日喀则地区拉孜县","542327":"西藏自治区日喀则地区昂仁县","542328":"西藏自治区日喀则地区谢通门县","542329":"西藏自治区日喀则地区白朗县","542330":"西藏自治区日喀则地区仁布县","542331":"西藏自治区日喀则地区康马县","542332":"西藏自治区日喀则地区定结县","542333":"西藏自治区日喀则地区仲巴县","542334":"西藏自治区日喀则地区亚东县","542335":"西藏自治区日喀则地区吉隆县","542336":"西藏自治区日喀则地区聂拉木县","542337":"西藏自治区日喀则地区萨嘎县","542338":"西藏自治区日喀则地区岗巴县","542400":"西藏自治区那曲地区","542421":"西藏自治区那曲地区那曲县","542422":"西藏自治区那曲地区嘉黎县","542423":"西藏自治区那曲地区比如县","542424":"西藏自治区那曲地区聂荣县","542425":"西藏自治区那曲地区安多县","542426":"西藏自治区那曲地区申扎县","542427":"西藏自治区那曲地区索县","542428":"西藏自治区那曲地区班戈县","542429":"西藏自治区那曲地区巴青县","542430":"西藏自治区那曲地区尼玛县","542500":"西藏自治区阿里地区","542521":"西藏自治区阿里地区普兰县","542522":"西藏自治区阿里地区札达县","542523":"西藏自治区阿里地区噶尔县","542524":"西藏自治区阿里地区日土县","542525":"西藏自治区阿里地区革吉县","542526":"西藏自治区阿里地区改则县","542527":"西藏自治区阿里地区措勤县","542528":"西藏自治区阿里地区隆格尔县","542600":"西藏自治区林芝地区","542621":"西藏自治区林芝地区林芝县","542622":"西藏自治区林芝地区工布江达县","542623":"西藏自治区林芝地区米林县","542624":"西藏自治区林芝地区墨脱县","542625":"西藏自治区林芝地区波密县","542626":"西藏自治区林芝地区察隅县","542627":"西藏自治区林芝地区朗县","610000":"陕西省","610100":"陕西省西安市","610101":"陕西省西安市市辖区","610102":"陕西省西安市新城区","610103":"陕西省西安市碑林区","610104":"陕西省西安市莲湖区","610111":"陕西省西安市灞桥区","610112":"陕西省西安市未央区","610113":"陕西省西安市雁塔区","610114":"陕西省西安市阎良区","610115":"陕西省西安市临潼区","610121":"陕西省西安市长安县","610122":"陕西省西安市蓝田县","610124":"陕西省西安市周至县","610125":"陕西省西安市户县","610126":"陕西省西安市高陵县","610200":"陕西省铜川市","610201":"陕西省铜川市市辖区","610202":"陕西省铜川市城区","610203":"陕西省铜川市郊区","610221":"陕西省铜川市耀县","610222":"陕西省铜川市宜君县","610300":"陕西省宝鸡市","610301":"陕西省宝鸡市市辖区","610302":"陕西省宝鸡市渭滨区","610303":"陕西省宝鸡市金台区","610321":"陕西省宝鸡市宝鸡县","610322":"陕西省宝鸡市凤翔县","610323":"陕西省宝鸡市岐山县","610324":"陕西省宝鸡市扶风县","610326":"陕西省宝鸡市眉县","610327":"陕西省宝鸡市陇县","610328":"陕西省宝鸡市千阳县","610329":"陕西省宝鸡市麟游县","610330":"陕西省宝鸡市凤县","610331":"陕西省宝鸡市太白县","610400":"陕西省咸阳市","610401":"陕西省咸阳市市辖区","610402":"陕西省咸阳市秦都区","610403":"陕西省咸阳市杨陵区","610404":"陕西省咸阳市渭城区","610422":"陕西省咸阳市三原县","610423":"陕西省咸阳市泾阳县","610424":"陕西省咸阳市乾县","610425":"陕西省咸阳市礼泉县","610426":"陕西省咸阳市永寿县","610427":"陕西省咸阳市彬县","610428":"陕西省咸阳市长武县","610429":"陕西省咸阳市旬邑县","610430":"陕西省咸阳市淳化县","610431":"陕西省咸阳市武功县","610481":"陕西省咸阳市兴平市","610500":"陕西省渭南市","610501":"陕西省渭南市市辖区","610502":"陕西省渭南市临渭区","610521":"陕西省渭南市华县","610522":"陕西省渭南市潼关县","610523":"陕西省渭南市大荔县","610524":"陕西省渭南市合阳县","610525":"陕西省渭南市澄城县","610526":"陕西省渭南市蒲城县","610527":"陕西省渭南市白水县","610528":"陕西省渭南市富平县","610581":"陕西省渭南市韩城市","610582":"陕西省渭南市华阴市","610600":"陕西省延安市","610601":"陕西省延安市市辖区","610602":"陕西省延安市宝塔区","610621":"陕西省延安市延长县","610622":"陕西省延安市延川县","610623":"陕西省延安市子长县","610624":"陕西省延安市安塞县","610625":"陕西省延安市志丹县","610626":"陕西省延安市吴旗县","610627":"陕西省延安市甘泉县","610628":"陕西省延安市富县","610629":"陕西省延安市洛川县","610630":"陕西省延安市宜川县","610631":"陕西省延安市黄龙县","610632":"陕西省延安市黄陵县","610700":"陕西省汉中市","610701":"陕西省汉中市市辖区","610702":"陕西省汉中市汉台区","610721":"陕西省汉中市南郑县","610722":"陕西省汉中市城固县","610723":"陕西省汉中市洋县","610724":"陕西省汉中市西乡县","610725":"陕西省汉中市勉县","610726":"陕西省汉中市宁强县","610727":"陕西省汉中市略阳县","610728":"陕西省汉中市镇巴县","610729":"陕西省汉中市留坝县","610730":"陕西省汉中市佛坪县","612400":"陕西省安康地区","612401":"陕西省安康地区安康市","612422":"陕西省安康地区汉阴县","612423":"陕西省安康地区石泉县","612424":"陕西省安康地区宁陕县","612425":"陕西省安康地区紫阳县","612426":"陕西省安康地区岚皋县","612427":"陕西省安康地区平利县","612428":"陕西省安康地区镇坪县","612429":"陕西省安康地区旬阳县","612430":"陕西省安康地区白河县","612500":"陕西省商洛地区","612501":"陕西省商洛地区商州市","612522":"陕西省商洛地区洛南县","612523":"陕西省商洛地区丹凤县","612524":"陕西省商洛地区商南县","612525":"陕西省商洛地区山阳县","612526":"陕西省商洛地区镇安县","612527":"陕西省商洛地区柞水县","612700":"陕西省榆林地区","612701":"陕西省榆林地区榆林市","612722":"陕西省榆林地区神木县","612723":"陕西省榆林地区府谷县","612724":"陕西省榆林地区横山县","612725":"陕西省榆林地区靖边县","612726":"陕西省榆林地区定边县","612727":"陕西省榆林地区绥德县","612728":"陕西省榆林地区米脂县","612729":"陕西省榆林地区佳县","612730":"陕西省榆林地区吴堡县","612731":"陕西省榆林地区清涧县","612732":"陕西省榆林地区子洲县","620000":"甘肃省","620100":"甘肃省兰州市","620101":"甘肃省兰州市市辖区","620102":"甘肃省兰州市城关区","620103":"甘肃省兰州市七里河区","620104":"甘肃省兰州市西固区","620105":"甘肃省兰州市安宁区","620111":"甘肃省兰州市红古区","620121":"甘肃省兰州市永登县","620122":"甘肃省兰州市皋兰县","620123":"甘肃省兰州市榆中县","620200":"甘肃省嘉峪关市","620201":"甘肃省嘉峪关市市辖区","620300":"甘肃省嘉峪关市金昌市","620301":"甘肃省嘉峪关市市辖区","620302":"甘肃省嘉峪关市金川区","620321":"甘肃省嘉峪关市永昌县","620400":"甘肃省白银市","620401":"甘肃省白银市市辖区","620402":"甘肃省白银市白银区","620403":"甘肃省白银市平川区","620421":"甘肃省白银市靖远县","620422":"甘肃省白银市会宁县","620423":"甘肃省白银市景泰县","620500":"甘肃省天水市","620501":"甘肃省天水市市辖区","620502":"甘肃省天水市秦城区","620503":"甘肃省天水市北道区","620521":"甘肃省天水市清水县","620522":"甘肃省天水市秦安县","620523":"甘肃省天水市甘谷县","620524":"甘肃省天水市武山县","620525":"甘肃省天水市张家川回族自治县","622100":"甘肃省酒泉地区","622101":"甘肃省酒泉地区玉门市","622102":"甘肃省酒泉地区酒泉市","622103":"甘肃省酒泉地区敦煌市","622123":"甘肃省酒泉地区金塔县","622124":"甘肃省酒泉地区肃北蒙古族自治县","622125":"甘肃省酒泉地区阿克塞哈萨克族自治县","622126":"甘肃省酒泉地区安西县","622200":"甘肃省张掖地区","622201":"甘肃省张掖地区张掖市","622222":"甘肃省张掖地区肃南裕固族自治县","622223":"甘肃省张掖地区民乐县","622224":"甘肃省张掖地区临泽县","622225":"甘肃省张掖地区高台县","622226":"甘肃省张掖地区山丹县","622300":"甘肃省武威地区","622301":"甘肃省武威地区武威市","622322":"甘肃省武威地区民勤县","622323":"甘肃省武威地区古浪县","622326":"甘肃省武威地区天祝藏族自治县","622400":"甘肃省定西地区","622421":"甘肃省定西地区定西县","622424":"甘肃省定西地区通渭县","622425":"甘肃省定西地区陇西县","622426":"甘肃省定西地区渭源县","622427":"甘肃省定西地区临洮县","622428":"甘肃省定西地区漳县","622429":"甘肃省定西地区岷县","622600":"甘肃省陇南地区","622621":"甘肃省陇南地区武都县","622623":"甘肃省陇南地区宕昌县","622624":"甘肃省陇南地区成县","622625":"甘肃省陇南地区康县","622626":"甘肃省陇南地区文县","622627":"甘肃省陇南地区西和县","622628":"甘肃省陇南地区礼县","622629":"甘肃省陇南地区两当县","622630":"甘肃省陇南地区徽县","622700":"甘肃省平凉地区","622701":"甘肃省平凉地区平凉市","622722":"甘肃省平凉地区泾川县","622723":"甘肃省平凉地区灵台县","622724":"甘肃省平凉地区崇信县","622725":"甘肃省平凉地区华亭县","622726":"甘肃省平凉地区庄浪县","622727":"甘肃省平凉地区静宁县","622800":"甘肃省庆阳地区","622801":"甘肃省庆阳地区西峰市","622821":"甘肃省庆阳地区庆阳县","622822":"甘肃省庆阳地区环县","622823":"甘肃省庆阳地区华池县","622824":"甘肃省庆阳地区合水县","622825":"甘肃省庆阳地区正宁县","622826":"甘肃省庆阳地区宁县","622827":"甘肃省庆阳地区镇原县","622900":"甘肃省临夏回族自治州","622901":"甘肃省临夏回族自治州临夏市","622921":"甘肃省临夏回族自治州临夏县","622922":"甘肃省临夏回族自治州康乐县","622923":"甘肃省临夏回族自治州永靖县","622924":"甘肃省临夏回族自治州广河县","622925":"甘肃省临夏回族自治州和政县","622926":"甘肃省临夏回族自治州东乡族自治县","622927":"甘肃省临夏回族自治州积石山保安族东乡族撒拉族自治县","623000":"甘肃省甘南藏族自治州","623001":"甘肃省甘南藏族自治州合作市","623021":"甘肃省甘南藏族自治州临潭县","623022":"甘肃省甘南藏族自治州卓尼县","623023":"甘肃省甘南藏族自治州舟曲县","623024":"甘肃省甘南藏族自治州迭部县","623025":"甘肃省甘南藏族自治州玛曲县","623026":"甘肃省甘南藏族自治州碌曲县","623027":"甘肃省甘南藏族自治州夏河县","630000":"青海省","630100":"青海省西宁市","630101":"青海省西宁市市辖区","630102":"青海省西宁市城东区","630103":"青海省西宁市城中区","630104":"青海省西宁市城西区","630105":"青海省西宁市城北区","630121":"青海省西宁市大通回族土族自治县","632100":"青海省海东地区","632121":"青海省海东地区平安县","632122":"青海省海东地区民和回族土族自治县","632123":"青海省海东地区乐都县","632124":"青海省海东地区湟中县","632125":"青海省海东地区湟源县","632126":"青海省海东地区互助土族自治县","632127":"青海省海东地区化隆回族自治县","632128":"青海省海东地区循化撒拉族自治县","632200":"青海省海北藏族自治州","632221":"青海省海北藏族自治州门源回族自治县","632222":"青海省海北藏族自治州祁连县","632223":"青海省海北藏族自治州海晏县","632224":"青海省海北藏族自治州刚察县","632300":"青海省黄南藏族自治州","632321":"青海省黄南藏族自治州同仁县","632322":"青海省黄南藏族自治州尖扎县","632323":"青海省黄南藏族自治州泽库县","632324":"青海省黄南藏族自治州河南蒙古族自治县","632500":"青海省海南藏族自治州","632521":"青海省海南藏族自治州共和县","632522":"青海省海南藏族自治州同德县","632523":"青海省海南藏族自治州贵德县","632524":"青海省海南藏族自治州兴海县","632525":"青海省海南藏族自治州贵南县","632600":"青海省果洛藏族自治州","632621":"青海省果洛藏族自治州玛沁县","632622":"青海省果洛藏族自治州班玛县","632623":"青海省果洛藏族自治州甘德县","632624":"青海省果洛藏族自治州达日县","632625":"青海省果洛藏族自治州久治县","632626":"青海省果洛藏族自治州玛多县","632700":"青海省玉树藏族自治州","632721":"青海省玉树藏族自治州玉树县","632722":"青海省玉树藏族自治州杂多县","632723":"青海省玉树藏族自治州称多县","632724":"青海省玉树藏族自治州治多县","632725":"青海省玉树藏族自治州囊谦县","632726":"青海省玉树藏族自治州曲麻莱县","632800":"青海省海西蒙古族藏族自治州","632801":"青海省海西蒙古族藏族自治州格尔木市","632802":"青海省海西蒙古族藏族自治州德令哈市","632821":"青海省海西蒙古族藏族自治州乌兰县","632822":"青海省海西蒙古族藏族自治州都兰县","632823":"青海省海西蒙古族藏族自治州天峻县","640000":"宁夏回族自治区","640100":"宁夏回族自治区银川市","640101":"宁夏回族自治区银川市市辖区","640102":"宁夏回族自治区银川市城区","640103":"宁夏回族自治区银川市新城区","640111":"宁夏回族自治区银川市郊区","640121":"宁夏回族自治区银川市永宁县","640122":"宁夏回族自治区银川市贺兰县","640200":"宁夏回族自治区石嘴山市","640201":"宁夏回族自治区石嘴山市市辖区","640202":"宁夏回族自治区石嘴山市大武口区","640203":"宁夏回族自治区石嘴山市石嘴山区","640204":"宁夏回族自治区石嘴山市石炭井区","640221":"宁夏回族自治区石嘴山市平罗县","640222":"宁夏回族自治区石嘴山市陶乐县","640223":"宁夏回族自治区石嘴山市惠农县","640300":"宁夏回族自治区吴忠市","640301":"宁夏回族自治区吴忠市市辖区","640302":"宁夏回族自治区吴忠市利通区","640321":"宁夏回族自治区吴忠市中卫县","640322":"宁夏回族自治区吴忠市中宁县","640323":"宁夏回族自治区吴忠市盐池县","640324":"宁夏回族自治区吴忠市同心县","640381":"宁夏回族自治区吴忠市青铜峡市","640382":"宁夏回族自治区吴忠市灵武市","642200":"宁夏回族自治区固原地区","642221":"宁夏回族自治区固原地区固原县","642222":"宁夏回族自治区固原地区海原县","642223":"宁夏回族自治区固原地区西吉县","642224":"宁夏回族自治区固原地区隆德县","642225":"宁夏回族自治区固原地区泾源县","642226":"宁夏回族自治区固原地区彭阳县","650000":"新疆维吾尔自治区","650100":"新疆维吾尔族自治区乌鲁木齐市","650101":"新疆维吾尔族自治区乌鲁木齐市市辖区","650102":"新疆维吾尔族自治区乌鲁木齐市天山区","650103":"新疆维吾尔族自治区乌鲁木齐市沙依巴克区","650104":"新疆维吾尔族自治区乌鲁木齐市新市区","650105":"新疆维吾尔族自治区乌鲁木齐市水磨沟区","650106":"新疆维吾尔族自治区乌鲁木齐市头屯河区","650107":"新疆维吾尔族自治区乌鲁木齐市南山矿区","650108":"新疆维吾尔族自治区乌鲁木齐市东山区","650121":"新疆维吾尔族自治区乌鲁木齐市乌鲁木齐县","650200":"新疆维吾尔族自治区克拉玛依市","650201":"新疆维吾尔族自治区克拉玛依市市辖区","650202":"新疆维吾尔族自治区克拉玛依市独山子区","650203":"新疆维吾尔族自治区克拉玛依市克拉玛依区","650204":"新疆维吾尔族自治区克拉玛依市白碱滩区","650205":"新疆维吾尔族自治区克拉玛依市乌尔禾区","652100":"新疆维吾尔族自治区吐鲁番地区","652101":"新疆维吾尔族自治区吐鲁番地区吐鲁番市","652122":"新疆维吾尔族自治区吐鲁番地区鄯善县","652123":"新疆维吾尔族自治区吐鲁番地区托克逊县","652200":"新疆维吾尔族自治区哈密地区","652201":"新疆维吾尔族自治区哈密地区哈密市","652222":"新疆维吾尔族自治区哈密地区巴里坤哈萨克自治县","652223":"新疆维吾尔族自治区哈密地区伊吾县","652300":"新疆维吾尔族自治区昌吉回族自治州","652301":"新疆维吾尔族自治区昌吉回族自治州昌吉市","652302":"新疆维吾尔族自治区昌吉回族自治州阜康市","652303":"新疆维吾尔族自治区昌吉回族自治州米泉市","652323":"新疆维吾尔族自治区昌吉回族自治州呼图壁县","652324":"新疆维吾尔族自治区昌吉回族自治州玛纳斯县","652325":"新疆维吾尔族自治区昌吉回族自治州奇台县","652327":"新疆维吾尔族自治区昌吉回族自治州吉木萨尔县","652328":"新疆维吾尔族自治区昌吉回族自治州木垒哈萨克自治县","652700":"新疆维吾尔族自治区博尔塔拉蒙古自治州","652701":"新疆维吾尔族自治区博尔塔拉蒙古自治州博乐市","652722":"新疆维吾尔族自治区博尔塔拉蒙古自治州精河县","652723":"新疆维吾尔族自治区博尔塔拉蒙古自治州温泉县","652800":"新疆维吾尔族自治区巴音郭楞蒙古自治州","652801":"新疆维吾尔族自治区巴音郭楞蒙古自治州库尔勒市","652822":"新疆维吾尔族自治区巴音郭楞蒙古自治州轮台县","652823":"新疆维吾尔族自治区巴音郭楞蒙古自治州尉犁县","652824":"新疆维吾尔族自治区巴音郭楞蒙古自治州若羌县","652825":"新疆维吾尔族自治区巴音郭楞蒙古自治州且末县","652826":"新疆维吾尔族自治区巴音郭楞蒙古自治州焉耆回族自治县","652827":"新疆维吾尔族自治区巴音郭楞蒙古自治州和静县","652828":"新疆维吾尔族自治区巴音郭楞蒙古自治州和硕县","652829":"新疆维吾尔族自治区巴音郭楞蒙古自治州博湖县","652900":"新疆维吾尔族自治区阿克苏地区","652901":"新疆维吾尔族自治区阿克苏地区阿克苏市","652922":"新疆维吾尔族自治区阿克苏地区温宿县","652923":"新疆维吾尔族自治区阿克苏地区库车县","652924":"新疆维吾尔族自治区阿克苏地区沙雅县","652925":"新疆维吾尔族自治区阿克苏地区新和县","652926":"新疆维吾尔族自治区阿克苏地区拜城县","652927":"新疆维吾尔族自治区阿克苏地区乌什县","652928":"新疆维吾尔族自治区阿克苏地区阿瓦提县","652929":"新疆维吾尔族自治区阿克苏地区柯坪县","653000":"新疆维吾尔族自治区克孜勒苏柯尔克孜自治州","653001":"新疆维吾尔族自治区克孜勒苏柯尔克孜自治州阿图什市","653022":"新疆维吾尔族自治区克孜勒苏柯尔克孜自治州阿克陶县","653023":"新疆维吾尔族自治区克孜勒苏柯尔克孜自治州阿合奇县","653024":"新疆维吾尔族自治区克孜勒苏柯尔克孜自治州乌恰县","653100":"新疆维吾尔族自治区喀什地区","653101":"新疆维吾尔族自治区喀什地区喀什市","653121":"新疆维吾尔族自治区喀什地区疏附县","653122":"新疆维吾尔族自治区喀什地区疏勒县","653123":"新疆维吾尔族自治区喀什地区英吉沙县","653124":"新疆维吾尔族自治区喀什地区泽普县","653125":"新疆维吾尔族自治区喀什地区莎车县","653126":"新疆维吾尔族自治区喀什地区叶城县","653127":"新疆维吾尔族自治区喀什地区麦盖提县","653128":"新疆维吾尔族自治区喀什地区岳普湖县","653129":"新疆维吾尔族自治区喀什地区伽师县","653130":"新疆维吾尔族自治区喀什地区巴楚县","653131":"新疆维吾尔族自治区喀什地区塔什库尔干塔吉克自治县","653200":"新疆维吾尔族自治区和田地区","653201":"新疆维吾尔族自治区和田地区和田市","653221":"新疆维吾尔族自治区和田地区和田县","653222":"新疆维吾尔族自治区和田地区墨玉县","653223":"新疆维吾尔族自治区和田地区皮山县","653224":"新疆维吾尔族自治区和田地区洛浦县","653225":"新疆维吾尔族自治区和田地区策勒县","653226":"新疆维吾尔族自治区和田地区于田县","653227":"新疆维吾尔族自治区和田地区民丰县","654000":"新疆维吾尔族自治区伊犁哈萨克自治州","654001":"新疆维吾尔族自治区伊犁哈萨克自治州奎屯市","654100":"新疆维吾尔族自治区伊犁哈萨克自治州伊犁地区","654101":"新疆维吾尔族自治区伊犁哈萨克自治州伊宁市","654121":"新疆维吾尔族自治区伊犁哈萨克自治州伊宁县","654122":"新疆自治区伊犁哈萨克自治州察布查尔锡伯自治县","654123":"新疆维吾尔族自治区伊犁哈萨克自治州霍城县","654124":"新疆维吾尔族自治区伊犁哈萨克自治州巩留县","654125":"新疆维吾尔族自治区伊犁哈萨克自治州新源县","654126":"新疆维吾尔族自治区伊犁哈萨克自治州昭苏县","654127":"新疆维吾尔族自治区伊犁哈萨克自治州特克斯县","654128":"新疆维吾尔族自治区伊犁哈萨克自治州尼勒克县","654200":"新疆维吾尔族自治区塔城地区","654201":"新疆维吾尔族自治区塔城地区塔城市","654202":"新疆维吾尔族自治区塔城地区乌苏市","654221":"新疆维吾尔族自治区塔城地区额敏县","654223":"新疆维吾尔族自治区塔城地区沙湾县","654224":"新疆维吾尔族自治区塔城地区托里县","654225":"新疆维吾尔族自治区塔城地区裕民县","654226":"新疆维吾尔族自治区塔城地区和布克赛尔蒙古自治县","654300":"新疆维吾尔族自治区阿勒泰地区","654301":"新疆维吾尔族自治区阿勒泰地区阿勒泰市","654321":"新疆维吾尔族自治区阿勒泰地区布尔津县","654322":"新疆维吾尔族自治区阿勒泰地区富蕴县","654323":"新疆维吾尔族自治区阿勒泰地区福海县","654324":"新疆维吾尔族自治区阿勒泰地区哈巴河县","654325":"新疆维吾尔族自治区阿勒泰地区青河县","654326":"新疆维吾尔族自治区阿勒泰地区吉木乃县","659000":"新疆维吾尔族自治区直辖县级行政单位","659001":"新疆维吾尔族自治区石河子市"}; diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/legacy-auth-districts.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/presentation/legacy-auth-districts.ts.meta new file mode 100644 index 0000000..9c8cdbd --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/legacy-auth-districts.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "a834856f-7a53-445b-b517-6588ad346e59", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/lobby-menu.ts b/cocoscreator_projects/YouleNexus/assets/framework/presentation/lobby-menu.ts index a34ee16..75606c1 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/presentation/lobby-menu.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/lobby-menu.ts @@ -1,121 +1,131 @@ -import type { RemoteConfig } from '../config/remote-config.ts'; - -export interface LobbyTemplate { - isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean; -} -/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */ -export const LOBBY_TEMPLATE:Readonly=Object.freeze({ - isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false, -}); -export interface LobbyContext {versionState:0|1;launchMode:0|1;} -const HALL_DEFAULTS=Object.freeze({androidPay:0,rankList:0 as number|string,storeUrl:'',bagUrl:'',actData:0 as unknown,worldRoomList:0 as number|string,wareHouse:0 as number|string}); -const SYS_DEFAULTS=Object.freeze({hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1, - ofcShortCode:null as unknown,hideSpid:null as unknown}); -function object(value:unknown,name:string):Record{ - if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`); - return value as Record; -} -/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */ -export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly=LOBBY_TEMPLATE){ - const rawHall=remote.getValue('hall_config'); - const merged=rawHall==null?{}:object(rawHall,'hall_config'); - const hall={...HALL_DEFAULTS,...merged}; - for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){ - // 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData. - if(key==='actData')continue; - const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value}); - } - for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){ - if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`); - if(key==='androidPay')hall[key]=Number(hall[key]); - } - for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`); - const sys={...SYS_DEFAULTS}; - for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof SYS_DEFAULTS)[]){ - const value=remote.getValue(key);if(value!=null)Object.assign(sys,{[key]:value}); - } - for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){ - if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`); - if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]); - } - if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid'); - const rawAgent=remote.getValue('agentmode'); - const agentmode=rawAgent==null?1:Number(rawAgent); - if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode'); - return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template}); +import type { RemoteConfig } from '../config/remote-config.ts'; + +export interface LobbyTemplate { + isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean; +} +/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */ +export const LOBBY_TEMPLATE:Readonly=Object.freeze({ + isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false, +}); +export interface LobbyContext {versionState:0|1;launchMode:0|1;} +const HALL_DEFAULTS=Object.freeze({androidPay:0,rankList:0 as number|string,isReSetOwnerNote:0 as number|string,storeUrl:'',bagUrl:'',actData:0 as unknown,worldRoomList:0 as number|string,wareHouse:0 as number|string}); +const SYS_DEFAULTS=Object.freeze({vipRankLimit:10,hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1, + ofcShortCode:null as unknown,hideSpid:null as unknown}); +function object(value:unknown,name:string):Record{ + if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`); + return value as Record; +} +/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */ +export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly=LOBBY_TEMPLATE){ + const rawHall=remote.getValue('hall_config'); + const merged=rawHall==null?{}:object(rawHall,'hall_config'); + const hall={...HALL_DEFAULTS,...merged}; + for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){ + // 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData. + if(key==='actData')continue; + const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value}); + } + for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){ + if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`); + if(key==='androidPay')hall[key]=Number(hall[key]); + } + for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`); + const sys={...SYS_DEFAULTS}; + for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof SYS_DEFAULTS)[]){ + const value=remote.getValue(key);if(value!=null)Object.assign(sys,{[key]:value}); + } + if(!Number.isInteger(Number(sys.vipRankLimit))||Number(sys.vipRankLimit)<=0)throw Error('Invalid vipRankLimit'); + sys.vipRankLimit=Number(sys.vipRankLimit); + for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){ + if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`); + if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]); + } + if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid'); + const rawAgent=remote.getValue('agentmode'); + const agentmode=rawAgent==null?1:Number(rawAgent); + if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode'); + // Original GameUI draw cases 568/483/3139: missing vipConfig means unopened; + // the earlier matchhtml condition is commented out in the source. + const vipConfig=remote.getValue('vipConfig'); + const locked=Object.freeze({568:!hall.worldRoomList,483:!vipConfig,3139:!sys.ofcShortCode}); + return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template,locked}); } export type LobbyMenuConfig=ReturnType; -export interface LobbyMenuState {visible:Record;left:Record;top:Record;} - -/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */ -export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{ - const {hall,sys,template,agentmode}=config; - const visible:Record={},left:Record={},top:Record={}; - const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;}; - // Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X. - if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;} - hide([219,220]); - if(context.versionState===1){ - if(context.launchMode===0)hide([234]); - hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604, - 286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]); - }else{ - if(agentmode===1)hide([10,509]); - visible[235]=false; - if(hall.androidPay===1){visible[10]=true;visible[509]=true;} - visible[3018]=!template.isGameHall&&Array.isArray(hall.actData); - visible[3016]=Boolean(hall.storeUrl); - if(template.isGameHall||template.vipInfinite)hide([3139,679]); - if(template.mainMenuButton)hide([11,12,568,483]); - if(template.hideNotice)hide([16,180,193,194,682,178,179,145]); - if(sys.hideTaskBtn)hide([17]); - if(sys.miniPro||sys.hideWxShare)hide([14]); - if(!template.isGameHall){ - const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false); - shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);}); - } - visible[3233]=Boolean(hall.rankList); - visible[3256]=false; // Both original deviceLogin branches hide this entry. - if(template.isGameHall){ - hide([14,13,17,3120,99,3016,76,234,3057,145,3139,3232,3233,604]); - visible[235]=agentmode!==1;visible[15]=true;visible[5]=true; - const shown=[235,15,5,7].filter(id=>visible[id]!==false); - shown.forEach((id,index)=>{left[id]=300+520*index/(shown.length-1);top[id]=642;}); - } - } - if(sys.hideRoomCard===2)hide([286,10,287,3161]); - visible[3264]=sys.cdKey===2; - return {visible,left,top}; +/** Original click messages intentionally use different comma characters. */ +export function lobbyLockedTip(config:LobbyMenuConfig,action:'world-rooms'|'match'|'official-rooms'):string|null{ + if(action==='world-rooms')return config.locked[568]?'暂未开放,敬请期待':null; + return config.locked[action==='match'?483:3139]?'暂未开放,敬请期待':null; } - -/** openPallet runs its own visibility pass each time before placing visible entries. */ -export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{ - const visible:Record={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={3121:200},top:Record={}; - 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>>=Object.freeze({ - 'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块', - 'buy-roomcard':'购买房卡:尚未接入支付模块','buy-bean':'购买星星:尚未接入支付模块', - match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口', - activity:'活动:尚未接入活动身份参数与原生接口','my-rooms':'我的房间:尚未接入房间记录与进房',qrcode:'二维码:尚未接入分享模块', - 'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口', - rank:'排行:尚未接入排行榜', 'vip-rank':'VIP 排行:尚未接入排行榜','bind-phone':'绑定手机:尚未接入绑定流程', - cdkey:'兑换码:尚未接入兑换请求',bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口', - warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交', -}); +export interface LobbyMenuState {visible:Record;left:Record;top:Record;} + +/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */ +export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{ + const {hall,sys,template,agentmode}=config; + const visible:Record={},left:Record={},top:Record={}; + const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;}; + // Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X. + if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;} + hide([219,220]); + if(context.versionState===1){ + if(context.launchMode===0)hide([234]); + hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604, + 286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]); + }else{ + if(agentmode===1)hide([10,509]); + visible[235]=false; + if(hall.androidPay===1){visible[10]=true;visible[509]=true;} + visible[3018]=!template.isGameHall&&Array.isArray(hall.actData); + visible[3016]=Boolean(hall.storeUrl); + if(template.isGameHall||template.vipInfinite)hide([3139,679]); + if(template.mainMenuButton)hide([11,12,568,483]); + if(template.hideNotice)hide([16,180,193,194,682,178,179,145]); + if(sys.hideTaskBtn)hide([17]); + if(sys.miniPro||sys.hideWxShare)hide([14]); + if(!template.isGameHall){ + const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false); + shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);}); + } + visible[3233]=Boolean(hall.rankList); + visible[3256]=false; // Both original deviceLogin branches hide this entry. + if(template.isGameHall){ + hide([14,13,17,3120,99,3016,76,234,3057,145,3139,3232,3233,604]); + visible[235]=agentmode!==1;visible[15]=true;visible[5]=true; + const shown=[235,15,5,7].filter(id=>visible[id]!==false); + shown.forEach((id,index)=>{left[id]=300+520*index/(shown.length-1);top[id]=642;}); + } + } + if(sys.hideRoomCard===2)hide([286,10,287,3161]); + visible[3264]=sys.cdKey===2; + return {visible,left,top}; +} + +/** openPallet runs its own visibility pass each time before placing visible entries. */ +export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{ + const visible:Record={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={3121:200},top:Record={}; + 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>>=Object.freeze({ + 'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块', + match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口', + activity:'活动:尚未接入活动身份参数与原生接口', + 'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口', + 'bind-phone':'绑定手机:尚未接入绑定流程', + bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口', + warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交', +}); diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/lobby-model.ts b/cocoscreator_projects/YouleNexus/assets/framework/presentation/lobby-model.ts index 30317b5..bf5e329 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/presentation/lobby-model.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/lobby-model.ts @@ -1,66 +1,74 @@ -import type { RemoteConfig } from '../config/remote-config.ts'; -import type { PlatformState } from '../platform/stores/platform-types.ts'; -import { selectSelfPlayer } from '../platform/stores/selectors.ts'; -// Presentation checks required view inputs without depending on the protocol layer. -function requireNumber(value:unknown,path:string):number {if(typeof value!=='number'||!Number.isFinite(value))throw Error(`${path}: expected number`);return value;} -function requireInteger(value:unknown,path:string):number {const result=requireNumber(value,path);if(!Number.isInteger(result))throw Error(`${path}: expected integer`);return result;} -function requireString(value:unknown,path:string):string {if(typeof value!=='string')throw Error(`${path}: expected string`);return value;} -function requireRecord(value:unknown,path:string):Record {if(!value||typeof value!=='object'||Array.isArray(value))throw Error(`${path}: expected object`);return value as Record;} - -export function selectLobbyModel(state:PlatformState) { - const self=selectSelfPlayer(state); - if(!self)throw Error('lobby requires authenticated player'); - return Object.freeze({...self, - roomcard:requireNumber(self.roomcard,'players.self.roomcard'), - taskstate:requireInteger(self.taskstate,'players.self.taskstate'), - score:requireNumber(self.score,'players.self.score'), - sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~', - canAct:state.app.phase==='logged-in'&&state.room.kind==='outside', - }); -} +import type { RemoteConfig } from '../config/remote-config.ts'; +import type { PlatformState } from '../platform/stores/platform-types.ts'; +import { selectSelfPlayer } from '../platform/stores/selectors.ts'; +// Presentation checks required view inputs without depending on the protocol layer. +function requireNumber(value:unknown,path:string):number {if(typeof value!=='number'||!Number.isFinite(value))throw Error(`${path}: expected number`);return value;} +function requireInteger(value:unknown,path:string):number {const result=requireNumber(value,path);if(!Number.isInteger(result))throw Error(`${path}: expected integer`);return result;} +function requireString(value:unknown,path:string):string {if(typeof value!=='string')throw Error(`${path}: expected string`);return value;} +function requireRecord(value:unknown,path:string):Record {if(!value||typeof value!=='object'||Array.isArray(value))throw Error(`${path}: expected object`);return value as Record;} + +export function selectLobbyModel(state:PlatformState) { + const self=selectSelfPlayer(state); + if(!self)throw Error('lobby requires authenticated player'); + return Object.freeze({...self, + roomcard:requireNumber(self.roomcard,'players.self.roomcard'), + taskstate:requireInteger(self.taskstate,'players.self.taskstate'), + score:requireNumber(self.score,'players.self.score'), + sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~', + canAct:state.app.phase==='logged-in'&&state.room.kind==='outside', + }); +} export type LobbyModel=ReturnType; -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*36000000,textWidth,duration,left:width-(elapsedSeconds%duration)*100}; } +export interface Notice {type:0|1; text:string; image:string; url:string; height:number;} +export interface LobbyPolicy {noticeHours:number; notices:readonly Notice[]; scroll:string; + starName:string; roomCardName:string; contacts:readonly {label:string;value:string}[];} + +/** Source defaults: 04_Data.sysConfig and advertInfo_0/1/2; 12_Logic.get_paravalue. */ +export function resolveLobbyPolicy(remote:RemoteConfig):LobbyPolicy { + // 04_Data starts with 房卡; getConfig_Succ replaces only a truthy roomCardName. + const cardName=remote.getValue('roomCardName'); + const roomCardName=cardName?requireString(cardName,'roomCardName'):'房卡'; + const interval=remote.getValue('gameNoticeShow'); + const noticeHours=interval==null?0:Number(interval); + if(!Number.isFinite(noticeHours)||noticeHours< -1)throw Error('Invalid gameNoticeShow'); + const menuValue=remote.getValue('menunotice'); + const menu=menuValue==null?{}:requireRecord(menuValue,'menunotice'); + const notices:Notice[]=[]; + for(let index=0;index<3;index++){ + const value=menu[`advert${index}`]; + if(value==null){ + const message=index===0?remote.getValue('noticemsg'):null; + notices.push({type:0,text:message==null?'':requireString(message,'noticemsg').replace(/#/g,'\n'),image:'',url:'',height:1}); + continue; + } + const data=requireRecord(value,`menunotice.advert${index}`); + const type=requireInteger(data.type,`menunotice.advert${index}.type`); + if(type!==0&&type!==1)throw Error(`menunotice.advert${index}.type must be 0 or 1`); + // Empty text and URL explicitly mean no content/action in the source. + const field=(key:string)=>data[key]==null?'':requireString(data[key],`menunotice.advert${index}.${key}`); + notices.push({type,text:type===0?field('msg').replace(/#/g,'\n'):'',image:type===1?field('imgUrl'):'', + url:field(type===0?'url':'imgAdUrl'),height:type===1?requireNumber(data.height,`menunotice.advert${index}.height`):1}); + } + const scroll=remote.getValue('scrollmsg'); + const star=remote.getValue('starName'); + const contacts:Array<{label:string;value:string}>=[]; + for(const [key,title] of [['service_wechat','客服微信:'],['service_qq','投诉建议:'],['service_tel','客服手机:']]){ + const value=remote.getValue(key); + if(value!=null&&value!=='')contacts.push({label:title,value:requireString(value,key)}); + } + return Object.freeze({roomCardName,noticeHours,notices:Object.freeze(notices),scroll:scroll==null?'':requireString(scroll,'scrollmsg'), + starName:star==null?'星星':requireString(star,'starName'),contacts:Object.freeze(contacts)}); +} +/** Exact JudgeShow expiry: equality is not expired; reconnect never auto-opens. */ +export function shouldShowNotice(hours:number,previous:number|null,now:number,reconnect:boolean):boolean { + if(reconnect||hours===-1)return false; + return hours===0||!previous||previous+hours*3600000Object.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;index10?value%11:value); + return check===code.slice(-1)?null:'id'; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/profile-auth.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/presentation/profile-auth.ts.meta new file mode 100644 index 0000000..0566efb --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/profile-auth.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "11f5830e-5f4b-46fb-9e06-b2b65edbed89", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/request-loading.ts b/cocoscreator_projects/YouleNexus/assets/framework/presentation/request-loading.ts new file mode 100644 index 0000000..26317b2 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/request-loading.ts @@ -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(); + 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);}} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/request-loading.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/presentation/request-loading.ts.meta new file mode 100644 index 0000000..c4ccd3f --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/request-loading.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "8bb6a294-bfcf-4126-98f1-0845b58c72db", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/share-model.ts b/cocoscreator_projects/YouleNexus/assets/framework/presentation/share-model.ts index ae728bc..f164ece 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/presentation/share-model.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/share-model.ts @@ -1,19 +1,27 @@ import type { RuntimeConfig } from '../config/runtime-config.ts'; -/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */ -export function resolveSharePolicy(config:RuntimeConfig){ - const remote=config.remoteConfig,raw=remote.getValue('hall_config'); - if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config'); - const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record; - 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,friend:1|2){ - if(friend===2&&!policy.imageTimeline)return null; // Legacy case 75 has no link-sharing call. - return {sharefriend:friend,type:friend===1?1:3,webpageUrl:friend===1?policy.url:policy.image||policy.url, - title:friend===1?policy.title||policy.title+policy.description:policy.title+policy.description, - description:friend===1?policy.description:undefined,sharetype:0}; +/** Open_ewm uses this exact config key; absent configuration intentionally does nothing. */ +export function resolveQrCodePolicy(config:RuntimeConfig){ + const url=config.remoteConfig.getValue('WechatPublicNo_ewm'); + if(!url)return null; + if(typeof url!=='string')throw Error('Invalid WechatPublicNo_ewm'); + const policy=resolveSharePolicy(config); + return {url,share:{sharefriend:1,type:3,webpageUrl:url,title:policy.title+policy.description,description:undefined,sharetype:0}}; } +/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */ +export function resolveSharePolicy(config:RuntimeConfig){ + const remote=config.remoteConfig,raw=remote.getValue('hall_config'); + if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config'); + const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record; + 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,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}; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts index 5a13f30..6f858c4 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts @@ -1,52 +1,56 @@ -export { - hasOwn, - optionalField, - requireArray, - requireBoolean, - requireInteger, - requireNumber, - requireRecord, - requireString, - requireStringOrFiniteNumber, - type JsonRecord, -} from './validation.ts'; -export { - buildLoginRequest, - parseLoginResponse, - type DeviceLoginMode, - type FirstSliceRuntimeConfig, - type LoginAccountIdentity, - type LoginDeviceSnapshot, - type LoginPlayerIdMode, - type LoginRequestPayload, - type ParsedLoginResponse, -} from './login-contract.ts'; -export { - buildExitRoomRequest, - buildJoinRoomRequest, - buildPrepareRequest, - parseConnectAgentserverPayload, - parseConnectRoomserverPayload, - parseKickServerPayload, - parseOtherExitRoomPayload, - parseOtherJoinRoomPayload, - parseOtherOfflinePayload, - parseOtherOnlinePayload, - parsePlayerPreparePayload, - parseSelfExitRoomPayload, - parseSelfJoinRoomResponse, - type JoinRoomEnvironment, - type JoinRoomPayload, - type ParsedAgentserverPayload, - type ParsedKickPayload, - type ParsedOnlinePayload, - type ParsedRoomEntry, - type ParsedRoomResponse, - type ParsedRoomserverPayload, - type ParsedRoomSnapshot, - type ParsedSelfExitPayload, - type ParsedSeatPayload, - type ReconnectSnapshot, - type RoomActionPayload, - type RoomIdentity, -} from './room-contracts.ts'; +export { + hasOwn, + optionalField, + requireArray, + requireBoolean, + requireInteger, + requireNumber, + requireRecord, + requireString, + requireStringOrFiniteNumber, + type JsonRecord, +} from './validation.ts'; +export { + buildLoginRequest, + parseLoginResponse, + type DeviceLoginMode, + type FirstSliceRuntimeConfig, + type LoginAccountIdentity, + type LoginDeviceSnapshot, + type LoginPlayerIdMode, + type LoginRequestPayload, + type ParsedLoginResponse, +} from './login-contract.ts'; +export { + buildCreateRoomRequest, + parseCreateRoomResponse, + type ParsedCreateRoomResponse, + type CreateRoomEnvironment, + buildExitRoomRequest, + buildJoinRoomRequest, + buildPrepareRequest, + parseConnectAgentserverPayload, + parseConnectRoomserverPayload, + parseKickServerPayload, + parseOtherExitRoomPayload, + parseOtherJoinRoomPayload, + parseOtherOfflinePayload, + parseOtherOnlinePayload, + parsePlayerPreparePayload, + parseSelfExitRoomPayload, + parseSelfJoinRoomResponse, + type JoinRoomEnvironment, + type JoinRoomPayload, + type ParsedAgentserverPayload, + type ParsedKickPayload, + type ParsedOnlinePayload, + type ParsedRoomEntry, + type ParsedRoomResponse, + type ParsedRoomserverPayload, + type ParsedRoomSnapshot, + type ParsedSelfExitPayload, + type ParsedSeatPayload, + type ReconnectSnapshot, + type RoomActionPayload, + type RoomIdentity, +} from './room-contracts.ts'; diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/rank-list.ts b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/rank-list.ts new file mode 100644 index 0000000..aa1d32d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/rank-list.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;i255?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'):''});})); +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/rank-list.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/rank-list.ts.meta new file mode 100644 index 0000000..8b82385 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/rank-list.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "744452fb-2d7e-4a0e-9c49-2e6994f5df84", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-contracts.ts b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-contracts.ts index 65fb764..149ad2a 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-contracts.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-contracts.ts @@ -1,243 +1,302 @@ -import { - buildFirstSliceEnvelope, - type FirstSliceEnvelope, -} from '../first-slice-routes.ts'; -import { - hasOwn, - optionalField, - requireArray, - requireInteger, - requireNumber, - requireRecord, - requireString, - requireStringOrFiniteNumber, - type JsonRecord, -} from './validation.ts'; -import type { FirstSliceRuntimeConfig } from './login-contract.ts'; - -export type ReconnectSnapshot = - | { readonly present: false } - | { readonly present: true; readonly value: unknown }; - -export interface ParsedRoomSnapshot { - readonly roomcode: string; - readonly seat: number; - readonly roomtype: readonly unknown[]; - readonly players: readonly unknown[]; -} - -export interface ParsedRoomEntry { - readonly room: ParsedRoomSnapshot; - readonly reconnect: ReconnectSnapshot; - readonly raw: JsonRecord; -} - -export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot { - if (!Boolean(raw.deskinfo)) return { present: false }; - return { present: true, value: raw.deskinfo }; -} - -export function parseRoomEntryFields(raw: JsonRecord): ParsedRoomSnapshot { - const roomcode = requireString(raw.roomcode, '$.data.roomcode'); - const seat = requireInteger(raw.seat, '$.data.seat'); - const roomtype = requireArray(raw.roomtype, '$.data.roomtype'); - const players = requireArray(raw.players, '$.data.players'); - if (seat < 0 || seat >= players.length) { - throw new RangeError(`$.data.seat: expected server seat index within players, received number`); - } - return { roomcode, seat, roomtype, players }; -} - -export interface RoomIdentity { - readonly playerid: number; - readonly roomcode: string; -} - -export interface RoomActionPayload extends Readonly> { - readonly agentid: string | number; - readonly playerid: number; - readonly gameid: string | number; - readonly roomcode: string; -} - -function baseRoomAction( - runtimeConfig: FirstSliceRuntimeConfig, - room: RoomIdentity, -): RoomActionPayload { - const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity'); - const roomRecord = requireRecord(room, '$.room'); - return { - agentid: requireStringOrFiniteNumber( - identity.agentid, - '$.runtimeConfig.identity.agentid', - ), - playerid: requireInteger(roomRecord.playerid, '$.room.playerid'), - gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'), - roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'), - }; -} - -export function buildPrepareRequest( - runtimeConfig: FirstSliceRuntimeConfig, - room: RoomIdentity, -): FirstSliceEnvelope<'player_prepare', RoomActionPayload> { - return buildFirstSliceEnvelope('player_prepare', baseRoomAction(runtimeConfig, room)); -} - -export function buildExitRoomRequest( - runtimeConfig: FirstSliceRuntimeConfig, - room: RoomIdentity, -): FirstSliceEnvelope<'self_exit_room', RoomActionPayload> { - return buildFirstSliceEnvelope('self_exit_room', baseRoomAction(runtimeConfig, room)); -} - -export interface JoinRoomEnvironment { - readonly location: unknown; - readonly ip: string; - readonly vipMatch?: 1; - readonly match_id?: string | number; -} - -export interface JoinRoomPayload extends RoomActionPayload { - readonly location: unknown; - readonly ip: string; - readonly vipMatch?: 1; - readonly match_id?: string | number; -} - -export function buildJoinRoomRequest( - runtimeConfig: FirstSliceRuntimeConfig, - room: RoomIdentity, - environment: JoinRoomEnvironment, -): FirstSliceEnvelope<'self_join_room', JoinRoomPayload> { - const environmentRecord = requireRecord(environment, '$.environment'); - if (!hasOwn(environmentRecord, 'location') || environmentRecord.location === undefined) { - throw new TypeError('$.environment.location: expected present field, received undefined'); - } - if (environmentRecord.location !== null) { - requireRecord(environmentRecord.location, '$.environment.location'); - } - const data: Record = { - ...baseRoomAction(runtimeConfig, room), - location: environmentRecord.location, - ip: requireString(environmentRecord.ip, '$.environment.ip'), - }; - const vipMatch = optionalField( - environmentRecord, - 'vipMatch', - '$.environment', - (value, path): 1 => { - const parsed = requireNumber(value, path); - if (parsed !== 1) { - throw new RangeError(`${path}: expected number 1, received number`); - } - return 1; - }, - ); - if (vipMatch !== undefined) data.vipMatch = vipMatch; - - const matchId = optionalField( - environmentRecord, - 'match_id', - '$.environment', - requireStringOrFiniteNumber, - ); - if (matchId !== undefined) data.match_id = matchId; - return buildFirstSliceEnvelope('self_join_room', data as JoinRoomPayload); -} - -export interface ParsedRoomResponse { - readonly state: number; - readonly room: ParsedRoomSnapshot | null; - readonly reconnect: ReconnectSnapshot; - readonly stage: 0 | 1; - readonly raw: JsonRecord; -} - -export function parseSelfJoinRoomResponse(value: unknown): ParsedRoomResponse { - const raw = requireRecord(value, '$.data'); - const state = requireInteger(raw.state, '$.data.state'); - const room = state === 0 ? parseRoomEntryFields(raw) : null; - const parsedDeskwar = optionalField(raw, 'deskwar', '$.data', requireInteger); - if (parsedDeskwar !== undefined && parsedDeskwar !== 0 && parsedDeskwar !== 1) { - throw new RangeError('$.data.deskwar: expected protocol flag 0 or 1'); - } - const reconnect = reconnectFrom(raw); - const stage = reconnect.present ? 1 : parsedDeskwar ?? 0; - return { state, room, reconnect, stage, raw }; -} - -export interface ParsedSeatPayload { - readonly seat: number; - readonly raw: JsonRecord; -} - -function parseSeatPayload(value: unknown): ParsedSeatPayload { - const raw = requireRecord(value, '$.data'); - const seat = requireInteger(raw.seat, '$.data.seat'); - if (seat < 0) throw new RangeError('$.data.seat: expected non-negative server seat, received number'); - return { seat, raw }; -} - -export const parsePlayerPreparePayload = parseSeatPayload; -export const parseOtherJoinRoomPayload = parseSeatPayload; -export const parseOtherExitRoomPayload = parseSeatPayload; -export const parseOtherOfflinePayload = parseSeatPayload; - -export interface ParsedSelfExitPayload { - readonly seat?: number; - readonly raw: JsonRecord; -} - -export function parseSelfExitRoomPayload(value: unknown): ParsedSelfExitPayload { - const raw = requireRecord(value, '$.data'); - const seat = optionalField(raw, 'seat', '$.data', requireInteger); - if (seat !== undefined && seat < 0) { - throw new RangeError('$.data.seat: expected non-negative server seat, received number'); - } - return { ...(seat === undefined ? {} : { seat }), raw }; -} - -export interface ParsedOnlinePayload extends ParsedSeatPayload { - readonly ip: string; -} - -export function parseOtherOnlinePayload(value: unknown): ParsedOnlinePayload { - const parsed = parseSeatPayload(value); - return { seat: parsed.seat, ip: requireString(parsed.raw.ip, '$.data.ip'), raw: parsed.raw }; -} - -export interface ParsedRoomserverPayload { - readonly roomserver: string; - readonly raw: JsonRecord; -} - -export function parseConnectRoomserverPayload(value: unknown): ParsedRoomserverPayload { - const raw = requireRecord(value, '$.data'); - return { roomserver: requireString(raw.roomserver, '$.data.roomserver'), raw }; -} - -export interface ParsedAgentserverPayload { - readonly agentserver: string; - readonly opt: string; - readonly raw: JsonRecord; -} - -export function parseConnectAgentserverPayload(value: unknown): ParsedAgentserverPayload { - const raw = requireRecord(value, '$.data'); - return { - agentserver: requireString(raw.agentserver, '$.data.agentserver'), - opt: requireString(raw.opt, '$.data.opt'), - raw, - }; -} - -export interface ParsedKickPayload { - readonly msg: string; - readonly raw: JsonRecord; -} - -export function parseKickServerPayload(value: unknown): ParsedKickPayload { - const raw = requireRecord(value, '$.data'); - return { msg: requireString(raw.msg, '$.data.msg'), raw }; -} +import { + buildFirstSliceEnvelope, + type FirstSliceEnvelope, +} from '../first-slice-routes.ts'; +import { + hasOwn, + optionalField, + requireArray, + requireInteger, + requireNumber, + requireRecord, + requireString, + requireStringOrFiniteNumber, + type JsonRecord, +} from './validation.ts'; +import type { FirstSliceRuntimeConfig } from './login-contract.ts'; + +export type ReconnectSnapshot = + | { readonly present: false } + | { readonly present: true; readonly value: unknown }; + +export interface ParsedRoomSnapshot { + readonly roomcode: string; + readonly seat: number; + readonly roomtype: readonly unknown[]; + readonly players: readonly unknown[]; +} + +export interface ParsedRoomEntry { + readonly room: ParsedRoomSnapshot; + readonly reconnect: ReconnectSnapshot; + readonly raw: JsonRecord; +} + +export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot { + if (!Boolean(raw.deskinfo)) return { present: false }; + return { present: true, value: raw.deskinfo }; +} + +export function parseRoomEntryFields(raw: JsonRecord): ParsedRoomSnapshot { + const roomcode = requireString(raw.roomcode, '$.data.roomcode'); + const seat = requireInteger(raw.seat, '$.data.seat'); + const roomtype = requireArray(raw.roomtype, '$.data.roomtype'); + const players = requireArray(raw.players, '$.data.players'); + if (seat < 0 || seat >= players.length) { + throw new RangeError(`$.data.seat: expected server seat index within players, received number`); + } + return { roomcode, seat, roomtype, players }; +} + +export interface RoomIdentity { + readonly playerid: number; + readonly roomcode: string; +} + +export interface RoomActionPayload extends Readonly> { + readonly agentid: string | number; + readonly playerid: number; + readonly gameid: string | number; + readonly roomcode: string; +} + +function baseRoomAction( + runtimeConfig: FirstSliceRuntimeConfig, + room: RoomIdentity, +): RoomActionPayload { + const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity'); + const roomRecord = requireRecord(room, '$.room'); + return { + agentid: requireStringOrFiniteNumber( + identity.agentid, + '$.runtimeConfig.identity.agentid', + ), + playerid: requireInteger(roomRecord.playerid, '$.room.playerid'), + gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'), + roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'), + }; +} + +export function buildPrepareRequest( + runtimeConfig: FirstSliceRuntimeConfig, + room: RoomIdentity, +): FirstSliceEnvelope<'player_prepare', RoomActionPayload> { + return buildFirstSliceEnvelope('player_prepare', baseRoomAction(runtimeConfig, room)); +} + +export function buildExitRoomRequest( + runtimeConfig: FirstSliceRuntimeConfig, + room: RoomIdentity, +): FirstSliceEnvelope<'self_exit_room', RoomActionPayload> { + return buildFirstSliceEnvelope('self_exit_room', baseRoomAction(runtimeConfig, room)); +} + +export interface JoinRoomEnvironment { + readonly location: unknown; + readonly ip: string; + readonly vipMatch?: 1; + readonly match_id?: string | number; +} + +export interface JoinRoomPayload extends Omit { + readonly roomcode: string | number; + readonly location: unknown; + readonly ip: string; + readonly vipMatch?: 1; + readonly match_id?: string | number; +} + +export function buildJoinRoomRequest( + runtimeConfig: FirstSliceRuntimeConfig, + room: { readonly playerid: number; readonly roomcode: string | number }, + environment: JoinRoomEnvironment, +): FirstSliceEnvelope<'self_join_room', JoinRoomPayload> { + const environmentRecord = requireRecord(environment, '$.environment'); + if (!hasOwn(environmentRecord, 'location') || environmentRecord.location === undefined) { + throw new TypeError('$.environment.location: expected present field, received undefined'); + } + if (environmentRecord.location !== null) { + requireRecord(environmentRecord.location, '$.environment.location'); + } + const data: Record = { + ...baseRoomAction(runtimeConfig, {playerid: room.playerid, roomcode: String(room.roomcode)}), + roomcode: requireStringOrFiniteNumber(room.roomcode, '$.room.roomcode'), + location: environmentRecord.location, + ip: requireString(environmentRecord.ip, '$.environment.ip'), + }; + const vipMatch = optionalField( + environmentRecord, + 'vipMatch', + '$.environment', + (value, path): 1 => { + const parsed = requireNumber(value, path); + if (parsed !== 1) { + throw new RangeError(`${path}: expected number 1, received number`); + } + return 1; + }, + ); + if (vipMatch !== undefined) data.vipMatch = vipMatch; + + const matchId = optionalField( + environmentRecord, + 'match_id', + '$.environment', + requireStringOrFiniteNumber, + ); + if (matchId !== undefined) data.match_id = matchId; + return buildFirstSliceEnvelope('self_join_room', data as JoinRoomPayload); +} + +export interface ParsedRoomResponse { + readonly state: number; + readonly room: ParsedRoomSnapshot | null; + readonly reconnect: ReconnectSnapshot; + readonly stage: 0 | 1; + readonly raw: JsonRecord; +} + +export function parseSelfJoinRoomResponse(value: unknown): ParsedRoomResponse { + const raw = requireRecord(value, '$.data'); + const state = requireInteger(raw.state, '$.data.state'); + const room = state === 0 ? parseRoomEntryFields(raw) : null; + const parsedDeskwar = optionalField(raw, 'deskwar', '$.data', requireInteger); + if (parsedDeskwar !== undefined && parsedDeskwar !== 0 && parsedDeskwar !== 1) { + throw new RangeError('$.data.deskwar: expected protocol flag 0 or 1'); + } + const reconnect = reconnectFrom(raw); + const stage = reconnect.present ? 1 : parsedDeskwar ?? 0; + return { state, room, reconnect, stage, raw }; +} + +export interface ParsedSeatPayload { + readonly seat: number; + readonly raw: JsonRecord; +} + +function parseSeatPayload(value: unknown): ParsedSeatPayload { + const raw = requireRecord(value, '$.data'); + const seat = requireInteger(raw.seat, '$.data.seat'); + if (seat < 0) throw new RangeError('$.data.seat: expected non-negative server seat, received number'); + return { seat, raw }; +} + +export const parsePlayerPreparePayload = parseSeatPayload; +export const parseOtherJoinRoomPayload = parseSeatPayload; +export const parseOtherExitRoomPayload = parseSeatPayload; +export const parseOtherOfflinePayload = parseSeatPayload; + +export interface ParsedSelfExitPayload { + readonly seat?: number; + readonly raw: JsonRecord; +} + +export function parseSelfExitRoomPayload(value: unknown): ParsedSelfExitPayload { + const raw = requireRecord(value, '$.data'); + const seat = optionalField(raw, 'seat', '$.data', requireInteger); + if (seat !== undefined && seat < 0) { + throw new RangeError('$.data.seat: expected non-negative server seat, received number'); + } + return { ...(seat === undefined ? {} : { seat }), raw }; +} + +export interface ParsedOnlinePayload extends ParsedSeatPayload { + readonly ip: string; +} + +export function parseOtherOnlinePayload(value: unknown): ParsedOnlinePayload { + const parsed = parseSeatPayload(value); + return { seat: parsed.seat, ip: requireString(parsed.raw.ip, '$.data.ip'), raw: parsed.raw }; +} + +export interface ParsedRoomserverPayload { + readonly roomserver: string; + readonly raw: JsonRecord; +} + +export function parseConnectRoomserverPayload(value: unknown): ParsedRoomserverPayload { + const raw = requireRecord(value, '$.data'); + return { roomserver: requireString(raw.roomserver, '$.data.roomserver'), raw }; +} + +export interface ParsedAgentserverPayload { + readonly agentserver: string; + readonly opt: string; + readonly raw: JsonRecord; +} + +export function parseConnectAgentserverPayload(value: unknown): ParsedAgentserverPayload { + const raw = requireRecord(value, '$.data'); + return { + agentserver: requireString(raw.agentserver, '$.data.agentserver'), + opt: requireString(raw.opt, '$.data.opt'), + raw, + }; +} + +export interface ParsedKickPayload { + readonly msg: string; + readonly raw: JsonRecord; +} + +export function parseKickServerPayload(value: unknown): ParsedKickPayload { + const raw = requireRecord(value, '$.data'); + return { msg: requireString(raw.msg, '$.data.msg'), raw }; +} + + +export interface CreateRoomEnvironment { + readonly roomtype: readonly unknown[]; + readonly location: unknown; + readonly ip: string; +} + +/** Transport contract only: roomtype contents belong to the subgame. */ +export function buildCreateRoomRequest( + runtimeConfig: FirstSliceRuntimeConfig, + playerid: number, + command: CreateRoomEnvironment, +): FirstSliceEnvelope<'create_room', Readonly>> { + 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, + }; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/first-slice-routes.ts b/cocoscreator_projects/YouleNexus/assets/framework/protocol/first-slice-routes.ts index 9e34dc1..665ddfd 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/protocol/first-slice-routes.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/first-slice-routes.ts @@ -1,77 +1,91 @@ -import { APP } from '../core/constants.ts'; - -/** Legacy 12_Logic dispatches login replies from all three platform routes by rpc. - * This inbound contract is independent of the outbound agent route below. */ -export function isLoginResponse(message:{route:string;rpc:string}):boolean{ - return message.rpc==='player_login'&&(message.route==='agent'||message.route==='room'||message.route==='platform'); -} - -export type FirstSliceRpc = - | 'getInfoByShortCode' - | 'get_player_task' - | 'get_task_award' - | 'update_roomcard' - | 'player_finish_task' - | 'player_login' - | 'self_join_room' - | 'player_prepare' - | 'self_exit_room' - | 'other_join_room' - | 'other_exit_room' - | 'other_offline' - | 'other_online' - | 'connect_roomserver' - | 'connect_agentserver' - | 'kick_server' - | 'kick_offline' - | 'show_message'; - -export type FirstSliceRoute = 'platform' | 'agent' | 'room'; - -/** 第一纵向切片 rpc → route 的唯一权威映射。 */ -export const FIRST_SLICE_RPC_ROUTE = { - getInfoByShortCode: 'agent', - get_player_task: 'agent', - get_task_award: 'agent', - update_roomcard: 'agent', - player_finish_task: 'agent', - player_login: 'agent', - self_join_room: 'agent', - player_prepare: 'room', - self_exit_room: 'room', - other_join_room: 'room', - other_exit_room: 'room', - other_offline: 'room', - other_online: 'room', - connect_roomserver: 'room', - connect_agentserver: 'agent', - kick_server: 'agent', - kick_offline: 'agent', - show_message: 'agent', -} as const satisfies Record; - -export function routeForFirstSlice( - rpc: Rpc, -): (typeof FIRST_SLICE_RPC_ROUTE)[Rpc] { - if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, rpc)) { - throw new Error(`FIRST_SLICE_RPC_ROUTE 未定义 rpc: ${String(rpc)}`); - } - return FIRST_SLICE_RPC_ROUTE[rpc]; -} - -export interface FirstSliceEnvelope< - Rpc extends FirstSliceRpc, - Data extends Readonly>, -> { - 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>, ->(rpc: Rpc, data: Data): FirstSliceEnvelope { - return { app: APP, route: routeForFirstSlice(rpc), rpc, data }; -} +import { APP } from '../core/constants.ts'; + +/** Legacy 12_Logic dispatches login replies from all three platform routes by rpc. + * This inbound contract is independent of the outbound agent route below. */ +export function isLoginResponse(message:{route:string;rpc:string}):boolean{ + return message.rpc==='player_login'&&(message.route==='agent'||message.route==='room'||message.route==='platform'); +} + +export type FirstSliceRpc = + | 'getVipRankList' + | 'get_treasurelist' + | 'topup_card' + | 'get_player_invitecode' + | 'binding_invitecode' + | 'getInfoByShortCode' + | 'get_player_task' + | 'get_task_award' + | 'update_bean' + | 'update_roomcard' + | 'player_finish_task' + | 'player_login' + | 'create_room' + | 'self_join_room' + | 'player_prepare' + | 'self_exit_room' + | 'other_join_room' + | 'other_exit_room' + | 'other_offline' + | 'other_online' + | 'connect_roomserver' + | 'connect_agentserver' + | 'kick_server' + | 'kick_offline' + | 'show_message'; + +export type FirstSliceRoute = 'platform' | 'agent' | 'room'; + +/** 第一纵向切片 rpc → route 的唯一权威映射。 */ +export const FIRST_SLICE_RPC_ROUTE = { + getVipRankList: 'agent', + get_treasurelist: 'agent', + topup_card: 'agent', + get_player_invitecode: 'agent', + binding_invitecode: 'agent', + getInfoByShortCode: 'agent', + get_player_task: 'agent', + get_task_award: 'agent', + update_bean: 'agent', + update_roomcard: 'agent', + player_finish_task: 'agent', + player_login: 'agent', + create_room: 'agent', + self_join_room: 'agent', + player_prepare: 'room', + self_exit_room: 'room', + other_join_room: 'room', + other_exit_room: 'room', + other_offline: 'room', + other_online: 'room', + connect_roomserver: 'room', + connect_agentserver: 'agent', + kick_server: 'agent', + kick_offline: 'agent', + show_message: 'agent', +} as const satisfies Record; + +export function routeForFirstSlice( + rpc: Rpc, +): (typeof FIRST_SLICE_RPC_ROUTE)[Rpc] { + if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, rpc)) { + throw new Error(`FIRST_SLICE_RPC_ROUTE 未定义 rpc: ${String(rpc)}`); + } + return FIRST_SLICE_RPC_ROUTE[rpc]; +} + +export interface FirstSliceEnvelope< + Rpc extends FirstSliceRpc, + Data extends Readonly>, +> { + 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>, +>(rpc: Rpc, data: Data): FirstSliceEnvelope { + return { app: APP, route: routeForFirstSlice(rpc), rpc, data }; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts b/cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts index dec3fec..9804e1b 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts @@ -1,206 +1,228 @@ -import type { InboundMessage } from '../core/types/envelope.ts'; -import type { GameSessionHost } from '../sdk/runtime/game-session-host.ts'; -import type { ConnectionControlPort } from '../platform/connection-intent.ts'; -import type { RuntimeSession } from '../platform/runtime-session.ts'; -import type { PlatformStore } from '../platform/stores/platform-store.ts'; -import { requireRecord, requireString } from './contracts/validation.ts'; -import { - parseConnectAgentserverPayload, - parseConnectRoomserverPayload, - parseKickServerPayload, - parseOtherExitRoomPayload, - parseOtherJoinRoomPayload, - parseOtherOfflinePayload, - parseOtherOnlinePayload, - parsePlayerPreparePayload, - parseSelfExitRoomPayload, -} from './contracts/index.ts'; -import { - buildFirstSliceEnvelope, - isLoginResponse, - FIRST_SLICE_RPC_ROUTE, - routeForFirstSlice, - type FirstSliceRpc, -} from './first-slice-routes.ts'; - -export interface PlatformHandlersOptions { - readonly store: PlatformStore; - readonly gameSession: GameSessionHost; - readonly session: RuntimeSession; - readonly connection: ConnectionControlPort; - readonly showMessage?: (message:string,time:number,source:'show_message'|'update_roomcard')=>void; -} - -export type PlatformDispatchResult = 'handled-platform'; - -/** Strict first-slice platform RPC dispatcher. */ -export class PlatformHandlers { - constructor(private readonly options: PlatformHandlersOptions) {} - - dispatch(message: InboundMessage): PlatformDispatchResult { - if (!isPlatformRoute(message.route)) { - throw new Error( - `PlatformHandlers received non-platform route: route=${message.route} rpc=${message.rpc}`, - ); - } - if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, message.rpc)) { - throw new Error( - `Unsupported platform rpc: route=${message.route} rpc=${message.rpc}`, - ); - } - const rpc = message.rpc as FirstSliceRpc; - const expectedRoute = routeForFirstSlice(rpc); - if (message.route !== expectedRoute && !isLoginResponse(message)) { - throw new Error( - `Platform rpc route mismatch: rpc=${rpc} expected=${expectedRoute} received=${message.route}`, - ); - } - - switch (rpc) { - case 'getInfoByShortCode':this.options.store.applyVipRooms(message.data);break; - case 'update_roomcard': { - const raw=requireRecord(message.data,'$.data'); - const text=raw.text?requireString(raw.text,'$.data.text'):null; - this.options.store.applyRoomcardUpdate(raw); - if(raw.change===undefined&&text){ - if(!this.options.showMessage)throw Error('update_roomcard requires a message UI'); - this.options.showMessage(text,0,'update_roomcard'); - } - break; - } - case 'get_player_task':this.options.store.applyTaskList(message.data);break; - case 'get_task_award':this.options.store.applyTaskAward(message.data);break; - case 'player_finish_task':this.options.store.applyTaskFinished(message.data);break; - case 'player_login': - this.options.session.handleLogin(message.data); - break; - case 'self_join_room': - this.options.session.handleSelfJoin(message.data); - break; - case 'player_prepare': { - const parsed = parsePlayerPreparePayload(message.data); - this.requireActiveGame(rpc); - this.options.store.playerReady(parsed); - this.options.gameSession.publish({ type: 'room.player-ready', seat: parsed.seat }); - break; - } - case 'self_exit_room': { - const parsed = parseSelfExitRoomPayload(message.data); - this.options.session.handleSelfExit(parsed); - break; - } - case 'other_join_room': { - const parsed = parseOtherJoinRoomPayload(message.data); - this.requireActiveGame(rpc); - this.options.store.playerJoin(parsed); - this.options.gameSession.publish({ type: 'room.player-joined', seat: parsed.seat }); - break; - } - case 'other_exit_room': { - const parsed = parseOtherExitRoomPayload(message.data); - this.requireActiveGame(rpc); - this.options.store.playerExit(parsed); - this.options.gameSession.publish({ type: 'room.player-left', seat: parsed.seat }); - break; - } - case 'other_offline': { - const parsed = parseOtherOfflinePayload(message.data); - this.requireActiveGame(rpc); - this.options.store.playerOffline(parsed); - this.options.gameSession.publish({ type: 'room.player-offline', seat: parsed.seat }); - break; - } - case 'other_online': { - const parsed = parseOtherOnlinePayload(message.data); - this.requireActiveGame(rpc); - this.options.store.playerOnline(parsed); - this.options.gameSession.publish({ type: 'room.player-online', seat: parsed.seat }); - break; - } - case 'connect_roomserver': { - const parsed = parseConnectRoomserverPayload(message.data); - const target = parseSwitchTarget(parsed.roomserver, '$.data.roomserver'); - const resend = buildFirstSliceEnvelope('connect_roomserver', parsed.raw); - this.options.session.handleServerSwitch( - () => this.options.connection.switchServer(target, resend), - ); - break; - } - case 'connect_agentserver': { - const parsed = parseConnectAgentserverPayload(message.data); - const target = parseSwitchTarget(parsed.agentserver, '$.data.agentserver'); - const resend = buildFirstSliceEnvelope('connect_agentserver', parsed.raw); - this.options.session.handleServerSwitch( - () => this.options.connection.switchServer(target, resend), - ); - break; - } - case 'show_message': { - const data=requireRecord(message.data,'$.data'); - const text=requireString(data.msg,'$.data.msg'); - // Desk.show_message converts the original time with Number; only >0 auto-closes. - const time=Number(data.time); - if(!this.options.showMessage)throw Error('show_message requires a message UI'); - this.options.showMessage(text,time,'show_message'); - break; - } - case 'kick_offline': { - const data=requireRecord(message.data,'$.data'); - this.options.session.handleKick(data,()=>this.options.connection.stopForKick(data)); - break; - } - case 'kick_server': { - const parsed = parseKickServerPayload(message.data); - this.options.session.handleKick( - parsed.raw, - () => this.options.connection.stopForKick(parsed.raw), - ); - break; - } - } - return 'handled-platform'; - } - - private requireActiveGame(rpc: string): void { - if (this.options.gameSession.state !== 'active') { - throw new Error(`${rpc} requires an active game session`); - } - } -} - -function isPlatformRoute(route: string): route is 'platform' | 'agent' | 'room' { - return route === 'platform' || route === 'agent' || route === 'room'; -} - -function parseSwitchTarget(address: string, path: string): string { - if (address.length === 0 || address.trim() !== address) { - throw new TypeError(`${path}: expected a non-empty WebSocket server address`); - } - const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(address); - const target = scheme ? address : `ws://${address}`; - const authority = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#]*)/.exec(target)?.[1]; - if (authority === undefined || authority.length === 0) { - throw new TypeError(`${path}: expected a WebSocket address with a non-empty host`); - } - if (target.includes('#') || authority.includes('@')) { - throw new TypeError(`${path}: expected a WebSocket address without fragment or userinfo`); - } - - let parsed: URL; - try { - parsed = new URL(target); - } catch { - throw new TypeError(`${path}: expected a valid WebSocket server address`); - } - const protocol = parsed.protocol.toLowerCase(); - if ( - (protocol !== 'ws:' && protocol !== 'wss:') - || parsed.hostname.length === 0 - || parsed.username.length > 0 - || parsed.password.length > 0 - || parsed.hash.length > 0 - ) { - throw new TypeError(`${path}: expected a valid WebSocket server address`); - } - return target; -} +import type { InboundMessage } from '../core/types/envelope.ts'; +import type { GameSessionHost } from '../sdk/runtime/game-session-host.ts'; +import type { ConnectionControlPort } from '../platform/connection-intent.ts'; +import type { RuntimeSession } from '../platform/runtime-session.ts'; +import type { PlatformStore } from '../platform/stores/platform-store.ts'; +import { requireRecord, requireString } from './contracts/validation.ts'; +import { + parseConnectAgentserverPayload, + parseConnectRoomserverPayload, + parseKickServerPayload, + parseOtherExitRoomPayload, + parseOtherJoinRoomPayload, + parseOtherOfflinePayload, + parseOtherOnlinePayload, + parsePlayerPreparePayload, + parseSelfExitRoomPayload, +} from './contracts/index.ts'; +import { + buildFirstSliceEnvelope, + isLoginResponse, + FIRST_SLICE_RPC_ROUTE, + routeForFirstSlice, + type FirstSliceRpc, +} from './first-slice-routes.ts'; + +export interface PlatformHandlersOptions { + readonly store: PlatformStore; + readonly gameSession: GameSessionHost; + readonly session: RuntimeSession; + readonly connection: ConnectionControlPort; + readonly showMessage?: (message:string,time:number,source:'show_message'|'update_roomcard'|'update_bean'|'binding_invitecode')=>void; +} + +export type PlatformDispatchResult = 'handled-platform'; + +/** Strict first-slice platform RPC dispatcher. */ +export class PlatformHandlers { + constructor(private readonly options: PlatformHandlersOptions) {} + + dispatch(message: InboundMessage): PlatformDispatchResult { + if (!isPlatformRoute(message.route)) { + throw new Error( + `PlatformHandlers received non-platform route: route=${message.route} rpc=${message.rpc}`, + ); + } + if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, message.rpc)) { + throw new Error( + `Unsupported platform rpc: route=${message.route} rpc=${message.rpc}`, + ); + } + const rpc = message.rpc as FirstSliceRpc; + const expectedRoute = routeForFirstSlice(rpc); + if (message.route !== expectedRoute && !isLoginResponse(message) && !(rpc==='update_bean'&&message.route==='room')) { + throw new Error( + `Platform rpc route mismatch: rpc=${rpc} expected=${expectedRoute} received=${message.route}`, + ); + } + + switch (rpc) { + case 'getVipRankList':this.options.store.applyVipRank(message.data);break; + case 'get_treasurelist':this.options.store.applyTreasureList(message.data);break; + case 'topup_card':break; // Original response is empty; updates arrive separately. + case 'get_player_invitecode':this.options.store.applyInvitecode(message.data);break; + case 'binding_invitecode':{ + if(!this.options.showMessage)throw Error('binding_invitecode requires a message UI'); + const text=this.options.store.applyInviteBinding(message.data); + this.options.showMessage(text,3000,'binding_invitecode');break; + } + case 'getInfoByShortCode':this.options.store.applyVipRooms(message.data);break; + case 'update_bean': { + const raw=requireRecord(message.data,'$.data'); + const text=raw.text?requireString(raw.text,'$.data.text'):null; + this.options.store.applyBeanUpdate(raw); + if(raw.change===undefined&&raw.seat===undefined&&text){ + if(!this.options.showMessage)throw Error('update_bean requires a message UI'); + this.options.showMessage(text,0,'update_bean'); + } + break; + } + case 'update_roomcard': { + const raw=requireRecord(message.data,'$.data'); + const text=raw.text?requireString(raw.text,'$.data.text'):null; + this.options.store.applyRoomcardUpdate(raw); + if(raw.change===undefined&&text){ + if(!this.options.showMessage)throw Error('update_roomcard requires a message UI'); + this.options.showMessage(text,0,'update_roomcard'); + } + break; + } + case 'get_player_task':this.options.store.applyTaskList(message.data);break; + case 'get_task_award':this.options.store.applyTaskAward(message.data);break; + case 'player_finish_task':this.options.store.applyTaskFinished(message.data);break; + case 'player_login': + this.options.session.handleLogin(message.data); + break; + case 'create_room': + this.options.session.handleCreateRoom(message.data); + break; + case 'self_join_room': + this.options.session.handleSelfJoin(message.data); + break; + case 'player_prepare': { + const parsed = parsePlayerPreparePayload(message.data); + this.requireActiveGame(rpc); + this.options.store.playerReady(parsed); + this.options.gameSession.publish({ type: 'room.player-ready', seat: parsed.seat }); + break; + } + case 'self_exit_room': { + const parsed = parseSelfExitRoomPayload(message.data); + this.options.session.handleSelfExit(parsed); + break; + } + case 'other_join_room': { + const parsed = parseOtherJoinRoomPayload(message.data); + this.requireActiveGame(rpc); + this.options.store.playerJoin(parsed); + this.options.gameSession.publish({ type: 'room.player-joined', seat: parsed.seat }); + break; + } + case 'other_exit_room': { + const parsed = parseOtherExitRoomPayload(message.data); + this.requireActiveGame(rpc); + this.options.store.playerExit(parsed); + this.options.gameSession.publish({ type: 'room.player-left', seat: parsed.seat }); + break; + } + case 'other_offline': { + const parsed = parseOtherOfflinePayload(message.data); + this.requireActiveGame(rpc); + this.options.store.playerOffline(parsed); + this.options.gameSession.publish({ type: 'room.player-offline', seat: parsed.seat }); + break; + } + case 'other_online': { + const parsed = parseOtherOnlinePayload(message.data); + this.requireActiveGame(rpc); + this.options.store.playerOnline(parsed); + this.options.gameSession.publish({ type: 'room.player-online', seat: parsed.seat }); + break; + } + case 'connect_roomserver': { + const parsed = parseConnectRoomserverPayload(message.data); + const target = parseSwitchTarget(parsed.roomserver, '$.data.roomserver'); + const resend = buildFirstSliceEnvelope('connect_roomserver', parsed.raw); + this.options.session.handleServerSwitch( + () => this.options.connection.switchServer(target, resend), + ); + break; + } + case 'connect_agentserver': { + const parsed = parseConnectAgentserverPayload(message.data); + const target = parseSwitchTarget(parsed.agentserver, '$.data.agentserver'); + const resend = buildFirstSliceEnvelope('connect_agentserver', parsed.raw); + this.options.session.handleServerSwitch( + () => this.options.connection.switchServer(target, resend), + ); + break; + } + case 'show_message': { + const data=requireRecord(message.data,'$.data'); + const text=requireString(data.msg,'$.data.msg'); + // Desk.show_message converts the original time with Number; only >0 auto-closes. + const time=Number(data.time); + if(!this.options.showMessage)throw Error('show_message requires a message UI'); + this.options.showMessage(text,time,'show_message'); + break; + } + case 'kick_offline': { + const data=requireRecord(message.data,'$.data'); + this.options.session.handleKick(data,()=>this.options.connection.stopForKick(data)); + break; + } + case 'kick_server': { + const parsed = parseKickServerPayload(message.data); + this.options.session.handleKick( + parsed.raw, + () => this.options.connection.stopForKick(parsed.raw), + ); + break; + } + } + return 'handled-platform'; + } + + private requireActiveGame(rpc: string): void { + if (this.options.gameSession.state !== 'active') { + throw new Error(`${rpc} requires an active game session`); + } + } +} + +function isPlatformRoute(route: string): route is 'platform' | 'agent' | 'room' { + return route === 'platform' || route === 'agent' || route === 'room'; +} + +function parseSwitchTarget(address: string, path: string): string { + if (address.length === 0 || address.trim() !== address) { + throw new TypeError(`${path}: expected a non-empty WebSocket server address`); + } + const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(address); + const target = scheme ? address : `ws://${address}`; + const authority = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#]*)/.exec(target)?.[1]; + if (authority === undefined || authority.length === 0) { + throw new TypeError(`${path}: expected a WebSocket address with a non-empty host`); + } + if (target.includes('#') || authority.includes('@')) { + throw new TypeError(`${path}: expected a WebSocket address without fragment or userinfo`); + } + + let parsed: URL; + try { + parsed = new URL(target); + } catch { + throw new TypeError(`${path}: expected a valid WebSocket server address`); + } + const protocol = parsed.protocol.toLowerCase(); + if ( + (protocol !== 'ws:' && protocol !== 'wss:') + || parsed.hostname.length === 0 + || parsed.username.length > 0 + || parsed.password.length > 0 + || parsed.hash.length > 0 + ) { + throw new TypeError(`${path}: expected a valid WebSocket server address`); + } + return target; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/create-room-page.ts b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/create-room-page.ts new file mode 100644 index 0000000..37c5993 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/create-room-page.ts @@ -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; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/create-room-page.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/create-room-page.ts.meta new file mode 100644 index 0000000..573b0f6 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/create-room-page.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "fd4c9495-d6d5-49da-8476-a510df24f1c4", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts index 4a9fe88..e70c0d0 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts @@ -1,6 +1,7 @@ export * from './game-entry.ts'; -export * from './game-host.ts'; -export * from './game-module.ts'; -export * from './game-seat-mapper.ts'; -export * from './platform-events.ts'; -export * from './snapshots.ts'; +export * from './create-room-page.ts'; +export * from './game-host.ts'; +export * from './game-module.ts'; +export * from './game-seat-mapper.ts'; +export * from './platform-events.ts'; +export * from './snapshots.ts'; diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/AnnouncementHorn.anim b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/AnnouncementHorn.anim new file mode 100644 index 0000000..8e24e25 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/AnnouncementHorn.anim @@ -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 + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/AnnouncementHorn.anim.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/AnnouncementHorn.anim.meta new file mode 100644 index 0000000..9d29a74 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/AnnouncementHorn.anim.meta @@ -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" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/RequestLoading.anim b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/RequestLoading.anim new file mode 100644 index 0000000..ff11a41 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/RequestLoading.anim @@ -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 + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/RequestLoading.anim.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/RequestLoading.anim.meta new file mode 100644 index 0000000..edc17e0 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/RequestLoading.anim.meta @@ -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" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-common/00042.png.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-common/00042.png.meta index 8772856..a42b9fd 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-common/00042.png.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-common/00042.png.meta @@ -1,134 +1,134 @@ -{ - "ver": "1.0.27", - "importer": "image", - "imported": true, - "uuid": "34daf5f4-3869-4f71-84c4-d09103701be6", - "files": [ - ".json", - ".png" - ], - "subMetas": { - "6c48a": { - "importer": "texture", - "uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a", - "displayName": "00042", - "id": "6c48a", - "name": "texture", - "userData": { - "wrapModeS": "clamp-to-edge", - "wrapModeT": "clamp-to-edge", - "imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6", - "isUuid": true, - "visible": false, - "minfilter": "linear", - "magfilter": "linear", - "mipfilter": "none", - "anisotropy": 0 - }, - "ver": "1.0.22", - "imported": true, - "files": [ - ".json" - ], - "subMetas": {} - }, - "f9941": { - "importer": "sprite-frame", - "uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@f9941", - "displayName": "00042", - "id": "f9941", - "name": "spriteFrame", - "userData": { - "trimThreshold": 1, - "rotated": false, - "offsetX": 0, - "offsetY": 0, - "trimX": 0, - "trimY": 0, - "width": 108, - "height": 98, - "rawWidth": 108, - "rawHeight": 98, - "borderTop": 48, - "borderBottom": 48, - "borderLeft": 53, - "borderRight": 53, - "packable": true, - "pixelsToUnit": 100, - "pivotX": 0.5, - "pivotY": 0.5, - "meshType": 0, - "vertices": { - "rawPosition": [ - -54, - -49, - 0, - 54, - -49, - 0, - -54, - 49, - 0, - 54, - 49, - 0 - ], - "indexes": [ - 0, - 1, - 2, - 2, - 1, - 3 - ], - "uv": [ - 0, - 98, - 108, - 98, - 0, - 0, - 108, - 0 - ], - "nuv": [ - 0, - 0, - 1, - 0, - 0, - 1, - 1, - 1 - ], - "minPos": [ - -54, - -49, - 0 - ], - "maxPos": [ - 54, - 49, - 0 - ] - }, - "isUuid": true, - "imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a", - "atlasUuid": "", - "trimType": "none" - }, - "ver": "1.0.12", - "imported": true, - "files": [ - ".json" - ], - "subMetas": {} - } - }, - "userData": { - "type": "sprite-frame", - "fixAlphaTransparencyArtifacts": false, - "hasAlpha": true, - "redirect": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a" - } -} +{ + "ver": "1.0.27", + "importer": "image", + "imported": true, + "uuid": "34daf5f4-3869-4f71-84c4-d09103701be6", + "files": [ + ".json", + ".png" + ], + "subMetas": { + "6c48a": { + "importer": "texture", + "uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a", + "displayName": "00042", + "id": "6c48a", + "name": "texture", + "userData": { + "wrapModeS": "clamp-to-edge", + "wrapModeT": "clamp-to-edge", + "imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6", + "isUuid": true, + "visible": false, + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "anisotropy": 0 + }, + "ver": "1.0.22", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "f9941": { + "importer": "sprite-frame", + "uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@f9941", + "displayName": "00042", + "id": "f9941", + "name": "spriteFrame", + "userData": { + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 108, + "height": 98, + "rawWidth": 108, + "rawHeight": 98, + "borderTop": 26, + "borderBottom": 34, + "borderLeft": 30, + "borderRight": 31, + "packable": true, + "pixelsToUnit": 100, + "pivotX": 0.5, + "pivotY": 0.5, + "meshType": 0, + "vertices": { + "rawPosition": [ + -54, + -49, + 0, + 54, + -49, + 0, + -54, + 49, + 0, + 54, + 49, + 0 + ], + "indexes": [ + 0, + 1, + 2, + 2, + 1, + 3 + ], + "uv": [ + 0, + 98, + 108, + 98, + 0, + 0, + 108, + 0 + ], + "nuv": [ + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1 + ], + "minPos": [ + -54, + -49, + 0 + ], + "maxPos": [ + 54, + 49, + 0 + ] + }, + "isUuid": true, + "imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a", + "atlasUuid": "", + "trimType": "none" + }, + "ver": "1.0.12", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + } + }, + "userData": { + "type": "sprite-frame", + "fixAlphaTransparencyArtifacts": false, + "hasAlpha": true, + "redirect": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-hall/00001.png b/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-hall/00001.png index c389a4b..187e4ec 100644 Binary files a/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-hall/00001.png and b/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-hall/00001.png differ diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-hall/00001.png.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-hall/00001.png.meta index 16aa701..5fa5f54 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-hall/00001.png.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-hall/00001.png.meta @@ -45,9 +45,9 @@ "offsetY": 0, "trimX": 0, "trimY": 0, - "width": 1600, + "width": 1280, "height": 720, - "rawWidth": 1600, + "rawWidth": 1280, "rawHeight": 720, "borderTop": 0, "borderBottom": 0, @@ -60,16 +60,16 @@ "meshType": 0, "vertices": { "rawPosition": [ - -800, + -640, -360, 0, - 800, + 640, -360, 0, - -800, + -640, 360, 0, - 800, + 640, 360, 0 ], @@ -84,11 +84,11 @@ "uv": [ 0, 720, - 1600, + 1280, 720, 0, 0, - 1600, + 1280, 0 ], "nuv": [ @@ -102,12 +102,12 @@ 1 ], "minPos": [ - -800, + -640, -360, 0 ], "maxPos": [ - 800, + 640, 360, 0 ] diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items.meta new file mode 100644 index 0000000..eb2f79f --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "ad5e871f-b5d8-439a-852b-53d4d1d9f61b", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/OwnerRoomItem.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/OwnerRoomItem.prefab new file mode 100644 index 0000000..aede2c9 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/OwnerRoomItem.prefab @@ -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 + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/OwnerRoomItem.prefab.meta similarity index 60% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab.meta rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/OwnerRoomItem.prefab.meta index b62acba..6ac577d 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/OwnerRoomItem.prefab.meta @@ -2,12 +2,12 @@ "ver": "1.1.50", "importer": "prefab", "imported": true, - "uuid": "b13f487a-451e-41ce-a810-1fab0d52248f", + "uuid": "67117675-9f6d-4def-a67c-db0e9fb19b98", "files": [ ".json" ], "subMetas": {}, "userData": { - "syncNodeName": "Layer9_Feedback" + "syncNodeName": "OwnerRoomItem" } } diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/PlayerInfoTextItem.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/PlayerInfoTextItem.prefab new file mode 100644 index 0000000..098667a --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/PlayerInfoTextItem.prefab @@ -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 + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/PlayerInfoTextItem.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/PlayerInfoTextItem.prefab.meta new file mode 100644 index 0000000..32d1b25 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/PlayerInfoTextItem.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "9e6dbfc6-26c5-4eac-9cc8-8060a677d5ee", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "PlayerInfoTextItem" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/TaskItem.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/TaskItem.prefab similarity index 94% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/TaskItem.prefab rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/TaskItem.prefab index 116d967..c1ad5c9 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/TaskItem.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/TaskItem.prefab @@ -1,1922 +1,1922 @@ -[ - { - "__type__": "cc.Prefab", - "_name": "TaskItem", - "_objFlags": 0, - "__editorExtras__": {}, - "_native": "", - "data": { - "__id__": 1 - }, - "optimizationPolicy": 0, - "persistent": false - }, - { - "__type__": "cc.Node", - "_name": "TaskItem", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": null, - "_children": [ - { - "__id__": 2 - } - ], - "_active": true, - "_components": [ - { - "__id__": 78 - }, - { - "__id__": 80 - } - ], - "_prefab": { - "__id__": 82 - }, - "_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": "Spirit123", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 3 - }, - { - "__id__": 13 - }, - { - "__id__": 29 - }, - { - "__id__": 37 - }, - { - "__id__": 45 - }, - { - "__id__": 53 - }, - { - "__id__": 63 - } - ], - "_active": true, - "_components": [ - { - "__id__": 71 - }, - { - "__id__": 73 - }, - { - "__id__": 75 - } - ], - "_prefab": { - "__id__": 77 - }, - "_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": "Spirit125", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 4 - }, - { - "__id__": 6 - }, - { - "__id__": 8 - }, - { - "__id__": 10 - } - ], - "_prefab": { - "__id__": 12 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -2.464, - "y": -118.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": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 3 - }, - "_enabled": true, - "__prefab": { - "__id__": 5 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 149, - "height": 30 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "9gVwI/fQPbV4KZhECObW+Q" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 3 - }, - "_enabled": true, - "__prefab": { - "__id__": 7 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "0179e156-c67a-4155-97be-19f95c20c5d6@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": "6VF4ptORY33CS6juYupa6g" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 3 - }, - "_enabled": true, - "__prefab": { - "__id__": 9 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -2.464, - "_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": "7Aw14+SOF+paCzaqzKSYzA" - }, - { - "__type__": "cc.Button", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 3 - }, - "_enabled": true, - "__prefab": { - "__id__": 11 - }, - "clickEvents": [], - "_interactable": true, - "_transition": 0, - "_normalColor": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_hoverColor": { - "__type__": "cc.Color", - "r": 211, - "g": 211, - "b": 211, - "a": 255 - }, - "_pressedColor": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_disabledColor": { - "__type__": "cc.Color", - "r": 124, - "g": 124, - "b": 124, - "a": 255 - }, - "_normalSprite": null, - "_hoverSprite": null, - "_pressedSprite": null, - "_disabledSprite": null, - "_duration": 0.1, - "_zoomScale": 1.2, - "_target": null, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "/YnHme5Ti8u7Iy7uQRlorQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "e1Mzg313taK22LnZWbkRQg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit311", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [ - { - "__id__": 14 - } - ], - "_active": true, - "_components": [ - { - "__id__": 20 - }, - { - "__id__": 22 - }, - { - "__id__": 24 - }, - { - "__id__": 26 - } - ], - "_prefab": { - "__id__": 28 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -0.964, - "y": -164, - "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": "Bar", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 13 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 15 - }, - { - "__id__": 17 - } - ], - "_prefab": { - "__id__": 19 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -101, - "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__": 14 - }, - "_enabled": true, - "__prefab": { - "__id__": 16 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 101, - "height": 21 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "JsTW4FfafohQMJbAL+XVwg" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 14 - }, - "_enabled": true, - "__prefab": { - "__id__": 18 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "5aa742e1-c2b1-4de6-ae02-13675a9165b4@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": "UsEGM56lE1Kl2kYnihmtVQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "JMcwe6AUYocOfSeU6BRnUQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 13 - }, - "_enabled": true, - "__prefab": { - "__id__": 21 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 202, - "height": 21 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "XyrtM9lHkLleY4auzbXklg" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 13 - }, - "_enabled": true, - "__prefab": { - "__id__": 23 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "4076f965-095a-47cb-8278-5683330e8f28@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": "kWyOGiDcvOzaw7BN466xhA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 13 - }, - "_enabled": true, - "__prefab": { - "__id__": 25 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -0.964, - "_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": "cOmhJ3KG0TrjwCVnhmRkKQ" - }, - { - "__type__": "cc.ProgressBar", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 13 - }, - "_enabled": true, - "__prefab": { - "__id__": 27 - }, - "_barSprite": { - "__id__": 17 - }, - "_mode": 0, - "_totalLength": 202, - "_progress": 0.5, - "_reverse": false, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "uQl3+DZuuzh6RqXcHmxIjg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "ik4aURcU9n7IPte002oUjA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit328", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 30 - }, - { - "__id__": 32 - }, - { - "__id__": 34 - } - ], - "_prefab": { - "__id__": 36 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 2.716, - "y": -151.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": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 29 - }, - "_enabled": true, - "__prefab": { - "__id__": 31 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 81, - "height": 26 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "S5AjL7dMBPGefCj63fjYlg" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 29 - }, - "_enabled": true, - "__prefab": { - "__id__": 33 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "cffa3693-5396-4c45-99bd-611d00ab3be2@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": "W5AIn+LFa/9SqpkqCNtYNg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 29 - }, - "_enabled": true, - "__prefab": { - "__id__": 35 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 2.716, - "_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": "5K36QYw56purblf2rjANHw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "StgzHZws/y2/oxcv5U/e2w", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit135", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 38 - }, - { - "__id__": 40 - }, - { - "__id__": 42 - } - ], - "_prefab": { - "__id__": 44 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -4.964, - "y": 173.016, - "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__": 37 - }, - "_enabled": true, - "__prefab": { - "__id__": 39 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 199.9682520689229, - "height": 25.200000000000003 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "zrSft4sJb1Qe78pGsBz6LA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 37 - }, - "_enabled": true, - "__prefab": { - "__id__": 41 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 102, - "g": 41, - "b": 16, - "a": 255 - }, - "_string": "任务一描述一一依依一", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 21.083333333333336, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 20, - "_overflow": 0, - "_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": "MwgoLGEzuCbEgWv4z3ahtg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 37 - }, - "_enabled": true, - "__prefab": { - "__id__": 43 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -4.964, - "_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": "i0G1CCbOwroZ4/8VY2CVdQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "A3C9OG5IhaovH245yIiaog", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit307", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 46 - }, - { - "__id__": 48 - }, - { - "__id__": 50 - } - ], - "_prefab": { - "__id__": 52 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -7.566, - "y": -62.769, - "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__": 45 - }, - "_enabled": true, - "__prefab": { - "__id__": 47 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 17.995243600234684, - "height": 22.680000000000003 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "TJZIXTR0lpAwo1+xDWBBxQ" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 45 - }, - "_enabled": true, - "__prefab": { - "__id__": 49 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "啊", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 18.975, - "_fontSize": 18, - "_fontFamily": "Arial", - "_lineHeight": 18, - "_overflow": 0, - "_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": "i8TC/Vb7wZpC+/ThqXtFYA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 45 - }, - "_enabled": true, - "__prefab": { - "__id__": 51 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -7.566, - "_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": "DLyx4e2p5+xwsPL7xE7n/A" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "vRgDS9nckBl7Y7Bv2GQudQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit124", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 54 - }, - { - "__id__": 56 - }, - { - "__id__": 58 - }, - { - "__id__": 60 - } - ], - "_prefab": { - "__id__": 62 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -3.464, - "y": -170.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": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 53 - }, - "_enabled": true, - "__prefab": { - "__id__": 55 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 171, - "height": 58 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "yciQCKeuyV4ID4JQWG2wGQ" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 53 - }, - "_enabled": true, - "__prefab": { - "__id__": 57 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "5b3f363e-0430-464b-9695-320d93846dcf@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": "IMIPmsZGwkb0QKH+X0Bv3A" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 53 - }, - "_enabled": true, - "__prefab": { - "__id__": 59 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -3.464, - "_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": "dV3YWNRjKCOwW8simPUlLg" - }, - { - "__type__": "cc.Button", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 53 - }, - "_enabled": true, - "__prefab": { - "__id__": 61 - }, - "clickEvents": [], - "_interactable": true, - "_transition": 0, - "_normalColor": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_hoverColor": { - "__type__": "cc.Color", - "r": 211, - "g": 211, - "b": 211, - "a": 255 - }, - "_pressedColor": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_disabledColor": { - "__type__": "cc.Color", - "r": 124, - "g": 124, - "b": 124, - "a": 255 - }, - "_normalSprite": null, - "_hoverSprite": null, - "_pressedSprite": null, - "_disabledSprite": null, - "_duration": 0.1, - "_zoomScale": 1.2, - "_target": null, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Jr/JzSpSVmvEayUCDJalqw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "a+rKdBTyaWDVg3RpJ9X3zQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit319", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 64 - }, - { - "__id__": 66 - }, - { - "__id__": 68 - } - ], - "_prefab": { - "__id__": 70 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 8.091, - "y": -162.303, - "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__": 63 - }, - "_enabled": true, - "__prefab": { - "__id__": 65 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 143.9430882148592, - "height": 20.16 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "4ez1CJdeEOZLOtFUlDbzMA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 63 - }, - "_enabled": true, - "__prefab": { - "__id__": 67 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "任务一进度文字说明", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 16.866666666666667, - "_fontSize": 16, - "_fontFamily": "Arial", - "_lineHeight": 16, - "_overflow": 0, - "_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": "bFZXF0Z0/P3OeZAKWbWM5w" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 63 - }, - "_enabled": true, - "__prefab": { - "__id__": 69 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 8.091, - "_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": "uYn2umV4kq0uYF8QH2rkEg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "lnj36XCFHIn6f340ujsvog", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": { - "__id__": 72 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 269, - "height": 453 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "DVKQdZ1Uvt94Sflk4xRRSw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": { - "__id__": 74 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "2327b915-6dba-405a-9604-c2e7e9410638@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": "fXPv5gmpAzX/dlAhNEhOWQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": { - "__id__": 76 - }, - "_alignFlags": 18, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "ILEtaIsz2iR5xpGTrXMn9g" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "flBbLIw7JQWriUwFo/qDpw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 1 - }, - "_enabled": true, - "__prefab": { - "__id__": 79 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 296, - "height": 453 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "FrQJeTcgETb/0IwUL5WMFA" - }, - { - "__type__": "bb88dYbmEdGvJxxStbRltUB", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 1 - }, - "_enabled": true, - "__prefab": { - "__id__": 81 - }, - "titleLabel": { - "__id__": 40 - }, - "rewardLabel": { - "__id__": 48 - }, - "progressLabel": { - "__id__": 66 - }, - "progressBar": { - "__id__": 26 - }, - "doButton": { - "__id__": 10 - }, - "claimButton": { - "__id__": 60 - }, - "claimedMark": { - "__id__": 29 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "PavEb8MCMiur7jLMrg90nA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "dsjKEosywAmq4SaLktDAhg", - "instance": null, - "targetOverrides": null - } +[ + { + "__type__": "cc.Prefab", + "_name": "TaskItem", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "persistent": false + }, + { + "__type__": "cc.Node", + "_name": "TaskItem", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [ + { + "__id__": 78 + }, + { + "__id__": 80 + } + ], + "_prefab": { + "__id__": 82 + }, + "_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": "Spirit123", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 13 + }, + { + "__id__": 29 + }, + { + "__id__": 37 + }, + { + "__id__": 45 + }, + { + "__id__": 53 + }, + { + "__id__": 63 + } + ], + "_active": true, + "_components": [ + { + "__id__": 71 + }, + { + "__id__": 73 + }, + { + "__id__": 75 + } + ], + "_prefab": { + "__id__": 77 + }, + "_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": "Spirit125", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + }, + { + "__id__": 6 + }, + { + "__id__": 8 + }, + { + "__id__": 10 + } + ], + "_prefab": { + "__id__": 12 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -2.464, + "y": -118.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": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 5 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 149, + "height": 30 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "9gVwI/fQPbV4KZhECObW+Q" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 7 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "0179e156-c67a-4155-97be-19f95c20c5d6@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": "6VF4ptORY33CS6juYupa6g" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 9 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -2.464, + "_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": "7Aw14+SOF+paCzaqzKSYzA" + }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 11 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "/YnHme5Ti8u7Iy7uQRlorQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "e1Mzg313taK22LnZWbkRQg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit311", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 14 + } + ], + "_active": true, + "_components": [ + { + "__id__": 20 + }, + { + "__id__": 22 + }, + { + "__id__": 24 + }, + { + "__id__": 26 + } + ], + "_prefab": { + "__id__": 28 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.964, + "y": -164, + "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": "Bar", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 13 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 15 + }, + { + "__id__": 17 + } + ], + "_prefab": { + "__id__": 19 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -101, + "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__": 14 + }, + "_enabled": true, + "__prefab": { + "__id__": 16 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 101, + "height": 21 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "JsTW4FfafohQMJbAL+XVwg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 14 + }, + "_enabled": true, + "__prefab": { + "__id__": 18 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "5aa742e1-c2b1-4de6-ae02-13675a9165b4@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": "UsEGM56lE1Kl2kYnihmtVQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "JMcwe6AUYocOfSeU6BRnUQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 13 + }, + "_enabled": true, + "__prefab": { + "__id__": 21 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 202, + "height": 21 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "XyrtM9lHkLleY4auzbXklg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 13 + }, + "_enabled": true, + "__prefab": { + "__id__": 23 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "4076f965-095a-47cb-8278-5683330e8f28@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": "kWyOGiDcvOzaw7BN466xhA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 13 + }, + "_enabled": true, + "__prefab": { + "__id__": 25 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -0.964, + "_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": "cOmhJ3KG0TrjwCVnhmRkKQ" + }, + { + "__type__": "cc.ProgressBar", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 13 + }, + "_enabled": true, + "__prefab": { + "__id__": 27 + }, + "_barSprite": { + "__id__": 17 + }, + "_mode": 0, + "_totalLength": 202, + "_progress": 0.5, + "_reverse": false, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "uQl3+DZuuzh6RqXcHmxIjg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ik4aURcU9n7IPte002oUjA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit328", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 30 + }, + { + "__id__": 32 + }, + { + "__id__": 34 + } + ], + "_prefab": { + "__id__": 36 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 2.716, + "y": -151.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": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 29 + }, + "_enabled": true, + "__prefab": { + "__id__": 31 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 81, + "height": 26 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "S5AjL7dMBPGefCj63fjYlg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 29 + }, + "_enabled": true, + "__prefab": { + "__id__": 33 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "cffa3693-5396-4c45-99bd-611d00ab3be2@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": "W5AIn+LFa/9SqpkqCNtYNg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 29 + }, + "_enabled": true, + "__prefab": { + "__id__": 35 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 2.716, + "_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": "5K36QYw56purblf2rjANHw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "StgzHZws/y2/oxcv5U/e2w", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit135", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 38 + }, + { + "__id__": 40 + }, + { + "__id__": 42 + } + ], + "_prefab": { + "__id__": 44 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -4.964, + "y": 173.016, + "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__": 37 + }, + "_enabled": true, + "__prefab": { + "__id__": 39 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 199.9682520689229, + "height": 25.200000000000003 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "zrSft4sJb1Qe78pGsBz6LA" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 37 + }, + "_enabled": true, + "__prefab": { + "__id__": 41 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 102, + "g": 41, + "b": 16, + "a": 255 + }, + "_string": "任务一描述一一依依一", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 21.083333333333336, + "_fontSize": 20, + "_fontFamily": "Arial", + "_lineHeight": 20, + "_overflow": 0, + "_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": "MwgoLGEzuCbEgWv4z3ahtg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 37 + }, + "_enabled": true, + "__prefab": { + "__id__": 43 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -4.964, + "_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": "i0G1CCbOwroZ4/8VY2CVdQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "A3C9OG5IhaovH245yIiaog", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit307", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 46 + }, + { + "__id__": 48 + }, + { + "__id__": 50 + } + ], + "_prefab": { + "__id__": 52 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -7.566, + "y": -62.769, + "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__": 45 + }, + "_enabled": true, + "__prefab": { + "__id__": 47 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 17.995243600234684, + "height": 22.680000000000003 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "TJZIXTR0lpAwo1+xDWBBxQ" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 45 + }, + "_enabled": true, + "__prefab": { + "__id__": 49 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_string": "啊", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 18.975, + "_fontSize": 18, + "_fontFamily": "Arial", + "_lineHeight": 18, + "_overflow": 0, + "_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": "i8TC/Vb7wZpC+/ThqXtFYA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 45 + }, + "_enabled": true, + "__prefab": { + "__id__": 51 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -7.566, + "_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": "DLyx4e2p5+xwsPL7xE7n/A" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "vRgDS9nckBl7Y7Bv2GQudQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit124", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 54 + }, + { + "__id__": 56 + }, + { + "__id__": 58 + }, + { + "__id__": 60 + } + ], + "_prefab": { + "__id__": 62 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -3.464, + "y": -170.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": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 53 + }, + "_enabled": true, + "__prefab": { + "__id__": 55 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 171, + "height": 58 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "yciQCKeuyV4ID4JQWG2wGQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 53 + }, + "_enabled": true, + "__prefab": { + "__id__": 57 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "5b3f363e-0430-464b-9695-320d93846dcf@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": "IMIPmsZGwkb0QKH+X0Bv3A" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 53 + }, + "_enabled": true, + "__prefab": { + "__id__": 59 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -3.464, + "_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": "dV3YWNRjKCOwW8simPUlLg" + }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 53 + }, + "_enabled": true, + "__prefab": { + "__id__": 61 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Jr/JzSpSVmvEayUCDJalqw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "a+rKdBTyaWDVg3RpJ9X3zQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit319", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 64 + }, + { + "__id__": 66 + }, + { + "__id__": 68 + } + ], + "_prefab": { + "__id__": 70 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 8.091, + "y": -162.303, + "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__": 63 + }, + "_enabled": true, + "__prefab": { + "__id__": 65 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 143.9430882148592, + "height": 20.16 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "4ez1CJdeEOZLOtFUlDbzMA" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 63 + }, + "_enabled": true, + "__prefab": { + "__id__": 67 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "任务一进度文字说明", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 16.866666666666667, + "_fontSize": 16, + "_fontFamily": "Arial", + "_lineHeight": 16, + "_overflow": 0, + "_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": "bFZXF0Z0/P3OeZAKWbWM5w" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 63 + }, + "_enabled": true, + "__prefab": { + "__id__": 69 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 8.091, + "_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": "uYn2umV4kq0uYF8QH2rkEg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "lnj36XCFHIn6f340ujsvog", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 72 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 269, + "height": 453 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "DVKQdZ1Uvt94Sflk4xRRSw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 74 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "2327b915-6dba-405a-9604-c2e7e9410638@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": "fXPv5gmpAzX/dlAhNEhOWQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 76 + }, + "_alignFlags": 18, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ILEtaIsz2iR5xpGTrXMn9g" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "flBbLIw7JQWriUwFo/qDpw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 79 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 296, + "height": 453 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "FrQJeTcgETb/0IwUL5WMFA" + }, + { + "__type__": "bb88dYbmEdGvJxxStbRltUB", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 81 + }, + "titleLabel": { + "__id__": 40 + }, + "rewardLabel": { + "__id__": 48 + }, + "progressLabel": { + "__id__": 66 + }, + "progressBar": { + "__id__": 26 + }, + "doButton": { + "__id__": 10 + }, + "claimButton": { + "__id__": 60 + }, + "claimedMark": { + "__id__": 29 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "PavEb8MCMiur7jLMrg90nA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "dsjKEosywAmq4SaLktDAhg", + "instance": null, + "targetOverrides": null + } ] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/TaskItem.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/TaskItem.prefab.meta similarity index 94% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/TaskItem.prefab.meta rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/TaskItem.prefab.meta index 5a6c891..2718436 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/TaskItem.prefab.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/TaskItem.prefab.meta @@ -1,13 +1,13 @@ -{ - "ver": "1.1.50", - "importer": "prefab", - "imported": true, - "uuid": "b9f6f400-7b1b-4e51-a841-cc84dcddca82", - "files": [ - ".json" - ], - "subMetas": {}, - "userData": { - "syncNodeName": "TaskItem" - } -} +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "b9f6f400-7b1b-4e51-a841-cc84dcddca82", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "TaskItem" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRankItem.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRankItem.prefab new file mode 100644 index 0000000..3e240f3 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRankItem.prefab @@ -0,0 +1,1216 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "VipRankItem", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "persistent": false + }, + { + "__type__": "cc.Node", + "_name": "VipRankItem", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + }, + { + "__id__": 10 + }, + { + "__id__": 18 + }, + { + "__id__": 34 + }, + { + "__id__": 42 + } + ], + "_active": true, + "_components": [ + { + "__id__": 50 + } + ], + "_prefab": { + "__id__": 52 + }, + "_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": "d9fxiv1NZGv6ESgwForAyK" + }, + { + "__type__": "cc.Node", + "_name": "Spirit610", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 3 + }, + { + "__id__": 5 + }, + { + "__id__": 7 + } + ], + "_prefab": { + "__id__": 9 + }, + "_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": "c43LV1qTZH36MWBWQWgDJk" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 4 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 420, + "height": 87 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "d31xYYGbpOzJJHwy4yQyw/" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "9eGofBV9qYtDvYsDklJYGw" + }, + { + "__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__": "fe744ec6-7245-428f-aa0e-b88e9c2bb60f@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "79QKD/ZCtHOoCiYPgvwYv0" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "WR2eljPUPdGbxC/JsizEQw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 8 + }, + "_alignFlags": 18, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "d9FiHqSGpEFbB32nPbuHjs" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "/ixXZ/5o50+ed7lE8Vapig" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "21uMtubZ0D0wr8W+dWRo4A", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit629", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 11 + }, + { + "__id__": 13 + }, + { + "__id__": 15 + } + ], + "_prefab": { + "__id__": 17 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -162.316, + "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": "98kCw4G/xKk5I0ltMbAoZB" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 10 + }, + "_enabled": true, + "__prefab": { + "__id__": 12 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 88, + "height": 93 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "69sL6cE5VLrpYQMH7kEByB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "9zegiik96cRTREb/rGMzEQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 10 + }, + "_enabled": true, + "__prefab": { + "__id__": 14 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "fb86ab44-8140-4d8e-82f1-84ae4b8f3216@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": "cfZFzs8qJK5oxvFvOOmzst" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "G4zZL6K1Et0ODNdhwMsqPA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 10 + }, + "_enabled": true, + "__prefab": { + "__id__": 16 + }, + "_alignFlags": 18, + "_target": null, + "_left": -156.316, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -162.316, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "fc3aasGhlLv5xEwjuZMJgp" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "+q68OK/P7j/LKSiTiTFqow" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "T4KmzYMpZBvrkkTa0jp5Gg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit630", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 19 + } + ], + "_active": true, + "_components": [ + { + "__id__": 27 + }, + { + "__id__": 29 + }, + { + "__id__": 31 + } + ], + "_prefab": { + "__id__": 33 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -54.129, + "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": "90wXi+QMlAlLcFdDAim/wD" + }, + { + "__type__": "cc.Node", + "_name": "Spirit631", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 18 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 20 + }, + { + "__id__": 22 + }, + { + "__id__": 24 + } + ], + "_prefab": { + "__id__": 26 + }, + "_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": "b1dWPsEF5PPaJQQiYqxtRc" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 21 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 80, + "height": 80 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "39ZaBXwedPMoMUp/2nopqL" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "d6UmQFsW0KsdKAn22z+Uuw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 23 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "b6310388-a682-483c-9d39-17c8876f0b49@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": "2e1E3rl1pAVIFjwDnQi9GI" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "GwM5fNRfLXupxVuDmg0rgw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 25 + }, + "_alignFlags": 18, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "29a0HbsfFLH7S7U3Xyt4Pd" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "fRk9+d14ecrAnfbBgdtENg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "KvXFhqPrSBFJHYXzXQZ3rg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 18 + }, + "_enabled": true, + "__prefab": { + "__id__": 28 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 65, + "height": 65 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "a6Wlc6qZ1DTpavtSvCulVG" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "DQO1K6dzCZQErksjSG0Rmw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 18 + }, + "_enabled": true, + "__prefab": { + "__id__": 30 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "0c7ab485-8adf-432e-8dfb-27dff1dde280@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": "66uihIAhtLlJliJxv7OqTh" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2NQDi2sdc+BBpjCJyEmmew" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 18 + }, + "_enabled": true, + "__prefab": { + "__id__": 32 + }, + "_alignFlags": 18, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -54.129, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "013HG2dGRMNqHbLAEzVd+h" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "HmLjJjE/Zq+XM7i02AulbA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ex9RjEMfbpyzWo83xL5a/Q", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit632", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 35 + }, + { + "__id__": 37 + }, + { + "__id__": 39 + } + ], + "_prefab": { + "__id__": 41 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 95.964, + "y": 17.128, + "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": "7bYti2M4RKM7IZ2yiLusIW" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 34 + }, + "_enabled": true, + "__prefab": { + "__id__": 36 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 143.8084007534585, + "height": 17.64 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "3e1mOBp8FJb6Ws/O89zQXE" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "7TGcqu3TLYeOWzNnp9jXQw" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 34 + }, + "_enabled": true, + "__prefab": { + "__id__": 38 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_string": "色丰富飞的放到的是是 ", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 14.758333333333333, + "_fontSize": 14, + "_fontFamily": "Arial", + "_lineHeight": 14, + "_overflow": 0, + "_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": "acn5dHluhF9JuVtpjuMVDG" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "0fQUNQkXJtGy/zQnvmR/LQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 34 + }, + "_enabled": true, + "__prefab": { + "__id__": 40 + }, + "_alignFlags": 18, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 95.964, + "_verticalCenter": 17.128, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "97x6xMrHdHcrejv1/Y9NT/" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "sUZj6Xfy6SWbOnMlfWrEGw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ZGtsfJ3qP7XhenO/4R4nAA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit633", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 43 + }, + { + "__id__": 45 + }, + { + "__id__": 47 + } + ], + "_prefab": { + "__id__": 49 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 35.964, + "y": -15.872, + "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": "cb34mCsUtKbYnAB1SiymDF" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 42 + }, + "_enabled": true, + "__prefab": { + "__id__": 44 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 55.22368299160079, + "height": 17.64 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "6aRnkQkthEOYuzq18qn7Kr" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "w+So1WXr18f7wpQT84+zVQ" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 42 + }, + "_enabled": true, + "__prefab": { + "__id__": 46 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_string": "Spirit632", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 14.758333333333333, + "_fontSize": 14, + "_fontFamily": "Arial", + "_lineHeight": 14, + "_overflow": 0, + "_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": "105DFnG6hAEZxVddJMl2in" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "AUly0w7TV5/7042vhXu4Fg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 42 + }, + "_enabled": true, + "__prefab": { + "__id__": 48 + }, + "_alignFlags": 18, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 35.964, + "_verticalCenter": -15.872, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "a4Ns+95w5CmYbzK/6GfhVD" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "hdG+cxkJGESJydEWzrr9+A" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "hdU+nUKLhgXdhNEWAFe/Hw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 51 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 420, + "height": 93 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "ddqTPpRgRIh5AJQdYd/WMk" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "KHpgIYNCg1gqWSZyedLUGA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "D9rLVXBlwKlRj35JEsdnvw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRankItem.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRankItem.prefab.meta new file mode 100644 index 0000000..be7ce2a --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRankItem.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "5925d1d2-f4b2-44c1-a37a-fb2bcdcf664e", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "VipRankItem" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem1.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem1.prefab similarity index 94% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem1.prefab rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem1.prefab index 91188e3..4172dec 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem1.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem1.prefab @@ -1,1328 +1,1328 @@ -[ - { - "__type__": "cc.Prefab", - "_name": "VipRoomItem1", - "_objFlags": 0, - "__editorExtras__": {}, - "_native": "", - "data": { - "__id__": 1 - }, - "optimizationPolicy": 0, - "persistent": false - }, - { - "__type__": "cc.Node", - "_name": "VipRoomItem1", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": null, - "_children": [ - { - "__id__": 2 - }, - { - "__id__": 8 - }, - { - "__id__": 14 - }, - { - "__id__": 20 - }, - { - "__id__": 26 - }, - { - "__id__": 32 - }, - { - "__id__": 38 - }, - { - "__id__": 44 - } - ], - "_active": true, - "_components": [ - { - "__id__": 50 - } - ], - "_prefab": { - "__id__": 52 - }, - "_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": "b9tPUkVJFHGrv6p4Mrza04" - }, - { - "__type__": "cc.Node", - "_name": "Spirit3176", - "_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": "46giBWb2RG65Jy3DvBy8z3" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": { - "__id__": 4 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 631, - "height": 194 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "397GTyGKdBwYAFYb7BXwcW" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "1Z6ELeD3xOwYf9U2gB4Fpg" - }, - { - "__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__": "b008b157-313a-445b-a5ea-501d55f3bee7@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": "589Tly1mFMFKeVFnUt3CPp" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "f1IisW5yc45hNKrNk4yzHQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "YNSWn4F5Osr8vhz2+t4zZg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3192", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 9 - }, - { - "__id__": 11 - } - ], - "_prefab": { - "__id__": 13 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 198.5, - "y": 55, - "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": "163DjhQj5CH460HrxnKB9W" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 8 - }, - "_enabled": true, - "__prefab": { - "__id__": 10 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 102, - "height": 103 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0, - "y": 1 - }, - "_id": "590KspZclPzrDXX0Xgs8sC" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "gg4YSern2+1CmiIRoXTpKA" - }, - { - "__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__": "f2a266cb-1400-4948-9083-1c8dd029ce7a@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 0, - "_fillType": 0, - "_sizeMode": 1, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "e0u065ZLVHsIAGMH38OgEn" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "b8sQj2edqOCJQRm0UwJYrg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "b5RA09+IP0w/Fa3RnnwxdA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3189", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 15 - }, - { - "__id__": 17 - } - ], - "_prefab": { - "__id__": 19 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -290.5, - "y": 47, - "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": "098tChC4FP36mpspyLN7Ud" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 14 - }, - "_enabled": true, - "__prefab": { - "__id__": 16 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 480, - "height": 92 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0, - "y": 1 - }, - "_id": "37+KNwseJLkZ8ge8oaCrQo" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "GZNC3MbtptOiyfqoeTXIzw" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 14 - }, - "_enabled": true, - "__prefab": { - "__id__": 18 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 60, - "g": 40, - "b": 10, - "a": 255 - }, - "_string": "111", - "_horizontalAlign": 1, - "_verticalAlign": 0, - "_actualFontSize": 14.758333333333333, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 1, - "_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": "09dSnD3o9EapV6kuX6ERb1" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "X/uhI2dw9NNbzee2xJ0QtA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "hQpAzxr+9OYplkfTrWEf9g", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3177", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 21 - }, - { - "__id__": 23 - } - ], - "_prefab": { - "__id__": 25 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -295.5, - "y": 94, - "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": "0f0igi6q1BopkzWlYvFZOL" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 20 - }, - "_enabled": true, - "__prefab": { - "__id__": 22 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 470, - "height": 32 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0, - "y": 1 - }, - "_id": "81nszdictALIpMLHn3gcVY" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "r/QehkilI4PWlxWo4eTEEA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 20 - }, - "_enabled": true, - "__prefab": { - "__id__": 24 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "22", - "_horizontalAlign": 1, - "_verticalAlign": 0, - "_actualFontSize": 14.758333333333333, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 1, - "_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": "9aZo7iZP5AR59e2P6tDMWx" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "pbME044jViJQvj6grieUmw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "4BPsB5ChKrIjwf3qclNxgg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3178", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 27 - }, - { - "__id__": 29 - } - ], - "_prefab": { - "__id__": 31 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -245.5, - "y": -51, - "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": "dekMHE2itH8qQ3Yeg7C3r0" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 26 - }, - "_enabled": true, - "__prefab": { - "__id__": 28 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 170, - "height": 36 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0, - "y": 1 - }, - "_id": "98JPlT5dVBCLHYHTFP6tvO" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "YLAV5gn8XOGtPkifLSZf5Q" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 26 - }, - "_enabled": true, - "__prefab": { - "__id__": 30 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 60, - "g": 40, - "b": 10, - "a": 255 - }, - "_string": "22", - "_horizontalAlign": 1, - "_verticalAlign": 0, - "_actualFontSize": 14.758333333333333, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 1, - "_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": "56nobuKYFOL68DsEIuiS4f" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "2t+tKAE2BEvBOTG8jHjv5Q" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "ykY8PtUSpH45qke6W4GkUg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3179", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 33 - }, - { - "__id__": 35 - } - ], - "_prefab": { - "__id__": 37 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -45.5, - "y": -49, - "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": "caXOJ9wodENIz5AdU5Ryvz" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 32 - }, - "_enabled": true, - "__prefab": { - "__id__": 34 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 230, - "height": 36 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0, - "y": 1 - }, - "_id": "e1s15lS1FCiJUXP59S9cuO" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "LTYdRAhrddN08JsXcB3BWw" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 32 - }, - "_enabled": true, - "__prefab": { - "__id__": 36 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 60, - "g": 40, - "b": 10, - "a": 255 - }, - "_string": "22", - "_horizontalAlign": 1, - "_verticalAlign": 0, - "_actualFontSize": 14.758333333333333, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 1, - "_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": "43Sc4imMZJcIsd/x86Xj2P" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Zu87/derw8N2Nx5RTFAwBw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "dBx5YmTCwjRtAIBCt8UnKQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3180", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 39 - }, - { - "__id__": 41 - } - ], - "_prefab": { - "__id__": 43 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 209.5, - "y": -49, - "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": "697wvEvoFBfKqfG4/3hNu7" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 38 - }, - "_enabled": true, - "__prefab": { - "__id__": 40 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 105, - "height": 36 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0, - "y": 1 - }, - "_id": "10/tD/4ZFOpJpK+Abd5LZf" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "emZobBP08XTuNzV/fkOPwA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 38 - }, - "_enabled": true, - "__prefab": { - "__id__": 42 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 60, - "g": 40, - "b": 10, - "a": 255 - }, - "_string": "22", - "_horizontalAlign": 1, - "_verticalAlign": 0, - "_actualFontSize": 14.758333333333333, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 1, - "_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": "17gsG9zihA+b3iJ4qlnrCu" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "1zzOGzyRUkCQ7QjMWY5/bA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "4uVUqLXtiGE40+uO/g0E3A", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3181", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": false, - "_components": [ - { - "__id__": 45 - }, - { - "__id__": 47 - } - ], - "_prefab": { - "__id__": 49 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 267.5, - "y": 97, - "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": "2eyTqUzu1B5q0WeCabmXFw" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 44 - }, - "_enabled": true, - "__prefab": { - "__id__": 46 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 44, - "height": 44 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0, - "y": 1 - }, - "_id": "a99Rg3pEhGqK7zzKXictwG" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "pTf4RFx9JSmRcfYN5R04gA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 44 - }, - "_enabled": true, - "__prefab": { - "__id__": 48 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "362534c2-ef9e-4fc6-b4b8-aaa1be24d54f@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 0, - "_fillType": 0, - "_sizeMode": 1, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "05rdhmM4lEAoDlz3Uy16xh" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "O0t6BABWspehHS45DMHcDQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "ASar8ekfDo06PplMzicy8w", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 1 - }, - "_enabled": true, - "__prefab": { - "__id__": 51 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 631, - "height": 194 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "d5ABf5QkhGsoAcsyVAmBEG" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Un1j2HAwrOYb3CwZyzAShQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "BIhPE52vX6k23+Dz99ezFw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - } +[ + { + "__type__": "cc.Prefab", + "_name": "VipRoomItem1", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "persistent": false + }, + { + "__type__": "cc.Node", + "_name": "VipRoomItem1", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + }, + { + "__id__": 8 + }, + { + "__id__": 14 + }, + { + "__id__": 20 + }, + { + "__id__": 26 + }, + { + "__id__": 32 + }, + { + "__id__": 38 + }, + { + "__id__": 44 + } + ], + "_active": true, + "_components": [ + { + "__id__": 50 + } + ], + "_prefab": { + "__id__": 52 + }, + "_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": "b9tPUkVJFHGrv6p4Mrza04" + }, + { + "__type__": "cc.Node", + "_name": "Spirit3176", + "_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": "46giBWb2RG65Jy3DvBy8z3" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 4 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 631, + "height": 194 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "397GTyGKdBwYAFYb7BXwcW" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "1Z6ELeD3xOwYf9U2gB4Fpg" + }, + { + "__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__": "b008b157-313a-445b-a5ea-501d55f3bee7@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": "589Tly1mFMFKeVFnUt3CPp" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "f1IisW5yc45hNKrNk4yzHQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "YNSWn4F5Osr8vhz2+t4zZg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3192", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 9 + }, + { + "__id__": 11 + } + ], + "_prefab": { + "__id__": 13 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 198.5, + "y": 55, + "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": "163DjhQj5CH460HrxnKB9W" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 8 + }, + "_enabled": true, + "__prefab": { + "__id__": 10 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 102, + "height": 103 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "590KspZclPzrDXX0Xgs8sC" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "gg4YSern2+1CmiIRoXTpKA" + }, + { + "__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__": "f2a266cb-1400-4948-9083-1c8dd029ce7a@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "e0u065ZLVHsIAGMH38OgEn" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "b8sQj2edqOCJQRm0UwJYrg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "b5RA09+IP0w/Fa3RnnwxdA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3189", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 15 + }, + { + "__id__": 17 + } + ], + "_prefab": { + "__id__": 19 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -290.5, + "y": 47, + "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": "098tChC4FP36mpspyLN7Ud" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 14 + }, + "_enabled": true, + "__prefab": { + "__id__": 16 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 480, + "height": 92 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "37+KNwseJLkZ8ge8oaCrQo" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "GZNC3MbtptOiyfqoeTXIzw" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 14 + }, + "_enabled": true, + "__prefab": { + "__id__": 18 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 60, + "g": 40, + "b": 10, + "a": 255 + }, + "_string": "111", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 14.758333333333333, + "_fontSize": 14, + "_fontFamily": "Arial", + "_lineHeight": 14, + "_overflow": 1, + "_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": "09dSnD3o9EapV6kuX6ERb1" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "X/uhI2dw9NNbzee2xJ0QtA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "hQpAzxr+9OYplkfTrWEf9g", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3177", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 21 + }, + { + "__id__": 23 + } + ], + "_prefab": { + "__id__": 25 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -295.5, + "y": 94, + "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": "0f0igi6q1BopkzWlYvFZOL" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 20 + }, + "_enabled": true, + "__prefab": { + "__id__": 22 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 470, + "height": 32 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "81nszdictALIpMLHn3gcVY" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "r/QehkilI4PWlxWo4eTEEA" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 20 + }, + "_enabled": true, + "__prefab": { + "__id__": 24 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "22", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 14.758333333333333, + "_fontSize": 14, + "_fontFamily": "Arial", + "_lineHeight": 14, + "_overflow": 1, + "_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": "9aZo7iZP5AR59e2P6tDMWx" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "pbME044jViJQvj6grieUmw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "4BPsB5ChKrIjwf3qclNxgg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3178", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 27 + }, + { + "__id__": 29 + } + ], + "_prefab": { + "__id__": 31 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -245.5, + "y": -51, + "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": "dekMHE2itH8qQ3Yeg7C3r0" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 26 + }, + "_enabled": true, + "__prefab": { + "__id__": 28 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 170, + "height": 36 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "98JPlT5dVBCLHYHTFP6tvO" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "YLAV5gn8XOGtPkifLSZf5Q" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 26 + }, + "_enabled": true, + "__prefab": { + "__id__": 30 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 60, + "g": 40, + "b": 10, + "a": 255 + }, + "_string": "22", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 14.758333333333333, + "_fontSize": 14, + "_fontFamily": "Arial", + "_lineHeight": 14, + "_overflow": 1, + "_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": "56nobuKYFOL68DsEIuiS4f" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2t+tKAE2BEvBOTG8jHjv5Q" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ykY8PtUSpH45qke6W4GkUg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3179", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 33 + }, + { + "__id__": 35 + } + ], + "_prefab": { + "__id__": 37 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -45.5, + "y": -49, + "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": "caXOJ9wodENIz5AdU5Ryvz" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 32 + }, + "_enabled": true, + "__prefab": { + "__id__": 34 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 230, + "height": 36 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "e1s15lS1FCiJUXP59S9cuO" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "LTYdRAhrddN08JsXcB3BWw" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 32 + }, + "_enabled": true, + "__prefab": { + "__id__": 36 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 60, + "g": 40, + "b": 10, + "a": 255 + }, + "_string": "22", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 14.758333333333333, + "_fontSize": 14, + "_fontFamily": "Arial", + "_lineHeight": 14, + "_overflow": 1, + "_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": "43Sc4imMZJcIsd/x86Xj2P" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Zu87/derw8N2Nx5RTFAwBw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "dBx5YmTCwjRtAIBCt8UnKQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3180", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 39 + }, + { + "__id__": 41 + } + ], + "_prefab": { + "__id__": 43 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 209.5, + "y": -49, + "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": "697wvEvoFBfKqfG4/3hNu7" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 38 + }, + "_enabled": true, + "__prefab": { + "__id__": 40 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 105, + "height": 36 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "10/tD/4ZFOpJpK+Abd5LZf" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "emZobBP08XTuNzV/fkOPwA" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 38 + }, + "_enabled": true, + "__prefab": { + "__id__": 42 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 60, + "g": 40, + "b": 10, + "a": 255 + }, + "_string": "22", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 14.758333333333333, + "_fontSize": 14, + "_fontFamily": "Arial", + "_lineHeight": 14, + "_overflow": 1, + "_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": "17gsG9zihA+b3iJ4qlnrCu" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "1zzOGzyRUkCQ7QjMWY5/bA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "4uVUqLXtiGE40+uO/g0E3A", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3181", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 45 + }, + { + "__id__": 47 + } + ], + "_prefab": { + "__id__": 49 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 267.5, + "y": 97, + "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": "2eyTqUzu1B5q0WeCabmXFw" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 44 + }, + "_enabled": true, + "__prefab": { + "__id__": 46 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 44, + "height": 44 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "a99Rg3pEhGqK7zzKXictwG" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "pTf4RFx9JSmRcfYN5R04gA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 44 + }, + "_enabled": true, + "__prefab": { + "__id__": 48 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "362534c2-ef9e-4fc6-b4b8-aaa1be24d54f@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "05rdhmM4lEAoDlz3Uy16xh" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "O0t6BABWspehHS45DMHcDQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ASar8ekfDo06PplMzicy8w", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 51 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 631, + "height": 194 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "d5ABf5QkhGsoAcsyVAmBEG" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Un1j2HAwrOYb3CwZyzAShQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "BIhPE52vX6k23+Dz99ezFw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + } ] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem1.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem1.prefab.meta similarity index 94% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem1.prefab.meta rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem1.prefab.meta index e73e2b3..5026cba 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem1.prefab.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem1.prefab.meta @@ -1,13 +1,13 @@ -{ - "ver": "1.1.50", - "importer": "prefab", - "imported": true, - "uuid": "fed2e71c-22ac-4e48-a42a-68e48a1929a7", - "files": [ - ".json" - ], - "subMetas": {}, - "userData": { - "syncNodeName": "VipRoomItem1" - } -} +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "fed2e71c-22ac-4e48-a42a-68e48a1929a7", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "VipRoomItem1" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem2.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem2.prefab similarity index 94% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem2.prefab rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem2.prefab index 6f5c980..c512514 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem2.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem2.prefab @@ -1,567 +1,567 @@ -[ - { - "__type__": "cc.Prefab", - "_name": "VipRoomItem2", - "_objFlags": 0, - "__editorExtras__": {}, - "_native": "", - "data": { - "__id__": 1 - }, - "optimizationPolicy": 0, - "persistent": false - }, - { - "__type__": "cc.Node", - "_name": "VipRoomItem2", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": null, - "_children": [ - { - "__id__": 2 - }, - { - "__id__": 8 - }, - { - "__id__": 14 - } - ], - "_active": true, - "_components": [ - { - "__id__": 20 - } - ], - "_prefab": { - "__id__": 22 - }, - "_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": "f4Kcf+dBJGvJ3lSAd5F5Ih" - }, - { - "__type__": "cc.Node", - "_name": "Spirit3186", - "_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": "7cTfssnmBMgYt620U/5vxo" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": { - "__id__": 4 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 250, - "height": 131 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "c2trXiZ1ZAJ5Y5lZaOpb1D" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "+0Vs+WlahzQn4e0UW6eFxA" - }, - { - "__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__": "cc2a414b-903f-41c2-b404-7152cf6453ab@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 0, - "_fillType": 0, - "_sizeMode": 1, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "51HrP92lRB87ffasO841t7" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "UF4UyU5UPRCnxPRqDxg3wQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "dnrLWLQZIYne4vlY0Y5MPg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3187", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 9 - }, - { - "__id__": 11 - } - ], - "_prefab": { - "__id__": 13 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 40.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": "79EwPJZx1HcrSr338i0waK" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 8 - }, - "_enabled": true, - "__prefab": { - "__id__": 10 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 138, - "height": 72 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 1 - }, - "_id": "dfU9okyEBGiIKGGxGUur0W" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Pku3ZVIiexAUdGI8yxQqxw" - }, - { - "__type__": "cc.Label", - "_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 - }, - "_string": "1", - "_horizontalAlign": 1, - "_verticalAlign": 0, - "_actualFontSize": 72, - "_fontSize": 72, - "_fontFamily": "Arial", - "_lineHeight": 72, - "_overflow": 1, - "_enableWrapText": true, - "_font": { - "__uuid__": "e34631de-cae2-4a7c-bed5-86e5adfdac60", - "__expectedType__": "cc.BitmapFont" - }, - "_isSystemFontUsed": false, - "_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": "033cA/YzFBjLEZf1JousI1" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "bC9EsL/KqAN6eIq0Y3PyNw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "ueCrPSOTL5F3B19pL1F8yw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3188", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 15 - }, - { - "__id__": 17 - } - ], - "_prefab": { - "__id__": 19 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": -13, - "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": "426+r1b3pHgKg3UzZQjryY" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 14 - }, - "_enabled": true, - "__prefab": { - "__id__": 16 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 220, - "height": 36 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "d8eIRryr5M7auqLGLoqvXi" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "046TXwam4vgSyQuN47dIDA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 14 - }, - "_enabled": true, - "__prefab": { - "__id__": 18 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "33", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14.758333333333333, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 1, - "_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": "a8NtMTNqlHXoG1YKO5Drru" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "D4Btlqy18WccxSt5VcHKpw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "0s5KwwT6+bM6CB9Hk4AHSw", - "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": 250, - "height": 131 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "55aaqEW3VMAYDJpPIP1vWN" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "jn8ZOs/Ka9jM1CSFgLhHVQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "8uzcIlSV57ajlt9Ozltlhw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - } +[ + { + "__type__": "cc.Prefab", + "_name": "VipRoomItem2", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "persistent": false + }, + { + "__type__": "cc.Node", + "_name": "VipRoomItem2", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + }, + { + "__id__": 8 + }, + { + "__id__": 14 + } + ], + "_active": true, + "_components": [ + { + "__id__": 20 + } + ], + "_prefab": { + "__id__": 22 + }, + "_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": "f4Kcf+dBJGvJ3lSAd5F5Ih" + }, + { + "__type__": "cc.Node", + "_name": "Spirit3186", + "_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": "7cTfssnmBMgYt620U/5vxo" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 4 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 250, + "height": 131 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "c2trXiZ1ZAJ5Y5lZaOpb1D" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "+0Vs+WlahzQn4e0UW6eFxA" + }, + { + "__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__": "cc2a414b-903f-41c2-b404-7152cf6453ab@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "51HrP92lRB87ffasO841t7" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "UF4UyU5UPRCnxPRqDxg3wQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "dnrLWLQZIYne4vlY0Y5MPg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3187", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 9 + }, + { + "__id__": 11 + } + ], + "_prefab": { + "__id__": 13 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 40.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": "79EwPJZx1HcrSr338i0waK" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 8 + }, + "_enabled": true, + "__prefab": { + "__id__": 10 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 138, + "height": 72 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 1 + }, + "_id": "dfU9okyEBGiIKGGxGUur0W" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Pku3ZVIiexAUdGI8yxQqxw" + }, + { + "__type__": "cc.Label", + "_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 + }, + "_string": "1", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 72, + "_fontSize": 72, + "_fontFamily": "Arial", + "_lineHeight": 72, + "_overflow": 1, + "_enableWrapText": true, + "_font": { + "__uuid__": "e34631de-cae2-4a7c-bed5-86e5adfdac60", + "__expectedType__": "cc.BitmapFont" + }, + "_isSystemFontUsed": false, + "_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": "033cA/YzFBjLEZf1JousI1" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "bC9EsL/KqAN6eIq0Y3PyNw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ueCrPSOTL5F3B19pL1F8yw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3188", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 15 + }, + { + "__id__": 17 + } + ], + "_prefab": { + "__id__": 19 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": -13, + "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": "426+r1b3pHgKg3UzZQjryY" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 14 + }, + "_enabled": true, + "__prefab": { + "__id__": 16 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 220, + "height": 36 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "d8eIRryr5M7auqLGLoqvXi" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "046TXwam4vgSyQuN47dIDA" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 14 + }, + "_enabled": true, + "__prefab": { + "__id__": 18 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "33", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 14.758333333333333, + "_fontSize": 14, + "_fontFamily": "Arial", + "_lineHeight": 14, + "_overflow": 1, + "_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": "a8NtMTNqlHXoG1YKO5Drru" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "D4Btlqy18WccxSt5VcHKpw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "0s5KwwT6+bM6CB9Hk4AHSw", + "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": 250, + "height": 131 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "55aaqEW3VMAYDJpPIP1vWN" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "jn8ZOs/Ka9jM1CSFgLhHVQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "8uzcIlSV57ajlt9Ozltlhw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + } ] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem2.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem2.prefab.meta similarity index 94% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem2.prefab.meta rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem2.prefab.meta index 56a5415..848d60f 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/VipRoomItem2.prefab.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/items/VipRoomItem2.prefab.meta @@ -1,13 +1,13 @@ -{ - "ver": "1.1.50", - "importer": "prefab", - "imported": true, - "uuid": "8cb43df4-d8fa-4b3c-ab01-497b23c68435", - "files": [ - ".json" - ], - "subMetas": {}, - "userData": { - "syncNodeName": "VipRoomItem2" - } -} +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "8cb43df4-d8fa-4b3c-ab01-497b23c68435", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "VipRoomItem2" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab index 1a70063..569ed22 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab @@ -25,14 +25,17 @@ "_active": true, "_components": [ { - "__id__": 216 + "__id__": 250 }, { - "__id__": 218 + "__id__": 252 + }, + { + "__id__": 254 } ], "_prefab": { - "__id__": 220 + "__id__": 256 }, "_lpos": { "__type__": "cc.Vec3", @@ -61,7 +64,7 @@ "y": 0, "z": 0 }, - "_id": "c5sPn0cKlONrvCqS4Ei1rD" + "_id": "84t6JB/+hFUJnEr6zkgHmh" }, { "__type__": "cc.Node", @@ -76,67 +79,55 @@ "__id__": 3 }, { - "__id__": 11 + "__id__": 13 }, { - "__id__": 19 + "__id__": 23 }, { - "__id__": 27 + "__id__": 31 }, { - "__id__": 35 - }, - { - "__id__": 43 + "__id__": 41 }, { "__id__": 51 }, { - "__id__": 59 + "__id__": 61 }, { - "__id__": 67 + "__id__": 71 }, { - "__id__": 75 - }, - { - "__id__": 83 + "__id__": 81 }, { "__id__": 91 }, { - "__id__": 99 + "__id__": 101 }, { - "__id__": 107 + "__id__": 111 }, { - "__id__": 115 - }, - { - "__id__": 123 + "__id__": 121 }, { "__id__": 131 }, { - "__id__": 139 + "__id__": 141 }, { - "__id__": 147 + "__id__": 151 }, { - "__id__": 155 + "__id__": 161 }, { - "__id__": 163 - }, - { - "__id__": 171 + "__id__": 169 }, { "__id__": 179 @@ -149,19 +140,31 @@ }, { "__id__": 203 + }, + { + "__id__": 211 + }, + { + "__id__": 219 + }, + { + "__id__": 227 + }, + { + "__id__": 235 } ], "_active": true, "_components": [ { - "__id__": 211 + "__id__": 245 }, { - "__id__": 213 + "__id__": 247 } ], "_prefab": { - "__id__": 215 + "__id__": 249 }, "_lpos": { "__type__": "cc.Vec3", @@ -190,7 +193,7 @@ "y": 0, "z": 0 }, - "_id": "60khrewCVNUqkpGd/cKN7E" + "_id": "fe5NoAuOJCxoFC1u1vZiIh" }, { "__type__": "cc.Node", @@ -211,10 +214,13 @@ }, { "__id__": 8 + }, + { + "__id__": 10 } ], "_prefab": { - "__id__": 10 + "__id__": 12 }, "_lpos": { "__type__": "cc.Vec3", @@ -243,7 +249,7 @@ "y": 0, "z": 0 }, - "_id": "d0FETE0J9BZL7iFV2Zj/T7" + "_id": "7fxian4CVEmpfQeSxlVTco" }, { "__type__": "cc.UITransform", @@ -267,7 +273,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "86WirXRnlPObEPsXwO+n+R" + "_id": "8ffhgKWa9LLYu7SK0mLn3R" }, { "__type__": "cc.CompPrefabInfo", @@ -312,7 +318,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "ecda1LtgVDro7kjRgaDwTm" + "_id": "8cd3n48w5APJ1gxamBy789" }, { "__type__": "cc.CompPrefabInfo", @@ -348,12 +354,68 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "03N09GDx1IKYUeAbGUeBRq" + "_id": "88BH0A5UJPt7e6eoub64SO" }, { "__type__": "cc.CompPrefabInfo", "fileId": "g9hwBxAmeRc//rEDvm+NWQ" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 11 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "78cbziNB1N6oCaj358Lvyf" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "SF5n3uBv+UgYmhUphilctA" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -378,18 +440,21 @@ "_children": [], "_active": true, "_components": [ - { - "__id__": 12 - }, { "__id__": 14 }, { "__id__": 16 + }, + { + "__id__": 18 + }, + { + "__id__": 20 } ], "_prefab": { - "__id__": 18 + "__id__": 22 }, "_lpos": { "__type__": "cc.Vec3", @@ -418,7 +483,7 @@ "y": 0, "z": 0 }, - "_id": "a67zrXSApEL48jWWKjkzKk" + "_id": "f1Xzkxje9OkaOu8sXQerCN" }, { "__type__": "cc.UITransform", @@ -426,11 +491,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 13 }, "_enabled": true, "__prefab": { - "__id__": 13 + "__id__": 15 }, "_contentSize": { "__type__": "cc.Size", @@ -442,7 +507,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "bfv8UWS3tFoJWwM1dOtVNs" + "_id": "cdf0ElOHtCOp436sEC3AXc" }, { "__type__": "cc.CompPrefabInfo", @@ -454,11 +519,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 13 }, "_enabled": true, "__prefab": { - "__id__": 15 + "__id__": 17 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -487,7 +552,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "60UiSz3NpJEJ4GfJkfT5Ug" + "_id": "25HwU43qROlbIBRqswHqJm" }, { "__type__": "cc.CompPrefabInfo", @@ -499,11 +564,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 13 }, "_enabled": true, "__prefab": { - "__id__": 17 + "__id__": 19 }, "_alignFlags": 16, "_target": null, @@ -523,12 +588,30 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "20G3mU3tJMAKP+bbfi1xcX" + "_id": "5bc+dRvQVOHJzLK7U5iknH" }, { "__type__": "cc.CompPrefabInfo", "fileId": "2Pf9kQ7VfVIchVO4XxE5jQ" }, + { + "__type__": "cc.BlockInputEvents", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 13 + }, + "_enabled": true, + "__prefab": { + "__id__": 21 + }, + "_id": "0bjQr/aExBuI9f4nq7BTxI" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "u/4/5Ke40Hmp+D9lw7iAQQ" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -553,18 +636,18 @@ "_children": [], "_active": true, "_components": [ - { - "__id__": 20 - }, - { - "__id__": 22 - }, { "__id__": 24 + }, + { + "__id__": 26 + }, + { + "__id__": 28 } ], "_prefab": { - "__id__": 26 + "__id__": 30 }, "_lpos": { "__type__": "cc.Vec3", @@ -593,7 +676,7 @@ "y": 0, "z": 0 }, - "_id": "54lunSelZKF7tJa0f5dotk" + "_id": "94jD5EDFxH3I1jbRU0nyJ5" }, { "__type__": "cc.UITransform", @@ -601,11 +684,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 23 }, "_enabled": true, "__prefab": { - "__id__": 21 + "__id__": 25 }, "_contentSize": { "__type__": "cc.Size", @@ -617,7 +700,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "56Ue5yE3NHiZaZ7bvz67HZ" + "_id": "feYQJhOp1FyKFsDuRvVcbD" }, { "__type__": "cc.CompPrefabInfo", @@ -629,11 +712,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 23 }, "_enabled": true, "__prefab": { - "__id__": 23 + "__id__": 27 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -662,7 +745,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "7fmrvqatNJpbzVVlDLPSmb" + "_id": "37ZHY1o1dBmLU1BKViDp7B" }, { "__type__": "cc.CompPrefabInfo", @@ -674,11 +757,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 23 }, "_enabled": true, "__prefab": { - "__id__": 25 + "__id__": 29 }, "_alignFlags": 16, "_target": null, @@ -698,7 +781,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "3fPRftdbZAtq1LhDlQqBkw" + "_id": "01eeoRJlpIn5qGIYgbFwg2" }, { "__type__": "cc.CompPrefabInfo", @@ -728,18 +811,21 @@ "_children": [], "_active": true, "_components": [ - { - "__id__": 28 - }, - { - "__id__": 30 - }, { "__id__": 32 + }, + { + "__id__": 34 + }, + { + "__id__": 36 + }, + { + "__id__": 38 } ], "_prefab": { - "__id__": 34 + "__id__": 40 }, "_lpos": { "__type__": "cc.Vec3", @@ -768,7 +854,7 @@ "y": 0, "z": 0 }, - "_id": "d8bzU7P5xG+YSU7knBaLLq" + "_id": "fb3hKwlz9NAYvQxjaELJuV" }, { "__type__": "cc.UITransform", @@ -776,11 +862,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 29 + "__id__": 33 }, "_contentSize": { "__type__": "cc.Size", @@ -792,7 +878,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "9fcnODL7BMuqmoSfd9bQO8" + "_id": "4f/uA08aFIC7eHp0c+KlDQ" }, { "__type__": "cc.CompPrefabInfo", @@ -804,11 +890,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 31 + "__id__": 35 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -837,7 +923,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "13trVUGQBE4rRcPqsyAQvO" + "_id": "549mXncl9M7YJfu7OzzZDz" }, { "__type__": "cc.CompPrefabInfo", @@ -849,11 +935,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 37 }, "_alignFlags": 16, "_target": null, @@ -873,12 +959,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "c0yGW2XdJK0JR+m0tlGXmK" + "_id": "60eU7fFOVNRrP4vcgIbPTB" }, { "__type__": "cc.CompPrefabInfo", "fileId": "pKWIKx6OLzxqmd7mw++sqQ" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 31 + }, + "_enabled": true, + "__prefab": { + "__id__": 39 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "6aocM1gaZB2rUqjjtZKP8K" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "KgNRT+1ncEAbDalypUi4CA" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -904,17 +1046,20 @@ "_active": true, "_components": [ { - "__id__": 36 + "__id__": 42 }, { - "__id__": 38 + "__id__": 44 }, { - "__id__": 40 + "__id__": 46 + }, + { + "__id__": 48 } ], "_prefab": { - "__id__": 42 + "__id__": 50 }, "_lpos": { "__type__": "cc.Vec3", @@ -943,7 +1088,7 @@ "y": 0, "z": 0 }, - "_id": "cdVJBaxltBe44b/D6TtdbX" + "_id": "64g1aaFtJA3YXzNkJIpThN" }, { "__type__": "cc.UITransform", @@ -951,11 +1096,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 41 }, "_enabled": true, "__prefab": { - "__id__": 37 + "__id__": 43 }, "_contentSize": { "__type__": "cc.Size", @@ -967,7 +1112,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "0dW3OBSvND/aB7wnu1/qBZ" + "_id": "574N9iKDxBpL6e8PVA4QvU" }, { "__type__": "cc.CompPrefabInfo", @@ -979,11 +1124,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 41 }, "_enabled": true, "__prefab": { - "__id__": 39 + "__id__": 45 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1012,7 +1157,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "6dUe+MiDlFGZYvtY6+NIH3" + "_id": "4bNkZo6dZA0ogm6j62BN8g" }, { "__type__": "cc.CompPrefabInfo", @@ -1024,11 +1169,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 41 }, "_enabled": true, "__prefab": { - "__id__": 41 + "__id__": 47 }, "_alignFlags": 16, "_target": null, @@ -1048,12 +1193,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "7fYQCZhTNC2bgzwP0LLx0i" + "_id": "04Ln9c82ZC7qONrCL7LU3F" }, { "__type__": "cc.CompPrefabInfo", "fileId": "1T3I5SQvmJIXM49/aKcydw" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 41 + }, + "_enabled": true, + "__prefab": { + "__id__": 49 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "3azvgkgLpAOrhbMUFL0mZq" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ExIQ6edgXNWFm7TOS6OXKA" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1079,17 +1280,20 @@ "_active": true, "_components": [ { - "__id__": 44 + "__id__": 52 }, { - "__id__": 46 + "__id__": 54 }, { - "__id__": 48 + "__id__": 56 + }, + { + "__id__": 58 } ], "_prefab": { - "__id__": 50 + "__id__": 60 }, "_lpos": { "__type__": "cc.Vec3", @@ -1118,7 +1322,7 @@ "y": 0, "z": 0 }, - "_id": "1b8Gsr++NMwrPVkMsJxFDU" + "_id": "darSVawKJNkoXJqmTLBoZS" }, { "__type__": "cc.UITransform", @@ -1126,11 +1330,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 45 + "__id__": 53 }, "_contentSize": { "__type__": "cc.Size", @@ -1142,7 +1346,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "71is5VRwxB7rhXfd0SJoIA" + "_id": "76v/DTEqlNRL6vHysrKc1+" }, { "__type__": "cc.CompPrefabInfo", @@ -1154,11 +1358,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 47 + "__id__": 55 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1187,7 +1391,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "72+UOvLFNHtJVq/6BWbIiC" + "_id": "42athuSm1JyJq40Copqdsd" }, { "__type__": "cc.CompPrefabInfo", @@ -1199,11 +1403,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 49 + "__id__": 57 }, "_alignFlags": 16, "_target": null, @@ -1223,12 +1427,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "18R/GM4llIZ5PY9ATfK0mL" + "_id": "015tqZgf1NzqV6MTqKdhsH" }, { "__type__": "cc.CompPrefabInfo", "fileId": "bJrbZcBnJxqwGIuKMi7Y2w" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 51 + }, + "_enabled": true, + "__prefab": { + "__id__": 59 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "4dridaOQFEzJX5uQVJZLH0" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Eo5hBeQFNnWc9nFWI4Qz1A" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1254,17 +1514,20 @@ "_active": true, "_components": [ { - "__id__": 52 + "__id__": 62 }, { - "__id__": 54 + "__id__": 64 }, { - "__id__": 56 + "__id__": 66 + }, + { + "__id__": 68 } ], "_prefab": { - "__id__": 58 + "__id__": 70 }, "_lpos": { "__type__": "cc.Vec3", @@ -1293,7 +1556,7 @@ "y": 0, "z": 0 }, - "_id": "f0PGtHnuVNWa0/XpbX7stS" + "_id": "a1lbp0WMFN0q29jSudp8uo" }, { "__type__": "cc.UITransform", @@ -1301,11 +1564,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 61 }, "_enabled": true, "__prefab": { - "__id__": 53 + "__id__": 63 }, "_contentSize": { "__type__": "cc.Size", @@ -1317,7 +1580,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "94ST3iNjdCiYGDgxg+H35R" + "_id": "ffT7anvc9Dm7EijUkyUbl9" }, { "__type__": "cc.CompPrefabInfo", @@ -1329,11 +1592,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 61 }, "_enabled": true, "__prefab": { - "__id__": 55 + "__id__": 65 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1362,7 +1625,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "fe7KcBT8VChIYVB/SFGOhc" + "_id": "77KjuuxfpNM5xRH9tAcB/Z" }, { "__type__": "cc.CompPrefabInfo", @@ -1374,11 +1637,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 61 }, "_enabled": true, "__prefab": { - "__id__": 57 + "__id__": 67 }, "_alignFlags": 16, "_target": null, @@ -1398,12 +1661,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "bf9akSF8RMz7sk4s8YU3iN" + "_id": "34Xj/Q1qdH/orhj55RKBAR" }, { "__type__": "cc.CompPrefabInfo", "fileId": "KTjRkG9B+eMC0JF8l2pZNA" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 61 + }, + "_enabled": true, + "__prefab": { + "__id__": 69 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "681EM972dIdKIJr/s2J+TT" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "strPoc6Z7zAGe4z79V5vTQ" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1429,17 +1748,20 @@ "_active": true, "_components": [ { - "__id__": 60 + "__id__": 72 }, { - "__id__": 62 + "__id__": 74 }, { - "__id__": 64 + "__id__": 76 + }, + { + "__id__": 78 } ], "_prefab": { - "__id__": 66 + "__id__": 80 }, "_lpos": { "__type__": "cc.Vec3", @@ -1468,7 +1790,7 @@ "y": 0, "z": 0 }, - "_id": "58CDHiBgNEWpjBI1pSzGlp" + "_id": "f1qiuVhgtFTIG6ML/75pa/" }, { "__type__": "cc.UITransform", @@ -1476,11 +1798,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 71 }, "_enabled": true, "__prefab": { - "__id__": 61 + "__id__": 73 }, "_contentSize": { "__type__": "cc.Size", @@ -1492,7 +1814,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "c82RAAYyFGtLc3i03+doTf" + "_id": "dbu8DpSpFHspgd2rqZFtOD" }, { "__type__": "cc.CompPrefabInfo", @@ -1504,11 +1826,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 71 }, "_enabled": true, "__prefab": { - "__id__": 63 + "__id__": 75 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1537,7 +1859,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "37+X3TmxdIb5nfh+3JGgBz" + "_id": "6ddLyFQAZFSI0+utVGjJ/H" }, { "__type__": "cc.CompPrefabInfo", @@ -1549,11 +1871,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 71 }, "_enabled": true, "__prefab": { - "__id__": 65 + "__id__": 77 }, "_alignFlags": 16, "_target": null, @@ -1573,12 +1895,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "1eskPC98xGPqutjjEhpX+e" + "_id": "dcHp2NBmdCmajFqB9jgvfa" }, { "__type__": "cc.CompPrefabInfo", "fileId": "kMu+Zjd0UUFQ0CfCQOysoA" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 71 + }, + "_enabled": true, + "__prefab": { + "__id__": 79 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "16Ks97GRxIGqgzjZ5TBkbP" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "fa1fPZ/sxTbTzE2z0gIV8g" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1604,17 +1982,20 @@ "_active": true, "_components": [ { - "__id__": 68 + "__id__": 82 }, { - "__id__": 70 + "__id__": 84 }, { - "__id__": 72 + "__id__": 86 + }, + { + "__id__": 88 } ], "_prefab": { - "__id__": 74 + "__id__": 90 }, "_lpos": { "__type__": "cc.Vec3", @@ -1643,7 +2024,7 @@ "y": 0, "z": 0 }, - "_id": "09BJN0J0ZGfLzQP09kb/Pp" + "_id": "68b+l9C6hGDZc8xrv2UNhj" }, { "__type__": "cc.UITransform", @@ -1651,11 +2032,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 81 }, "_enabled": true, "__prefab": { - "__id__": 69 + "__id__": 83 }, "_contentSize": { "__type__": "cc.Size", @@ -1667,7 +2048,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "a1WR65YCNPo7NaDeaJHRIk" + "_id": "34VapnKmBJLZOpIxj0bz+D" }, { "__type__": "cc.CompPrefabInfo", @@ -1679,11 +2060,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 81 }, "_enabled": true, "__prefab": { - "__id__": 71 + "__id__": 85 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1712,7 +2093,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "034o7kdyhJmp85Y1gyFyvF" + "_id": "a6UiHc1JlBo70WMKQMbmT5" }, { "__type__": "cc.CompPrefabInfo", @@ -1724,11 +2105,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 81 }, "_enabled": true, "__prefab": { - "__id__": 73 + "__id__": 87 }, "_alignFlags": 16, "_target": null, @@ -1748,12 +2129,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "06whyARsJIxZSuU6DOELP3" + "_id": "51Ok2WucBLXaXszHsly+6s" }, { "__type__": "cc.CompPrefabInfo", "fileId": "gcUSN/Za9H5KnvwgpKGbww" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 81 + }, + "_enabled": true, + "__prefab": { + "__id__": 89 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "28DLwV0dhNU7lr6FQOabOg" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "CI01KTd0anDXh8jTu7W/hw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1779,17 +2216,20 @@ "_active": true, "_components": [ { - "__id__": 76 + "__id__": 92 }, { - "__id__": 78 + "__id__": 94 }, { - "__id__": 80 + "__id__": 96 + }, + { + "__id__": 98 } ], "_prefab": { - "__id__": 82 + "__id__": 100 }, "_lpos": { "__type__": "cc.Vec3", @@ -1818,7 +2258,7 @@ "y": 0, "z": 0 }, - "_id": "6aMO/Dw5VOxL9DiNHfO0fm" + "_id": "62Nftik+pNkIrkbOfQPNaA" }, { "__type__": "cc.UITransform", @@ -1826,11 +2266,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 91 }, "_enabled": true, "__prefab": { - "__id__": 77 + "__id__": 93 }, "_contentSize": { "__type__": "cc.Size", @@ -1842,7 +2282,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "475vfyT9ZFpqeu1S69xw9k" + "_id": "05nKpoxWpKRICzvqEGmZAg" }, { "__type__": "cc.CompPrefabInfo", @@ -1854,11 +2294,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 91 }, "_enabled": true, "__prefab": { - "__id__": 79 + "__id__": 95 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1887,7 +2327,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "54NtNAPd9G8IZwkKViVWh9" + "_id": "d83d6Q2B5Ab5mmQ48BSFcm" }, { "__type__": "cc.CompPrefabInfo", @@ -1899,11 +2339,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 91 }, "_enabled": true, "__prefab": { - "__id__": 81 + "__id__": 97 }, "_alignFlags": 16, "_target": null, @@ -1923,12 +2363,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "90qMdJZWZM+KpSZ9EG75ID" + "_id": "f7HKp3zWtJuahfORpsKuz6" }, { "__type__": "cc.CompPrefabInfo", "fileId": "WMWiKWRLPusX1+QVOckG0w" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 91 + }, + "_enabled": true, + "__prefab": { + "__id__": 99 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "ce2HAnIu9Afb6//ALkck4w" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "eSRQi8ouVZtrw76l5/F8JQ" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1954,17 +2450,20 @@ "_active": true, "_components": [ { - "__id__": 84 + "__id__": 102 }, { - "__id__": 86 + "__id__": 104 }, { - "__id__": 88 + "__id__": 106 + }, + { + "__id__": 108 } ], "_prefab": { - "__id__": 90 + "__id__": 110 }, "_lpos": { "__type__": "cc.Vec3", @@ -1993,7 +2492,7 @@ "y": 0, "z": 0 }, - "_id": "92065r7+9Fs7dwWlPi2cTN" + "_id": "f1HSKS7CdLNJ+tscBrGA69" }, { "__type__": "cc.UITransform", @@ -2001,11 +2500,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 101 }, "_enabled": true, "__prefab": { - "__id__": 85 + "__id__": 103 }, "_contentSize": { "__type__": "cc.Size", @@ -2017,7 +2516,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "0am9hW9TxJG5Se32ZWA54s" + "_id": "a9bmMdjw5J956Ezk2f36UB" }, { "__type__": "cc.CompPrefabInfo", @@ -2029,11 +2528,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 101 }, "_enabled": true, "__prefab": { - "__id__": 87 + "__id__": 105 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2062,7 +2561,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "213MPY8hJAtb9MtH3Og8d7" + "_id": "e6aDeSSHVL16QUVGmPJg4o" }, { "__type__": "cc.CompPrefabInfo", @@ -2074,11 +2573,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 101 }, "_enabled": true, "__prefab": { - "__id__": 89 + "__id__": 107 }, "_alignFlags": 16, "_target": null, @@ -2098,12 +2597,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "5cUzNlT51KILcdO36KA/qb" + "_id": "5207amRtBK+bBBEOPN8bc7" }, { "__type__": "cc.CompPrefabInfo", "fileId": "hBw4HtDi1cFs7X7laC7PsA" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 101 + }, + "_enabled": true, + "__prefab": { + "__id__": 109 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "37EruLD2xL6bIWlUvZ7OkP" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "lqisW7bjkGcrJmGG8ToK3g" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -2129,17 +2684,20 @@ "_active": true, "_components": [ { - "__id__": 92 + "__id__": 112 }, { - "__id__": 94 + "__id__": 114 }, { - "__id__": 96 + "__id__": 116 + }, + { + "__id__": 118 } ], "_prefab": { - "__id__": 98 + "__id__": 120 }, "_lpos": { "__type__": "cc.Vec3", @@ -2168,7 +2726,7 @@ "y": 0, "z": 0 }, - "_id": "3cyWXtOKBBlI11mO1xy0hO" + "_id": "bbd5rbV4hKWK2Mt7we38xC" }, { "__type__": "cc.UITransform", @@ -2176,11 +2734,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 111 }, "_enabled": true, "__prefab": { - "__id__": 93 + "__id__": 113 }, "_contentSize": { "__type__": "cc.Size", @@ -2192,7 +2750,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "3bzfVRtkRLjZUm2EEkW8fD" + "_id": "28zj02cBNM9Yz+FKEndUmF" }, { "__type__": "cc.CompPrefabInfo", @@ -2204,11 +2762,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 111 }, "_enabled": true, "__prefab": { - "__id__": 95 + "__id__": 115 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2237,7 +2795,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "6dvs2FDuJK2ZIDotFNenZF" + "_id": "eevQGqhJxAgKB7Cm4Ttf7y" }, { "__type__": "cc.CompPrefabInfo", @@ -2249,11 +2807,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 111 }, "_enabled": true, "__prefab": { - "__id__": 97 + "__id__": 117 }, "_alignFlags": 16, "_target": null, @@ -2273,12 +2831,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "83HhHtYfBLd4el1w7ZwDI8" + "_id": "a2ATYs3kpIGYBkTn7qObFo" }, { "__type__": "cc.CompPrefabInfo", "fileId": "ofcq2XEn/g2kRmZSAgh/+w" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 111 + }, + "_enabled": true, + "__prefab": { + "__id__": 119 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "daqkgOzHtE9qkRscUE/4lF" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "UaAPPHnsEVzM8QdtH4hZKw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -2304,17 +2918,20 @@ "_active": true, "_components": [ { - "__id__": 100 + "__id__": 122 }, { - "__id__": 102 + "__id__": 124 }, { - "__id__": 104 + "__id__": 126 + }, + { + "__id__": 128 } ], "_prefab": { - "__id__": 106 + "__id__": 130 }, "_lpos": { "__type__": "cc.Vec3", @@ -2343,7 +2960,7 @@ "y": 0, "z": 0 }, - "_id": "55M6gspelOEbj8uwcn8zBU" + "_id": "34YlzNB0tKFYVch8WNNz/1" }, { "__type__": "cc.UITransform", @@ -2351,11 +2968,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 121 }, "_enabled": true, "__prefab": { - "__id__": 101 + "__id__": 123 }, "_contentSize": { "__type__": "cc.Size", @@ -2367,7 +2984,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "7fK7tjOQtHvqfz4kNj9T2g" + "_id": "66EK0GADBE/qOYKWiG34zP" }, { "__type__": "cc.CompPrefabInfo", @@ -2379,11 +2996,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 121 }, "_enabled": true, "__prefab": { - "__id__": 103 + "__id__": 125 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2412,7 +3029,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "01Kwki3XdKILct7mKslB57" + "_id": "b1K93U+q5Lfag65E56QupC" }, { "__type__": "cc.CompPrefabInfo", @@ -2424,11 +3041,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 121 }, "_enabled": true, "__prefab": { - "__id__": 105 + "__id__": 127 }, "_alignFlags": 16, "_target": null, @@ -2448,12 +3065,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "fdo6yfLlRB9Ye7s214QR+Z" + "_id": "74+TDYhlZNFK+iiqU81NY7" }, { "__type__": "cc.CompPrefabInfo", "fileId": "VboA5HcOsKJLn5W6pMHX7Q" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 121 + }, + "_enabled": true, + "__prefab": { + "__id__": 129 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "d93tc0ss9ESIosA9kYUMUn" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "moXY+8nCXTsnA4kxCNjskw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -2479,17 +3152,20 @@ "_active": true, "_components": [ { - "__id__": 108 + "__id__": 132 }, { - "__id__": 110 + "__id__": 134 }, { - "__id__": 112 + "__id__": 136 + }, + { + "__id__": 138 } ], "_prefab": { - "__id__": 114 + "__id__": 140 }, "_lpos": { "__type__": "cc.Vec3", @@ -2518,7 +3194,7 @@ "y": 0, "z": 0 }, - "_id": "d699Kd5QhI8IYAF6wLj31V" + "_id": "16/IURpvZIO7RENkpGrGUz" }, { "__type__": "cc.UITransform", @@ -2526,11 +3202,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 131 }, "_enabled": true, "__prefab": { - "__id__": 109 + "__id__": 133 }, "_contentSize": { "__type__": "cc.Size", @@ -2542,7 +3218,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "ea+Rr1pE1Pl44JUkLuE5Oi" + "_id": "6dx1QH3i1CkbyO82QD9gLY" }, { "__type__": "cc.CompPrefabInfo", @@ -2554,11 +3230,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 131 }, "_enabled": true, "__prefab": { - "__id__": 111 + "__id__": 135 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2587,7 +3263,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "e1vSyKvutLpZIyHOIL4mjN" + "_id": "c211MkS9dElrAn99LggG9z" }, { "__type__": "cc.CompPrefabInfo", @@ -2599,11 +3275,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 131 }, "_enabled": true, "__prefab": { - "__id__": 113 + "__id__": 137 }, "_alignFlags": 16, "_target": null, @@ -2623,12 +3299,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "eaojLNjLxNr4gz4Z7a4nlW" + "_id": "1e5auOi25FVI7g8b07bBfz" }, { "__type__": "cc.CompPrefabInfo", "fileId": "kzUFsHSeMZ7qMtUzUIL/2g" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 131 + }, + "_enabled": true, + "__prefab": { + "__id__": 139 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "08B0RzhChEB55yOslKU8mq" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ynyloaKKdXX4LNjg3NR8JA" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -2654,17 +3386,20 @@ "_active": true, "_components": [ { - "__id__": 116 + "__id__": 142 }, { - "__id__": 118 + "__id__": 144 }, { - "__id__": 120 + "__id__": 146 + }, + { + "__id__": 148 } ], "_prefab": { - "__id__": 122 + "__id__": 150 }, "_lpos": { "__type__": "cc.Vec3", @@ -2693,7 +3428,7 @@ "y": 0, "z": 0 }, - "_id": "82USz9gV1OG6JtW2QP0L46" + "_id": "93WwKvTstI6aavv3CUQKrf" }, { "__type__": "cc.UITransform", @@ -2701,11 +3436,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 141 }, "_enabled": true, "__prefab": { - "__id__": 117 + "__id__": 143 }, "_contentSize": { "__type__": "cc.Size", @@ -2717,7 +3452,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "705vBZatlIcLxWQjW2Ucc2" + "_id": "cbsfJ02U5JdZpfTtEMaNcF" }, { "__type__": "cc.CompPrefabInfo", @@ -2729,11 +3464,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 141 }, "_enabled": true, "__prefab": { - "__id__": 119 + "__id__": 145 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2762,7 +3497,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "bduMOnLxxJp7kMzpu2QF3B" + "_id": "ebDhxxSS9PQ77MzERIJqTU" }, { "__type__": "cc.CompPrefabInfo", @@ -2774,11 +3509,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 141 }, "_enabled": true, "__prefab": { - "__id__": 121 + "__id__": 147 }, "_alignFlags": 16, "_target": null, @@ -2798,12 +3533,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "9aTDCY/7NMGbbDWf3dS6X/" + "_id": "3dUgoDV7ZKSrKuXohf3Max" }, { "__type__": "cc.CompPrefabInfo", "fileId": "/MLWE0PZcSuPm9pkF7b1EA" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 141 + }, + "_enabled": true, + "__prefab": { + "__id__": 149 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "f06+nKPeVGupIAaw0uBlIg" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "lnJlTBDgbUc53VFEYC93rg" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -2829,17 +3620,20 @@ "_active": true, "_components": [ { - "__id__": 124 + "__id__": 152 }, { - "__id__": 126 + "__id__": 154 }, { - "__id__": 128 + "__id__": 156 + }, + { + "__id__": 158 } ], "_prefab": { - "__id__": 130 + "__id__": 160 }, "_lpos": { "__type__": "cc.Vec3", @@ -2868,7 +3662,7 @@ "y": 0, "z": 0 }, - "_id": "48rkbSRWNGdLQzox1C4qXv" + "_id": "5cC7vueQ9ISa/RDsLEfeTL" }, { "__type__": "cc.UITransform", @@ -2876,11 +3670,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 151 }, "_enabled": true, "__prefab": { - "__id__": 125 + "__id__": 153 }, "_contentSize": { "__type__": "cc.Size", @@ -2892,7 +3686,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "25XUu91M9CkKAn55XdwwbC" + "_id": "e4+T82cflEcKqu8JMY7bvp" }, { "__type__": "cc.CompPrefabInfo", @@ -2904,11 +3698,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 151 }, "_enabled": true, "__prefab": { - "__id__": 127 + "__id__": 155 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2937,7 +3731,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "0fBd3d5o1Pdr9coCwZ20pi" + "_id": "10ttcRJ/RCBrQ6QWA7DTVK" }, { "__type__": "cc.CompPrefabInfo", @@ -2949,11 +3743,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 151 }, "_enabled": true, "__prefab": { - "__id__": 129 + "__id__": 157 }, "_alignFlags": 16, "_target": null, @@ -2973,12 +3767,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "c742KL/UtGmYWrmR21/P8i" + "_id": "5cJTp8LdxPsYEpY82Y/t09" }, { "__type__": "cc.CompPrefabInfo", "fileId": "Gh6s3WrZQD5aei1I+HJF/g" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 151 + }, + "_enabled": true, + "__prefab": { + "__id__": 159 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "d532XAsNBP57EJ35wHcKel" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "jYqbyl1dels1/PDg9g/AGA" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -3004,17 +3854,17 @@ "_active": true, "_components": [ { - "__id__": 132 + "__id__": 162 }, { - "__id__": 134 + "__id__": 164 }, { - "__id__": 136 + "__id__": 166 } ], "_prefab": { - "__id__": 138 + "__id__": 168 }, "_lpos": { "__type__": "cc.Vec3", @@ -3043,7 +3893,7 @@ "y": 0, "z": 0 }, - "_id": "10ci9txhdIwKaRbASzDa5m" + "_id": "9bpbXikodF2I2OArFtyz/4" }, { "__type__": "cc.UITransform", @@ -3051,11 +3901,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 161 }, "_enabled": true, "__prefab": { - "__id__": 133 + "__id__": 163 }, "_contentSize": { "__type__": "cc.Size", @@ -3067,7 +3917,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "80AwtP5rJMMK6/58jUaTBr" + "_id": "3aTdruJ05Dp521psRJiETk" }, { "__type__": "cc.CompPrefabInfo", @@ -3079,11 +3929,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 161 }, "_enabled": true, "__prefab": { - "__id__": 135 + "__id__": 165 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3112,7 +3962,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "4cGssXY89KqLVjEN7cDMM5" + "_id": "32Ez9Q06dO24SfsCSwZBjC" }, { "__type__": "cc.CompPrefabInfo", @@ -3124,11 +3974,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 161 }, "_enabled": true, "__prefab": { - "__id__": 137 + "__id__": 167 }, "_alignFlags": 16, "_target": null, @@ -3148,7 +3998,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "84PA7IkzJEd4UWBhTa+zvf" + "_id": "e0aHPbuxpPpbtY62XVbN+l" }, { "__type__": "cc.CompPrefabInfo", @@ -3179,17 +4029,20 @@ "_active": true, "_components": [ { - "__id__": 140 + "__id__": 170 }, { - "__id__": 142 + "__id__": 172 }, { - "__id__": 144 + "__id__": 174 + }, + { + "__id__": 176 } ], "_prefab": { - "__id__": 146 + "__id__": 178 }, "_lpos": { "__type__": "cc.Vec3", @@ -3218,7 +4071,7 @@ "y": 0, "z": 0 }, - "_id": "1303552SpJ7KhZgxMJp2zd" + "_id": "bdojG3CQ9K3oX2roNBFj4V" }, { "__type__": "cc.UITransform", @@ -3226,11 +4079,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 169 }, "_enabled": true, "__prefab": { - "__id__": 141 + "__id__": 171 }, "_contentSize": { "__type__": "cc.Size", @@ -3242,7 +4095,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "52FPzYe0NLB768Cg2NbKzH" + "_id": "23h7YuN45AqqzYEj4NgoPF" }, { "__type__": "cc.CompPrefabInfo", @@ -3254,11 +4107,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 169 }, "_enabled": true, "__prefab": { - "__id__": 143 + "__id__": 173 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3287,7 +4140,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "9fzDkd3TpEJ4hKrtstBMST" + "_id": "1eiuI9eqJIXJUpsuT/+3Va" }, { "__type__": "cc.CompPrefabInfo", @@ -3299,11 +4152,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 169 }, "_enabled": true, "__prefab": { - "__id__": 145 + "__id__": 175 }, "_alignFlags": 16, "_target": null, @@ -3323,12 +4176,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "38+kWhLEpEgLduZjmRIP1t" + "_id": "75qsucu81Pr4zFaZ/bGiYq" }, { "__type__": "cc.CompPrefabInfo", "fileId": "UJSDVLt3CCAccRcO1mmpSg" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 169 + }, + "_enabled": true, + "__prefab": { + "__id__": 177 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "4euqsFvFBO26HXNj5RXpgf" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "bOA+rXD0ujZHE5XfSB8iPg" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -3351,20 +4260,20 @@ "__id__": 2 }, "_children": [], - "_active": true, + "_active": false, "_components": [ { - "__id__": 148 + "__id__": 180 }, { - "__id__": 150 + "__id__": 182 }, { - "__id__": 152 + "__id__": 184 } ], "_prefab": { - "__id__": 154 + "__id__": 186 }, "_lpos": { "__type__": "cc.Vec3", @@ -3393,7 +4302,7 @@ "y": 0, "z": 0 }, - "_id": "46J5E1LodPypQLSqgSP64P" + "_id": "12BQGEKDtMyokXXO7qvud2" }, { "__type__": "cc.UITransform", @@ -3401,84 +4310,39 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 147 + "__id__": 179 }, "_enabled": true, "__prefab": { - "__id__": 149 + "__id__": 181 }, "_contentSize": { "__type__": "cc.Size", - "width": 30, - "height": 30 + "width": 0, + "height": 40 }, "_anchorPoint": { "__type__": "cc.Vec2", "x": 0.5, "y": 0.5 }, - "_id": "53KmvKAg5CnIOABmc4IwGu" + "_id": "5ccvO4OaNF9Yt96wtu659a" }, { "__type__": "cc.CompPrefabInfo", "fileId": "x8IA9UZM4Xvj6Qvnb1j+nA" }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 147 - }, - "_enabled": true, - "__prefab": { - "__id__": 151 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "f3f3eadc-4916-4192-af76-8a3a0e2d01d7@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": "9dat2qOuxIT69kifDL432G" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "wKGrGhO0q0HqF+BTtezu0A" - }, { "__type__": "cc.Widget", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 147 + "__id__": 179 }, "_enabled": true, "__prefab": { - "__id__": 153 + "__id__": 183 }, "_alignFlags": 16, "_target": null, @@ -3498,12 +4362,83 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "96mrbSl6FMcKNVNRN9WvHp" + "_id": "f7ZZB3m5VFdqF8U3S037VV" }, { "__type__": "cc.CompPrefabInfo", "fileId": "00gde9i44+WD9S6x4dpsww" }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 179 + }, + "_enabled": true, + "__prefab": { + "__id__": 185 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 40, + "_fontSize": 72, + "_fontFamily": "Arial", + "_lineHeight": 32, + "_overflow": 0, + "_enableWrapText": true, + "_font": { + "__uuid__": "46c72f1b-4348-46ce-8e86-c0a8a6f8385a", + "__expectedType__": "cc.BitmapFont" + }, + "_isSystemFontUsed": false, + "_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": "f2r6I0hftJN60UidWcxnjt" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ok892kHh7RAWIXv23k9w5g" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -3526,20 +4461,20 @@ "__id__": 2 }, "_children": [], - "_active": true, + "_active": false, "_components": [ { - "__id__": 156 + "__id__": 188 }, { - "__id__": 158 + "__id__": 190 }, { - "__id__": 160 + "__id__": 192 } ], "_prefab": { - "__id__": 162 + "__id__": 194 }, "_lpos": { "__type__": "cc.Vec3", @@ -3568,7 +4503,7 @@ "y": 0, "z": 0 }, - "_id": "1dVCdq53ZFqbiw4jOgSiEO" + "_id": "efDRJ0IkRJIqsusWyZL2eO" }, { "__type__": "cc.UITransform", @@ -3576,84 +4511,39 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 155 + "__id__": 187 }, "_enabled": true, "__prefab": { - "__id__": 157 + "__id__": 189 }, "_contentSize": { "__type__": "cc.Size", - "width": 30, - "height": 30 + "width": 0, + "height": 40 }, "_anchorPoint": { "__type__": "cc.Vec2", "x": 0.5, "y": 0.5 }, - "_id": "4bLR3Yv0FJDI3hErT5KfH2" + "_id": "c91Q5EfKFNaLUKxVeyk75t" }, { "__type__": "cc.CompPrefabInfo", "fileId": "mSIb1Pszfo6uTk6QnMy/nA" }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 155 - }, - "_enabled": true, - "__prefab": { - "__id__": 159 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "f3f3eadc-4916-4192-af76-8a3a0e2d01d7@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": "49UZK/AL5Br6nCjR8YzkN5" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "wo4yYJYTVPZraN1Foo6btA" - }, { "__type__": "cc.Widget", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 155 + "__id__": 187 }, "_enabled": true, "__prefab": { - "__id__": 161 + "__id__": 191 }, "_alignFlags": 16, "_target": null, @@ -3673,12 +4563,83 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "ffbQYAIrhOHaNjzX+dkQgC" + "_id": "79CFIl3W5C8rD8qfc5HNP3" }, { "__type__": "cc.CompPrefabInfo", "fileId": "hls20foxaGaxM3dQEU8V4Q" }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 187 + }, + "_enabled": true, + "__prefab": { + "__id__": 193 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 40, + "_fontSize": 72, + "_fontFamily": "Arial", + "_lineHeight": 32, + "_overflow": 0, + "_enableWrapText": true, + "_font": { + "__uuid__": "46c72f1b-4348-46ce-8e86-c0a8a6f8385a", + "__expectedType__": "cc.BitmapFont" + }, + "_isSystemFontUsed": false, + "_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": "aahg+bYvdLt47SxaJn5vzB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "OM/IntTeYf7RnQ2V6bxlqw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -3701,20 +4662,20 @@ "__id__": 2 }, "_children": [], - "_active": true, + "_active": false, "_components": [ { - "__id__": 164 + "__id__": 196 }, { - "__id__": 166 + "__id__": 198 }, { - "__id__": 168 + "__id__": 200 } ], "_prefab": { - "__id__": 170 + "__id__": 202 }, "_lpos": { "__type__": "cc.Vec3", @@ -3743,7 +4704,7 @@ "y": 0, "z": 0 }, - "_id": "14WkZPqOpJIIvx6UQ5Q1J9" + "_id": "40W+/Og6ZPi6wWNwTQMwfD" }, { "__type__": "cc.UITransform", @@ -3751,84 +4712,39 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 163 + "__id__": 195 }, "_enabled": true, "__prefab": { - "__id__": 165 + "__id__": 197 }, "_contentSize": { "__type__": "cc.Size", - "width": 30, - "height": 30 + "width": 0, + "height": 40 }, "_anchorPoint": { "__type__": "cc.Vec2", "x": 0.5, "y": 0.5 }, - "_id": "7f99cyAXlEKoNJbOVe2OLQ" + "_id": "740fuUUYZKXbV6yKJz6fSx" }, { "__type__": "cc.CompPrefabInfo", "fileId": "2kPj7P+CxuBbXc90sSsFkg" }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 163 - }, - "_enabled": true, - "__prefab": { - "__id__": 167 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "f3f3eadc-4916-4192-af76-8a3a0e2d01d7@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": "123V8/RNNFdY0wgBjrl4ty" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "qWeXL8LsHwS/3LCySriamw" - }, { "__type__": "cc.Widget", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 163 + "__id__": 195 }, "_enabled": true, "__prefab": { - "__id__": 169 + "__id__": 199 }, "_alignFlags": 16, "_target": null, @@ -3848,12 +4764,83 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "6ehnbhQvVERLZdf1yA4+cl" + "_id": "dcFFlo0TNFhb0IOtbj6kYQ" }, { "__type__": "cc.CompPrefabInfo", "fileId": "fYx3DJuZ/j6HlPrCAY3uMA" }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 195 + }, + "_enabled": true, + "__prefab": { + "__id__": 201 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 40, + "_fontSize": 72, + "_fontFamily": "Arial", + "_lineHeight": 32, + "_overflow": 0, + "_enableWrapText": true, + "_font": { + "__uuid__": "46c72f1b-4348-46ce-8e86-c0a8a6f8385a", + "__expectedType__": "cc.BitmapFont" + }, + "_isSystemFontUsed": false, + "_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": "7czkeN6qFNvIFV4kFYKJc8" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "8HNbdhZA9vb+B1hXydj6Ew" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -3876,20 +4863,20 @@ "__id__": 2 }, "_children": [], - "_active": true, + "_active": false, "_components": [ { - "__id__": 172 + "__id__": 204 }, { - "__id__": 174 + "__id__": 206 }, { - "__id__": 176 + "__id__": 208 } ], "_prefab": { - "__id__": 178 + "__id__": 210 }, "_lpos": { "__type__": "cc.Vec3", @@ -3918,7 +4905,7 @@ "y": 0, "z": 0 }, - "_id": "e1W8NVIotBMqMsp7MjBAt/" + "_id": "cd3foj4OlAIJpSKfMPMUCU" }, { "__type__": "cc.UITransform", @@ -3926,84 +4913,39 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 171 + "__id__": 203 }, "_enabled": true, "__prefab": { - "__id__": 173 + "__id__": 205 }, "_contentSize": { "__type__": "cc.Size", - "width": 30, - "height": 30 + "width": 0, + "height": 40 }, "_anchorPoint": { "__type__": "cc.Vec2", "x": 0.5, "y": 0.5 }, - "_id": "36Hll4petARYs/uEMkjZ6n" + "_id": "f0zYNAbpJAbpyHAongVVxY" }, { "__type__": "cc.CompPrefabInfo", "fileId": "GmCeKngbSEgZVuuzMQIqzQ" }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 171 - }, - "_enabled": true, - "__prefab": { - "__id__": 175 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "f3f3eadc-4916-4192-af76-8a3a0e2d01d7@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": "08LdNNBrBOsbqWiOSig9ZN" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "6U1iTA1XIvFtrGIgS35gCg" - }, { "__type__": "cc.Widget", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 171 + "__id__": 203 }, "_enabled": true, "__prefab": { - "__id__": 177 + "__id__": 207 }, "_alignFlags": 16, "_target": null, @@ -4023,12 +4965,83 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "37X4EAEPlAa5LCmXMzZB8H" + "_id": "73BTLY0c9Dgbwm5MtcEZhc" }, { "__type__": "cc.CompPrefabInfo", "fileId": "tmJTzqBkFBONKUCoBaE7wQ" }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 203 + }, + "_enabled": true, + "__prefab": { + "__id__": 209 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 40, + "_fontSize": 72, + "_fontFamily": "Arial", + "_lineHeight": 32, + "_overflow": 0, + "_enableWrapText": true, + "_font": { + "__uuid__": "46c72f1b-4348-46ce-8e86-c0a8a6f8385a", + "__expectedType__": "cc.BitmapFont" + }, + "_isSystemFontUsed": false, + "_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": "a4kvXPDKpBrYz68DzD7DCw" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "4B1P2v6/BbkaAIl58aSGLg" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -4051,20 +5064,20 @@ "__id__": 2 }, "_children": [], - "_active": true, + "_active": false, "_components": [ { - "__id__": 180 + "__id__": 212 }, { - "__id__": 182 + "__id__": 214 }, { - "__id__": 184 + "__id__": 216 } ], "_prefab": { - "__id__": 186 + "__id__": 218 }, "_lpos": { "__type__": "cc.Vec3", @@ -4093,7 +5106,7 @@ "y": 0, "z": 0 }, - "_id": "513Otb8WlCF5ib92eOrfMe" + "_id": "24Z6B8O3tBB55WkCr+ybN7" }, { "__type__": "cc.UITransform", @@ -4101,84 +5114,39 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 179 + "__id__": 211 }, "_enabled": true, "__prefab": { - "__id__": 181 + "__id__": 213 }, "_contentSize": { "__type__": "cc.Size", - "width": 30, - "height": 30 + "width": 0, + "height": 40 }, "_anchorPoint": { "__type__": "cc.Vec2", "x": 0.5, "y": 0.5 }, - "_id": "7dgV/nYJhOcr13p4/vrhAp" + "_id": "4cfUo66XdPo6q9ldFc6ekJ" }, { "__type__": "cc.CompPrefabInfo", "fileId": "u9jz484mUmXEcuzOxINDcw" }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 179 - }, - "_enabled": true, - "__prefab": { - "__id__": 183 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "f3f3eadc-4916-4192-af76-8a3a0e2d01d7@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": "afb/GUx5BEDLVbXjB9/1xr" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "MfdnxQf5hHo7c+tRl/ijvg" - }, { "__type__": "cc.Widget", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 179 + "__id__": 211 }, "_enabled": true, "__prefab": { - "__id__": 185 + "__id__": 215 }, "_alignFlags": 16, "_target": null, @@ -4198,12 +5166,83 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "9a2+DxfvpMg4VIZ0mxceKd" + "_id": "e8Hj0ilFlCJqdppxDaIFQF" }, { "__type__": "cc.CompPrefabInfo", "fileId": "h2zyldOiqe9Q8b263YaQVw" }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 211 + }, + "_enabled": true, + "__prefab": { + "__id__": 217 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 40, + "_fontSize": 72, + "_fontFamily": "Arial", + "_lineHeight": 32, + "_overflow": 0, + "_enableWrapText": true, + "_font": { + "__uuid__": "46c72f1b-4348-46ce-8e86-c0a8a6f8385a", + "__expectedType__": "cc.BitmapFont" + }, + "_isSystemFontUsed": false, + "_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": "3aboEo8E1DdqC7qdacGvn3" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "lQM7S4KKxZBZgjI1qXpgPg" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -4226,20 +5265,20 @@ "__id__": 2 }, "_children": [], - "_active": true, + "_active": false, "_components": [ { - "__id__": 188 + "__id__": 220 }, { - "__id__": 190 + "__id__": 222 }, { - "__id__": 192 + "__id__": 224 } ], "_prefab": { - "__id__": 194 + "__id__": 226 }, "_lpos": { "__type__": "cc.Vec3", @@ -4268,7 +5307,7 @@ "y": 0, "z": 0 }, - "_id": "0fcSSXHZVID5agZ5k4uVNi" + "_id": "c2SyfqJz1DgKIRydkiHjbB" }, { "__type__": "cc.UITransform", @@ -4276,84 +5315,39 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 187 + "__id__": 219 }, "_enabled": true, "__prefab": { - "__id__": 189 + "__id__": 221 }, "_contentSize": { "__type__": "cc.Size", - "width": 30, - "height": 30 + "width": 0, + "height": 40 }, "_anchorPoint": { "__type__": "cc.Vec2", "x": 0.5, "y": 0.5 }, - "_id": "d1+Dm0nxJFK6RYU9c4TvNQ" + "_id": "20uochNzdFhoAmYcoNaWo7" }, { "__type__": "cc.CompPrefabInfo", "fileId": "6zdl/5BBTnOpwiIM9tM3SQ" }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 187 - }, - "_enabled": true, - "__prefab": { - "__id__": 191 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "f3f3eadc-4916-4192-af76-8a3a0e2d01d7@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": "1bsbAySQVEzbeEkRGLf3c0" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "j9R3WNjLRZP3NcpcrmCTsA" - }, { "__type__": "cc.Widget", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 187 + "__id__": 219 }, "_enabled": true, "__prefab": { - "__id__": 193 + "__id__": 223 }, "_alignFlags": 16, "_target": null, @@ -4373,12 +5367,83 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "bfcqzDZyVHLbdcZMbuHXlw" + "_id": "c7VeXQ1qpG8pTPU4PECNyt" }, { "__type__": "cc.CompPrefabInfo", "fileId": "HGwXkY4XlF5YRuDUZO6LCg" }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 219 + }, + "_enabled": true, + "__prefab": { + "__id__": 225 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 1, + "_verticalAlign": 0, + "_actualFontSize": 40, + "_fontSize": 72, + "_fontFamily": "Arial", + "_lineHeight": 32, + "_overflow": 0, + "_enableWrapText": true, + "_font": { + "__uuid__": "46c72f1b-4348-46ce-8e86-c0a8a6f8385a", + "__expectedType__": "cc.BitmapFont" + }, + "_isSystemFontUsed": false, + "_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": "5dCWJnVNBOS4qWBXdPLDDk" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "iriALXyxuRfk+sj/crZ3OA" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -4404,17 +5469,17 @@ "_active": true, "_components": [ { - "__id__": 196 + "__id__": 228 }, { - "__id__": 198 + "__id__": 230 }, { - "__id__": 200 + "__id__": 232 } ], "_prefab": { - "__id__": 202 + "__id__": 234 }, "_lpos": { "__type__": "cc.Vec3", @@ -4443,7 +5508,7 @@ "y": 0, "z": 0 }, - "_id": "02u46/GCpE3ILpTqmQtBIC" + "_id": "fbpVZ1EVxGILX129kmH0fh" }, { "__type__": "cc.UITransform", @@ -4451,11 +5516,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 195 + "__id__": 227 }, "_enabled": true, "__prefab": { - "__id__": 197 + "__id__": 229 }, "_contentSize": { "__type__": "cc.Size", @@ -4467,7 +5532,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "48atMpDDZHnKC+QDw/zCeZ" + "_id": "ec64q0lRJCEq0KWygwwOCC" }, { "__type__": "cc.CompPrefabInfo", @@ -4479,11 +5544,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 195 + "__id__": 227 }, "_enabled": true, "__prefab": { - "__id__": 199 + "__id__": 231 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -4512,7 +5577,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "c2C/Rl+wBIop+UqK4eID3d" + "_id": "0bvTKmmIBHEJFVUL80zAbQ" }, { "__type__": "cc.CompPrefabInfo", @@ -4524,11 +5589,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 195 + "__id__": 227 }, "_enabled": true, "__prefab": { - "__id__": 201 + "__id__": 233 }, "_alignFlags": 16, "_target": null, @@ -4548,7 +5613,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "f5lkPNJipEa4j7NibIzMji" + "_id": "77Al/EpcZMObBBRGlxMWpQ" }, { "__type__": "cc.CompPrefabInfo", @@ -4579,17 +5644,20 @@ "_active": true, "_components": [ { - "__id__": 204 + "__id__": 236 }, { - "__id__": 206 + "__id__": 238 }, { - "__id__": 208 + "__id__": 240 + }, + { + "__id__": 242 } ], "_prefab": { - "__id__": 210 + "__id__": 244 }, "_lpos": { "__type__": "cc.Vec3", @@ -4618,7 +5686,7 @@ "y": 0, "z": 0 }, - "_id": "56tEgeNRNNSoTeoDQJtlH4" + "_id": "1danDIHnlBYr6R/1Nm6W4v" }, { "__type__": "cc.UITransform", @@ -4626,11 +5694,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 203 + "__id__": 235 }, "_enabled": true, "__prefab": { - "__id__": 205 + "__id__": 237 }, "_contentSize": { "__type__": "cc.Size", @@ -4642,7 +5710,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "bbeYd50qVHZr4JfkJb3fBW" + "_id": "01xq9GqOlLp5mZbQ7Fc0Uq" }, { "__type__": "cc.CompPrefabInfo", @@ -4654,11 +5722,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 203 + "__id__": 235 }, "_enabled": true, "__prefab": { - "__id__": 207 + "__id__": 239 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -4687,7 +5755,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "cc3IBHJ4pLE7binKFJYC7I" + "_id": "7dmcnmEEdHoKCNZ9iLMB0c" }, { "__type__": "cc.CompPrefabInfo", @@ -4699,11 +5767,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 203 + "__id__": 235 }, "_enabled": true, "__prefab": { - "__id__": 209 + "__id__": 241 }, "_alignFlags": 16, "_target": null, @@ -4723,12 +5791,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "f5rFVZRHhMfYKyn2RI2kZ1" + "_id": "26jNMkxfJFEaSs2ZjYlgHU" }, { "__type__": "cc.CompPrefabInfo", "fileId": "Z8kYijgp7p0B7ia0e8lvSw" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 235 + }, + "_enabled": true, + "__prefab": { + "__id__": 243 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "54+fmItAFJH6JwcJkBsMA1" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "1uuiw8Z0iXKIDBodSzV3Sg" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -4752,7 +5876,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 212 + "__id__": 246 }, "_contentSize": { "__type__": "cc.Size", @@ -4764,7 +5888,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "3cQHhB0bJKoJTmiTGW0P0Y" + "_id": "99xxOQVNdB641ReugErmOn" }, { "__type__": "cc.CompPrefabInfo", @@ -4780,7 +5904,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 214 + "__id__": 248 }, "_alignFlags": 45, "_target": null, @@ -4800,7 +5924,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "21CAGzHyxKAaeTCMNYyyZo" + "_id": "70y/P7MH5BqbB36t0k7jHV" }, { "__type__": "cc.CompPrefabInfo", @@ -4829,7 +5953,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 217 + "__id__": 251 }, "_contentSize": { "__type__": "cc.Size", @@ -4841,7 +5965,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "52/p8OJ+ZF2Ksi6QNN9JON" + "_id": "f5v3dKzPBGy6B4WXYmJulm" }, { "__type__": "cc.CompPrefabInfo", @@ -4857,7 +5981,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 219 + "__id__": 253 }, "_alignFlags": 45, "_target": null, @@ -4877,12 +6001,102 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "0dLZw88GRLgZVENluiCNIv" + "_id": "a0y97Ie8NAMK/TvUWS921O" }, { "__type__": "cc.CompPrefabInfo", "fileId": "8Bkooexp1mTVniht1bQBRA" }, + { + "__type__": "033abnd4aFI+5/WVHTBpF6y", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 255 + }, + "digitButtons": [ + { + "__id__": 38 + }, + { + "__id__": 48 + }, + { + "__id__": 58 + }, + { + "__id__": 68 + }, + { + "__id__": 78 + }, + { + "__id__": 88 + }, + { + "__id__": 98 + }, + { + "__id__": 108 + }, + { + "__id__": 118 + }, + { + "__id__": 128 + } + ], + "slots": [ + { + "__id__": 184 + }, + { + "__id__": 192 + }, + { + "__id__": 200 + }, + { + "__id__": 208 + }, + { + "__id__": 216 + }, + { + "__id__": 224 + } + ], + "clearButton": { + "__id__": 138 + }, + "deleteButton": { + "__id__": 148 + }, + "pasteButton": { + "__id__": 158 + }, + "submitButton": { + "__id__": 242 + }, + "cancelButtons": [ + { + "__id__": 10 + }, + { + "__id__": 176 + } + ], + "_id": "a0BfnHLKtMxqbZbNn9hyd7" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "C8yc37AeAuNgHCrEAd/o2A" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -4896,4 +6110,4 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null } -] +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer16_Bind.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer16_Bind.prefab index 4022708..8b517a7 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer16_Bind.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer16_Bind.prefab @@ -25,19 +25,19 @@ "_active": true, "_components": [ { - "__id__": 64 + "__id__": 88 }, { - "__id__": 66 + "__id__": 90 } ], "_prefab": { - "__id__": 68 + "__id__": 92 }, "_lpos": { "__type__": "cc.Vec3", - "x": 0, - "y": 0, + "x": -800, + "y": -360, "z": 0 }, "_lrot": { @@ -61,7 +61,7 @@ "y": 0, "z": 0 }, - "_id": "fa6eARfpRAt4AtfjPQepbx" + "_id": "" }, { "__type__": "cc.Node", @@ -79,32 +79,32 @@ "__id__": 11 }, { - "__id__": 19 + "__id__": 21 }, { - "__id__": 27 + "__id__": 29 }, { - "__id__": 35 + "__id__": 59 }, { - "__id__": 43 + "__id__": 67 }, { - "__id__": 51 + "__id__": 75 } ], "_active": true, "_components": [ { - "__id__": 59 + "__id__": 83 }, { - "__id__": 61 + "__id__": 85 } ], "_prefab": { - "__id__": 63 + "__id__": 87 }, "_lpos": { "__type__": "cc.Vec3", @@ -133,7 +133,7 @@ "y": 0, "z": 0 }, - "_id": "ceggeylERGZbH4VyVBpod3" + "_id": "" }, { "__type__": "cc.Node", @@ -186,7 +186,7 @@ "y": 0, "z": 0 }, - "_id": "aemDSwTlBIR7c2euoLTwWY" + "_id": "" }, { "__type__": "cc.UITransform", @@ -210,7 +210,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "95QmoZPT1Pf7XsROBMj9X1" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -255,7 +255,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "b5d2WzUvFNXrRszsOUxmq/" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -291,7 +291,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "8d4vLxLd1C8I1Ei3kY0Nd0" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -329,10 +329,13 @@ }, { "__id__": 16 + }, + { + "__id__": 18 } ], "_prefab": { - "__id__": 18 + "__id__": 20 }, "_lpos": { "__type__": "cc.Vec3", @@ -361,7 +364,7 @@ "y": 0, "z": 0 }, - "_id": "e2jWAlXmREMoGucsD9hFFb" + "_id": "" }, { "__type__": "cc.UITransform", @@ -385,7 +388,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "9edkpbyq5K85jI+2hdfcdx" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -430,7 +433,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "a4d24+vKlFG7hlWyuTFj8b" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -466,12 +469,30 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "71XSRiMcpJQqINIgElOeA+" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", "fileId": "YO1dqDzIT1S89phG5gmzrQ" }, + { + "__type__": "cc.BlockInputEvents", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 19 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "baAUyBhfZBe4coUhjyyS9X" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -496,18 +517,18 @@ "_children": [], "_active": true, "_components": [ - { - "__id__": 20 - }, { "__id__": 22 }, { "__id__": 24 + }, + { + "__id__": 26 } ], "_prefab": { - "__id__": 26 + "__id__": 28 }, "_lpos": { "__type__": "cc.Vec3", @@ -536,7 +557,7 @@ "y": 0, "z": 0 }, - "_id": "52QA+L1dNK2r2rZ7wCsovx" + "_id": "" }, { "__type__": "cc.UITransform", @@ -544,11 +565,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 21 }, "_enabled": true, "__prefab": { - "__id__": 21 + "__id__": 23 }, "_contentSize": { "__type__": "cc.Size", @@ -560,7 +581,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "7fiWd2jK9OjJw1M+Xx9TGM" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -572,11 +593,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 21 }, "_enabled": true, "__prefab": { - "__id__": 23 + "__id__": 25 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -605,7 +626,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "c3RYNfe2pOQqzI1Ns3URQt" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -617,11 +638,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 21 }, "_enabled": true, "__prefab": { - "__id__": 25 + "__id__": 27 }, "_alignFlags": 16, "_target": null, @@ -641,7 +662,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "30Ep39jgtNSa0MfKY/pDzl" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -668,21 +689,25 @@ "_parent": { "__id__": 2 }, - "_children": [], + "_children": [ + { + "__id__": 30 + } + ], "_active": true, "_components": [ { - "__id__": 28 + "__id__": 52 }, { - "__id__": 30 + "__id__": 54 }, { - "__id__": 32 + "__id__": 56 } ], "_prefab": { - "__id__": 34 + "__id__": 58 }, "_lpos": { "__type__": "cc.Vec3", @@ -711,7 +736,120 @@ "y": 0, "z": 0 }, - "_id": "8cVqqcqklIpY4FiJ4rK8u6" + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "InputEditBox", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 29 + }, + "_children": [ + { + "__id__": 31 + }, + { + "__id__": 37 + } + ], + "_active": true, + "_components": [ + { + "__id__": 43 + }, + { + "__id__": 45 + }, + { + "__id__": 47 + }, + { + "__id__": 49 + } + ], + "_prefab": { + "__id__": 51 + }, + "_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": "Text", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 30 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 32 + }, + { + "__id__": 34 + } + ], + "_prefab": { + "__id__": 36 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -223, + "y": 30, + "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", @@ -719,27 +857,27 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 29 + "__id__": 33 }, "_contentSize": { "__type__": "cc.Size", - "width": 0, - "height": 25.2 + "width": 448, + "height": 60 }, "_anchorPoint": { "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 + "x": 0, + "y": 1 }, - "_id": "22pAGJW/RHcbliBNUxrUMP" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "OtgTzJDMktkfcKsfHclwWw" + "fileId": "9bOhss0qpHzobD5j66ILSq" }, { "__type__": "cc.Label", @@ -747,11 +885,170 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 31 + "__id__": 35 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 0, + "_verticalAlign": 1, + "_actualFontSize": 0, + "_fontSize": 35, + "_fontFamily": "Arial", + "_lineHeight": 35, + "_overflow": 1, + "_enableWrapText": false, + "_font": null, + "_isSystemFontUsed": true, + "_spacingX": 0, + "_isItalic": false, + "_isBold": true, + "_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": "10IIRB5+1A7ayqyrVTpH6T" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "cbpaZn9pRPM6zcxcTKlG+o", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Placeholder", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 30 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 38 + }, + { + "__id__": 40 + } + ], + "_prefab": { + "__id__": 42 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -223, + "y": 30, + "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__": 37 + }, + "_enabled": true, + "__prefab": { + "__id__": 39 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 448, + "height": 60 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "91zo1bqFZOjrlvxyyzjt9M" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 37 + }, + "_enabled": true, + "__prefab": { + "__id__": 41 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -764,7 +1061,276 @@ "a": 255 }, "_string": "", - "_horizontalAlign": 1, + "_horizontalAlign": 0, + "_verticalAlign": 1, + "_actualFontSize": 21.083333333333336, + "_fontSize": 20, + "_fontFamily": "Arial", + "_lineHeight": 24, + "_overflow": 1, + "_enableWrapText": false, + "_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": "b7XnvPmuhJKpPJXElWtFFi" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "01/qoYLSxHWoCF5gDbACsG", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 30 + }, + "_enabled": true, + "__prefab": { + "__id__": 44 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 450, + "height": 60 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "89S5/QHpRNbojwWi92sDbW" + }, + { + "__type__": "cc.EditBox", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 30 + }, + "_enabled": true, + "__prefab": { + "__id__": 46 + }, + "editingDidBegan": [], + "textChanged": [], + "editingDidEnded": [], + "editingReturn": [], + "_textLabel": { + "__id__": 34 + }, + "_placeholderLabel": { + "__id__": 40 + }, + "_returnType": 0, + "_string": "", + "_tabIndex": 0, + "_backgroundImage": { + "__uuid__": "34daf5f4-3869-4f71-84c4-d09103701be6@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_inputFlag": 5, + "_inputMode": 2, + "_maxLength": -1, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2fk6R2bpxCBa76PyFviN9+" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 30 + }, + "_enabled": true, + "__prefab": { + "__id__": 48 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "34daf5f4-3869-4f71-84c4-d09103701be6@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_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": "8e4R9TK1lExorSHLI8Q3yS" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 30 + }, + "_enabled": true, + "__prefab": { + "__id__": 50 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 108, + "_originalHeight": 98, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "d76n4nGZVNe5vkGGMW0tT8" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "86dzYw5lpHrJSYUTQ5kcxz", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 29 + }, + "_enabled": true, + "__prefab": { + "__id__": 53 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 450, + "height": 60 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "OtgTzJDMktkfcKsfHclwWw" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 29 + }, + "_enabled": false, + "__prefab": { + "__id__": 55 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 0, "_verticalAlign": 1, "_actualFontSize": 20, "_fontSize": 20, @@ -803,7 +1369,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "6fmtM0dqZMWL3sy63P7Cd2" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -815,11 +1381,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 29 }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 57 }, "_alignFlags": 16, "_target": null, @@ -839,7 +1405,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "13wmREtu9Iupnu6OCd3zU0" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -870,17 +1436,17 @@ "_active": true, "_components": [ { - "__id__": 36 + "__id__": 60 }, { - "__id__": 38 + "__id__": 62 }, { - "__id__": 40 + "__id__": 64 } ], "_prefab": { - "__id__": 42 + "__id__": 66 }, "_lpos": { "__type__": "cc.Vec3", @@ -909,7 +1475,7 @@ "y": 0, "z": 0 }, - "_id": "baCoZPVB1Ch6+cFB9F03Ce" + "_id": "" }, { "__type__": "cc.UITransform", @@ -917,11 +1483,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 59 }, "_enabled": true, "__prefab": { - "__id__": 37 + "__id__": 61 }, "_contentSize": { "__type__": "cc.Size", @@ -933,7 +1499,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "8c5leNunxOgrvzk/0pcPq6" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -945,11 +1511,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 59 }, "_enabled": true, "__prefab": { - "__id__": 39 + "__id__": 63 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -978,7 +1544,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "828nvCXmNNF6VhLAAi36eb" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -990,11 +1556,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 59 }, "_enabled": true, "__prefab": { - "__id__": 41 + "__id__": 65 }, "_alignFlags": 16, "_target": null, @@ -1014,7 +1580,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "5e6cJlMulFiqHO1mZtrSHI" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1045,17 +1611,17 @@ "_active": true, "_components": [ { - "__id__": 44 + "__id__": 68 }, { - "__id__": 46 + "__id__": 70 }, { - "__id__": 48 + "__id__": 72 } ], "_prefab": { - "__id__": 50 + "__id__": 74 }, "_lpos": { "__type__": "cc.Vec3", @@ -1084,7 +1650,7 @@ "y": 0, "z": 0 }, - "_id": "03BJSuGftNkLG7MhhxCJKx" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1092,11 +1658,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 67 }, "_enabled": true, "__prefab": { - "__id__": 45 + "__id__": 69 }, "_contentSize": { "__type__": "cc.Size", @@ -1108,7 +1674,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "6c7jlDP4VJSblGyhwW+aFe" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1120,11 +1686,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 67 }, "_enabled": true, "__prefab": { - "__id__": 47 + "__id__": 71 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1153,7 +1719,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "83Z6TbB/FJTr4VNwEiVCHu" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1165,11 +1731,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 67 }, "_enabled": true, "__prefab": { - "__id__": 49 + "__id__": 73 }, "_alignFlags": 16, "_target": null, @@ -1189,7 +1755,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "abHpK2aUpOh63ZI+9Pw/h0" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1220,21 +1786,21 @@ "_active": true, "_components": [ { - "__id__": 52 + "__id__": 76 }, { - "__id__": 54 + "__id__": 78 }, { - "__id__": 56 + "__id__": 80 } ], "_prefab": { - "__id__": 58 + "__id__": 82 }, "_lpos": { "__type__": "cc.Vec3", - "x": -342.5, + "x": -397, "y": 232.5, "z": 0 }, @@ -1259,7 +1825,7 @@ "y": 0, "z": 0 }, - "_id": "4avzn5lghBTKIOGfJwGfq1" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1267,23 +1833,23 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 75 }, "_enabled": true, "__prefab": { - "__id__": 53 + "__id__": 77 }, "_contentSize": { "__type__": "cc.Size", - "width": 78.935546875, - "height": 25.2 + "width": 138.11529709887597, + "height": 44.1 }, "_anchorPoint": { "__type__": "cc.Vec2", - "x": 0.5, + "x": 0, "y": 0.5 }, - "_id": "792ah0iaZIsphDs56gQxnE" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1295,29 +1861,29 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 75 }, "_enabled": true, "__prefab": { - "__id__": 55 + "__id__": 79 }, "_customMaterial": null, "_srcBlendFactor": 2, "_dstBlendFactor": 4, "_color": { "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, + "r": 255, + "g": 255, + "b": 255, "a": 255 }, "_string": "Spirit368", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, + "_actualFontSize": 36.895833333333336, + "_fontSize": 35, "_fontFamily": "Arial", - "_lineHeight": 20, + "_lineHeight": 35, "_overflow": 0, "_enableWrapText": true, "_font": null, @@ -1351,7 +1917,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "a4clrjzGNLJ58kSKPzQf8W" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1363,15 +1929,17 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 75 }, "_enabled": true, "__prefab": { - "__id__": 57 + "__id__": 81 }, - "_alignFlags": 16, - "_target": null, - "_left": 0, + "_alignFlags": 8, + "_target": { + "__id__": 1 + }, + "_left": 403, "_right": 0, "_top": 0, "_bottom": 0, @@ -1387,7 +1955,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "0e8udsiaNA478RC65Fxakn" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1416,7 +1984,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 60 + "__id__": 84 }, "_contentSize": { "__type__": "cc.Size", @@ -1428,7 +1996,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "f8sooh2tpEco3BQgcTkRbW" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1444,7 +2012,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 62 + "__id__": 86 }, "_alignFlags": 45, "_target": null, @@ -1464,7 +2032,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "40Q8XVRdBNabxu4EfarNwO" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1493,7 +2061,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 65 + "__id__": 89 }, "_contentSize": { "__type__": "cc.Size", @@ -1505,7 +2073,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "95bmIpX6BANaKeg+xvPBKr" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1521,7 +2089,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 67 + "__id__": 91 }, "_alignFlags": 45, "_target": null, @@ -1541,7 +2109,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "5e7NzuCGhL1r+Ugzrorgh2" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1557,7 +2125,6 @@ }, "fileId": "ji5kR2RpkQLInnBXh2Twog", "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null + "targetOverrides": null } -] +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer17_PlayerInfo.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer17_PlayerInfo.prefab index cad34cd..a44feba 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer17_PlayerInfo.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer17_PlayerInfo.prefab @@ -25,19 +25,22 @@ "_active": true, "_components": [ { - "__id__": 128 + "__id__": 84 }, { - "__id__": 130 + "__id__": 86 + }, + { + "__id__": 88 } ], "_prefab": { - "__id__": 132 + "__id__": 90 }, "_lpos": { "__type__": "cc.Vec3", - "x": 0, - "y": 0, + "x": -800, + "y": -360, "z": 0 }, "_lrot": { @@ -61,7 +64,7 @@ "y": 0, "z": 0 }, - "_id": "4fSSAyM/xClqgjz6BVeN15" + "_id": "" }, { "__type__": "cc.Node", @@ -92,43 +95,19 @@ }, { "__id__": 51 - }, - { - "__id__": 59 - }, - { - "__id__": 67 - }, - { - "__id__": 75 - }, - { - "__id__": 83 - }, - { - "__id__": 91 - }, - { - "__id__": 99 - }, - { - "__id__": 107 - }, - { - "__id__": 115 } ], "_active": true, "_components": [ { - "__id__": 123 + "__id__": 79 }, { - "__id__": 125 + "__id__": 81 } ], "_prefab": { - "__id__": 127 + "__id__": 83 }, "_lpos": { "__type__": "cc.Vec3", @@ -157,7 +136,7 @@ "y": 0, "z": 0 }, - "_id": "d9iTvMiaBIM4izu8sVvw51" + "_id": "" }, { "__type__": "cc.Node", @@ -210,7 +189,7 @@ "y": 0, "z": 0 }, - "_id": "1dxg46UQFKNr31dlG1YFGT" + "_id": "" }, { "__type__": "cc.UITransform", @@ -234,7 +213,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "4dU7HMOtZCB4aKJcrdjH+R" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -279,7 +258,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "d2s076oAFO/5PaG5p4zvo4" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -315,7 +294,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "ba1JRBwhFCXaq2ouTPwahN" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -385,7 +364,7 @@ "y": 0, "z": 0 }, - "_id": "08MubxE3NE9Y0C+kR4T4Sb" + "_id": "" }, { "__type__": "cc.UITransform", @@ -409,7 +388,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "8fa3vOQNNGgJAB7tpRM+Rq" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -454,7 +433,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "bfbKgBImlAOLCIs7IMZ+3u" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -490,7 +469,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "38U8ykUQ9Pj4AzM6LBEscx" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -560,7 +539,7 @@ "y": 0, "z": 0 }, - "_id": "7bBDoJHJRLC7JSIopNhTUN" + "_id": "" }, { "__type__": "cc.UITransform", @@ -584,7 +563,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "16YwueSaZC64z68m2n3Czz" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -629,7 +608,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "65jZXdoSxPBqqF2GK3LdO9" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -665,7 +644,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "59zUudWFNJmLl8OowKzVaP" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -735,7 +714,7 @@ "y": 0, "z": 0 }, - "_id": "9c8UQjPeVLg5c5NMGzVCIS" + "_id": "" }, { "__type__": "cc.UITransform", @@ -759,7 +738,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "ec0caBrQtJt4Z1rz9z165W" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -804,7 +783,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "05cuvK46ZKmranEAi52L1I" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -840,7 +819,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "c1xkc7vkJI9LnFwxE+bk6U" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -910,7 +889,7 @@ "y": 0, "z": 0 }, - "_id": "2ctDP2SS9J0IPlsfWZDTYE" + "_id": "" }, { "__type__": "cc.UITransform", @@ -934,7 +913,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "45EgEEtQVIWZGX7kVcRkrN" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -979,7 +958,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "90prbCfy1CeoDNVDBVOkjB" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1015,7 +994,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "c3XaLHSypNKY9sOZJKGx4K" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1085,7 +1064,7 @@ "y": 0, "z": 0 }, - "_id": "abPXilYDtFQKEImj+daxED" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1109,7 +1088,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "1aUR/hodpLUrKnjqdcVZUx" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1154,7 +1133,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "37uCfsOwdJPqOuxBtnrU+u" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1190,7 +1169,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "23PCC+JhtBpowZjXb4hq4T" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1211,32 +1190,39 @@ }, { "__type__": "cc.Node", - "_name": "Spirit3156", + "_name": "InfoText", "_objFlags": 0, "__editorExtras__": {}, "_parent": { "__id__": 2 }, - "_children": [], + "_children": [ + { + "__id__": 52 + } + ], "_active": true, "_components": [ { - "__id__": 52 + "__id__": 70 }, { - "__id__": 54 + "__id__": 72 }, { - "__id__": 56 + "__id__": 74 + }, + { + "__id__": 76 } ], "_prefab": { - "__id__": 58 + "__id__": 78 }, "_lpos": { "__type__": "cc.Vec3", - "x": 77.5, - "y": -129.5, + "x": -118, + "y": 159, "z": 0 }, "_lrot": { @@ -1260,7 +1246,165 @@ "y": 0, "z": 0 }, - "_id": "8eSnFd4zxD/qvvsfBr82Av" + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Content", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 51 + }, + "_children": [ + { + "__id__": 53 + } + ], + "_active": true, + "_components": [ + { + "__id__": 65 + }, + { + "__id__": 67 + } + ], + "_prefab": { + "__id__": 69 + }, + "_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": "Spirit3156", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 52 + }, + "_children": [ + { + "__id__": 54 + } + ], + "_active": true, + "_components": [ + { + "__id__": 60 + }, + { + "__id__": 62 + } + ], + "_prefab": { + "__id__": 64 + }, + "_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": "Spirit3157", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 53 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 55 + }, + { + "__id__": 57 + } + ], + "_prefab": { + "__id__": 59 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 10, + "y": -10, + "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", @@ -1268,12 +1412,121 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 54 }, "_enabled": true, "__prefab": { + "__id__": 56 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 371, + "height": 22.680000000000003 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "AdPYmfztq9DGc8OU9wbShw" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 54 + }, + "_enabled": true, + "__prefab": { + "__id__": 58 + }, + "_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": "zN4MEERMr54w9SNKHUDdig" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "TxAmyulpXwMhAKI77vk4Tw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { "__id__": 53 }, + "_enabled": true, + "__prefab": { + "__id__": 61 + }, "_contentSize": { "__type__": "cc.Size", "width": 391, @@ -1281,10 +1534,10 @@ }, "_anchorPoint": { "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 + "x": 0, + "y": 1 }, - "_id": "a8/ooM8iNEi65B2cU5ip1J" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1296,11 +1549,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 53 }, "_enabled": true, "__prefab": { - "__id__": 55 + "__id__": 63 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1329,48 +1582,12 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "f1Jr1Yil1FGLG0yOdTMgIf" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", "fileId": "/BuyFxH5WaP821RBH65u9w" }, - { - "__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": 77.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "bece9osbJCToHEVc6LuKY1" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "5hRWQtfvznB/PDUr41yWHg" - }, { "__type__": "cc.PrefabInfo", "root": { @@ -1384,190 +1601,71 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, - { - "__type__": "cc.Node", - "_name": "Spirit196", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 60 - }, - { - "__id__": 62 - }, - { - "__id__": 64 - } - ], - "_prefab": { - "__id__": 66 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -80.5, - "y": 149.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": "17SdT0E3xDGZGsN88uU5T9" - }, { "__type__": "cc.UITransform", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 52 }, "_enabled": true, "__prefab": { - "__id__": 61 + "__id__": 66 }, "_contentSize": { "__type__": "cc.Size", - "width": 55.2548828125, - "height": 17.64 + "width": 391, + "height": 89 }, "_anchorPoint": { "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 + "x": 0, + "y": 1 }, - "_id": "269RB8ClFGBKrRqicZArBr" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "1x0G0QxDxshGQ/MgCyR6Ug" + "fileId": "/9ja/zzPI7ZwHxGwTuzgEg" }, { - "__type__": "cc.Label", + "__type__": "cc.Layout", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 52 }, "_enabled": true, "__prefab": { - "__id__": 63 + "__id__": 68 }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 + "_resizeMode": 1, + "_layoutType": 2, + "_cellSize": { + "__type__": "cc.Size", + "width": 40, + "height": 40 }, - "_string": "Spirit196", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, + "_startAxis": 0, + "_paddingLeft": 0, + "_paddingRight": 0, + "_paddingTop": 0, + "_paddingBottom": 10, "_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": "81LWXtrlhNIpY31InzhYvp" + "_spacingY": 10, + "_verticalDirection": 1, + "_horizontalDirection": 0, + "_constraint": 0, + "_constraintNum": 2, + "_affectedByScale": false, + "_isAlign": false, + "_id": "" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "Lby7HR3PbmrTviVUyiPmrQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 59 - }, - "_enabled": true, - "__prefab": { - "__id__": 65 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -80.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "d1Eh3aG+BI64D25W9JnWQh" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "0TtSZJlxqoug0quFcCEOfg" + "fileId": "MoxGuAvE8McKGwCzJMUGpA" }, { "__type__": "cc.PrefabInfo", @@ -1577,393 +1675,137 @@ "asset": { "__id__": 0 }, - "fileId": "HKtNJY9YnjZi7EfgJZeBig", + "fileId": "bmRwIO4oGtE8etmRgd9XYg", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null }, - { - "__type__": "cc.Node", - "_name": "Spirit197", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 68 - }, - { - "__id__": 70 - }, - { - "__id__": 72 - } - ], - "_prefab": { - "__id__": 74 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -80.5, - "y": 114.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": "bdtX/XXI5BO7wDXsgQNso+" - }, { "__type__": "cc.UITransform", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 - }, - "_enabled": true, - "__prefab": { - "__id__": 69 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 55.2548828125, - "height": 17.64 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "6eXTjeBBJFs4NuqNToIz3z" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "GF3YQScKcLkGAcFTJgp3nQ" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 67 + "__id__": 51 }, "_enabled": true, "__prefab": { "__id__": 71 }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 + "_contentSize": { + "__type__": "cc.Size", + "width": 391, + "height": 340 }, - "_string": "Spirit766", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 0, - "_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": { + "_anchorPoint": { "__type__": "cc.Vec2", - "x": 2, - "y": 2 + "x": 0, + "y": 1 }, - "_shadowBlur": 2, - "_id": "c6WB6EtjNLBIRRZH/AUAbz" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "iVCZNNujX4TvgA7BdHhZLA" + "fileId": "4aM8eoWWFOIbB0uzmvqk+V" }, { - "__type__": "cc.Widget", + "__type__": "cc.Mask", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 51 }, "_enabled": true, "__prefab": { "__id__": 73 }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -80.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "9dB+pUEvdNM5hUZosVKOgE" + "_type": 0, + "_inverted": false, + "_segments": 64, + "_alphaThreshold": 0.1, + "_id": "" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "FyBvdZIJYhEO7H91aG9s6Q" + "fileId": "dbYxxymqPapdinxLU/r74A" }, { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "UCZdYwXe6iBu8pyKAQppiw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit198", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 76 - }, - { - "__id__": 78 - }, - { - "__id__": 80 - } - ], - "_prefab": { - "__id__": 82 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -80.5, - "y": 79.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": "a6LIqLBmhPuaeLk/neaAA9" - }, - { - "__type__": "cc.UITransform", + "__type__": "cc.ScrollView", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { + "__id__": 51 + }, + "_enabled": true, + "__prefab": { "__id__": 75 }, + "bounceDuration": 1, + "brake": 0.5, + "elastic": true, + "inertia": true, + "horizontal": false, + "vertical": true, + "cancelInnerEvents": true, + "scrollEvents": [], + "_content": { + "__id__": 52 + }, + "_horizontalScrollBar": null, + "_verticalScrollBar": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "nLMZGVmHrHNwI3EAaLF7uQ" + }, + { + "__type__": "cc.Graphics", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 51 + }, "_enabled": true, "__prefab": { "__id__": 77 }, - "_contentSize": { - "__type__": "cc.Size", - "width": 55.2548828125, - "height": 17.64 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "cbt+oZy2hBTqLecNmCnnPl" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "ZlD6uJfBvV9ElsBaoDDYGw" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 75 - }, - "_enabled": true, - "__prefab": { - "__id__": 79 - }, "_customMaterial": null, "_srcBlendFactor": 2, "_dstBlendFactor": 4, "_color": { "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, + "r": 255, + "g": 255, + "b": 255, "a": 255 }, - "_string": "Spirit766", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { + "_lineWidth": 1, + "_strokeColor": { "__type__": "cc.Color", "r": 0, "g": 0, "b": 0, "a": 255 }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { + "_lineJoin": 2, + "_lineCap": 0, + "_fillColor": { "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 + "r": 255, + "g": 255, + "b": 255, + "a": 0 }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "56JJbuaRVFWLZAcXrYEQN7" + "_miterLimit": 10, + "_id": "" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "/PyOH2xORn/aaXmVJIMDUg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 75 - }, - "_enabled": true, - "__prefab": { - "__id__": 81 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -80.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "a3pr3l5WxKKYbj7WWmIIYa" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "8Fw2DDU3OeQjZ3muuBMQkg" + "fileId": "9cLkbqJrpO8KEjF/TjejC6" }, { "__type__": "cc.PrefabInfo", @@ -1973,997 +1815,7 @@ "asset": { "__id__": 0 }, - "fileId": "PuwYyWVMbLd7bNrcJdXlPw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit199", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 84 - }, - { - "__id__": 86 - }, - { - "__id__": 88 - } - ], - "_prefab": { - "__id__": 90 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -80.5, - "y": 44.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": "32gFOHZKhM4rkRnFRi2xvJ" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 83 - }, - "_enabled": true, - "__prefab": { - "__id__": 85 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 55.2548828125, - "height": 17.64 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "60Qcceg+9MVJKaHpjpqdE1" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "FA8jMYTMwO57m/FpXnjayQ" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 83 - }, - "_enabled": true, - "__prefab": { - "__id__": 87 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "Spirit766", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 0, - "_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": "d7fBd2u2ZKSbFwczaTBBeC" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "rCvugw0rivnDtHo1TXXC2w" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 83 - }, - "_enabled": true, - "__prefab": { - "__id__": 89 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -80.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "c9nwPDciBP9J3jhVA0vACz" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "iIIx+Gpf4guD7itsYioggQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "PaJkt5YnjmexYAiHeouymw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit200", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 92 - }, - { - "__id__": 94 - }, - { - "__id__": 96 - } - ], - "_prefab": { - "__id__": 98 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -80.5, - "y": -25.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": "93OkILwbFHhIBR/3+gBkoI" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 91 - }, - "_enabled": true, - "__prefab": { - "__id__": 93 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 55.2548828125, - "height": 17.64 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "a5X3SfCelI3r6G0oIAUf/S" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "RgBdjVMIRzFlF4IC5Op+BA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 91 - }, - "_enabled": true, - "__prefab": { - "__id__": 95 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "Spirit766", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 0, - "_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": "b9f7KG/JdPmoZ0ZJsOCvYE" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "U8yis8hzdMhWNxYtCNexrQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 91 - }, - "_enabled": true, - "__prefab": { - "__id__": 97 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -80.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "d2Bw+C179CC6qkj3gDPtGI" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "jVsVtdNQgjFwgjozbMwGcg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "tjoLRqCtlXMuJ68+XwK2Kg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit510", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 100 - }, - { - "__id__": 102 - }, - { - "__id__": 104 - } - ], - "_prefab": { - "__id__": 106 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -80.5, - "y": 9.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": "ab45NpWeVL3KoOr5CMq4Rb" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 99 - }, - "_enabled": true, - "__prefab": { - "__id__": 101 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 55.2548828125, - "height": 17.64 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "9ceFRn6EVMcLpDMttPWQFo" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Co4cUJRlPyw5rUX/lw7QdA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 99 - }, - "_enabled": true, - "__prefab": { - "__id__": 103 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "Spirit766", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 0, - "_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": "62IBSDrBBLOZzq9bv7Uk6H" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "PfY3zrla2Ts0QXqIoyUjqw" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 99 - }, - "_enabled": true, - "__prefab": { - "__id__": 105 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -80.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "e2Symosi5C25c6V7sKKRs9" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "XiwpasPHR+TidVAIxvzwAQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "Sg0rBGOxV5GrYDNxWfYfrQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3152", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 108 - }, - { - "__id__": 110 - }, - { - "__id__": 112 - } - ], - "_prefab": { - "__id__": 114 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -80.5, - "y": -61.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": "7fw5Pjoz5L1qbWWcX+X+Hg" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 107 - }, - "_enabled": true, - "__prefab": { - "__id__": 109 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 55.2548828125, - "height": 17.64 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "54SZw0Ec9H4pSS0GY2jV9x" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "H/xiV3QEz4ji9dlKdyutCg" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 107 - }, - "_enabled": true, - "__prefab": { - "__id__": 111 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "Spirit766", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 0, - "_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": "6d69jEzKxFy4HS92l5xmjZ" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "9kGNGsulwz8C6NA1N4kGMQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 107 - }, - "_enabled": true, - "__prefab": { - "__id__": 113 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -80.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "8ePpFFbJBEDrUd4lY/kP1f" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "hcgGmbaOSGajtEEb6h/RDw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "M9iHxSyzRALZ4/D+uGIq8w", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3157", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 116 - }, - { - "__id__": 118 - }, - { - "__id__": 120 - } - ], - "_prefab": { - "__id__": 122 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -66.5, - "y": -113.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": "140nwNvjpLs6/kvQxzsFsZ" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 115 - }, - "_enabled": true, - "__prefab": { - "__id__": 117 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 55.2548828125, - "height": 17.64 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "b1fgnhiZhBjp5v/QZygSEY" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "AdPYmfztq9DGc8OU9wbShw" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 115 - }, - "_enabled": true, - "__prefab": { - "__id__": 119 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "Spirit766", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 0, - "_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": "33vsSgpEhFXK6SwJrdLqG6" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "zN4MEERMr54w9SNKHUDdig" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 115 - }, - "_enabled": true, - "__prefab": { - "__id__": 121 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -66.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "a0qDDZbrJHR7LtSxi7PuoQ" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "ucIsCzHpJ3MzAz4Tm1+P9g" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "TxAmyulpXwMhAKI77vk4Tw", + "fileId": "c5QwYeucBP/7X/9YQ4iISW", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -2978,7 +1830,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 124 + "__id__": 80 }, "_contentSize": { "__type__": "cc.Size", @@ -2990,7 +1842,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "2asTaoK8VEN5vVHVkBm/uu" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3006,7 +1858,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 126 + "__id__": 82 }, "_alignFlags": 45, "_target": null, @@ -3026,7 +1878,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "9cCMHmXeRKp5aLeK5iGS5J" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3055,7 +1907,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 129 + "__id__": 85 }, "_contentSize": { "__type__": "cc.Size", @@ -3067,7 +1919,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "dfSUyiR/VOb5a1nLtBxMgB" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3083,7 +1935,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 131 + "__id__": 87 }, "_alignFlags": 45, "_target": null, @@ -3103,12 +1955,50 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "48ZMitMsBJeJaN6VKmNg+Z" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", "fileId": "f3yPVzuueePKltAiXHyBPQ" }, + { + "__type__": "b1570sXpBFLFaZvJW2GXWHl", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 89 + }, + "textItemPrefab": { + "__uuid__": "9e6dbfc6-26c5-4eac-9cc8-8060a677d5ee", + "__expectedType__": "cc.Prefab" + }, + "authPrefab": { + "__uuid__": "bc89b6a6-c07e-4a96-a0ef-8e8cafd8f1f2", + "__expectedType__": "cc.Prefab" + }, + "content": { + "__id__": 52 + }, + "scroll": { + "__id__": 74 + }, + "signature": { + "__id__": 57 + }, + "signatureBox": { + "__id__": 60 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "C349RNTg/sqy60Nc4e6+Iw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -3119,7 +2009,6 @@ }, "fileId": "fPw6Dt9Usab9pm1NGP2S4Q", "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null + "targetOverrides": null } -] +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer18_Auth.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer18_Auth.prefab index 760031b..b7d8242 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer18_Auth.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer18_Auth.prefab @@ -25,14 +25,14 @@ "_active": true, "_components": [ { - "__id__": 80 + "__id__": 102 }, { - "__id__": 82 + "__id__": 104 } ], "_prefab": { - "__id__": 84 + "__id__": 106 }, "_lpos": { "__type__": "cc.Vec3", @@ -61,7 +61,7 @@ "y": 0, "z": 0 }, - "_id": "bdWM5Xu11IQJ88XHvNnjrB" + "_id": "846iHBEQ9M4q9B2AS7sPso" }, { "__type__": "cc.Node", @@ -79,38 +79,38 @@ "__id__": 11 }, { - "__id__": 19 + "__id__": 21 }, { - "__id__": 27 + "__id__": 29 }, { - "__id__": 35 + "__id__": 37 }, { - "__id__": 43 + "__id__": 45 }, { - "__id__": 51 + "__id__": 53 }, { - "__id__": 59 + "__id__": 61 }, { - "__id__": 67 + "__id__": 79 } ], "_active": true, "_components": [ { - "__id__": 75 + "__id__": 97 }, { - "__id__": 77 + "__id__": 99 } ], "_prefab": { - "__id__": 79 + "__id__": 101 }, "_lpos": { "__type__": "cc.Vec3", @@ -139,7 +139,7 @@ "y": 0, "z": 0 }, - "_id": "50j6m6x/VO8ZEnQnM5Sx6S" + "_id": "5bJz65M1dMpaa+LCTs+FoA" }, { "__type__": "cc.Node", @@ -150,7 +150,7 @@ "__id__": 2 }, "_children": [], - "_active": false, + "_active": true, "_components": [ { "__id__": 4 @@ -192,7 +192,7 @@ "y": 0, "z": 0 }, - "_id": "73NWNeYjpNEKtLmKIO91eS" + "_id": "9aEUpSFK9HEYUdEa1OQxkt" }, { "__type__": "cc.UITransform", @@ -216,7 +216,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "5eYt8WjjtAbZbdsYGHRyYm" + "_id": "f6ud1pbjtFB4VGFjg7vMtV" }, { "__type__": "cc.CompPrefabInfo", @@ -261,7 +261,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "d4HVr3DpRKcY87KJyEk+vF" + "_id": "aegEChGFlPaolKg83ZVfUO" }, { "__type__": "cc.CompPrefabInfo", @@ -297,7 +297,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "32tEJhdbRHEbnU9NwjAKQL" + "_id": "a5S8qqdTNAP5ShpcWK/Slm" }, { "__type__": "cc.CompPrefabInfo", @@ -335,10 +335,13 @@ }, { "__id__": 16 + }, + { + "__id__": 18 } ], "_prefab": { - "__id__": 18 + "__id__": 20 }, "_lpos": { "__type__": "cc.Vec3", @@ -367,7 +370,7 @@ "y": 0, "z": 0 }, - "_id": "b7HtvX/LtIlIUW0x2WVNpp" + "_id": "f2kErpDwJCj6Lw7yeaDNIB" }, { "__type__": "cc.UITransform", @@ -391,7 +394,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "38Nk2nOplFzJHWiz07z6Y8" + "_id": "b5OpVyBqZGLqQtLa5hE1MD" }, { "__type__": "cc.CompPrefabInfo", @@ -436,7 +439,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "aflAXhWQJM97feCcA0MpeQ" + "_id": "85qtPmrktHCYcvtOm1UkLP" }, { "__type__": "cc.CompPrefabInfo", @@ -472,12 +475,30 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "ddRViL9zpAqLdCOZEwrL+L" + "_id": "a0KmMzguVFbrc7WjvbLVDa" }, { "__type__": "cc.CompPrefabInfo", "fileId": "Nv7cqdbmVCpfWaU8ud1h+Q" }, + { + "__type__": "cc.BlockInputEvents", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 19 + }, + "_id": "514jqgKiRIl7XWnk4aPGf9" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "20yQyNbdZ9Tz85NzGHaNGw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -502,18 +523,18 @@ "_children": [], "_active": true, "_components": [ - { - "__id__": 20 - }, { "__id__": 22 }, { "__id__": 24 + }, + { + "__id__": 26 } ], "_prefab": { - "__id__": 26 + "__id__": 28 }, "_lpos": { "__type__": "cc.Vec3", @@ -542,7 +563,7 @@ "y": 0, "z": 0 }, - "_id": "38OrU1XbBEQ7cwZvAK2N0/" + "_id": "e1ZUdr9i5OC4/P3AZ09frx" }, { "__type__": "cc.UITransform", @@ -550,11 +571,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 21 }, "_enabled": true, "__prefab": { - "__id__": 21 + "__id__": 23 }, "_contentSize": { "__type__": "cc.Size", @@ -566,7 +587,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "0bElr848tMD4R5Fn2VF5LJ" + "_id": "81xFrA+a5DR7mdBV1+extf" }, { "__type__": "cc.CompPrefabInfo", @@ -578,11 +599,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 21 }, "_enabled": true, "__prefab": { - "__id__": 23 + "__id__": 25 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -611,7 +632,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "35ziFTXzNIlrUGs5Cae5Kh" + "_id": "00avGLsklF3biIg5R96II8" }, { "__type__": "cc.CompPrefabInfo", @@ -623,11 +644,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 21 }, "_enabled": true, "__prefab": { - "__id__": 25 + "__id__": 27 }, "_alignFlags": 16, "_target": null, @@ -647,7 +668,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "c1feuRZeVBwIAEoEjrkkgl" + "_id": "43yk+spyRLpbrbBrX+Y3pS" }, { "__type__": "cc.CompPrefabInfo", @@ -677,18 +698,18 @@ "_children": [], "_active": true, "_components": [ - { - "__id__": 28 - }, { "__id__": 30 }, { "__id__": 32 + }, + { + "__id__": 34 } ], "_prefab": { - "__id__": 34 + "__id__": 36 }, "_lpos": { "__type__": "cc.Vec3", @@ -717,7 +738,7 @@ "y": 0, "z": 0 }, - "_id": "04KEoM/AhCk79IV7lqOO6x" + "_id": "94n8JxCbZEEoCR6qIMaOfI" }, { "__type__": "cc.UITransform", @@ -725,11 +746,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 29 }, "_enabled": true, "__prefab": { - "__id__": 29 + "__id__": 31 }, "_contentSize": { "__type__": "cc.Size", @@ -741,7 +762,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "79/yAOY1xCZKvA5OoGIpVB" + "_id": "bawIa0eitMuLqRqD7WSYwA" }, { "__type__": "cc.CompPrefabInfo", @@ -753,11 +774,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 29 }, "_enabled": true, "__prefab": { - "__id__": 31 + "__id__": 33 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -786,7 +807,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "0eDbmNCJBCc4vfplXcvSVm" + "_id": "dbyNVW0ZNK04Fa58DYtoBQ" }, { "__type__": "cc.CompPrefabInfo", @@ -798,11 +819,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 29 }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 35 }, "_alignFlags": 16, "_target": null, @@ -822,7 +843,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "062kJh7vtAlqCcCZAGJDnc" + "_id": "889ng+wu1B6ou+AVIdRY4l" }, { "__type__": "cc.CompPrefabInfo", @@ -841,402 +862,6 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, - { - "__type__": "cc.Node", - "_name": "Spirit489", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 36 - }, - { - "__id__": 38 - }, - { - "__id__": 40 - } - ], - "_prefab": { - "__id__": 42 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -116, - "y": 292, - "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": "82jqkCcRpPaqCuLSE66TAF" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 35 - }, - "_enabled": true, - "__prefab": { - "__id__": 37 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 0, - "height": 25.2 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "63Ero2fPJJQKcn46JWbbTE" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "RIYGfzICkAdy5iE3sjBeJw" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 35 - }, - "_enabled": true, - "__prefab": { - "__id__": 39 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 20, - "_overflow": 0, - "_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": "d7VpZ+8hJMXrJVCiy9pcOj" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "cC6XtMgHyboEMoF0u0Z/nQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 35 - }, - "_enabled": true, - "__prefab": { - "__id__": 41 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -116, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "a8kvrIzORJFpNGPmr5Olo+" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Kw+fA7qf+HslBB0k+8xurw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "QnfAYSsefzddE6eEBMLOTQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit490", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 44 - }, - { - "__id__": 46 - }, - { - "__id__": 48 - } - ], - "_prefab": { - "__id__": 50 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -16, - "y": 232, - "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": "f6ddgTWIBKlI2//779bNuP" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 45 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 0, - "height": 25.2 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "a2KueCNpdKZbQEG/4XH/Uo" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Pc2N92HNfLlurD7PcnKfug" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 47 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 20, - "_overflow": 0, - "_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": "61x3YWRrxDtKWoxUc7w3Bz" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "pAFMX4tr3MnOsHfyjFTKTA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 49 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -16, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "870JWkG2JDAbrGIZbTlwsh" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Aul+NJ0vkflxsM22b5TFWA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "yUPROsXjpcBn9qKOJGaAhQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, { "__type__": "cc.Node", "_name": "Spirit491", @@ -1246,20 +871,20 @@ "__id__": 2 }, "_children": [], - "_active": true, + "_active": false, "_components": [ { - "__id__": 52 + "__id__": 38 }, { - "__id__": 54 + "__id__": 40 }, { - "__id__": 56 + "__id__": 42 } ], "_prefab": { - "__id__": 58 + "__id__": 44 }, "_lpos": { "__type__": "cc.Vec3", @@ -1288,7 +913,7 @@ "y": 0, "z": 0 }, - "_id": "1dQDeZuSVJ3YZJeQhKHJJM" + "_id": "31ZFZECENMU66B/c++jSVL" }, { "__type__": "cc.UITransform", @@ -1296,11 +921,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 53 + "__id__": 39 }, "_contentSize": { "__type__": "cc.Size", @@ -1312,7 +937,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "c2YSFT8XBF3JU7zFerQZOV" + "_id": "abCzOwzyJLUJlcUCsTkAp9" }, { "__type__": "cc.CompPrefabInfo", @@ -1324,11 +949,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 55 + "__id__": 41 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1380,7 +1005,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "d1ZS3to0hARaWkJuvfBUrQ" + "_id": "e8J/fPQ85A8Kg3AEIdPoFU" }, { "__type__": "cc.CompPrefabInfo", @@ -1392,11 +1017,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 57 + "__id__": 43 }, "_alignFlags": 16, "_target": null, @@ -1416,7 +1041,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "6dcx3w755M97EWo0+p8Lv1" + "_id": "3fBr+6ospPpIameMVyR3z0" }, { "__type__": "cc.CompPrefabInfo", @@ -1444,20 +1069,20 @@ "__id__": 2 }, "_children": [], - "_active": true, + "_active": false, "_components": [ { - "__id__": 60 + "__id__": 46 }, { - "__id__": 62 + "__id__": 48 }, { - "__id__": 64 + "__id__": 50 } ], "_prefab": { - "__id__": 66 + "__id__": 52 }, "_lpos": { "__type__": "cc.Vec3", @@ -1486,7 +1111,7 @@ "y": 0, "z": 0 }, - "_id": "769hL64JlKY5pZVJp/cpaF" + "_id": "a7iHq2iitMqZS7o7Tm8qUF" }, { "__type__": "cc.UITransform", @@ -1494,11 +1119,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 45 }, "_enabled": true, "__prefab": { - "__id__": 61 + "__id__": 47 }, "_contentSize": { "__type__": "cc.Size", @@ -1510,7 +1135,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "34gBaa0qhBJIlt9hJ/f8Cm" + "_id": "8a10BIvcpBZ5fZYPtIVob2" }, { "__type__": "cc.CompPrefabInfo", @@ -1522,11 +1147,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 45 }, "_enabled": true, "__prefab": { - "__id__": 63 + "__id__": 49 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1578,7 +1203,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "30PlhbfEBFh4ihFM5KvOLZ" + "_id": "60MYW8l2hKm6mKqJFdcrJD" }, { "__type__": "cc.CompPrefabInfo", @@ -1590,11 +1215,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 45 }, "_enabled": true, "__prefab": { - "__id__": 65 + "__id__": 51 }, "_alignFlags": 16, "_target": null, @@ -1614,7 +1239,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "9cq0lC8GJOLJorV8Y6o02u" + "_id": "73lYPRBZpFs7tNAeAKBd8Q" }, { "__type__": "cc.CompPrefabInfo", @@ -1645,17 +1270,17 @@ "_active": true, "_components": [ { - "__id__": 68 + "__id__": 54 }, { - "__id__": 70 + "__id__": 56 }, { - "__id__": 72 + "__id__": 58 } ], "_prefab": { - "__id__": 74 + "__id__": 60 }, "_lpos": { "__type__": "cc.Vec3", @@ -1684,7 +1309,7 @@ "y": 0, "z": 0 }, - "_id": "797b1rz7FKi46aNIpTqRGo" + "_id": "d2no+P1vRCdKzt73SGy+S/" }, { "__type__": "cc.UITransform", @@ -1692,11 +1317,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 53 }, "_enabled": true, "__prefab": { - "__id__": 69 + "__id__": 55 }, "_contentSize": { "__type__": "cc.Size", @@ -1708,7 +1333,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "defGZC2x5BA7bHFvIT/5yY" + "_id": "2eidB0kshBebqSKLwuRYCO" }, { "__type__": "cc.CompPrefabInfo", @@ -1720,11 +1345,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 53 }, "_enabled": true, "__prefab": { - "__id__": 71 + "__id__": 57 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1753,7 +1378,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "ecdgIr8IdPkI1NQKC99f7n" + "_id": "81CQ+vy65LZr78n9qU2lrM" }, { "__type__": "cc.CompPrefabInfo", @@ -1765,11 +1390,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 53 }, "_enabled": true, "__prefab": { - "__id__": 73 + "__id__": 59 }, "_alignFlags": 16, "_target": null, @@ -1789,7 +1414,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "cbUqec9E9OR4hM7hEQea8v" + "_id": "76oHzSEHNLraRoieUe48Ij" }, { "__type__": "cc.CompPrefabInfo", @@ -1808,6 +1433,908 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, + { + "__type__": "cc.Node", + "_name": "NameInput", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 62 + }, + { + "__id__": 68 + } + ], + "_active": true, + "_components": [ + { + "__id__": 74 + }, + { + "__id__": 76 + } + ], + "_prefab": { + "__id__": 78 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 292, + "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": "7cOPXwHiBL+LpVI8WpDsv7" + }, + { + "__type__": "cc.Node", + "_name": "Spirit489", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 61 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 63 + }, + { + "__id__": 65 + } + ], + "_prefab": { + "__id__": 67 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -180, + "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": "4214n1Cr1MHJVqxSJxHjW4" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 62 + }, + "_enabled": true, + "__prefab": { + "__id__": 64 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 360, + "height": 30 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "48cfCmNSVDnrZDb97kKRVL" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "RIYGfzICkAdy5iE3sjBeJw" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 62 + }, + "_enabled": true, + "__prefab": { + "__id__": 66 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 0, + "_verticalAlign": 1, + "_actualFontSize": 20, + "_fontSize": 20, + "_fontFamily": "Arial", + "_lineHeight": 20, + "_overflow": 1, + "_enableWrapText": false, + "_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": "6447BLB/BALrUVE6XkHcQm" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "cC6XtMgHyboEMoF0u0Z/nQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "QnfAYSsefzddE6eEBMLOTQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Placeholder", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 61 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 69 + }, + { + "__id__": 71 + } + ], + "_prefab": { + "__id__": 73 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -180, + "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": "e1dQqjsZ5FobTmHDZLxQhm" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 68 + }, + "_enabled": true, + "__prefab": { + "__id__": 70 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 360, + "height": 30 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "866ub42xhAnJYgrEDB8/1l" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Vx0Jk7o478CutZ1gW91TiA" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 68 + }, + "_enabled": true, + "__prefab": { + "__id__": 72 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 130, + "g": 130, + "b": 130, + "a": 255 + }, + "_string": "请输入姓名", + "_horizontalAlign": 0, + "_verticalAlign": 1, + "_actualFontSize": 20, + "_fontSize": 20, + "_fontFamily": "Arial", + "_lineHeight": 20, + "_overflow": 1, + "_enableWrapText": false, + "_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": "25V7EP63hOXoCyrhgxo9EA" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2/D+Ha3yzO7RE6FPQbSvAg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "kHkiYRs+IMSl+lkZZfHdeg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 61 + }, + "_enabled": true, + "__prefab": { + "__id__": 75 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 380, + "height": 42 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "5bqfMXa6pNY7L8J/cZK96q" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "qCa9fEKuJduhOsfIRNLhPw" + }, + { + "__type__": "cc.EditBox", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 61 + }, + "_enabled": true, + "__prefab": { + "__id__": 77 + }, + "editingDidBegan": [], + "textChanged": [], + "editingDidEnded": [], + "editingReturn": [], + "_textLabel": { + "__id__": 65 + }, + "_placeholderLabel": { + "__id__": 71 + }, + "_returnType": 1, + "_string": "", + "_tabIndex": 0, + "_backgroundImage": null, + "_inputFlag": 5, + "_inputMode": 6, + "_maxLength": -1, + "_id": "f8hvQvFZlFn4KJPm2BpBlz" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "uE3LIayyHFDrDsu0TDELGg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "qqdOZIXf0zc0hJ/4XXYE6w", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "IdInput", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 80 + }, + { + "__id__": 86 + } + ], + "_active": true, + "_components": [ + { + "__id__": 92 + }, + { + "__id__": 94 + } + ], + "_prefab": { + "__id__": 96 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 232, + "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": "6eDqcDHONMLJFhF2ClZ6jR" + }, + { + "__type__": "cc.Node", + "_name": "Spirit490", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 79 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 81 + }, + { + "__id__": 83 + } + ], + "_prefab": { + "__id__": 85 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -180, + "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": "ce8NYN9pJPjqZ/Cx+qkNrZ" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 80 + }, + "_enabled": true, + "__prefab": { + "__id__": 82 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 360, + "height": 30 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "4d+fV+lhhO4qNKtBetTSac" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Pc2N92HNfLlurD7PcnKfug" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 80 + }, + "_enabled": true, + "__prefab": { + "__id__": 84 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 0, + "_verticalAlign": 1, + "_actualFontSize": 20, + "_fontSize": 20, + "_fontFamily": "Arial", + "_lineHeight": 20, + "_overflow": 1, + "_enableWrapText": false, + "_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": "84UUup4W1JIpgwfadJnW4Z" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "pAFMX4tr3MnOsHfyjFTKTA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "yUPROsXjpcBn9qKOJGaAhQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Placeholder", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 79 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 87 + }, + { + "__id__": 89 + } + ], + "_prefab": { + "__id__": 91 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -180, + "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": "f4d/sLKtVDF79XbGCivajq" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 86 + }, + "_enabled": true, + "__prefab": { + "__id__": 88 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 360, + "height": 30 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 1 + }, + "_id": "7bQ51RIHxPioVY8dRWYg6T" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "hMvCwR7AV/FFBnmDhgGjVA" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 86 + }, + "_enabled": true, + "__prefab": { + "__id__": 90 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 130, + "g": 130, + "b": 130, + "a": 255 + }, + "_string": "请输入身份证号", + "_horizontalAlign": 0, + "_verticalAlign": 1, + "_actualFontSize": 20, + "_fontSize": 20, + "_fontFamily": "Arial", + "_lineHeight": 20, + "_overflow": 1, + "_enableWrapText": false, + "_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": "10/9Tw3T5P0Ybmfspr2PwN" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "8s4nNz30mTlgE2XxLtsI6w" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "hqsUpeujzjXIz8teyGezEg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 79 + }, + "_enabled": true, + "__prefab": { + "__id__": 93 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 380, + "height": 42 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "9clCbcWDVGT54V5XYwNLNO" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "VJ/aXS+A5qNZcEHBtpPLQA" + }, + { + "__type__": "cc.EditBox", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 79 + }, + "_enabled": true, + "__prefab": { + "__id__": 95 + }, + "editingDidBegan": [], + "textChanged": [], + "editingDidEnded": [], + "editingReturn": [], + "_textLabel": { + "__id__": 83 + }, + "_placeholderLabel": { + "__id__": 89 + }, + "_returnType": 1, + "_string": "", + "_tabIndex": 0, + "_backgroundImage": null, + "_inputFlag": 5, + "_inputMode": 6, + "_maxLength": -1, + "_id": "e7aius77pCKbWhVde6zr5i" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "UQ9GJYhjjdT87DwaKOGkHQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "zFynmtVJzKqcinFcmYHrMA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.UITransform", "_name": "", @@ -1818,7 +2345,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 76 + "__id__": 98 }, "_contentSize": { "__type__": "cc.Size", @@ -1830,7 +2357,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "e13hDXIU9IuLw9FLHXXxfT" + "_id": "76Ubdi30NJ8ZL7Gmd9uWuv" }, { "__type__": "cc.CompPrefabInfo", @@ -1846,7 +2373,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 78 + "__id__": 100 }, "_alignFlags": 45, "_target": null, @@ -1866,7 +2393,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "40+PjZ0HVPeJyh1ZK5eo00" + "_id": "6307TK17ZG5aUOBndh4soa" }, { "__type__": "cc.CompPrefabInfo", @@ -1895,7 +2422,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 81 + "__id__": 103 }, "_contentSize": { "__type__": "cc.Size", @@ -1907,7 +2434,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "bbAYUGdVRJmZPEgXeLIPC+" + "_id": "1bMPftee1AjIFRNwZbvVuO" }, { "__type__": "cc.CompPrefabInfo", @@ -1923,7 +2450,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 83 + "__id__": 105 }, "_alignFlags": 45, "_target": null, @@ -1943,7 +2470,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "a1RZ3jputGKZoWzS2J4hUm" + "_id": "853sWv7XFGIJ4YdjCphdDg" }, { "__type__": "cc.CompPrefabInfo", @@ -1962,4 +2489,4 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null } -] +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer21_RankList.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer21_RankList.prefab index 9018a22..f450ebc 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer21_RankList.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer21_RankList.prefab @@ -25,19 +25,19 @@ "_active": true, "_components": [ { - "__id__": 168 + "__id__": 138 }, { - "__id__": 170 + "__id__": 140 } ], "_prefab": { - "__id__": 172 + "__id__": 142 }, "_lpos": { "__type__": "cc.Vec3", - "x": 0, - "y": 0, + "x": -800, + "y": -360, "z": 0 }, "_lrot": { @@ -61,7 +61,7 @@ "y": 0, "z": 0 }, - "_id": "b4HbWQuI5AQJErVSiDcIKz" + "_id": "" }, { "__type__": "cc.Node", @@ -76,74 +76,50 @@ "__id__": 3 }, { - "__id__": 11 + "__id__": 45 }, { - "__id__": 19 + "__id__": 53 }, { - "__id__": 27 + "__id__": 61 }, { - "__id__": 35 + "__id__": 69 }, { - "__id__": 43 + "__id__": 77 }, { - "__id__": 51 + "__id__": 85 }, { - "__id__": 59 + "__id__": 93 }, { - "__id__": 67 + "__id__": 101 }, { - "__id__": 75 + "__id__": 109 }, { - "__id__": 83 + "__id__": 117 }, { - "__id__": 91 - }, - { - "__id__": 99 - }, - { - "__id__": 107 - }, - { - "__id__": 115 - }, - { - "__id__": 123 - }, - { - "__id__": 131 - }, - { - "__id__": 139 - }, - { - "__id__": 147 - }, - { - "__id__": 155 + "__id__": 125 } ], "_active": true, "_components": [ { - "__id__": 163 + "__id__": 133 }, { - "__id__": 165 + "__id__": 135 } ], "_prefab": { - "__id__": 167 + "__id__": 137 }, "_lpos": { "__type__": "cc.Vec3", @@ -172,7 +148,7 @@ "y": 0, "z": 0 }, - "_id": "fbIXcp+HROSKyw1UeL1hWq" + "_id": "" }, { "__type__": "cc.Node", @@ -182,21 +158,25 @@ "_parent": { "__id__": 2 }, - "_children": [], + "_children": [ + { + "__id__": 4 + } + ], "_active": true, "_components": [ { - "__id__": 4 + "__id__": 38 }, { - "__id__": 6 + "__id__": 40 }, { - "__id__": 8 + "__id__": 42 } ], "_prefab": { - "__id__": 10 + "__id__": 44 }, "_lpos": { "__type__": "cc.Vec3", @@ -225,7 +205,234 @@ "y": 0, "z": 0 }, - "_id": "d5s5BHNQdP67jljtxhNRQ9" + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Spirit606", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 3 + }, + "_children": [ + { + "__id__": 5 + } + ], + "_active": true, + "_components": [ + { + "__id__": 31 + }, + { + "__id__": 33 + }, + { + "__id__": 35 + } + ], + "_prefab": { + "__id__": 37 + }, + "_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": "Spirit607", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 4 + }, + "_children": [ + { + "__id__": 6 + } + ], + "_active": true, + "_components": [ + { + "__id__": 22 + }, + { + "__id__": 24 + }, + { + "__id__": 26 + }, + { + "__id__": 28 + } + ], + "_prefab": { + "__id__": 30 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -277.273, + "y": -47.77199999999999, + "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": "view", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 5 + }, + "_children": [ + { + "__id__": 7 + } + ], + "_active": true, + "_components": [ + { + "__id__": 13 + }, + { + "__id__": 15 + }, + { + "__id__": 17 + }, + { + "__id__": 19 + } + ], + "_prefab": { + "__id__": 21 + }, + "_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": "content", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 6 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 8 + }, + { + "__id__": 10 + } + ], + "_prefab": { + "__id__": 12 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 255.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": "" }, { "__type__": "cc.UITransform", @@ -233,39 +440,140 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 3 + "__id__": 7 }, "_enabled": true, "__prefab": { - "__id__": 5 + "__id__": 9 }, "_contentSize": { "__type__": "cc.Size", - "width": 1600, - "height": 720 + "width": 465, + "height": 8 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 1 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "a1vOsko9pFYoNPgmHqh4v0" + }, + { + "__type__": "cc.Layout", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 7 + }, + "_enabled": true, + "__prefab": { + "__id__": 11 + }, + "_resizeMode": 1, + "_layoutType": 2, + "_cellSize": { + "__type__": "cc.Size", + "width": 40, + "height": 40 + }, + "_startAxis": 0, + "_paddingLeft": 0, + "_paddingRight": 0, + "_paddingTop": 8, + "_paddingBottom": 8, + "_spacingX": 0, + "_spacingY": 8, + "_verticalDirection": 1, + "_horizontalDirection": 0, + "_constraint": 0, + "_constraintNum": 2, + "_affectedByScale": false, + "_isAlign": false, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "b2z2Tn5q1LRq9K9ywLgJBs" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9fe0Z9Rp1CioAt/SBH5cr5", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 6 + }, + "_enabled": true, + "__prefab": { + "__id__": 14 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 465, + "height": 511 }, "_anchorPoint": { "__type__": "cc.Vec2", "x": 0.5, "y": 0.5 }, - "_id": "cd/GKL4dtCgp7xAjhk8gTA" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "f93ZVApcgrISUdQQu8b08A" + "fileId": "53NpVvWyVFeoLUD2idlmeb" }, { - "__type__": "cc.Sprite", + "__type__": "cc.Mask", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 3 + "__id__": 6 }, "_enabled": true, "__prefab": { - "__id__": 7 + "__id__": 16 + }, + "_type": 0, + "_inverted": false, + "_segments": 64, + "_alphaThreshold": 0.1, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "8djH/p5UtKKJ7r/UBJg+YH" + }, + { + "__type__": "cc.Graphics", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 6 + }, + "_enabled": true, + "__prefab": { + "__id__": 18 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -277,28 +585,29 @@ "b": 255, "a": 255 }, - "_spriteFrame": { - "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@f9941", - "__expectedType__": "cc.SpriteFrame" + "_lineWidth": 1, + "_strokeColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 }, - "_type": 0, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 + "_lineJoin": 2, + "_lineCap": 0, + "_fillColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 0 }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "80nfl+R3ZP1JEJnIAtQ6zB" + "_miterLimit": 10, + "_id": "" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "Nf2LpjdC338++Px94L7R6w" + "fileId": "45wc9JWENHUaLGPDIIYPYq" }, { "__type__": "cc.Widget", @@ -306,11 +615,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 3 + "__id__": 6 }, "_enabled": true, "__prefab": { - "__id__": 9 + "__id__": 20 }, "_alignFlags": 45, "_target": null, @@ -326,15 +635,15 @@ "_isAbsBottom": true, "_isAbsHorizontalCenter": true, "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, + "_originalWidth": 465, + "_originalHeight": 511, "_alignMode": 2, "_lockFlags": 0, - "_id": "e7nVIB5k5GZbi6TWwhOKrj" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "ImB+FSxmoEamYZIurs6odw" + "fileId": "c6YhseU/VLI6SoZpu6y4hq" }, { "__type__": "cc.PrefabInfo", @@ -344,250 +653,22 @@ "asset": { "__id__": 0 }, - "fileId": "X9Q0v1QEIaEHnWeOO7HfhA", + "fileId": "1dM4HX14JEbrgrdZtgnIgL", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null }, - { - "__type__": "cc.Node", - "_name": "Spirit606", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 12 - }, - { - "__id__": 14 - }, - { - "__id__": 16 - } - ], - "_prefab": { - "__id__": 18 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 8, - "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": "b6lPknOB9BXo0YRKqMJKvd" - }, { "__type__": "cc.UITransform", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 5 }, "_enabled": true, "__prefab": { - "__id__": 13 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1100, - "height": 692 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "d5Eo6+tRREMZ6i615tYTW8" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Vm/xEXlmR5+gAK4oaf4vyw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 11 - }, - "_enabled": true, - "__prefab": { - "__id__": 15 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "8eb7acc3-aba1-4fcf-b375-a3bfb5303472@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "3f5yuKAxdNIJnGoSp0EMRS" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "c/zNB+z4veBtmKplPZ7GeA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 11 - }, - "_enabled": true, - "__prefab": { - "__id__": 17 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "e6UKyD0nlMs6VnE1VNlu+u" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Dr79S8KHEGWJKL32YUB6yQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "6IIpcpz5vhK1L59+usnWNg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit607", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 20 - }, - { - "__id__": 22 - }, - { - "__id__": 24 - } - ], - "_prefab": { - "__id__": 26 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -269.5, - "y": -37.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": "17Ms3TftRIgZRN97/E9GNt" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 19 - }, - "_enabled": true, - "__prefab": { - "__id__": 21 + "__id__": 23 }, "_contentSize": { "__type__": "cc.Size", @@ -599,7 +680,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "d47tzuydxBiInlJi7rD0/e" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -611,11 +692,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 5 }, "_enabled": true, "__prefab": { - "__id__": 23 + "__id__": 25 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -644,7 +725,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "ebpVwQY/9Bgb0XokZBGRsa" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -656,19 +737,19 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 5 }, "_enabled": true, "__prefab": { - "__id__": 25 + "__id__": 27 }, - "_alignFlags": 16, + "_alignFlags": 12, "_target": null, - "_left": 0, + "_left": 40.226999999999975, "_right": 0, "_top": 0, - "_bottom": 0, - "_horizontalCenter": -269.5, + "_bottom": 42.72799999999999, + "_horizontalCenter": -277.273, "_verticalCenter": 0, "_isAbsLeft": true, "_isAbsRight": true, @@ -680,12 +761,43 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "edgxgiR7ZGSbXQHlT0645/" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", "fileId": "SBvK8wGMvx75GPv697zn5g" }, + { + "__type__": "cc.ScrollView", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 5 + }, + "_enabled": true, + "__prefab": { + "__id__": 29 + }, + "bounceDuration": 1, + "brake": 0.5, + "elastic": true, + "inertia": true, + "horizontal": false, + "vertical": true, + "cancelInnerEvents": true, + "scrollEvents": [], + "_content": { + "__id__": 7 + }, + "_horizontalScrollBar": null, + "_verticalScrollBar": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "d5fINZ17JP8pdUouHLe6uS" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -699,6 +811,250 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 4 + }, + "_enabled": true, + "__prefab": { + "__id__": 32 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1100, + "height": 692 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Vm/xEXlmR5+gAK4oaf4vyw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 4 + }, + "_enabled": true, + "__prefab": { + "__id__": 34 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "8eb7acc3-aba1-4fcf-b375-a3bfb5303472@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_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": "c/zNB+z4veBtmKplPZ7GeA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 4 + }, + "_enabled": true, + "__prefab": { + "__id__": 36 + }, + "_alignFlags": 18, + "_target": null, + "_left": 250, + "_right": 0, + "_top": 0, + "_bottom": 22, + "_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": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Dr79S8KHEGWJKL32YUB6yQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "6IIpcpz5vhK1L59+usnWNg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 39 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "f93ZVApcgrISUdQQu8b08A" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 41 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@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": "Nf2LpjdC338++Px94L7R6w" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 43 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ImB+FSxmoEamYZIurs6odw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "X9Q0v1QEIaEHnWeOO7HfhA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.Node", "_name": "Spirit608", @@ -711,17 +1067,17 @@ "_active": true, "_components": [ { - "__id__": 28 + "__id__": 46 }, { - "__id__": 30 + "__id__": 48 }, { - "__id__": 32 + "__id__": 50 } ], "_prefab": { - "__id__": 34 + "__id__": 52 }, "_lpos": { "__type__": "cc.Vec3", @@ -750,7 +1106,7 @@ "y": 0, "z": 0 }, - "_id": "6d6M3+YydErperbLnid98k" + "_id": "" }, { "__type__": "cc.UITransform", @@ -758,11 +1114,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 45 }, "_enabled": true, "__prefab": { - "__id__": 29 + "__id__": 47 }, "_contentSize": { "__type__": "cc.Size", @@ -774,7 +1130,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "ceoQ9MQJlMjKMQRYHjlk+C" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -786,11 +1142,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 45 }, "_enabled": true, "__prefab": { - "__id__": 31 + "__id__": 49 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -819,7 +1175,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "85wVtQqpNK1oWRHPi8jqCL" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -831,11 +1187,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 45 }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 51 }, "_alignFlags": 16, "_target": null, @@ -855,7 +1211,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "248nduw0FEzqRNPsjhgygX" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -886,17 +1242,17 @@ "_active": true, "_components": [ { - "__id__": 36 + "__id__": 54 }, { - "__id__": 38 + "__id__": 56 }, { - "__id__": 40 + "__id__": 58 } ], "_prefab": { - "__id__": 42 + "__id__": 60 }, "_lpos": { "__type__": "cc.Vec3", @@ -925,7 +1281,7 @@ "y": 0, "z": 0 }, - "_id": "96zaJfmXtGLa5n2kRNcdSz" + "_id": "" }, { "__type__": "cc.UITransform", @@ -933,11 +1289,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 53 }, "_enabled": true, "__prefab": { - "__id__": 37 + "__id__": 55 }, "_contentSize": { "__type__": "cc.Size", @@ -949,7 +1305,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "daAEBv0B1PBr6yZNHQtWL0" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -961,11 +1317,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 53 }, "_enabled": true, "__prefab": { - "__id__": 39 + "__id__": 57 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -994,7 +1350,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "b8XQ8bckVLCr2C0yLLYszZ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1006,11 +1362,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 53 }, "_enabled": true, "__prefab": { - "__id__": 41 + "__id__": 59 }, "_alignFlags": 16, "_target": null, @@ -1030,7 +1386,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "2c9sJmQmZBlbqzfR5Z4xgK" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1049,706 +1405,6 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, - { - "__type__": "cc.Node", - "_name": "Spirit610", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 44 - }, - { - "__id__": 46 - }, - { - "__id__": 48 - } - ], - "_prefab": { - "__id__": 50 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -304, - "y": -423.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": "edQ2XAhG1CG4U7w76qDcU0" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 45 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 420, - "height": 87 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "58PZHHn7lNR7YxAN9ej8st" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Nwh6Ijeunt3VWGH+MEPpmA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 47 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "fe744ec6-7245-428f-aa0e-b88e9c2bb60f@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "81N0FVvixMhYBnFj5STj7j" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "6Tz+S59phHxmcei2uqFIcQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 49 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -304, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "be4msEs21M+pvH49mqO7nL" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "i+afOLPiJyNPCC7JmMZO0Q" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "B0hAMRyLtj7BMrQHmiKwtg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit629", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 52 - }, - { - "__id__": 54 - }, - { - "__id__": 56 - } - ], - "_prefab": { - "__id__": 58 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -487, - "y": -421.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": "9cBlpuxzRBQ7gv4P40QY+/" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 51 - }, - "_enabled": true, - "__prefab": { - "__id__": 53 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 88, - "height": 93 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "2dKBbwAKdHd6zbATH7I0vc" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "RHW1RAJtRZyOFWtP4GVTTQ" - }, - { - "__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__": "fb86ab44-8140-4d8e-82f1-84ae4b8f3216@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": "9chMe+8w9DKLW01fGY4+se" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "9W996fSfaJcBjjX4ERj52w" - }, - { - "__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": -487, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "abQIAme6RGnY+3GNsidAgu" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "wuAuOyVEZYARxPxs0g7npg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "A1G/yuuNfTGtrguIV1Oa4w", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit630", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 60 - }, - { - "__id__": 62 - }, - { - "__id__": 64 - } - ], - "_prefab": { - "__id__": 66 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -387.5, - "y": -423.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": "d7TQEnavpPTbpkdRaht7J9" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 59 - }, - "_enabled": true, - "__prefab": { - "__id__": 61 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 65, - "height": 65 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "12ulF0jM9IKIV3uVONzyqF" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "3X1HgLIx2jIcoDM6xZzzdQ" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 59 - }, - "_enabled": true, - "__prefab": { - "__id__": 63 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "0c7ab485-8adf-432e-8dfb-27dff1dde280@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": "59czbZcQhI1YOtdD0Lh4bN" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "n7F+BXQt2QUWld02MbOBlg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 59 - }, - "_enabled": true, - "__prefab": { - "__id__": 65 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -387.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "ebWIUKZHRHN5GtMMmbHQ6U" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "I7ZafOJuEMVYpYYbhFmWkQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "z3TXOW8MyPObRFfGKLKklg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit631", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 68 - }, - { - "__id__": 70 - }, - { - "__id__": 72 - } - ], - "_prefab": { - "__id__": 74 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -388, - "y": -423, - "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": "3cnoCydsxIhoeerWCTsCcq" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 67 - }, - "_enabled": true, - "__prefab": { - "__id__": 69 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 80, - "height": 80 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "16sT+C8mNKQKPTDpjSO2O6" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "3qwjPNmdCtSBjtAcUYm3nQ" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 67 - }, - "_enabled": true, - "__prefab": { - "__id__": 71 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "b6310388-a682-483c-9d39-17c8876f0b49@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": "0bxb5rlI1G/4HWiDHcRZka" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "D3F+vukwdaYIBLdm1cTMrQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 67 - }, - "_enabled": true, - "__prefab": { - "__id__": 73 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -388, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "e04ir/xZFGHIhvw0gnoEpy" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "vy8FDIIQTjQSwQQ94JVFxA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "ZkAI72k46m/gxeG0z4fdRQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, { "__type__": "cc.Node", "_name": "Spirit634", @@ -1761,17 +1417,17 @@ "_active": true, "_components": [ { - "__id__": 76 + "__id__": 62 }, { - "__id__": 78 + "__id__": 64 }, { - "__id__": 80 + "__id__": 66 } ], "_prefab": { - "__id__": 82 + "__id__": 68 }, "_lpos": { "__type__": "cc.Vec3", @@ -1800,7 +1456,7 @@ "y": 0, "z": 0 }, - "_id": "a3d6zhkfhM2rQgT9h4+iUB" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1808,11 +1464,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 61 }, "_enabled": true, "__prefab": { - "__id__": 77 + "__id__": 63 }, "_contentSize": { "__type__": "cc.Size", @@ -1824,7 +1480,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "53e/ryITJOxL//yotuJZuT" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1836,11 +1492,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 61 }, "_enabled": true, "__prefab": { - "__id__": 79 + "__id__": 65 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1869,7 +1525,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "a7YRGlb6pE0qKOe5O2yKHV" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1881,11 +1537,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 61 }, "_enabled": true, "__prefab": { - "__id__": 81 + "__id__": 67 }, "_alignFlags": 16, "_target": null, @@ -1905,7 +1561,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "004aM7VpJMOYqVnbjTNEuG" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1936,22 +1592,22 @@ "_active": true, "_components": [ { - "__id__": 84 + "__id__": 70 }, { - "__id__": 86 + "__id__": 72 }, { - "__id__": 88 + "__id__": 74 } ], "_prefab": { - "__id__": 90 + "__id__": 76 }, "_lpos": { "__type__": "cc.Vec3", - "x": 24, - "y": 453, + "x": -1293.002, + "y": 723.097, "z": 0 }, "_lrot": { @@ -1975,7 +1631,7 @@ "y": 0, "z": 0 }, - "_id": "ecufLk3apOF40TIMR6NnXF" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1983,11 +1639,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 69 }, "_enabled": true, "__prefab": { - "__id__": 85 + "__id__": 71 }, "_contentSize": { "__type__": "cc.Size", @@ -1999,7 +1655,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "c11Ou1agRKZbsvEOJsa74y" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2011,11 +1667,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 69 }, "_enabled": true, "__prefab": { - "__id__": 87 + "__id__": 73 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2044,7 +1700,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "08xsoaA4pISqgg3wlk4+Am" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2056,11 +1712,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 69 }, "_enabled": true, "__prefab": { - "__id__": 89 + "__id__": 75 }, "_alignFlags": 16, "_target": null, @@ -2068,7 +1724,7 @@ "_right": 0, "_top": 0, "_bottom": 0, - "_horizontalCenter": 24, + "_horizontalCenter": -1293.002, "_verticalCenter": 0, "_isAbsLeft": true, "_isAbsRight": true, @@ -2080,7 +1736,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "a0IadSB3NBYIzgR+xXR5P2" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2111,17 +1767,17 @@ "_active": true, "_components": [ { - "__id__": 92 + "__id__": 78 }, { - "__id__": 94 + "__id__": 80 }, { - "__id__": 96 + "__id__": 82 } ], "_prefab": { - "__id__": 98 + "__id__": 84 }, "_lpos": { "__type__": "cc.Vec3", @@ -2150,7 +1806,7 @@ "y": 0, "z": 0 }, - "_id": "4dxr5gJlZL9r+R1HHpHxPq" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2158,11 +1814,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 77 }, "_enabled": true, "__prefab": { - "__id__": 93 + "__id__": 79 }, "_contentSize": { "__type__": "cc.Size", @@ -2174,7 +1830,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "972bySpRlPo42qylQd9MXP" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2186,11 +1842,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 77 }, "_enabled": true, "__prefab": { - "__id__": 95 + "__id__": 81 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2219,7 +1875,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "f4VdCzYGdGS4p1Hd4/eWqz" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2231,11 +1887,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 77 }, "_enabled": true, "__prefab": { - "__id__": 97 + "__id__": 83 }, "_alignFlags": 16, "_target": null, @@ -2255,7 +1911,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "bd37ovfERDR6Q/NHiw3u09" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2286,17 +1942,17 @@ "_active": true, "_components": [ { - "__id__": 100 + "__id__": 86 }, { - "__id__": 102 + "__id__": 88 }, { - "__id__": 104 + "__id__": 90 } ], "_prefab": { - "__id__": 106 + "__id__": 92 }, "_lpos": { "__type__": "cc.Vec3", @@ -2325,7 +1981,7 @@ "y": 0, "z": 0 }, - "_id": "25xmvVyWRE9a74Lvb6L9Rh" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2333,11 +1989,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 101 + "__id__": 87 }, "_contentSize": { "__type__": "cc.Size", @@ -2349,7 +2005,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "27rE9+LshFF7juRgi1EnSm" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2361,11 +2017,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 103 + "__id__": 89 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2394,7 +2050,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "3aGoUqa6NAwbkqndGjgRr5" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2406,11 +2062,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 105 + "__id__": 91 }, "_alignFlags": 16, "_target": null, @@ -2430,7 +2086,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "60VKW8ZF1JsY9cOihoAksX" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2449,402 +2105,6 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, - { - "__type__": "cc.Node", - "_name": "Spirit632", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 108 - }, - { - "__id__": 110 - }, - { - "__id__": 112 - } - ], - "_prefab": { - "__id__": 114 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -222.5, - "y": -406.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": "933XfHzL1MmoXx8MkULxI1" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 107 - }, - "_enabled": true, - "__prefab": { - "__id__": 109 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 143.8896484375, - "height": 17.64 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "22NCkw7DpJqrFmVJFZcKeb" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "koRH8USkMNn+923Ek85ESA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 107 - }, - "_enabled": true, - "__prefab": { - "__id__": 111 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "色丰富飞的放到的是是 ", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 0, - "_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": "40hlmfB29Ptq+GCHdOhAD0" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "gtX0YS/cxPkBadNtM6Np8Q" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 107 - }, - "_enabled": true, - "__prefab": { - "__id__": 113 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -222.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "58NSM62SZNOJ+eq9aBHLO1" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "azh2gglKCKAtSMUoXnSwBg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "IHEQgdLsuv4aAoI46svDJw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit633", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 116 - }, - { - "__id__": 118 - }, - { - "__id__": 120 - } - ], - "_prefab": { - "__id__": 122 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -282.5, - "y": -439.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": "79SfJGAn9PH6C0Of40xyzb" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 115 - }, - "_enabled": true, - "__prefab": { - "__id__": 117 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 55.2548828125, - "height": 17.64 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "91VyKb+lVPY4tBOd60Mg5w" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "kL2jULANfD7At1ahubiGEA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 115 - }, - "_enabled": true, - "__prefab": { - "__id__": 119 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "Spirit632", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 14, - "_fontSize": 14, - "_fontFamily": "Arial", - "_lineHeight": 14, - "_overflow": 0, - "_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": "b2PIRziapAwYlFE7rlVnIe" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "p6KJdKg4HjyR92Wchz3trg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 115 - }, - "_enabled": true, - "__prefab": { - "__id__": 121 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -282.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "81j6hyinlA+IWC65MWVQ7j" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "RsWtDKfgruwYiEoyyItdMw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "AJ22io3uQj273HdYhJB+eg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, { "__type__": "cc.Node", "_name": "Spirit636", @@ -2857,17 +2117,17 @@ "_active": true, "_components": [ { - "__id__": 124 + "__id__": 94 }, { - "__id__": 126 + "__id__": 96 }, { - "__id__": 128 + "__id__": 98 } ], "_prefab": { - "__id__": 130 + "__id__": 100 }, "_lpos": { "__type__": "cc.Vec3", @@ -2896,7 +2156,7 @@ "y": 0, "z": 0 }, - "_id": "ffAANBH1VCapyzyah93z9O" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2904,15 +2164,15 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 93 }, "_enabled": true, "__prefab": { - "__id__": 125 + "__id__": 95 }, "_contentSize": { "__type__": "cc.Size", - "width": 63.1484375, + "width": 63.12341742835968, "height": 20.16 }, "_anchorPoint": { @@ -2920,7 +2180,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "09gSrzI55MDqK5YXfGyBfB" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2932,11 +2192,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 93 }, "_enabled": true, "__prefab": { - "__id__": 127 + "__id__": 97 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2951,7 +2211,7 @@ "_string": "Spirit632", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 16, + "_actualFontSize": 16.866666666666667, "_fontSize": 16, "_fontFamily": "Arial", "_lineHeight": 16, @@ -2988,7 +2248,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "8eWIbWkrdEDalvnL6xr9Vq" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3000,11 +2260,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 93 }, "_enabled": true, "__prefab": { - "__id__": 129 + "__id__": 99 }, "_alignFlags": 16, "_target": null, @@ -3024,7 +2284,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "ff+kemXWhNVLGLw2S7b19u" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3055,17 +2315,17 @@ "_active": true, "_components": [ { - "__id__": 132 + "__id__": 102 }, { - "__id__": 134 + "__id__": 104 }, { - "__id__": 136 + "__id__": 106 } ], "_prefab": { - "__id__": 138 + "__id__": 108 }, "_lpos": { "__type__": "cc.Vec3", @@ -3094,7 +2354,7 @@ "y": 0, "z": 0 }, - "_id": "39ZFex7XhH4IAr0D9R8fqm" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3102,15 +2362,15 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 101 }, "_enabled": true, "__prefab": { - "__id__": 133 + "__id__": 103 }, "_contentSize": { "__type__": "cc.Size", - "width": 63.1484375, + "width": 63.12341742835968, "height": 20.16 }, "_anchorPoint": { @@ -3118,7 +2378,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "2cU458SKVI7JrnDD2edLuC" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3130,11 +2390,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 101 }, "_enabled": true, "__prefab": { - "__id__": 135 + "__id__": 105 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3149,7 +2409,7 @@ "_string": "Spirit632", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 16, + "_actualFontSize": 16.866666666666667, "_fontSize": 16, "_fontFamily": "Arial", "_lineHeight": 16, @@ -3186,7 +2446,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "d6fYAzJ4tAYrlqYItN2yym" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3198,11 +2458,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 101 }, "_enabled": true, "__prefab": { - "__id__": 137 + "__id__": 107 }, "_alignFlags": 16, "_target": null, @@ -3222,7 +2482,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "84qvDEYV9O5ot0w7qqevyK" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3253,17 +2513,17 @@ "_active": true, "_components": [ { - "__id__": 140 + "__id__": 110 }, { - "__id__": 142 + "__id__": 112 }, { - "__id__": 144 + "__id__": 114 } ], "_prefab": { - "__id__": 146 + "__id__": 116 }, "_lpos": { "__type__": "cc.Vec3", @@ -3292,7 +2552,7 @@ "y": 0, "z": 0 }, - "_id": "5eiTgwt+dJMpvvz+Fkp4iD" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3300,15 +2560,15 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 109 }, "_enabled": true, "__prefab": { - "__id__": 141 + "__id__": 111 }, "_contentSize": { "__type__": "cc.Size", - "width": 63.1484375, + "width": 63.12341742835968, "height": 20.16 }, "_anchorPoint": { @@ -3316,7 +2576,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "ffHmvDE19IuJ0mFea01RKh" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3328,11 +2588,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 109 }, "_enabled": true, "__prefab": { - "__id__": 143 + "__id__": 113 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3347,7 +2607,7 @@ "_string": "Spirit632", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 16, + "_actualFontSize": 16.866666666666667, "_fontSize": 16, "_fontFamily": "Arial", "_lineHeight": 16, @@ -3384,7 +2644,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "7a45WGdD1KeL2NGqgV+irb" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3396,11 +2656,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 109 }, "_enabled": true, "__prefab": { - "__id__": 145 + "__id__": 115 }, "_alignFlags": 16, "_target": null, @@ -3420,7 +2680,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "65dda35ORF26VkWZSqECoR" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3451,17 +2711,17 @@ "_active": true, "_components": [ { - "__id__": 148 + "__id__": 118 }, { - "__id__": 150 + "__id__": 120 }, { - "__id__": 152 + "__id__": 122 } ], "_prefab": { - "__id__": 154 + "__id__": 124 }, "_lpos": { "__type__": "cc.Vec3", @@ -3490,7 +2750,7 @@ "y": 0, "z": 0 }, - "_id": "7ftFCYiTpGa4InqYT0xahn" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3498,15 +2758,15 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 147 + "__id__": 117 }, "_enabled": true, "__prefab": { - "__id__": 149 + "__id__": 119 }, "_contentSize": { "__type__": "cc.Size", - "width": 63.1484375, + "width": 63.12341742835968, "height": 20.16 }, "_anchorPoint": { @@ -3514,7 +2774,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "1f7cs0M1VANZq+RqZ7V11v" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3526,11 +2786,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 147 + "__id__": 117 }, "_enabled": true, "__prefab": { - "__id__": 151 + "__id__": 121 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3545,7 +2805,7 @@ "_string": "Spirit632", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 16, + "_actualFontSize": 16.866666666666667, "_fontSize": 16, "_fontFamily": "Arial", "_lineHeight": 16, @@ -3582,7 +2842,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "58s46lXJJJB51lYXjMUD7w" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3594,11 +2854,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 147 + "__id__": 117 }, "_enabled": true, "__prefab": { - "__id__": 153 + "__id__": 123 }, "_alignFlags": 16, "_target": null, @@ -3618,7 +2878,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "dd658Ar3hKu6R5pe/TmQKK" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3649,17 +2909,17 @@ "_active": true, "_components": [ { - "__id__": 156 + "__id__": 126 }, { - "__id__": 158 + "__id__": 128 }, { - "__id__": 160 + "__id__": 130 } ], "_prefab": { - "__id__": 162 + "__id__": 132 }, "_lpos": { "__type__": "cc.Vec3", @@ -3688,7 +2948,7 @@ "y": 0, "z": 0 }, - "_id": "92hjsyd2lKYqzLRPYNIqpg" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3696,15 +2956,15 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 155 + "__id__": 125 }, "_enabled": true, "__prefab": { - "__id__": 157 + "__id__": 127 }, "_contentSize": { "__type__": "cc.Size", - "width": 63.1484375, + "width": 63.12341742835968, "height": 20.16 }, "_anchorPoint": { @@ -3712,7 +2972,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "b9XCJjuV1BfoXJ5b2KTolr" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3724,11 +2984,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 155 + "__id__": 125 }, "_enabled": true, "__prefab": { - "__id__": 159 + "__id__": 129 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3743,7 +3003,7 @@ "_string": "Spirit632", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 16, + "_actualFontSize": 16.866666666666667, "_fontSize": 16, "_fontFamily": "Arial", "_lineHeight": 16, @@ -3780,7 +3040,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "e7rbbS5LpDLJZwTRPs+SrN" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3792,11 +3052,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 155 + "__id__": 125 }, "_enabled": true, "__prefab": { - "__id__": 161 + "__id__": 131 }, "_alignFlags": 16, "_target": null, @@ -3816,7 +3076,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "40xEwg51ZBSYRxZLph3NBQ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3845,7 +3105,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 164 + "__id__": 134 }, "_contentSize": { "__type__": "cc.Size", @@ -3857,7 +3117,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "83sN7WtA1L4Jfk7/ZzQr80" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3873,14 +3133,14 @@ }, "_enabled": true, "__prefab": { - "__id__": 166 + "__id__": 136 }, - "_alignFlags": 45, + "_alignFlags": 18, "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, + "_left": 800, + "_right": -800, + "_top": -360, + "_bottom": 360, "_horizontalCenter": 0, "_verticalCenter": 0, "_isAbsLeft": true, @@ -3893,7 +3153,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "53pbYg6uJCqpfb7P69FTz1" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3922,7 +3182,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 169 + "__id__": 139 }, "_contentSize": { "__type__": "cc.Size", @@ -3934,7 +3194,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "b0JJWDC/9ItI4DwiWzh7ai" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3950,7 +3210,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 171 + "__id__": 141 }, "_alignFlags": 45, "_target": null, @@ -3970,7 +3230,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "38+fnp4bVB2b9D51nV+BLc" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3986,7 +3246,6 @@ }, "fileId": "sEw24Fw5bHk3AtCysLGJ3Q", "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null + "targetOverrides": null } -] +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer27_CreateRoom.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer27_CreateRoom.prefab index 7f2955c..bacfbfa 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer27_CreateRoom.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer27_CreateRoom.prefab @@ -25,14 +25,17 @@ "_active": true, "_components": [ { - "__id__": 160 + "__id__": 190 }, { - "__id__": 162 + "__id__": 192 + }, + { + "__id__": 194 } ], "_prefab": { - "__id__": 164 + "__id__": 196 }, "_lpos": { "__type__": "cc.Vec3", @@ -61,7 +64,7 @@ "y": 0, "z": 0 }, - "_id": "53DFimD9FKHZ047deKo4SY" + "_id": "382anYqJ9GVIwWlmUrCZKH" }, { "__type__": "cc.Node", @@ -76,71 +79,65 @@ "__id__": 3 }, { - "__id__": 11 + "__id__": 13 }, { - "__id__": 19 + "__id__": 23 }, { - "__id__": 27 + "__id__": 31 }, { - "__id__": 35 + "__id__": 39 }, { - "__id__": 43 + "__id__": 47 }, { - "__id__": 51 + "__id__": 55 }, { - "__id__": 59 + "__id__": 65 }, { - "__id__": 67 + "__id__": 73 }, { - "__id__": 75 + "__id__": 79 }, { - "__id__": 83 + "__id__": 85 }, { "__id__": 91 }, { - "__id__": 99 + "__id__": 97 }, { - "__id__": 107 + "__id__": 103 }, { - "__id__": 115 + "__id__": 109 }, { - "__id__": 123 + "__id__": 117 }, { - "__id__": 131 - }, - { - "__id__": 139 - }, - { - "__id__": 147 + "__id__": 151 } ], "_active": true, "_components": [ { - "__id__": 155 + "__id__": 185 }, { - "__id__": 157 + "__id__": 187 } ], "_prefab": { - "__id__": 159 + "__id__": 189 }, "_lpos": { "__type__": "cc.Vec3", @@ -169,7 +166,7 @@ "y": 0, "z": 0 }, - "_id": "a7v/gYWwZDpqs8+Eksniu4" + "_id": "a4X5KhSV5P4IxdM4/aWhIw" }, { "__type__": "cc.Node", @@ -190,10 +187,13 @@ }, { "__id__": 8 + }, + { + "__id__": 10 } ], "_prefab": { - "__id__": 10 + "__id__": 12 }, "_lpos": { "__type__": "cc.Vec3", @@ -222,7 +222,7 @@ "y": 0, "z": 0 }, - "_id": "ffzJyr53JGTbnxB9d/uSrb" + "_id": "531e+XujVMO4qHhDFPhNsc" }, { "__type__": "cc.UITransform", @@ -246,7 +246,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "7923aiVAhAP7XZK90RlEFk" + "_id": "3amkY1DeRNhrOxsGih2urx" }, { "__type__": "cc.CompPrefabInfo", @@ -291,7 +291,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "24fUsJUyJKUYh0P3EJbq8O" + "_id": "6arVJ6XdVPdabSFgb+fD10" }, { "__type__": "cc.CompPrefabInfo", @@ -327,12 +327,68 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "baT4Q27chAM5wKxE9vuGlX" + "_id": "f8zuuY9X9M+aGr3o2nCVVk" }, { "__type__": "cc.CompPrefabInfo", "fileId": "j7EoQfigp1yiEcBLbjr2dg" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 11 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "12sx9Dxs1PQq8yShV7hQvW" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "chk3ccwx9qbA+sjSTtiu/A" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -357,18 +413,21 @@ "_children": [], "_active": true, "_components": [ - { - "__id__": 12 - }, { "__id__": 14 }, { "__id__": 16 + }, + { + "__id__": 18 + }, + { + "__id__": 20 } ], "_prefab": { - "__id__": 18 + "__id__": 22 }, "_lpos": { "__type__": "cc.Vec3", @@ -397,7 +456,7 @@ "y": 0, "z": 0 }, - "_id": "37f37O2W5IN7dSnfV1tZGN" + "_id": "81+e/DdgxBXb2RXKn3v4gl" }, { "__type__": "cc.UITransform", @@ -405,11 +464,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 13 }, "_enabled": true, "__prefab": { - "__id__": 13 + "__id__": 15 }, "_contentSize": { "__type__": "cc.Size", @@ -421,7 +480,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "90fCSOZ2tDy5DuJFNQB7mQ" + "_id": "22Y6cE5glLd7gl0II7Kt+o" }, { "__type__": "cc.CompPrefabInfo", @@ -433,11 +492,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 13 }, "_enabled": true, "__prefab": { - "__id__": 15 + "__id__": 17 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -466,7 +525,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "64BVG/kdpB4JaZcmCk0KBV" + "_id": "c0p+dv/pRBFpF7mHihXIJ0" }, { "__type__": "cc.CompPrefabInfo", @@ -478,11 +537,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 13 }, "_enabled": true, "__prefab": { - "__id__": 17 + "__id__": 19 }, "_alignFlags": 16, "_target": null, @@ -502,12 +561,30 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "c77yC3++ZB16pUvC26Irru" + "_id": "ddJx6LW0BHXY0W823Kbf3a" }, { "__type__": "cc.CompPrefabInfo", "fileId": "+imCBG00ETEIh6j1SVa0Rg" }, + { + "__type__": "cc.BlockInputEvents", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 13 + }, + "_enabled": true, + "__prefab": { + "__id__": 21 + }, + "_id": "a2ZMgULqRIBISIe417SexT" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "mLP3zaykcc+T84Lar05M4Q" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -532,18 +609,18 @@ "_children": [], "_active": true, "_components": [ - { - "__id__": 20 - }, - { - "__id__": 22 - }, { "__id__": 24 + }, + { + "__id__": 26 + }, + { + "__id__": 28 } ], "_prefab": { - "__id__": 26 + "__id__": 30 }, "_lpos": { "__type__": "cc.Vec3", @@ -572,7 +649,7 @@ "y": 0, "z": 0 }, - "_id": "d1+Dm3RfVGZIfnnNmcTiio" + "_id": "b1Hs8Ud4FAdb573F8hq4GV" }, { "__type__": "cc.UITransform", @@ -580,11 +657,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 23 }, "_enabled": true, "__prefab": { - "__id__": 21 + "__id__": 25 }, "_contentSize": { "__type__": "cc.Size", @@ -596,7 +673,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "4dZb4tGFZG/I7rTKFDP2Mt" + "_id": "62zVr0LHhE85VDoDViE1lj" }, { "__type__": "cc.CompPrefabInfo", @@ -608,11 +685,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 23 }, "_enabled": true, "__prefab": { - "__id__": 23 + "__id__": 27 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -641,7 +718,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "c7VO/M1+BItLnPR2KxcCDX" + "_id": "56e2U2N3NGDJEViQh4pI3e" }, { "__type__": "cc.CompPrefabInfo", @@ -653,11 +730,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 23 }, "_enabled": true, "__prefab": { - "__id__": 25 + "__id__": 29 }, "_alignFlags": 16, "_target": null, @@ -677,7 +754,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "3bScDf4ZlGKayQSqzsXlrN" + "_id": "a1NEGjMhVCiK5cdJm67qso" }, { "__type__": "cc.CompPrefabInfo", @@ -696,356 +773,6 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, - { - "__type__": "cc.Node", - "_name": "Spirit20", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 28 - }, - { - "__id__": 30 - }, - { - "__id__": 32 - } - ], - "_prefab": { - "__id__": 34 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -196, - "y": -38, - "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": "c1EBMFBaROH7hYmnO50o2A" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 27 - }, - "_enabled": true, - "__prefab": { - "__id__": 29 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 50, - "height": 50 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "0a5/MPIbdEUJQjl/Ni5DSW" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "n5WjM8JQkUZAS8qGzMznzg" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 27 - }, - "_enabled": true, - "__prefab": { - "__id__": 31 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "4b924bd2-6ccf-40d7-95b2-cfa58e897913@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": "39MCgKUwhOh6jWmMIu5ley" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "czZlv+wFaA8cH0sgGj7TOg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 27 - }, - "_enabled": true, - "__prefab": { - "__id__": 33 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -196, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "5b+CEyRYxN47V9lP5sQVz5" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "0qxYKVUW+gaeWm9CJKTeTA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "NJEjrA8pY4Y+tuddpGbPEg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit21", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 36 - }, - { - "__id__": 38 - }, - { - "__id__": 40 - } - ], - "_prefab": { - "__id__": 42 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 330, - "y": -38, - "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": "46hTOM2d9EBo3ufkLAyKZ8" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 35 - }, - "_enabled": true, - "__prefab": { - "__id__": 37 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 50, - "height": 50 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "04C/5qdaBNdowRxTJwqELU" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "cqAZrQ6PBFp5NG6XL+fu7g" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 35 - }, - "_enabled": true, - "__prefab": { - "__id__": 39 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "4b924bd2-6ccf-40d7-95b2-cfa58e897913@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": "8f6iV0OUFGwYbdG0mf+uvH" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "/vtPRi7TbfOfjaKRv5tq7g" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 35 - }, - "_enabled": true, - "__prefab": { - "__id__": 41 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 330, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "2aJjD7pDVP1InYUjxOmQ4f" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "izk1eX9Avsd6uV4NiQeaWA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "lN/8ZKm5vNGA+SNKQvf0Ow", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, { "__type__": "cc.Node", "_name": "Spirit22", @@ -1058,17 +785,17 @@ "_active": true, "_components": [ { - "__id__": 44 + "__id__": 32 }, { - "__id__": 46 + "__id__": 34 }, { - "__id__": 48 + "__id__": 36 } ], "_prefab": { - "__id__": 50 + "__id__": 38 }, "_lpos": { "__type__": "cc.Vec3", @@ -1097,7 +824,7 @@ "y": 0, "z": 0 }, - "_id": "9dHPsKiuFIPr/iRbDUojBc" + "_id": "b9h68/aStP54PxL+nF2iWw" }, { "__type__": "cc.UITransform", @@ -1105,11 +832,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 45 + "__id__": 33 }, "_contentSize": { "__type__": "cc.Size", @@ -1121,7 +848,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "53RVh6rc9OU5jdX+F7WnYw" + "_id": "38ffyEvGZO1Z/Mvmk2t4j5" }, { "__type__": "cc.CompPrefabInfo", @@ -1133,11 +860,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 47 + "__id__": 35 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1166,7 +893,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "19+fJ5Zo1K/r7QqzRJs83t" + "_id": "7eNLgJwvhBJ4H0SGSvieGI" }, { "__type__": "cc.CompPrefabInfo", @@ -1178,11 +905,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 49 + "__id__": 37 }, "_alignFlags": 16, "_target": null, @@ -1202,7 +929,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "f2bahuMfpAnKVvseg15kSS" + "_id": "e7boeXGK5PSbfVLIVNlTwy" }, { "__type__": "cc.CompPrefabInfo", @@ -1233,17 +960,17 @@ "_active": true, "_components": [ { - "__id__": 52 + "__id__": 40 }, { - "__id__": 54 + "__id__": 42 }, { - "__id__": 56 + "__id__": 44 } ], "_prefab": { - "__id__": 58 + "__id__": 46 }, "_lpos": { "__type__": "cc.Vec3", @@ -1272,7 +999,7 @@ "y": 0, "z": 0 }, - "_id": "a6fHIW0e9Fg58eCx38qO1h" + "_id": "2brXutKoJFnpEv1mCG+l9d" }, { "__type__": "cc.UITransform", @@ -1280,11 +1007,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 39 }, "_enabled": true, "__prefab": { - "__id__": 53 + "__id__": 41 }, "_contentSize": { "__type__": "cc.Size", @@ -1296,7 +1023,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "a9gvp8xZ9Eb4UfX7w3fINg" + "_id": "a7BmwwtLlBJKh46qY5Y0Im" }, { "__type__": "cc.CompPrefabInfo", @@ -1308,11 +1035,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 39 }, "_enabled": true, "__prefab": { - "__id__": 55 + "__id__": 43 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1341,7 +1068,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "9548lG0NNEjKwmOn1vKQjI" + "_id": "07sshNUYtHJazGkm/XZE85" }, { "__type__": "cc.CompPrefabInfo", @@ -1353,11 +1080,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 39 }, "_enabled": true, "__prefab": { - "__id__": 57 + "__id__": 45 }, "_alignFlags": 16, "_target": null, @@ -1377,7 +1104,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "ae80QNSIREqLy6Kh2KEzz1" + "_id": "28qC+X1y1NvK3RUmyJHwlS" }, { "__type__": "cc.CompPrefabInfo", @@ -1408,17 +1135,17 @@ "_active": true, "_components": [ { - "__id__": 60 + "__id__": 48 }, { - "__id__": 62 + "__id__": 50 }, { - "__id__": 64 + "__id__": 52 } ], "_prefab": { - "__id__": 66 + "__id__": 54 }, "_lpos": { "__type__": "cc.Vec3", @@ -1447,7 +1174,7 @@ "y": 0, "z": 0 }, - "_id": "97ybQGjqVIMqf7YW9UGUMm" + "_id": "3bi57FIiRJ46Oyrn3MzDBQ" }, { "__type__": "cc.UITransform", @@ -1455,11 +1182,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 47 }, "_enabled": true, "__prefab": { - "__id__": 61 + "__id__": 49 }, "_contentSize": { "__type__": "cc.Size", @@ -1471,7 +1198,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "64xekzl/ZBB5FYcSdSb+hx" + "_id": "eawye6gfJDb66Gj1qjhGtl" }, { "__type__": "cc.CompPrefabInfo", @@ -1483,11 +1210,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 47 }, "_enabled": true, "__prefab": { - "__id__": 63 + "__id__": 51 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1516,7 +1243,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "e1RKUSDelB25mjqC1ed5Pw" + "_id": "55yPtfQ4ZLUoSZuw4Vu6K0" }, { "__type__": "cc.CompPrefabInfo", @@ -1528,11 +1255,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 47 }, "_enabled": true, "__prefab": { - "__id__": 65 + "__id__": 53 }, "_alignFlags": 16, "_target": null, @@ -1552,7 +1279,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "26k1KraxRG47GLryYQivJc" + "_id": "32W0VxoCxDdZxYVq94mvHa" }, { "__type__": "cc.CompPrefabInfo", @@ -1583,17 +1310,20 @@ "_active": true, "_components": [ { - "__id__": 68 + "__id__": 56 }, { - "__id__": 70 + "__id__": 58 }, { - "__id__": 72 + "__id__": 60 + }, + { + "__id__": 62 } ], "_prefab": { - "__id__": 74 + "__id__": 64 }, "_lpos": { "__type__": "cc.Vec3", @@ -1622,7 +1352,7 @@ "y": 0, "z": 0 }, - "_id": "d3+EsFqYJJOKKlV1J1F3U6" + "_id": "1aMJgltmpCt7ghxJWNXFbo" }, { "__type__": "cc.UITransform", @@ -1630,11 +1360,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 55 }, "_enabled": true, "__prefab": { - "__id__": 69 + "__id__": 57 }, "_contentSize": { "__type__": "cc.Size", @@ -1646,7 +1376,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "1c2Om36ZJGg5Y1IbGtB9a5" + "_id": "75Ud0V6lRC54aNuft6R/pk" }, { "__type__": "cc.CompPrefabInfo", @@ -1658,11 +1388,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 55 }, "_enabled": true, "__prefab": { - "__id__": 71 + "__id__": 59 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1691,7 +1421,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "62wbGQXhNCm5almqjmX4F1" + "_id": "a5lU0XGlBJzqrDcqqd4xa5" }, { "__type__": "cc.CompPrefabInfo", @@ -1703,11 +1433,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 55 }, "_enabled": true, "__prefab": { - "__id__": 73 + "__id__": 61 }, "_alignFlags": 16, "_target": null, @@ -1727,12 +1457,68 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "74dL5DjJxBLoC4pwn+aRoS" + "_id": "56q3E86H9IGaydqgbddquV" }, { "__type__": "cc.CompPrefabInfo", "fileId": "DMo+p+yP4C7/DJCy3JKTTw" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 55 + }, + "_enabled": true, + "__prefab": { + "__id__": 63 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "09Bjak6yZHS42gh870BXta" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "l6cKB6NXr9epchpJL0WMFw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1746,356 +1532,6 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, - { - "__type__": "cc.Node", - "_name": "Spirit139", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 76 - }, - { - "__id__": 78 - }, - { - "__id__": 80 - } - ], - "_prefab": { - "__id__": 82 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -196, - "y": 37, - "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": "50FFAS7bNMAImvMc1wtQkx" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 75 - }, - "_enabled": true, - "__prefab": { - "__id__": 77 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 50, - "height": 50 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "c90n+oDmBLe5FthwV8Rufq" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "+7mDoSw3LlJbZCzJhAqT2Q" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 75 - }, - "_enabled": true, - "__prefab": { - "__id__": 79 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "4b924bd2-6ccf-40d7-95b2-cfa58e897913@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": "71UteJu1xGxaYCfD9gKUvd" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "2ciPeNocqYSTPE8yVzpQoQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 75 - }, - "_enabled": true, - "__prefab": { - "__id__": 81 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -196, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "e4BEj/IE1FSJWhOX6JFhku" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "A0qDBEjo4G0GfPhOg9cZjA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "PdZu6saIDw2qjpY6rkoglA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit140", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 84 - }, - { - "__id__": 86 - }, - { - "__id__": 88 - } - ], - "_prefab": { - "__id__": 90 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 330, - "y": 37, - "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": "a5EsQMXYdDDJGXOoxf+uhm" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 83 - }, - "_enabled": true, - "__prefab": { - "__id__": 85 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 50, - "height": 50 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "d3xTRBGAFNc7SP8x6UQR8w" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "IxOvo72yyyXRL7LbztoFnQ" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 83 - }, - "_enabled": true, - "__prefab": { - "__id__": 87 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "4b924bd2-6ccf-40d7-95b2-cfa58e897913@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": "65fgpA8ehOnpmFeWGx+uKn" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "np5WcclvCEC3ueuD91gKJw" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 83 - }, - "_enabled": true, - "__prefab": { - "__id__": 89 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 330, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "eaAI4tlI1B14Pb1/tEwRyC" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "SaxoQXl/4X7v24hzpSBfCA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "nLSOAGGwtQNqE0u5wDyVvQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, { "__type__": "cc.Node", "_name": "Spirit60", @@ -2108,22 +1544,22 @@ "_active": true, "_components": [ { - "__id__": 92 + "__id__": 66 }, { - "__id__": 94 + "__id__": 68 }, { - "__id__": 96 + "__id__": 70 } ], "_prefab": { - "__id__": 98 + "__id__": 72 }, "_lpos": { "__type__": "cc.Vec3", - "x": 783, - "y": 178, + "x": 405, + "y": 180, "z": 0 }, "_lrot": { @@ -2147,7 +1583,7 @@ "y": 0, "z": 0 }, - "_id": "32Kz51XVBFyatSaHJi5JMB" + "_id": "79E8eO9CNPr4MwPq/H0CZ8" }, { "__type__": "cc.UITransform", @@ -2155,11 +1591,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 65 }, "_enabled": true, "__prefab": { - "__id__": 93 + "__id__": 67 }, "_contentSize": { "__type__": "cc.Size", @@ -2171,7 +1607,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "d8uXzsKJxFHa+9+YwVPpwR" + "_id": "78CRmr8PBDjo7N1jfTAi6E" }, { "__type__": "cc.CompPrefabInfo", @@ -2183,11 +1619,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 65 }, "_enabled": true, "__prefab": { - "__id__": 95 + "__id__": 69 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2216,47 +1652,67 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "f1KJBZsOFC/IZ2suwIBU6F" + "_id": "47VPQYwopI2qUOztSL3L3b" }, { "__type__": "cc.CompPrefabInfo", "fileId": "V9j2AM9Bsqjqd0pU0/kWlA" }, { - "__type__": "cc.Widget", + "__type__": "cc.Button", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 65 }, "_enabled": true, "__prefab": { - "__id__": 97 + "__id__": 71 }, - "_alignFlags": 16, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 783, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "96fX+mWRJP8LTdUQFQo1An" + "_id": "a7HxjmNlNDl5m7JSoyFtOt" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "e/w3QGksjcy5ss/ZEBjgLw" + "fileId": "JaIeCaN8ddtnyFb1Dy3T2g" }, { "__type__": "cc.PrefabInfo", @@ -2283,22 +1739,19 @@ "_active": true, "_components": [ { - "__id__": 100 + "__id__": 74 }, { - "__id__": 102 - }, - { - "__id__": 104 + "__id__": 76 } ], "_prefab": { - "__id__": 106 + "__id__": 78 }, "_lpos": { "__type__": "cc.Vec3", - "x": -20, - "y": 41, + "x": -160, + "y": 40, "z": 0 }, "_lrot": { @@ -2322,7 +1775,7 @@ "y": 0, "z": 0 }, - "_id": "96OOj2Ku9NQ5FXmOjq/Vhw" + "_id": "c6vhULRUpCJ5vLxo1iLcA3" }, { "__type__": "cc.UITransform", @@ -2330,11 +1783,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 73 }, "_enabled": true, "__prefab": { - "__id__": 101 + "__id__": 75 }, "_contentSize": { "__type__": "cc.Size", @@ -2343,10 +1796,10 @@ }, "_anchorPoint": { "__type__": "cc.Vec2", - "x": 0.5, + "x": 0, "y": 0.5 }, - "_id": "3dpILXwzJEHrSrlGLdUjWb" + "_id": "349ZGTb3FPL5vkUNazh/us" }, { "__type__": "cc.CompPrefabInfo", @@ -2358,11 +1811,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 73 }, "_enabled": true, "__prefab": { - "__id__": 103 + "__id__": 77 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2374,13 +1827,13 @@ "b": 0, "a": 255 }, - "_string": "Spirit672", + "_string": "10局(房卡X1)", "_horizontalAlign": 1, "_verticalAlign": 1, "_actualFontSize": 16, - "_fontSize": 16, + "_fontSize": 28, "_fontFamily": "Arial", - "_lineHeight": 16, + "_lineHeight": 34, "_overflow": 0, "_enableWrapText": true, "_font": null, @@ -2414,48 +1867,12 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "7e45OKlQ5JMrmJ6zW8fDPs" + "_id": "57/Z43k4NMoqHIXujSIPOX" }, { "__type__": "cc.CompPrefabInfo", "fileId": "C0MMt55VC21VercwC6dKRg" }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 99 - }, - "_enabled": true, - "__prefab": { - "__id__": 105 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -20, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "11bgzCHLlD9r3u5THNxXr+" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "pEDtq2MWNdsssxVvBNra3A" - }, { "__type__": "cc.PrefabInfo", "root": { @@ -2481,22 +1898,19 @@ "_active": true, "_components": [ { - "__id__": 108 + "__id__": 80 }, { - "__id__": 110 - }, - { - "__id__": 112 + "__id__": 82 } ], "_prefab": { - "__id__": 114 + "__id__": 84 }, "_lpos": { "__type__": "cc.Vec3", - "x": 476, - "y": 41, + "x": 160, + "y": 40, "z": 0 }, "_lrot": { @@ -2520,7 +1934,7 @@ "y": 0, "z": 0 }, - "_id": "550ZkHMrZG6IBSTMZTgUGY" + "_id": "2fC/DFWilBx4vMUmrL8vwo" }, { "__type__": "cc.UITransform", @@ -2528,11 +1942,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 79 }, "_enabled": true, "__prefab": { - "__id__": 109 + "__id__": 81 }, "_contentSize": { "__type__": "cc.Size", @@ -2541,10 +1955,10 @@ }, "_anchorPoint": { "__type__": "cc.Vec2", - "x": 0.5, + "x": 0, "y": 0.5 }, - "_id": "3d+BpCqhJGDrdQ5Lo/nLoA" + "_id": "11ot4arrdE8KvUKsWEdkEV" }, { "__type__": "cc.CompPrefabInfo", @@ -2556,11 +1970,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 79 }, "_enabled": true, "__prefab": { - "__id__": 111 + "__id__": 83 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2572,13 +1986,13 @@ "b": 0, "a": 255 }, - "_string": "Spirit672", + "_string": "20局(房卡X2)", "_horizontalAlign": 1, "_verticalAlign": 1, "_actualFontSize": 16, - "_fontSize": 16, + "_fontSize": 28, "_fontFamily": "Arial", - "_lineHeight": 16, + "_lineHeight": 34, "_overflow": 0, "_enableWrapText": true, "_font": null, @@ -2612,48 +2026,12 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "cdzT+PerRBlpCxdSA86Bbx" + "_id": "cbBNBhPMdNA7dSBYlErEbc" }, { "__type__": "cc.CompPrefabInfo", "fileId": "inMCnuPrqZufjgC3s+Maxg" }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 107 - }, - "_enabled": true, - "__prefab": { - "__id__": 113 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 476, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "3cd0+Es7ZLlKmWp3FP1OM5" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "7eXeMpE5UuDRAPH97X/kig" - }, { "__type__": "cc.PrefabInfo", "root": { @@ -2679,22 +2057,19 @@ "_active": true, "_components": [ { - "__id__": 116 + "__id__": 86 }, { - "__id__": 118 - }, - { - "__id__": 120 + "__id__": 88 } ], "_prefab": { - "__id__": 122 + "__id__": 90 }, "_lpos": { "__type__": "cc.Vec3", - "x": -18, - "y": -39, + "x": -160, + "y": -40, "z": 0 }, "_lrot": { @@ -2718,7 +2093,7 @@ "y": 0, "z": 0 }, - "_id": "82fTSJHqZLCr+GjGwtgM9u" + "_id": "feEXnvn2tDIL7h/fBDuazm" }, { "__type__": "cc.UITransform", @@ -2726,11 +2101,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 117 + "__id__": 87 }, "_contentSize": { "__type__": "cc.Size", @@ -2739,10 +2114,10 @@ }, "_anchorPoint": { "__type__": "cc.Vec2", - "x": 0.5, + "x": 0, "y": 0.5 }, - "_id": "80N6+hc4xIkZ0i4b2tJ+TX" + "_id": "39g4mnKn1CBqXSrhkJjxD4" }, { "__type__": "cc.CompPrefabInfo", @@ -2754,11 +2129,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 119 + "__id__": 89 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2770,13 +2145,13 @@ "b": 0, "a": 255 }, - "_string": "Spirit672", + "_string": "房主扣卡", "_horizontalAlign": 1, "_verticalAlign": 1, "_actualFontSize": 16, - "_fontSize": 16, + "_fontSize": 28, "_fontFamily": "Arial", - "_lineHeight": 16, + "_lineHeight": 34, "_overflow": 0, "_enableWrapText": true, "_font": null, @@ -2810,48 +2185,12 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "30MIOGU7RDG4q5cZqHNcrK" + "_id": "62RC0Zs9tMWZ3w3qRAWyDq" }, { "__type__": "cc.CompPrefabInfo", "fileId": "DZ1ipBPGs6Rm4AhLiAkCOg" }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 115 - }, - "_enabled": true, - "__prefab": { - "__id__": 121 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -18, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "c764MPY8pHGbGtDTXupBRE" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "y83cjJ7UTbrBGVRHkSkYbg" - }, { "__type__": "cc.PrefabInfo", "root": { @@ -2877,22 +2216,19 @@ "_active": true, "_components": [ { - "__id__": 124 + "__id__": 92 }, { - "__id__": 126 - }, - { - "__id__": 128 + "__id__": 94 } ], "_prefab": { - "__id__": 130 + "__id__": 96 }, "_lpos": { "__type__": "cc.Vec3", - "x": 506, - "y": -35, + "x": 160, + "y": -40, "z": 0 }, "_lrot": { @@ -2916,7 +2252,7 @@ "y": 0, "z": 0 }, - "_id": "91SVVZpwxE2Lx2B+vvUqeT" + "_id": "c01sQBP5BKl6sa3vTcCHSy" }, { "__type__": "cc.UITransform", @@ -2924,11 +2260,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 91 }, "_enabled": true, "__prefab": { - "__id__": 125 + "__id__": 93 }, "_contentSize": { "__type__": "cc.Size", @@ -2937,10 +2273,10 @@ }, "_anchorPoint": { "__type__": "cc.Vec2", - "x": 0.5, + "x": 0, "y": 0.5 }, - "_id": "8bbuSkd5ZHyLtoBd28JRD7" + "_id": "c6LG1s+31MC7T4N7aZ35WK" }, { "__type__": "cc.CompPrefabInfo", @@ -2952,11 +2288,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 91 }, "_enabled": true, "__prefab": { - "__id__": 127 + "__id__": 95 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2968,13 +2304,13 @@ "b": 0, "a": 255 }, - "_string": "Spirit672", + "_string": "每人扣卡", "_horizontalAlign": 1, "_verticalAlign": 1, "_actualFontSize": 16, - "_fontSize": 16, + "_fontSize": 28, "_fontFamily": "Arial", - "_lineHeight": 16, + "_lineHeight": 34, "_overflow": 0, "_enableWrapText": true, "_font": null, @@ -3008,48 +2344,12 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "e7ZEMsD95Ha7q7cwg/fot9" + "_id": "b2WhZ1cR1KbJ3HiTwQKrTm" }, { "__type__": "cc.CompPrefabInfo", "fileId": "0Ia9S5TCsaX7H9dqDkp8+Q" }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 123 - }, - "_enabled": true, - "__prefab": { - "__id__": 129 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 506, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "aaml7e6yFOwZJuDxABKUax" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "uP+p64Wlo+4t+AB5B2LIPQ" - }, { "__type__": "cc.PrefabInfo", "root": { @@ -3075,22 +2375,19 @@ "_active": true, "_components": [ { - "__id__": 132 + "__id__": 98 }, { - "__id__": 134 - }, - { - "__id__": 136 + "__id__": 100 } ], "_prefab": { - "__id__": 138 + "__id__": 102 }, "_lpos": { "__type__": "cc.Vec3", - "x": -482, - "y": 40.5, + "x": -330, + "y": 40, "z": 0 }, "_lrot": { @@ -3114,7 +2411,7 @@ "y": 0, "z": 0 }, - "_id": "92iXctYeVDf7B9wA32Pr9F" + "_id": "b8KUHrAxxGYq60052/XNrS" }, { "__type__": "cc.UITransform", @@ -3122,11 +2419,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 97 }, "_enabled": true, "__prefab": { - "__id__": 133 + "__id__": 99 }, "_contentSize": { "__type__": "cc.Size", @@ -3138,7 +2435,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "166cbhZwtClKr9MwFm0AWM" + "_id": "c3QR7gTTlL3pl++8a2SUE6" }, { "__type__": "cc.CompPrefabInfo", @@ -3150,11 +2447,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 97 }, "_enabled": true, "__prefab": { - "__id__": 135 + "__id__": 101 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3183,48 +2480,12 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "3bdjtte3JMAbIdrWmGME/Z" + "_id": "62mmZSpnxLRa2aSXjiwV54" }, { "__type__": "cc.CompPrefabInfo", "fileId": "mB4Ulp3RW+UV0IORsHKxaA" }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 131 - }, - "_enabled": true, - "__prefab": { - "__id__": 137 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -482, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "b9Luu+GCZHNpMnOvbHcjxH" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "dI3NhNg69rHg6CVA+kxW3Q" - }, { "__type__": "cc.PrefabInfo", "root": { @@ -3250,22 +2511,19 @@ "_active": true, "_components": [ { - "__id__": 140 + "__id__": 104 }, { - "__id__": 142 - }, - { - "__id__": 144 + "__id__": 106 } ], "_prefab": { - "__id__": 146 + "__id__": 108 }, "_lpos": { "__type__": "cc.Vec3", - "x": -478, - "y": -37.5, + "x": -330, + "y": -40, "z": 0 }, "_lrot": { @@ -3289,7 +2547,7 @@ "y": 0, "z": 0 }, - "_id": "5fnI1+9ntJ4r547McP5ssK" + "_id": "4476iKmcVBmZEBJUTgyLO5" }, { "__type__": "cc.UITransform", @@ -3297,11 +2555,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 103 }, "_enabled": true, "__prefab": { - "__id__": 141 + "__id__": 105 }, "_contentSize": { "__type__": "cc.Size", @@ -3313,7 +2571,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "7dFHzkxkNJ0ZQ1xQ7hOnm3" + "_id": "0d5uCdnUFJ2IZDoRK5SoLE" }, { "__type__": "cc.CompPrefabInfo", @@ -3325,11 +2583,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 103 }, "_enabled": true, "__prefab": { - "__id__": 143 + "__id__": 107 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3358,48 +2616,12 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "a495drphBGFYhCBuGWOHJs" + "_id": "75PJr570RHI7OvlOiepWp1" }, { "__type__": "cc.CompPrefabInfo", "fileId": "VLI9Vp8vbmZFJk7smmp+tg" }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 139 - }, - "_enabled": true, - "__prefab": { - "__id__": 145 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -478, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "57eMPZeulNAbW05Bq6cstw" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "iZMFPyTV3g17RopwrW3V9Q" - }, { "__type__": "cc.PrefabInfo", "root": { @@ -3425,17 +2647,17 @@ "_active": true, "_components": [ { - "__id__": 148 + "__id__": 110 }, { - "__id__": 150 + "__id__": 112 }, { - "__id__": 152 + "__id__": 114 } ], "_prefab": { - "__id__": 154 + "__id__": 116 }, "_lpos": { "__type__": "cc.Vec3", @@ -3464,7 +2686,7 @@ "y": 0, "z": 0 }, - "_id": "8724t9gRRHb4YxuN2C3Ib7" + "_id": "fa6iEY0zlJ8pTuizQfWNE1" }, { "__type__": "cc.UITransform", @@ -3472,11 +2694,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 147 + "__id__": 109 }, "_enabled": true, "__prefab": { - "__id__": 149 + "__id__": 111 }, "_contentSize": { "__type__": "cc.Size", @@ -3488,7 +2710,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "9da18YY1xJyJyQdM7hmCj9" + "_id": "7fXXrFlDxMTIIVa5gLO+Sf" }, { "__type__": "cc.CompPrefabInfo", @@ -3500,11 +2722,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 147 + "__id__": 109 }, "_enabled": true, "__prefab": { - "__id__": 151 + "__id__": 113 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3533,7 +2755,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "5fhb6KclVPF7GuWvEOJEl/" + "_id": "6djC7CSFNPu66ix4rmHALb" }, { "__type__": "cc.CompPrefabInfo", @@ -3545,11 +2767,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 147 + "__id__": 109 }, "_enabled": true, "__prefab": { - "__id__": 153 + "__id__": 115 }, "_alignFlags": 16, "_target": null, @@ -3569,7 +2791,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "46yP1KxjtCgrvRf+uQey13" + "_id": "83OHZ9O61M5JL4UFiGys9n" }, { "__type__": "cc.CompPrefabInfo", @@ -3588,6 +2810,1602 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, + { + "__type__": "cc.Node", + "_name": "OptionGroup1", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 118 + }, + { + "__id__": 132 + } + ], + "_active": true, + "_components": [ + { + "__id__": 146 + }, + { + "__id__": 148 + } + ], + "_prefab": { + "__id__": 150 + }, + "_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": "ff8rm4DetBJb6PWUG9f4mA" + }, + { + "__type__": "cc.Node", + "_name": "Spirit20", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 117 + }, + "_children": [ + { + "__id__": 119 + } + ], + "_active": true, + "_components": [ + { + "__id__": 125 + }, + { + "__id__": 127 + }, + { + "__id__": 129 + } + ], + "_prefab": { + "__id__": 131 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -195, + "y": 40, + "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": "92xkcUWShBwbf6QwuWw9t6" + }, + { + "__type__": "cc.Node", + "_name": "check", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 118 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 120 + }, + { + "__id__": 122 + } + ], + "_prefab": { + "__id__": 124 + }, + "_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": "21R3thB2dNRJIVAA665fEL" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 119 + }, + "_enabled": true, + "__prefab": { + "__id__": 121 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 40, + "height": 40 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "61vbC0fnxPAaSBfDHEjNVy" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "J9GlaQjh/I5vcqdY49mvxQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 119 + }, + "_enabled": true, + "__prefab": { + "__id__": 123 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "83e400a9-4b21-47e0-ab49-9ba2fe18ad54@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "aerSBGb4FMj5xJR01KLGYr" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "3m/BXYHXdSNgXY6P0qc+RA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "DA5D1RJj0lYcmR19XAQAcA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 118 + }, + "_enabled": true, + "__prefab": { + "__id__": 126 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 50, + "height": 50 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "a93l28AUFM4Jd98ARlbE0z" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "n5WjM8JQkUZAS8qGzMznzg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 118 + }, + "_enabled": true, + "__prefab": { + "__id__": 128 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "4b924bd2-6ccf-40d7-95b2-cfa58e897913@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": "c7G22SyCZHlKa2xh+X1X3y" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "czZlv+wFaA8cH0sgGj7TOg" + }, + { + "__type__": "cc.Toggle", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 118 + }, + "_enabled": true, + "__prefab": { + "__id__": 130 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "checkEvents": [], + "_isChecked": true, + "_checkMark": { + "__id__": 122 + }, + "_id": "favXUcnP5K45sj5Aq1Tw5s" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "iBf91VQXOy5NAWwPY1n12g" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "NJEjrA8pY4Y+tuddpGbPEg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit21", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 117 + }, + "_children": [ + { + "__id__": 133 + } + ], + "_active": true, + "_components": [ + { + "__id__": 139 + }, + { + "__id__": 141 + }, + { + "__id__": 143 + } + ], + "_prefab": { + "__id__": 145 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 125, + "y": 40, + "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": "00sdwNc6RAfKxIKdE6UeVM" + }, + { + "__type__": "cc.Node", + "_name": "check", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 132 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 134 + }, + { + "__id__": 136 + } + ], + "_prefab": { + "__id__": 138 + }, + "_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": "12QtqfFedKWLRu3gC5QWeC" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 133 + }, + "_enabled": true, + "__prefab": { + "__id__": 135 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 40, + "height": 40 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "07Y8jGLv5JdIlp9nTs3xnY" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "g5Lh/mpLIw5NpphJc2T2PA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 133 + }, + "_enabled": true, + "__prefab": { + "__id__": 137 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "83e400a9-4b21-47e0-ab49-9ba2fe18ad54@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "45C0R4yslBCYeskUMfiG/L" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "S+8X41dawAp+mzB6Yq/AYA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "WncHBXJc2y7rEFXe72tS0A", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 132 + }, + "_enabled": true, + "__prefab": { + "__id__": 140 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 50, + "height": 50 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "70DJm7gmtGrJRZB3F4yVTq" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "cqAZrQ6PBFp5NG6XL+fu7g" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 132 + }, + "_enabled": true, + "__prefab": { + "__id__": 142 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "4b924bd2-6ccf-40d7-95b2-cfa58e897913@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": "89UYZFFhZPo6fGMHQ+ri7w" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "/vtPRi7TbfOfjaKRv5tq7g" + }, + { + "__type__": "cc.Toggle", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 132 + }, + "_enabled": true, + "__prefab": { + "__id__": 144 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "checkEvents": [], + "_isChecked": false, + "_checkMark": { + "__id__": 136 + }, + "_id": "62R4geJZ9DnK4W1mRIEdM3" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "U0lquDvl2u9Zp2PMGknmPA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "lN/8ZKm5vNGA+SNKQvf0Ow", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 117 + }, + "_enabled": true, + "__prefab": { + "__id__": 147 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1280, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "3d8lkoy+RMu4WAJqWXUFd/" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "R89X4j1STaLo1p0XWcfUng" + }, + { + "__type__": "cc.ToggleContainer", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 117 + }, + "_enabled": true, + "__prefab": { + "__id__": 149 + }, + "_allowSwitchOff": true, + "checkEvents": [], + "_id": "c7kbpic95BtLccOwXr8v+3" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "e2sZuku6yagtgJNMc/ufmA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "REOzSQ3xCzSEW6bhHGZY1A", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "OptionGroup2", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 152 + }, + { + "__id__": 166 + } + ], + "_active": true, + "_components": [ + { + "__id__": 180 + }, + { + "__id__": 182 + } + ], + "_prefab": { + "__id__": 184 + }, + "_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": "cbXyUxzL1AJJX4v2cMTfU1" + }, + { + "__type__": "cc.Node", + "_name": "Spirit139", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 151 + }, + "_children": [ + { + "__id__": 153 + } + ], + "_active": true, + "_components": [ + { + "__id__": 159 + }, + { + "__id__": 161 + }, + { + "__id__": 163 + } + ], + "_prefab": { + "__id__": 165 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -195, + "y": -40, + "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": "f48c0tnQ5D1bFZ5xEERBts" + }, + { + "__type__": "cc.Node", + "_name": "check", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 152 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 154 + }, + { + "__id__": 156 + } + ], + "_prefab": { + "__id__": 158 + }, + "_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": "aeNFCIBjVGM7JWhEofnsX5" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 153 + }, + "_enabled": true, + "__prefab": { + "__id__": 155 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 40, + "height": 40 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "52c8lTr/NHpJWvlMyeMfXb" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "csKppPHX6FSEboJM6+7LBQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 153 + }, + "_enabled": true, + "__prefab": { + "__id__": 157 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "83e400a9-4b21-47e0-ab49-9ba2fe18ad54@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "64Wgau/9JKKqj8VAlePyhS" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "v5mwvEcEb0FmmtecW2no7A" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "D14GWcjwntjgbuJ6QHXusg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 152 + }, + "_enabled": true, + "__prefab": { + "__id__": 160 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 50, + "height": 50 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "ecDxUyFfZIj7/aJD+lA/oI" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "+7mDoSw3LlJbZCzJhAqT2Q" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 152 + }, + "_enabled": true, + "__prefab": { + "__id__": 162 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "4b924bd2-6ccf-40d7-95b2-cfa58e897913@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": "80COVrs/dE1qMZ5di3imkc" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2ciPeNocqYSTPE8yVzpQoQ" + }, + { + "__type__": "cc.Toggle", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 152 + }, + "_enabled": true, + "__prefab": { + "__id__": 164 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "checkEvents": [], + "_isChecked": true, + "_checkMark": { + "__id__": 156 + }, + "_id": "f7ZtALqEBOV41jHnn0izg1" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2e51QnAz72duG6qYopNcPA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "PdZu6saIDw2qjpY6rkoglA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit140", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 151 + }, + "_children": [ + { + "__id__": 167 + } + ], + "_active": true, + "_components": [ + { + "__id__": 173 + }, + { + "__id__": 175 + }, + { + "__id__": 177 + } + ], + "_prefab": { + "__id__": 179 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 125, + "y": -40, + "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": "8c20wf8AJNH7JbFwtTUQdd" + }, + { + "__type__": "cc.Node", + "_name": "check", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 166 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 168 + }, + { + "__id__": 170 + } + ], + "_prefab": { + "__id__": 172 + }, + "_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": "02KhKuUs5Cdp7SnhF0L3uy" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 167 + }, + "_enabled": true, + "__prefab": { + "__id__": 169 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 40, + "height": 40 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "f7UDvQrOVF3Z/Vb5A+PIGQ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "5/uTbJxilb2vy15P846jOg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 167 + }, + "_enabled": true, + "__prefab": { + "__id__": 171 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "83e400a9-4b21-47e0-ab49-9ba2fe18ad54@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "001WzAeMFFIJDMhkcEK9ir" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "oAZayZrxqK0pHSmk7iqTjA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "w6eKQsEh2DSQg/EcwcNqnA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 166 + }, + "_enabled": true, + "__prefab": { + "__id__": 174 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 50, + "height": 50 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "b3+rr92kdHtZASA708alV0" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "IxOvo72yyyXRL7LbztoFnQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 166 + }, + "_enabled": true, + "__prefab": { + "__id__": 176 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "4b924bd2-6ccf-40d7-95b2-cfa58e897913@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": "4afc/rsBZKo6UN1xLM+j19" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "np5WcclvCEC3ueuD91gKJw" + }, + { + "__type__": "cc.Toggle", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 166 + }, + "_enabled": true, + "__prefab": { + "__id__": 178 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "checkEvents": [], + "_isChecked": false, + "_checkMark": { + "__id__": 170 + }, + "_id": "cbBBZoN4JMBLmuI/UfTWuS" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "YKUDLat2//DqP5NhIRmBQg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "nLSOAGGwtQNqE0u5wDyVvQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 151 + }, + "_enabled": true, + "__prefab": { + "__id__": 181 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1280, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "05yZ+QJrVF8qClh9mnBpH2" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "dlNK21IYnKN0KFdBXdqxAQ" + }, + { + "__type__": "cc.ToggleContainer", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 151 + }, + "_enabled": true, + "__prefab": { + "__id__": 183 + }, + "_allowSwitchOff": true, + "checkEvents": [], + "_id": "29zdtxYJpLL52fiozQNvwM" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "9VZ/EVYmdgEzGccYKzntfA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "lzaJR9iGfTT2EoCTm5thFw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.UITransform", "_name": "", @@ -3598,7 +4416,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 156 + "__id__": 186 }, "_contentSize": { "__type__": "cc.Size", @@ -3610,7 +4428,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "83Zd0257hEWKIB2ZZrJN1e" + "_id": "a7Xz7OwylBE4oLBNzEibFf" }, { "__type__": "cc.CompPrefabInfo", @@ -3626,7 +4444,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 158 + "__id__": 188 }, "_alignFlags": 45, "_target": null, @@ -3646,7 +4464,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "ealwgTx3ZFOLhvlJ11WYoO" + "_id": "194f3f5FNA0Lga+I0bY6ml" }, { "__type__": "cc.CompPrefabInfo", @@ -3675,7 +4493,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 161 + "__id__": 191 }, "_contentSize": { "__type__": "cc.Size", @@ -3687,7 +4505,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "bceYqA2EZNEoYxOqM9tEzn" + "_id": "2747XXeqRIM4iKetLbaztg" }, { "__type__": "cc.CompPrefabInfo", @@ -3703,7 +4521,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 163 + "__id__": 193 }, "_alignFlags": 45, "_target": null, @@ -3723,12 +4541,77 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "67ps61gwpE4amEVE+toqLj" + "_id": "ecH742UOtHXINZVr2OpMXZ" }, { "__type__": "cc.CompPrefabInfo", "fileId": "qBmptKePUAWbifRJwF4F/g" }, + { + "__type__": "ebbeffZe9pMYZsKHuVueBf7", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 195 + }, + "groups": [ + { + "__id__": 148 + }, + { + "__id__": 182 + } + ], + "options": [ + { + "__id__": 129 + }, + { + "__id__": 143 + }, + { + "__id__": 163 + }, + { + "__id__": 177 + } + ], + "optionLabels": [ + { + "__id__": 76 + }, + { + "__id__": 82 + }, + { + "__id__": 88 + }, + { + "__id__": 94 + } + ], + "submitButton": { + "__id__": 62 + }, + "cancelButtons": [ + { + "__id__": 10 + }, + { + "__id__": 70 + } + ], + "_id": "325LVqYI1Impug94VnV+gc" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "AijVn3v9Qsv6+Alq0ESFQw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -3742,4 +4625,4 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null } -] +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer6.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer6.prefab index 9357e91..8ad7360 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer6.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer6.prefab @@ -42,8 +42,8 @@ }, "_lpos": { "__type__": "cc.Vec3", - "x": 800, - "y": 360, + "x": 0, + "y": 0, "z": 0 }, "_lrot": { @@ -67,7 +67,7 @@ "y": 0, "z": 0 }, - "_id": "e4aCtM5OlGhJqUE99LL8dG" + "_id": "" }, { "__type__": "cc.Node", @@ -193,7 +193,7 @@ "y": 0, "z": 0 }, - "_id": "24RA1ak4hPm7U1wPlVW+LB" + "_id": "" }, { "__type__": "cc.Node", @@ -246,7 +246,7 @@ "y": 0, "z": 0 }, - "_id": "68CGN0RPVOUY2o1myssIKU" + "_id": "" }, { "__type__": "cc.UITransform", @@ -270,7 +270,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "12BTte+45DEpGRLcAh4gwF" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -315,7 +315,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "5cKZnooBBM3IAEe3nzi0wY" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -351,7 +351,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "ea1iKHuCVD+IBwpDy0YJ2k" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -421,7 +421,7 @@ "y": 0, "z": 0 }, - "_id": "9dzgFRm8FENbC5nNVX8M1o" + "_id": "" }, { "__type__": "cc.UITransform", @@ -445,7 +445,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "c1pjVbd5pKkIe2UUra+I4B" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -490,7 +490,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "46nUKgzMJKKJZi4x6B0dAs" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -526,7 +526,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "13wKSUGhxOVaOO8y/zJPb4" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -596,7 +596,7 @@ "y": 0, "z": 0 }, - "_id": "91EWpUq9ZJ54qAa7pMslEu" + "_id": "" }, { "__type__": "cc.UITransform", @@ -620,7 +620,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "efMpUlHVVFI5elWlV2iwxr" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -665,7 +665,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "d8/OZ7TH1NxohdVSfi27Qj" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -701,7 +701,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "dfCu54bAhFiKV6hD9wLTKP" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -771,7 +771,7 @@ "y": 0, "z": 0 }, - "_id": "0cfFXbxbhNPbO2RdAG9LVS" + "_id": "" }, { "__type__": "cc.UITransform", @@ -795,7 +795,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "a0kHUdsQpCc5hwYIehE+xY" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -840,7 +840,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "489G9syPJBCbce8XgZ0hDY" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -876,7 +876,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "b6vLD8hnJFKbk6NWjkVUDc" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -946,7 +946,7 @@ "y": 0, "z": 0 }, - "_id": "0cjBIiZ3dK5rVqyU1Kcx0x" + "_id": "" }, { "__type__": "cc.UITransform", @@ -970,7 +970,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "66FY71f6BHmr47/dIpQGJZ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1015,7 +1015,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "d27cnFO/ZNbIiBIgfsEhEM" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1051,7 +1051,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "890SToRuBAA6QZ5cen2aLX" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1121,7 +1121,7 @@ "y": 0, "z": 0 }, - "_id": "ccuJvDelxFsrIjRXORsEPo" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1145,7 +1145,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "7b9DXWu3FD44B4mGMd071D" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1190,7 +1190,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "c6nh4skxhEkatPzRkKyVsX" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1226,7 +1226,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "57tUNrDX9H0bo55C0fcq6S" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1296,7 +1296,7 @@ "y": 0, "z": 0 }, - "_id": "1dG94JyxlDF4Fwe8llGOVo" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1320,7 +1320,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "3bj/vNMflM85tJrtT1yD89" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1365,7 +1365,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "3f8WPNcidKka2GQ1HuOikH" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1401,7 +1401,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "3a5KIpe6NN55g57ynq124t" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1471,7 +1471,7 @@ "y": 0, "z": 0 }, - "_id": "0aa8cUf4ZC0K89V7AALZmx" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1495,7 +1495,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "1fg954501Bp4EVGD2AEzMK" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1540,7 +1540,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "03jLOLWsZBpqWj4cGV9gft" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1576,7 +1576,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "2bZs5ipY9KE69RDpq6GZ4/" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1646,7 +1646,7 @@ "y": 0, "z": 0 }, - "_id": "b8PmciIw5AI5a1FuJ6hhev" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1670,7 +1670,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "fbR7qKDBxHp4A7k+vDsQI7" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1715,7 +1715,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "a6h+4Zm6ZKWam+qvOoetcl" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1751,7 +1751,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "69D8kiT+lD15kYptz5o+nG" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1821,7 +1821,7 @@ "y": 0, "z": 0 }, - "_id": "952Z6lsNpPNriEFQaHlLF/" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1845,7 +1845,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "692kZFFyNPp5BkBaJexFI/" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1890,7 +1890,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "04H0e7iQlO3KSxp63VxluY" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1926,7 +1926,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "47Z8auE5lAN7HyWYcNOD1I" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1996,7 +1996,7 @@ "y": 0, "z": 0 }, - "_id": "2aBFGxAXVGh4TABm8gY2cc" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2020,7 +2020,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "2a4ujUUjNJ14CZWmaDOowU" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2065,7 +2065,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "60syi9uAlNSqTdrPsveBkP" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2101,7 +2101,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "e4Xrbef5pNYJysc561f1RP" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2171,7 +2171,7 @@ "y": 0, "z": 0 }, - "_id": "c2C4QOfbdK74UyKW+9HFC5" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2195,7 +2195,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "edYu69r6lEcKKU5T8KegBN" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2240,7 +2240,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "4fuV3qL/JEEJgCslwumuH6" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2276,7 +2276,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "77YSBE9X5IN6pHu1RCFgd4" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2346,7 +2346,7 @@ "y": 0, "z": 0 }, - "_id": "fcClCUt/1NHLWb+Z3rAXfJ" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2370,7 +2370,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "9b9IGyazJBGJdIrOBlWE0f" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2415,7 +2415,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "13muPq1h1Hq7SLIQqYdKlB" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2451,7 +2451,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "8b7spPmAlCSpqS0XGSqxAP" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2521,7 +2521,7 @@ "y": 0, "z": 0 }, - "_id": "ffJw4kjMpGDJ6RzxIPdWV4" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2545,7 +2545,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "80woJ8pJlP7I5vmISU8TQF" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2590,7 +2590,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "21fXfvs+5Ht7xalyagjaNO" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2626,7 +2626,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "1fAr79cypLRLLWUYPwLpkx" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2696,7 +2696,7 @@ "y": 0, "z": 0 }, - "_id": "a994CEDw5O36bfyKHG1FV7" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2720,7 +2720,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "4aFPbfWvxKjLAwyq+bo0mQ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2765,7 +2765,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "d6jlKlGDhEPK3jHT9Aid9S" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2801,7 +2801,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "c3xXX9GztALLn4VBHbpn4t" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2871,7 +2871,7 @@ "y": 0, "z": 0 }, - "_id": "9fBA6ucBFEurLP9gXumU45" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2895,7 +2895,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "01FJsisIBI8ZSyU6o5nk/T" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2940,7 +2940,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "8cnIDiIXdMz7b3ogxdV4LX" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2976,7 +2976,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "38QmAIBiVCDLxALhLmzrdW" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3046,7 +3046,7 @@ "y": 0, "z": 0 }, - "_id": "b2CGfthwNOLY4Rl3qKEetC" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3070,7 +3070,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "2a/hd4cqpKaoy3J75guFE+" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3115,7 +3115,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "2a76OifChJLrpXHCS8Gn1x" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3151,7 +3151,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "daM1DI1u9FvYJOXdVeex82" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3221,7 +3221,7 @@ "y": 0, "z": 0 }, - "_id": "30ZujceG1C0Z28o2Th+3+J" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3245,7 +3245,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "34zwI5tnpLspUOzEZ7ycZV" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3290,7 +3290,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "67PmJIo+dOLrmFQR2kDK+l" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3326,7 +3326,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "5bLebYAjtFIYkyI7LXFbu/" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3396,7 +3396,7 @@ "y": 0, "z": 0 }, - "_id": "76uX7nQeNKfpUqoXSTDXyh" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3420,7 +3420,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "98n5SuyINLuoCpXC09OKFF" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3465,7 +3465,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "6ci44xzABCEKwVvUqySYmJ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3501,7 +3501,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "d5g/Aq3jdLwJZ8rwfyE2M4" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3571,7 +3571,7 @@ "y": 0, "z": 0 }, - "_id": "f2AU+AOw5LT5QhE9PIC0XF" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3595,7 +3595,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "adnPvG4bBNsZBUtWYY1J2h" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3640,7 +3640,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "5db8B/OE1BCby9n0tqy3/m" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3676,7 +3676,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "47f5yq8G9KKZCilyFTnNMI" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3746,7 +3746,7 @@ "y": 0, "z": 0 }, - "_id": "7dk5sBOtFElINaLk9DTeNO" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3770,7 +3770,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "117lSERZpIxKtYcNLI8eL/" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3838,7 +3838,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "5fm/nkb/lFlpzz2xH5oL+W" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3874,7 +3874,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "c1g3ku/HlK/6BZu5xNGB4J" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3944,7 +3944,7 @@ "y": 0, "z": 0 }, - "_id": "24EZSwEINHNod4cuMkML+G" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3968,7 +3968,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "b7vPYFHX1MuZYzgiVloDfG" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4036,7 +4036,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "2bygJpvZ5Eb5JegVSiXLu4" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4072,7 +4072,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "3d4nkRYtBDJqKlmjoDeGqB" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4142,7 +4142,7 @@ "y": 0, "z": 0 }, - "_id": "1bAMUNNVBB4pZh3z+Ei/eN" + "_id": "" }, { "__type__": "cc.UITransform", @@ -4166,7 +4166,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "80Ug/jc11JRpovQG7d8yPm" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4234,7 +4234,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "20wVwO5ixDII8sGxfOtLRI" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4270,7 +4270,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "7b/GU2DnZMGKxshkcBEJld" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4347,7 +4347,7 @@ "y": 0, "z": 0 }, - "_id": "e5Pi6JfxRECbZRe/LhU4U+" + "_id": "" }, { "__type__": "cc.Node", @@ -4394,7 +4394,7 @@ "y": 0, "z": 0 }, - "_id": "9eMpcAyftO2ojCt/3pvahl" + "_id": "" }, { "__type__": "cc.UITransform", @@ -4418,7 +4418,7 @@ "x": 0, "y": 1 }, - "_id": "e7pHSde1RCn5f26jPUesli" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4459,7 +4459,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "b9FqcLor5EQYb3ZFWaTAZ9" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4481,7 +4481,7 @@ "_inverted": false, "_segments": 64, "_alphaThreshold": 0.1, - "_id": "60NV2efRtF97EvvPlgVtrt" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4527,7 +4527,7 @@ "a": 0 }, "_miterLimit": 10, - "_id": "3cV432G1tIopxxGLlUidcT" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4558,7 +4558,7 @@ }, "_horizontalScrollBar": null, "_verticalScrollBar": null, - "_id": "35sL+gsPBHN7PT92FoM/uY" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4590,7 +4590,7 @@ "__id__": 202 } ], - "_active": false, + "_active": true, "_components": [ { "__id__": 206 @@ -4635,7 +4635,7 @@ "y": 0, "z": 0 }, - "_id": "97Dfd2eDxCoI6V+WuYrC9l" + "_id": "" }, { "__type__": "cc.Node", @@ -4682,7 +4682,7 @@ "y": 0, "z": 0 }, - "_id": "ab7PowTutKk4TfeWxQ53zy" + "_id": "" }, { "__type__": "cc.UITransform", @@ -4706,7 +4706,7 @@ "x": 0, "y": 1 }, - "_id": "4fHQwyEehKPKpB7Wv8RHV4" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4747,7 +4747,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "72BZEKQ7VBiKSYoLI8ro+c" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4769,7 +4769,7 @@ "_inverted": false, "_segments": 64, "_alphaThreshold": 0.1, - "_id": "84MHn7a7dO/bpR1Vtm8mS8" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4815,7 +4815,7 @@ "a": 0 }, "_miterLimit": 10, - "_id": "92QghzkpNBxZDhZ9raycue" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4846,7 +4846,7 @@ }, "_horizontalScrollBar": null, "_verticalScrollBar": null, - "_id": "c38qAvS5xG4Z628tJtDW5D" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4887,7 +4887,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "07jHsUdulIcKYn3d0lzRmo" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4923,7 +4923,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "8dfYIu3eVJk4mQdMsbCgj3" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5033,7 +5033,7 @@ "y": 0, "z": 0 }, - "_id": "35fx4y1wZFqLE6ZP9Ks5/P" + "_id": "" }, { "__type__": "cc.Node", @@ -5086,7 +5086,7 @@ "y": 0, "z": 0 }, - "_id": "4e/KvUPXhKZIyDcyd2JHr2" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5110,7 +5110,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "5cTG709NVP0IvjfGOA0BG2" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5155,7 +5155,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "5cvRyA6apGB4EASptjPZ2k" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5191,7 +5191,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "49KDbfYv1E3q3U7Zcx8E8L" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5261,7 +5261,7 @@ "y": 0, "z": 0 }, - "_id": "c5h/YhcapAYIivmHXyUYGQ" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5285,7 +5285,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "d6Y1rgRrdJEIUXNeJHEV/L" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5330,7 +5330,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "cdrQKdbzxLdL3rWVAun/6h" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5366,7 +5366,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "53O07e1wtBspouC7XWUAjY" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5436,7 +5436,7 @@ "y": 0, "z": 0 }, - "_id": "6a5mcA3IBOG74zIO8PmqfP" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5460,7 +5460,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "b2sOQ3fr5L1IQdCC2h5HSz" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5505,7 +5505,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "50TEagAkxLVbdHTrtKiIvm" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5541,7 +5541,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "5bBG+0TCFB5aQ3+uu1/jHi" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5611,7 +5611,7 @@ "y": 0, "z": 0 }, - "_id": "a35tbSi9dCxK/urjMRNvlI" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5635,7 +5635,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "5eWasM8UFH8q3SbxaMU3kn" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5680,7 +5680,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "0eNccPeDVHQoCDrHKeWMLp" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5716,7 +5716,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "16ePTN7XNEUpFp5iCXNjDb" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5786,7 +5786,7 @@ "y": 0, "z": 0 }, - "_id": "31B/1z9EFLpKQ9vy0eZcKR" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5810,7 +5810,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "7fNUcVlitGbr1BCGRxVW5V" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5855,7 +5855,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "41xtqcmOlPnrHSBzUfvaK6" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5891,7 +5891,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "43WC34wVNGW69bBJGtRJsq" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5961,7 +5961,7 @@ "y": 0, "z": 0 }, - "_id": "7eEp8AVJxLuZMwZQq0WtWy" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5985,7 +5985,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "17QnFLpD1BEbi7Q+XvDJXF" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6030,7 +6030,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "70Z0o3zgZEUKIoZhc4l9bc" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6066,7 +6066,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "55FkG6+31PO7n+AS6EkHn+" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6136,7 +6136,7 @@ "y": 0, "z": 0 }, - "_id": "ccad4JYxZFNY+JoeQQQ/hl" + "_id": "" }, { "__type__": "cc.UITransform", @@ -6160,7 +6160,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "21Gb6YPBpIu7Ysu7pufB0J" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6205,7 +6205,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "a34arZEedCH6MUSB4ACTzK" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6241,7 +6241,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "29nZValfFKAqCSI4jPLng3" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6311,7 +6311,7 @@ "y": 0, "z": 0 }, - "_id": "d4r/+3ArFHCr7HkqWQZCm5" + "_id": "" }, { "__type__": "cc.UITransform", @@ -6335,7 +6335,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "47Lj8pNRVPY6oGdquGXpQR" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6380,7 +6380,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "58ltpJdetCH4Z2kVwS3Iiy" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6416,7 +6416,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "533TC8twVJkqd/Ek/qc4lx" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6486,7 +6486,7 @@ "y": 0, "z": 0 }, - "_id": "d3ImwI5YVG14Uq4C8r2yc3" + "_id": "" }, { "__type__": "cc.UITransform", @@ -6510,7 +6510,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "745sJVVaxCp6vFzngu77gx" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6555,7 +6555,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "aeTev5PNZFo4eX9qsfE7Yd" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6591,7 +6591,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "29qLNWfaZJ56tuwd1x5i3m" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6662,7 +6662,7 @@ "y": 0, "z": 0 }, - "_id": "b8XqM2RdxN7IpRWnJltq6c" + "_id": "" }, { "__type__": "cc.Node", @@ -6712,7 +6712,7 @@ "y": 0, "z": 0 }, - "_id": "2bVUWbNBpHm4ANB+SiMqJ0" + "_id": "" }, { "__type__": "cc.UITransform", @@ -6736,7 +6736,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "08h9K6aCJCYoU9dVxWW73x" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6807,7 +6807,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "57Gmm4YEtHeYCTs3zQFjqQ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6848,7 +6848,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "7fC+V9i4dOeKuiMQ4yuEsr" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6884,7 +6884,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "feB5jP/klHkq28jX/3+oHR" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6954,7 +6954,7 @@ "y": 0, "z": 0 }, - "_id": "61vgEWMbNNeqeyAm9ZenTC" + "_id": "" }, { "__type__": "cc.UITransform", @@ -6978,7 +6978,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "281C092plHYrJuub6iT/9w" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7023,7 +7023,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "b79iG6KDxM+L4are1zCrfz" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7059,7 +7059,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "a84nJVKw9LJogR8ED0j/LB" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7130,7 +7130,7 @@ "y": 0, "z": 0 }, - "_id": "94A5sTVJFLuYWyt44kdw8M" + "_id": "" }, { "__type__": "cc.Node", @@ -7180,7 +7180,7 @@ "y": 0, "z": 0 }, - "_id": "3dSewgag5EMZ+Nu5gUAytI" + "_id": "" }, { "__type__": "cc.UITransform", @@ -7204,7 +7204,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "4eiSBvAyVLI7wNfGow9+9b" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7275,7 +7275,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "012+hQ70lEYYsDCSQjVuq7" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7316,7 +7316,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "df2Vlb+0hJvaKYSEdZamgO" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7352,7 +7352,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "f0xXt/31pJ/oayjFBK9UK2" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7422,7 +7422,7 @@ "y": 0, "z": 0 }, - "_id": "12vWLXoM1OLr8mjFh858Aa" + "_id": "" }, { "__type__": "cc.UITransform", @@ -7446,7 +7446,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "9f0AjKMSFE05vyi5c2J8JR" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7491,7 +7491,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "e6X647Ts1Gq6rLgXdn1qYs" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7527,7 +7527,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "70M4ZEl0lJ/a8Hl3on4P4l" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7604,7 +7604,7 @@ "y": 0, "z": 0 }, - "_id": "92w3LMOo5DyaVyCs48Zy9C" + "_id": "" }, { "__type__": "cc.Node", @@ -7661,7 +7661,7 @@ "y": 0, "z": 0 }, - "_id": "0a7kf0jtRJrpY5VHGyNX/C" + "_id": "" }, { "__type__": "cc.Node", @@ -7714,7 +7714,7 @@ "y": 0, "z": 0 }, - "_id": "680WPn8LlGIIeKp9V633VC" + "_id": "" }, { "__type__": "cc.UITransform", @@ -7738,7 +7738,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "971jcvqUxAuIkYuKnXuFw8" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7783,7 +7783,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "3bv0LejYVPYok3hdAoB3au" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7819,7 +7819,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "15SpYUVHFKJahM8ELWJVKZ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7860,7 +7860,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "c22EHLZ0NPObFqn8TatBf6" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7905,7 +7905,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "921T+lr69M17R8ILRIJLtS" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7941,7 +7941,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "bfCFbbF11JoIVeYmEQB1Qx" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7986,7 +7986,7 @@ }, "_lpos": { "__type__": "cc.Vec3", - "x": 22.082999999999974, + "x": -24.89500000000001, "y": 0.566, "z": 0 }, @@ -8011,7 +8011,7 @@ "y": 0, "z": 0 }, - "_id": "2d4b4ZDeBG7L+/6LDoCPcL" + "_id": "" }, { "__type__": "cc.UITransform", @@ -8032,10 +8032,10 @@ }, "_anchorPoint": { "__type__": "cc.Vec2", - "x": 0.5, + "x": 0, "y": 0.5 }, - "_id": "92S57RCOlK5YDzC9YRlDeu" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8103,7 +8103,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "1bs2BcazpFLIGUldisWj6Q" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8123,7 +8123,7 @@ }, "_alignFlags": 8, "_target": null, - "_left": 286.1365057898962, + "_left": 251.15849999999998, "_right": 0, "_top": 0, "_bottom": 0, @@ -8139,7 +8139,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "f3ub8A/9lIAKXcGVfo85Rg" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8184,7 +8184,7 @@ }, "_lpos": { "__type__": "cc.Vec3", - "x": 32.839000000000006, + "x": -26.13900000000001, "y": 37.45, "z": 0 }, @@ -8209,7 +8209,7 @@ "y": 0, "z": 0 }, - "_id": "14dF+kHaFBwpM0ahasN1e7" + "_id": "" }, { "__type__": "cc.UITransform", @@ -8230,10 +8230,10 @@ }, "_anchorPoint": { "__type__": "cc.Vec2", - "x": 0.5, + "x": 0, "y": 0.5 }, - "_id": "5b0qPLVh5OaYWqvzerhH2v" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8301,7 +8301,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "bddquXGa1ED5WKCh+k7H4m" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8321,7 +8321,7 @@ }, "_alignFlags": 8, "_target": null, - "_left": 284.8925115797925, + "_left": 249.91449999999998, "_right": 0, "_top": 0, "_bottom": 0, @@ -8337,7 +8337,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "72KiKnjpNEhbnypnWIhw+J" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8378,7 +8378,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "f06RZZY5REw64GItGC5rBz" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8414,7 +8414,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "3498vPPh5PVZuweGWzrHDw" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8455,7 +8455,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "c1bYC0cA9GdozStoGw/4jZ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8491,7 +8491,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "83uUKjEkRGloR/fZuj2Bsc" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8532,7 +8532,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "0cmhWzLPtJYKWxRTjmuhHJ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8568,7 +8568,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "3dHrslIpdIbLwp1vPK6W+5" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8620,7 +8620,7 @@ "__expectedType__": "cc.SpriteFrame" } ], - "_id": "56A+j29e1Irq81mTznBKkS" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -8636,7 +8636,6 @@ }, "fileId": "C4uvwOyFhN4Z2eOS0/hX7g", "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null + "targetOverrides": null } ] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer609_Setting.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer609_Setting.prefab index fcd5fc6..47a7bf4 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer609_Setting.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer609_Setting.prefab @@ -25,14 +25,14 @@ "_active": true, "_components": [ { - "__id__": 128 + "__id__": 144 }, { - "__id__": 130 + "__id__": 146 } ], "_prefab": { - "__id__": 132 + "__id__": 148 }, "_lpos": { "__type__": "cc.Vec3", @@ -61,7 +61,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "f0Dhxuw1hOS4Hn5/KeJ+KF" }, { "__type__": "cc.Node", @@ -88,10 +88,7 @@ "__id__": 35 }, { - "__id__": 43 - }, - { - "__id__": 51 + "__id__": 47 }, { "__id__": 59 @@ -112,23 +109,26 @@ "__id__": 99 }, { - "__id__": 107 + "__id__": 111 }, { - "__id__": 115 + "__id__": 119 + }, + { + "__id__": 127 } ], "_active": true, "_components": [ { - "__id__": 123 + "__id__": 139 }, { - "__id__": 125 + "__id__": 141 } ], "_prefab": { - "__id__": 127 + "__id__": 143 }, "_lpos": { "__type__": "cc.Vec3", @@ -157,7 +157,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "a0SrwQOERGCLLjEzCp1Ziv" }, { "__type__": "cc.Node", @@ -210,7 +210,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "08E3Z7QUZA9IVAGPhm7K++" }, { "__type__": "cc.UITransform", @@ -234,7 +234,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "d9A9RkuDhI+YsaVraaQ4KM" }, { "__type__": "cc.CompPrefabInfo", @@ -279,7 +279,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "fcWYEgCgRFqql1H3I7DVmE" }, { "__type__": "cc.CompPrefabInfo", @@ -315,7 +315,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "18QRHskldGOpk7eTJ3rHhp" }, { "__type__": "cc.CompPrefabInfo", @@ -385,7 +385,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "78RqNXx0pBwJbIQD2qo59C" }, { "__type__": "cc.UITransform", @@ -409,7 +409,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "cfpUMm+txPnYo4568emXNK" }, { "__type__": "cc.CompPrefabInfo", @@ -454,7 +454,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "3dxKVVtedANYdQQ6MBSXGg" }, { "__type__": "cc.CompPrefabInfo", @@ -490,7 +490,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "09TRgPrHtJwa1gYO/ilGBP" }, { "__type__": "cc.CompPrefabInfo", @@ -560,7 +560,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "24J7/SivdMxLnjZJdNU4NU" }, { "__type__": "cc.UITransform", @@ -584,7 +584,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "53A1ydh0tLw6oSVkaPd9Rg" }, { "__type__": "cc.CompPrefabInfo", @@ -629,7 +629,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "b495O6IKxIJL8uR8IYcov7" }, { "__type__": "cc.CompPrefabInfo", @@ -665,7 +665,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "79Kp7CAY5HWYXJEaoGOEZK" }, { "__type__": "cc.CompPrefabInfo", @@ -735,7 +735,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "61gg31HW9PO6t6tD4xVi1C" }, { "__type__": "cc.UITransform", @@ -759,7 +759,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "beaobVTe9CL7XMT6BdGDba" }, { "__type__": "cc.CompPrefabInfo", @@ -804,7 +804,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "cezHnExtZLCJmjaUxIZ8b1" }, { "__type__": "cc.CompPrefabInfo", @@ -840,7 +840,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "65D6FEGI1MuZPAiguWCWY4" }, { "__type__": "cc.CompPrefabInfo", @@ -878,10 +878,16 @@ }, { "__id__": 40 + }, + { + "__id__": 42 + }, + { + "__id__": 44 } ], "_prefab": { - "__id__": 42 + "__id__": 46 }, "_lpos": { "__type__": "cc.Vec3", @@ -910,7 +916,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "0aqgJpZQBJ6aM/S0Lp6lwx" }, { "__type__": "cc.UITransform", @@ -934,7 +940,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "293Xg+DBREpo1ZHIx3Kwpb" }, { "__type__": "cc.CompPrefabInfo", @@ -979,7 +985,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "39FuB+k5NBVozBLwIlMl5A" }, { "__type__": "cc.CompPrefabInfo", @@ -1015,12 +1021,53 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "6aN3H2HohBWo4RBBV+/9oi" }, { "__type__": "cc.CompPrefabInfo", "fileId": "NGnnVz4Kg0+R1e9CiDlfUA" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 43 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "80plUMWkhHOrqYl7M5QU+z" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "6V+vN/Kdp1NBiisUBiQEbQ" + }, + { + "__type__": "3ba9bLWN3BC6op8VlQtJeKI", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 45 + }, + "onClip": null, + "offClip": null, + "_id": "8aXripSEBHOahzzKy9Boyg" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "+tGSsBOnMRDCOtoSKShcCw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1045,18 +1092,24 @@ "_children": [], "_active": true, "_components": [ - { - "__id__": 44 - }, - { - "__id__": 46 - }, { "__id__": 48 + }, + { + "__id__": 50 + }, + { + "__id__": 52 + }, + { + "__id__": 54 + }, + { + "__id__": 56 } ], "_prefab": { - "__id__": 50 + "__id__": 58 }, "_lpos": { "__type__": "cc.Vec3", @@ -1085,7 +1138,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "d1fq2AHfBCJpfVW7FsRznM" }, { "__type__": "cc.UITransform", @@ -1093,11 +1146,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 47 }, "_enabled": true, "__prefab": { - "__id__": 45 + "__id__": 49 }, "_contentSize": { "__type__": "cc.Size", @@ -1109,7 +1162,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "d7wuFaDnxKK4q/qeEh3CQm" }, { "__type__": "cc.CompPrefabInfo", @@ -1121,11 +1174,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 47 }, "_enabled": true, "__prefab": { - "__id__": 47 + "__id__": 51 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1154,7 +1207,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "edFz3AXZVLupcNvNTK+eLV" }, { "__type__": "cc.CompPrefabInfo", @@ -1166,11 +1219,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 47 }, "_enabled": true, "__prefab": { - "__id__": 49 + "__id__": 53 }, "_alignFlags": 16, "_target": null, @@ -1190,12 +1243,53 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "96RLWr87VJLaDsopYYEm/K" }, { "__type__": "cc.CompPrefabInfo", "fileId": "Q5LlMKAOS/Ml03RLSSmi/g" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 47 + }, + "_enabled": true, + "__prefab": { + "__id__": 55 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "96Bl09PKpO0JZzDqOsnQdV" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "z7TZ6vb5YV8SRV8yxWm1AQ" + }, + { + "__type__": "3ba9bLWN3BC6op8VlQtJeKI", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 47 + }, + "_enabled": true, + "__prefab": { + "__id__": 57 + }, + "onClip": null, + "offClip": null, + "_id": "0fsUC0xNZJPqavJomOJlpZ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "zGQ9rGIk4xLvT/P1d0RDSA" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1221,17 +1315,17 @@ "_active": true, "_components": [ { - "__id__": 52 + "__id__": 60 }, { - "__id__": 54 + "__id__": 62 }, { - "__id__": 56 + "__id__": 64 } ], "_prefab": { - "__id__": 58 + "__id__": 66 }, "_lpos": { "__type__": "cc.Vec3", @@ -1260,7 +1354,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "74jbaWbddLL7/ricPuT9AS" }, { "__type__": "cc.UITransform", @@ -1268,11 +1362,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 59 }, "_enabled": true, "__prefab": { - "__id__": 53 + "__id__": 61 }, "_contentSize": { "__type__": "cc.Size", @@ -1284,7 +1378,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "20WqXOJbNCSZF9rhJ9kPzc" }, { "__type__": "cc.CompPrefabInfo", @@ -1296,11 +1390,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 59 }, "_enabled": true, "__prefab": { - "__id__": 55 + "__id__": 63 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1329,7 +1423,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "ffLthedsJHwJk5Cyp3pqrW" }, { "__type__": "cc.CompPrefabInfo", @@ -1341,11 +1435,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 59 }, "_enabled": true, "__prefab": { - "__id__": 57 + "__id__": 65 }, "_alignFlags": 16, "_target": null, @@ -1365,7 +1459,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "487NKuRq9OApk3zr+Uxtkr" }, { "__type__": "cc.CompPrefabInfo", @@ -1396,17 +1490,17 @@ "_active": true, "_components": [ { - "__id__": 60 + "__id__": 68 }, { - "__id__": 62 + "__id__": 70 }, { - "__id__": 64 + "__id__": 72 } ], "_prefab": { - "__id__": 66 + "__id__": 74 }, "_lpos": { "__type__": "cc.Vec3", @@ -1435,7 +1529,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "79moPoAn1MVLsiGrRS/vpa" }, { "__type__": "cc.UITransform", @@ -1443,11 +1537,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 67 }, "_enabled": true, "__prefab": { - "__id__": 61 + "__id__": 69 }, "_contentSize": { "__type__": "cc.Size", @@ -1459,7 +1553,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "0dPx8hG7RGlYnzmzyrgMUJ" }, { "__type__": "cc.CompPrefabInfo", @@ -1471,11 +1565,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 67 }, "_enabled": true, "__prefab": { - "__id__": 63 + "__id__": 71 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1504,7 +1598,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "fchvbKJRlGv5I/iE4xKOFx" }, { "__type__": "cc.CompPrefabInfo", @@ -1516,11 +1610,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 67 }, "_enabled": true, "__prefab": { - "__id__": 65 + "__id__": 73 }, "_alignFlags": 16, "_target": null, @@ -1540,7 +1634,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "daK+Y+X5NEgLR5940q6GC2" }, { "__type__": "cc.CompPrefabInfo", @@ -1571,17 +1665,17 @@ "_active": true, "_components": [ { - "__id__": 68 + "__id__": 76 }, { - "__id__": 70 + "__id__": 78 }, { - "__id__": 72 + "__id__": 80 } ], "_prefab": { - "__id__": 74 + "__id__": 82 }, "_lpos": { "__type__": "cc.Vec3", @@ -1610,7 +1704,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "82FkkspCJMh425BUuz0Kt7" }, { "__type__": "cc.UITransform", @@ -1618,11 +1712,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 75 }, "_enabled": true, "__prefab": { - "__id__": 69 + "__id__": 77 }, "_contentSize": { "__type__": "cc.Size", @@ -1634,7 +1728,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "2c9mbM141Cz5X2Z/up2apb" }, { "__type__": "cc.CompPrefabInfo", @@ -1646,11 +1740,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 75 }, "_enabled": true, "__prefab": { - "__id__": 71 + "__id__": 79 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1679,7 +1773,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "0ajTYtmWFGebGLQY+z5JUh" }, { "__type__": "cc.CompPrefabInfo", @@ -1691,11 +1785,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 75 }, "_enabled": true, "__prefab": { - "__id__": 73 + "__id__": 81 }, "_alignFlags": 16, "_target": null, @@ -1715,7 +1809,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "23vy/kQRJHCIAKClKI18vR" }, { "__type__": "cc.CompPrefabInfo", @@ -1746,17 +1840,17 @@ "_active": true, "_components": [ { - "__id__": 76 + "__id__": 84 }, { - "__id__": 78 + "__id__": 86 }, { - "__id__": 80 + "__id__": 88 } ], "_prefab": { - "__id__": 82 + "__id__": 90 }, "_lpos": { "__type__": "cc.Vec3", @@ -1785,7 +1879,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "76wa3HSZxKY7uYaWc32B3K" }, { "__type__": "cc.UITransform", @@ -1793,11 +1887,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 83 }, "_enabled": true, "__prefab": { - "__id__": 77 + "__id__": 85 }, "_contentSize": { "__type__": "cc.Size", @@ -1809,7 +1903,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "0efE+ObhVIrIx/PhuSuxfV" }, { "__type__": "cc.CompPrefabInfo", @@ -1821,11 +1915,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 83 }, "_enabled": true, "__prefab": { - "__id__": 79 + "__id__": 87 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1854,7 +1948,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "29F13tvoFLNLAFddfo0p6s" }, { "__type__": "cc.CompPrefabInfo", @@ -1866,11 +1960,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 83 }, "_enabled": true, "__prefab": { - "__id__": 81 + "__id__": 89 }, "_alignFlags": 16, "_target": null, @@ -1890,7 +1984,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "c7uwibQUpGwbbuCDCxSp9c" }, { "__type__": "cc.CompPrefabInfo", @@ -1921,17 +2015,17 @@ "_active": true, "_components": [ { - "__id__": 84 + "__id__": 92 }, { - "__id__": 86 + "__id__": 94 }, { - "__id__": 88 + "__id__": 96 } ], "_prefab": { - "__id__": 90 + "__id__": 98 }, "_lpos": { "__type__": "cc.Vec3", @@ -1960,7 +2054,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "dacY9eiaZJo6VvQhQfx44A" }, { "__type__": "cc.UITransform", @@ -1968,11 +2062,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 91 }, "_enabled": true, "__prefab": { - "__id__": 85 + "__id__": 93 }, "_contentSize": { "__type__": "cc.Size", @@ -1984,7 +2078,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "5arTM0xK1H2ptuQXMNq2oc" }, { "__type__": "cc.CompPrefabInfo", @@ -1996,11 +2090,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 91 }, "_enabled": true, "__prefab": { - "__id__": 87 + "__id__": 95 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2029,7 +2123,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "5b3cWFKw9AP7B9bLB7gz/c" }, { "__type__": "cc.CompPrefabInfo", @@ -2041,11 +2135,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 91 }, "_enabled": true, "__prefab": { - "__id__": 89 + "__id__": 97 }, "_alignFlags": 16, "_target": null, @@ -2065,7 +2159,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "27wVS1uOpGuo2NjJAMqvPK" }, { "__type__": "cc.CompPrefabInfo", @@ -2096,17 +2190,23 @@ "_active": true, "_components": [ { - "__id__": 92 + "__id__": 100 }, { - "__id__": 94 + "__id__": 102 }, { - "__id__": 96 + "__id__": 104 + }, + { + "__id__": 106 + }, + { + "__id__": 108 } ], "_prefab": { - "__id__": 98 + "__id__": 110 }, "_lpos": { "__type__": "cc.Vec3", @@ -2135,7 +2235,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "aaM6I7JONGmJP7Z9MwX6wt" }, { "__type__": "cc.UITransform", @@ -2143,11 +2243,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 99 }, "_enabled": true, "__prefab": { - "__id__": 93 + "__id__": 101 }, "_contentSize": { "__type__": "cc.Size", @@ -2159,7 +2259,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "89tjLPxIdBIYKv8t1Gj8Bt" }, { "__type__": "cc.CompPrefabInfo", @@ -2171,11 +2271,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 99 }, "_enabled": true, "__prefab": { - "__id__": 95 + "__id__": 103 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2204,7 +2304,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "b9nKA89WhMza0vfrp90ZmG" }, { "__type__": "cc.CompPrefabInfo", @@ -2216,11 +2316,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 99 }, "_enabled": true, "__prefab": { - "__id__": 97 + "__id__": 105 }, "_alignFlags": 16, "_target": null, @@ -2240,12 +2340,53 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "6bWUShS2pC25AAcvbm4qkW" }, { "__type__": "cc.CompPrefabInfo", "fileId": "SGdsByLLA3hjOUqV205KnA" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 99 + }, + "_enabled": true, + "__prefab": { + "__id__": 107 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "1dZmS45I1I45qwWwXPPIeq" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "XAqdvFhK8MQmbC757F4SSw" + }, + { + "__type__": "3ba9bLWN3BC6op8VlQtJeKI", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 99 + }, + "_enabled": true, + "__prefab": { + "__id__": 109 + }, + "onClip": null, + "offClip": null, + "_id": "d502x34VhLEaXiBROkf5Of" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "0FkqUEot/OxJXdVLK1XDoA" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -2271,17 +2412,17 @@ "_active": true, "_components": [ { - "__id__": 100 + "__id__": 112 }, { - "__id__": 102 + "__id__": 114 }, { - "__id__": 104 + "__id__": 116 } ], "_prefab": { - "__id__": 106 + "__id__": 118 }, "_lpos": { "__type__": "cc.Vec3", @@ -2310,7 +2451,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "820cIyS0dPWIrf4ypUBnl8" }, { "__type__": "cc.UITransform", @@ -2318,11 +2459,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 111 }, "_enabled": true, "__prefab": { - "__id__": 101 + "__id__": 113 }, "_contentSize": { "__type__": "cc.Size", @@ -2334,7 +2475,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "4aYJGKG79GgpqbxxJH9zzD" }, { "__type__": "cc.CompPrefabInfo", @@ -2346,11 +2487,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 111 }, "_enabled": true, "__prefab": { - "__id__": 103 + "__id__": 115 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2379,7 +2520,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "b9CGgCS9NKSYGPsj3N53hE" }, { "__type__": "cc.CompPrefabInfo", @@ -2391,11 +2532,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 111 }, "_enabled": true, "__prefab": { - "__id__": 105 + "__id__": 117 }, "_alignFlags": 16, "_target": null, @@ -2415,7 +2556,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "95jyzZkL1O6JgwTuDprBWg" }, { "__type__": "cc.CompPrefabInfo", @@ -2446,17 +2587,17 @@ "_active": true, "_components": [ { - "__id__": 108 + "__id__": 120 }, { - "__id__": 110 + "__id__": 122 }, { - "__id__": 112 + "__id__": 124 } ], "_prefab": { - "__id__": 114 + "__id__": 126 }, "_lpos": { "__type__": "cc.Vec3", @@ -2485,7 +2626,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "15E7eUx51GT78Z4WFssS6b" }, { "__type__": "cc.UITransform", @@ -2493,11 +2634,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 119 }, "_enabled": true, "__prefab": { - "__id__": 109 + "__id__": 121 }, "_contentSize": { "__type__": "cc.Size", @@ -2509,7 +2650,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "72YPTA9+9M3oYPDxIyPrnj" }, { "__type__": "cc.CompPrefabInfo", @@ -2521,11 +2662,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 119 }, "_enabled": true, "__prefab": { - "__id__": 111 + "__id__": 123 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2554,7 +2695,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "40psopC/hP5YZ4iJZZVkb2" }, { "__type__": "cc.CompPrefabInfo", @@ -2566,11 +2707,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 119 }, "_enabled": true, "__prefab": { - "__id__": 113 + "__id__": 125 }, "_alignFlags": 16, "_target": null, @@ -2590,7 +2731,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "e1nPUGejlM/ad2qE6XtgK9" }, { "__type__": "cc.CompPrefabInfo", @@ -2621,17 +2762,23 @@ "_active": true, "_components": [ { - "__id__": 116 + "__id__": 128 }, { - "__id__": 118 + "__id__": 130 }, { - "__id__": 120 + "__id__": 132 + }, + { + "__id__": 134 + }, + { + "__id__": 136 } ], "_prefab": { - "__id__": 122 + "__id__": 138 }, "_lpos": { "__type__": "cc.Vec3", @@ -2660,7 +2807,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "a2U/rjo4pB578+vm3c1n4o" }, { "__type__": "cc.UITransform", @@ -2668,11 +2815,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 127 }, "_enabled": true, "__prefab": { - "__id__": 117 + "__id__": 129 }, "_contentSize": { "__type__": "cc.Size", @@ -2684,7 +2831,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "f1mL03AaFMBoq3vsKS5jUY" }, { "__type__": "cc.CompPrefabInfo", @@ -2696,11 +2843,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 127 }, "_enabled": true, "__prefab": { - "__id__": 119 + "__id__": 131 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2729,7 +2876,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "e8PCj6t+JKjpxwxVL1VlVV" }, { "__type__": "cc.CompPrefabInfo", @@ -2741,11 +2888,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 127 }, "_enabled": true, "__prefab": { - "__id__": 121 + "__id__": 133 }, "_alignFlags": 16, "_target": null, @@ -2765,12 +2912,53 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "96cXFvxmVBp4ZVQZvsr+Zl" }, { "__type__": "cc.CompPrefabInfo", "fileId": "bc3xJw9SqwrImunPCOpyRg" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 127 + }, + "_enabled": true, + "__prefab": { + "__id__": 135 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "f6p/9zWo1Ee7CAofyPeeU8" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "bsH4jDgkDA9GZwDFvHjxIg" + }, + { + "__type__": "3ba9bLWN3BC6op8VlQtJeKI", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 127 + }, + "_enabled": true, + "__prefab": { + "__id__": 137 + }, + "onClip": null, + "offClip": null, + "_id": "6di/3IrDNArIswNj1FWfh3" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "f8fkFMY6sVtoMLJhtIIJcg" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -2794,7 +2982,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 124 + "__id__": 140 }, "_contentSize": { "__type__": "cc.Size", @@ -2806,7 +2994,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "a8XhIMG/JBQ4unnSiE/vjl" }, { "__type__": "cc.CompPrefabInfo", @@ -2822,7 +3010,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 126 + "__id__": 142 }, "_alignFlags": 45, "_target": null, @@ -2842,7 +3030,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "4bbMIYPHJDkogg8hsRUwlz" }, { "__type__": "cc.CompPrefabInfo", @@ -2871,7 +3059,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 129 + "__id__": 145 }, "_contentSize": { "__type__": "cc.Size", @@ -2883,7 +3071,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "b1Pd4HbrlCQ5dGX+mcZVru" }, { "__type__": "cc.CompPrefabInfo", @@ -2899,7 +3087,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 131 + "__id__": 147 }, "_alignFlags": 45, "_target": null, @@ -2919,7 +3107,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "cdDTzZNAtOtr8o+Jvz3sZG" }, { "__type__": "cc.CompPrefabInfo", @@ -2935,6 +3123,7 @@ }, "fileId": "3EFdj56IbuTb8EVuqz7p3Q", "instance": null, - "targetOverrides": null + "targetOverrides": null, + "nestedPrefabInstanceRoots": null } ] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab index d495bac..10f9e60 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab @@ -25,14 +25,14 @@ "_active": true, "_components": [ { - "__id__": 24 + "__id__": 25 }, { - "__id__": 26 + "__id__": 27 } ], "_prefab": { - "__id__": 28 + "__id__": 29 }, "_lpos": { "__type__": "cc.Vec3", @@ -82,14 +82,14 @@ "_active": true, "_components": [ { - "__id__": 19 + "__id__": 20 }, { - "__id__": 21 + "__id__": 22 } ], "_prefab": { - "__id__": 23 + "__id__": 24 }, "_lpos": { "__type__": "cc.Vec3", @@ -186,6 +186,7 @@ "__id__": 5 }, "_contentSize": { + "__type__": "cc.Size", "width": 1600, "height": 720 }, @@ -313,10 +314,13 @@ }, { "__id__": 16 + }, + { + "__id__": 18 } ], "_prefab": { - "__id__": 18 + "__id__": 19 }, "_lpos": { "__type__": "cc.Vec3", @@ -360,6 +364,7 @@ "__id__": 13 }, "_contentSize": { + "__type__": "cc.Size", "width": 150, "height": 150 }, @@ -455,6 +460,29 @@ "__type__": "cc.CompPrefabInfo", "fileId": "3wlBO4ciKrxeTYsQdlQvBg" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": null, + "playOnLoad": false, + "_clips": [ + { + "__uuid__": "987771c5-7f29-487a-b61a-476981cc9216", + "__expectedType__": "cc.AnimationClip" + } + ], + "_defaultClip": { + "__uuid__": "987771c5-7f29-487a-b61a-476981cc9216", + "__expectedType__": "cc.AnimationClip" + }, + "_id": "1b3VMKLpRFPKzD9o608yTZ" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -478,9 +506,10 @@ }, "_enabled": true, "__prefab": { - "__id__": 20 + "__id__": 21 }, "_contentSize": { + "__type__": "cc.Size", "width": 1600, "height": 720 }, @@ -505,7 +534,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 22 + "__id__": 23 }, "_alignFlags": 45, "_target": null, @@ -554,9 +583,10 @@ }, "_enabled": true, "__prefab": { - "__id__": 25 + "__id__": 26 }, "_contentSize": { + "__type__": "cc.Size", "width": 1600, "height": 720 }, @@ -581,7 +611,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 27 + "__id__": 28 }, "_alignFlags": 45, "_target": null, diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer8_Notice.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer8_Notice.prefab index b2253e5..cd210ac 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer8_Notice.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer8_Notice.prefab @@ -28,14 +28,14 @@ "_active": true, "_components": [ { - "__id__": 86 + "__id__": 114 }, { - "__id__": 88 + "__id__": 116 } ], "_prefab": { - "__id__": 90 + "__id__": 118 }, "_lpos": { "__type__": "cc.Vec3", @@ -64,7 +64,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "0aubP5/idJN5q/5U5Z0fJR" }, { "__type__": "cc.Node", @@ -118,7 +118,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "8cdbGZmJFBP7WFAmuLES/n" }, { "__type__": "cc.Node", @@ -171,7 +171,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "c9Lw91eSVLAJ80lgmQ96uC" }, { "__type__": "cc.UITransform", @@ -195,7 +195,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "7aYAjHWNpCNb5c1kDWcARG" }, { "__type__": "cc.CompPrefabInfo", @@ -263,7 +263,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "" + "_id": "dcEYoelw9GH7WONpiiETEo" }, { "__type__": "cc.CompPrefabInfo", @@ -299,7 +299,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "12K/cOkUdJH7lZdiaPrQsA" }, { "__type__": "cc.CompPrefabInfo", @@ -340,7 +340,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "e7HX9pOEBGZZgM+6AZ46U4" }, { "__type__": "cc.CompPrefabInfo", @@ -376,7 +376,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "0cPeAe9E9EkYMUQ4EVClxk" }, { "__type__": "cc.CompPrefabInfo", @@ -411,35 +411,35 @@ "__id__": 25 }, { - "__id__": 33 - }, - { - "__id__": 41 + "__id__": 37 }, { "__id__": 49 }, { - "__id__": 57 - }, - { - "__id__": 65 + "__id__": 61 }, { "__id__": 73 + }, + { + "__id__": 85 + }, + { + "__id__": 97 } ], "_active": true, "_components": [ { - "__id__": 81 + "__id__": 109 }, { - "__id__": 83 + "__id__": 111 } ], "_prefab": { - "__id__": 85 + "__id__": 113 }, "_lpos": { "__type__": "cc.Vec3", @@ -468,7 +468,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "77M62T1WlFVJcA/i5ZZWzf" }, { "__type__": "cc.Node", @@ -479,7 +479,7 @@ "__id__": 16 }, "_children": [], - "_active": false, + "_active": true, "_components": [ { "__id__": 18 @@ -521,7 +521,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "1bXcxAaepJTIG1wofscFQ7" }, { "__type__": "cc.UITransform", @@ -545,7 +545,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "57cVsP8lFP66g1ksEfwPHZ" }, { "__type__": "cc.CompPrefabInfo", @@ -590,7 +590,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "c07j8EnDlDLqxGh5ZYrNDj" }, { "__type__": "cc.CompPrefabInfo", @@ -626,7 +626,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "49A2dt+qRCMI1UrhBTxHcV" }, { "__type__": "cc.CompPrefabInfo", @@ -664,10 +664,16 @@ }, { "__id__": 30 + }, + { + "__id__": 32 + }, + { + "__id__": 34 } ], "_prefab": { - "__id__": 32 + "__id__": 36 }, "_lpos": { "__type__": "cc.Vec3", @@ -696,7 +702,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "b8Tf+AC5VPkKwkC6rDRxHP" }, { "__type__": "cc.UITransform", @@ -720,7 +726,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "ecppufUMBKqLEXT3Njg5au" }, { "__type__": "cc.CompPrefabInfo", @@ -765,7 +771,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "36zvEw0K5HybjmftQUvbnp" }, { "__type__": "cc.CompPrefabInfo", @@ -801,12 +807,51 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "2bpAfsvWREjLyO7eSK8g1j" }, { "__type__": "cc.CompPrefabInfo", "fileId": "vwQB3S9MQC/vQBEzNyqB+A" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 25 + }, + "_enabled": true, + "__prefab": { + "__id__": 33 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "c3fI8llclOKIVpIm45fMeG" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "P7S6YlKTkuNGTVfBAcsOog" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 25 + }, + "_enabled": true, + "__prefab": { + "__id__": 35 + }, + "_id": "2fpEkZlCVHNJ4mH3QvlF6B" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "GzFxaPeKApYm+HQa2sK6Iw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -831,18 +876,24 @@ "_children": [], "_active": true, "_components": [ - { - "__id__": 34 - }, - { - "__id__": 36 - }, { "__id__": 38 + }, + { + "__id__": 40 + }, + { + "__id__": 42 + }, + { + "__id__": 44 + }, + { + "__id__": 46 } ], "_prefab": { - "__id__": 40 + "__id__": 48 }, "_lpos": { "__type__": "cc.Vec3", @@ -871,7 +922,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "admKln30BIXJKc7qtA7lKG" }, { "__type__": "cc.UITransform", @@ -879,11 +930,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 33 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 35 + "__id__": 39 }, "_contentSize": { "__type__": "cc.Size", @@ -895,7 +946,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "fdD3lBeSJCOri0al8UtQmZ" }, { "__type__": "cc.CompPrefabInfo", @@ -907,11 +958,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 33 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 37 + "__id__": 41 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -940,7 +991,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "ecvIuSeFNLY4vIxh04qu28" }, { "__type__": "cc.CompPrefabInfo", @@ -952,11 +1003,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 33 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 39 + "__id__": 43 }, "_alignFlags": 16, "_target": null, @@ -976,12 +1027,51 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "efRSvTa1xAYpAxp1Rcx8Pd" }, { "__type__": "cc.CompPrefabInfo", "fileId": "gD0V18AmpRXwcK8tOCfe3A" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 37 + }, + "_enabled": true, + "__prefab": { + "__id__": 45 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "c81oRRgvFCZpHMyAfe1tE1" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "LM+ywGLfyO7d7aoEJZOmQA" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 37 + }, + "_enabled": true, + "__prefab": { + "__id__": 47 + }, + "_id": "bb6LHLeCRC2ZYAfxFeIfaA" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "+L1S9YdJiDQZHHH3vvpSGg" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1007,17 +1097,23 @@ "_active": true, "_components": [ { - "__id__": 42 + "__id__": 50 }, { - "__id__": 44 + "__id__": 52 }, { - "__id__": 46 + "__id__": 54 + }, + { + "__id__": 56 + }, + { + "__id__": 58 } ], "_prefab": { - "__id__": 48 + "__id__": 60 }, "_lpos": { "__type__": "cc.Vec3", @@ -1046,7 +1142,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "8aSHe5AuNLeqUQ9X0Xmbmd" }, { "__type__": "cc.UITransform", @@ -1054,11 +1150,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 41 + "__id__": 49 }, "_enabled": true, "__prefab": { - "__id__": 43 + "__id__": 51 }, "_contentSize": { "__type__": "cc.Size", @@ -1070,7 +1166,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "eaPLkHzr1Asq8G7eU2dZn3" }, { "__type__": "cc.CompPrefabInfo", @@ -1082,11 +1178,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 41 + "__id__": 49 }, "_enabled": true, "__prefab": { - "__id__": 45 + "__id__": 53 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1115,7 +1211,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "2cGZaW6NVGEa1j6uV2mGX+" }, { "__type__": "cc.CompPrefabInfo", @@ -1127,11 +1223,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 41 + "__id__": 49 }, "_enabled": true, "__prefab": { - "__id__": 47 + "__id__": 55 }, "_alignFlags": 16, "_target": null, @@ -1151,12 +1247,51 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "c7xWkRDPhFZq9LMRMc5fHS" }, { "__type__": "cc.CompPrefabInfo", "fileId": "OMeqBy4LAdiCyyornBNLcQ" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 49 + }, + "_enabled": true, + "__prefab": { + "__id__": 57 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "4f5g4zH6hAAbw9m2gAhMXa" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "BmM6tNhKwufT72VtR63BfQ" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 49 + }, + "_enabled": true, + "__prefab": { + "__id__": 59 + }, + "_id": "36ElLRnNtABqX5hiMhijh8" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "y/Q1W7uJlvY37cIPe/dIDQ" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1182,17 +1317,23 @@ "_active": true, "_components": [ { - "__id__": 50 + "__id__": 62 }, { - "__id__": 52 + "__id__": 64 }, { - "__id__": 54 + "__id__": 66 + }, + { + "__id__": 68 + }, + { + "__id__": 70 } ], "_prefab": { - "__id__": 56 + "__id__": 72 }, "_lpos": { "__type__": "cc.Vec3", @@ -1221,7 +1362,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "c0FdOTfpRHt6XX9WNwbcga" }, { "__type__": "cc.UITransform", @@ -1229,11 +1370,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 49 + "__id__": 61 }, "_enabled": true, "__prefab": { - "__id__": 51 + "__id__": 63 }, "_contentSize": { "__type__": "cc.Size", @@ -1245,7 +1386,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "396rladJ9C0Y3IiKMYAzZV" }, { "__type__": "cc.CompPrefabInfo", @@ -1257,11 +1398,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 49 + "__id__": 61 }, "_enabled": true, "__prefab": { - "__id__": 53 + "__id__": 65 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1290,7 +1431,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "45BtfKqg5Hpa+gKO/N1OWG" }, { "__type__": "cc.CompPrefabInfo", @@ -1302,11 +1443,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 49 + "__id__": 61 }, "_enabled": true, "__prefab": { - "__id__": 55 + "__id__": 67 }, "_alignFlags": 16, "_target": null, @@ -1326,12 +1467,51 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "7f2tcDKeFGzqtOiY4E2TGC" }, { "__type__": "cc.CompPrefabInfo", "fileId": "15vrMN17M43QW5Dn6N19ww" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 61 + }, + "_enabled": true, + "__prefab": { + "__id__": 69 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "0eZ/SRTbpM37H5f0/KmdAl" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ot8yxj5MVPRJKFy6e8Oggw" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 61 + }, + "_enabled": true, + "__prefab": { + "__id__": 71 + }, + "_id": "1erRTxSuFLrKjiy54KoZfB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2laJQkUuyfjz6OBxg+cNoA" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1357,17 +1537,23 @@ "_active": true, "_components": [ { - "__id__": 58 + "__id__": 74 }, { - "__id__": 60 + "__id__": 76 }, { - "__id__": 62 + "__id__": 78 + }, + { + "__id__": 80 + }, + { + "__id__": 82 } ], "_prefab": { - "__id__": 64 + "__id__": 84 }, "_lpos": { "__type__": "cc.Vec3", @@ -1396,7 +1582,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "8132yv3uNAs574RVAxup3/" }, { "__type__": "cc.UITransform", @@ -1404,11 +1590,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 57 + "__id__": 73 }, "_enabled": true, "__prefab": { - "__id__": 59 + "__id__": 75 }, "_contentSize": { "__type__": "cc.Size", @@ -1420,7 +1606,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "fbqGKfsclKTKvas6RhpL49" }, { "__type__": "cc.CompPrefabInfo", @@ -1432,11 +1618,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 57 + "__id__": 73 }, "_enabled": true, "__prefab": { - "__id__": 61 + "__id__": 77 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1465,7 +1651,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "d7qg7RHsxApo618b+4PjHU" }, { "__type__": "cc.CompPrefabInfo", @@ -1477,11 +1663,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 57 + "__id__": 73 }, "_enabled": true, "__prefab": { - "__id__": 63 + "__id__": 79 }, "_alignFlags": 16, "_target": null, @@ -1501,12 +1687,51 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "a1TZn+QftOT4fxxTU9PeSO" }, { "__type__": "cc.CompPrefabInfo", "fileId": "LiL8ZRO1o42ClUv99e+Ovg" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 73 + }, + "_enabled": true, + "__prefab": { + "__id__": 81 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "39CB8fkppLdKSBaERI5ip3" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ScyvH+QiRty2+66Xs9xyhQ" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 73 + }, + "_enabled": true, + "__prefab": { + "__id__": 83 + }, + "_id": "29drnHayBC+q8U8dmUp/Wo" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "xZ1ixjm9QRLuiHR/KYWVag" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1532,17 +1757,23 @@ "_active": true, "_components": [ { - "__id__": 66 + "__id__": 86 }, { - "__id__": 68 + "__id__": 88 }, { - "__id__": 70 + "__id__": 90 + }, + { + "__id__": 92 + }, + { + "__id__": 94 } ], "_prefab": { - "__id__": 72 + "__id__": 96 }, "_lpos": { "__type__": "cc.Vec3", @@ -1571,7 +1802,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "aeD4xav55Mra+GyqO4oMvt" }, { "__type__": "cc.UITransform", @@ -1579,11 +1810,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 65 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 67 + "__id__": 87 }, "_contentSize": { "__type__": "cc.Size", @@ -1595,7 +1826,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "7aWj7EGQdOk4rwzuepIudn" }, { "__type__": "cc.CompPrefabInfo", @@ -1607,11 +1838,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 65 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 69 + "__id__": 89 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1640,7 +1871,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "90mtsFFDFPKLvaJyeOymxz" }, { "__type__": "cc.CompPrefabInfo", @@ -1652,11 +1883,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 65 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 71 + "__id__": 91 }, "_alignFlags": 16, "_target": null, @@ -1676,12 +1907,51 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "7fr4kdTcFAeqPZjLZlMDb+" }, { "__type__": "cc.CompPrefabInfo", "fileId": "bU+r7NWs52IFbhAJ9qIHKA" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 85 + }, + "_enabled": true, + "__prefab": { + "__id__": 93 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "fe4Pic6/dIkbJ07hP5m8KJ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "7V/BFzqwv7HeJE0tgdFpbg" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 85 + }, + "_enabled": true, + "__prefab": { + "__id__": 95 + }, + "_id": "eeXgVBbMVM1J54oAcXHzzJ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "g4HFDk/t2SgiEzP+JjYQWQ" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1707,17 +1977,23 @@ "_active": true, "_components": [ { - "__id__": 74 + "__id__": 98 }, { - "__id__": 76 + "__id__": 100 }, { - "__id__": 78 + "__id__": 102 + }, + { + "__id__": 104 + }, + { + "__id__": 106 } ], "_prefab": { - "__id__": 80 + "__id__": 108 }, "_lpos": { "__type__": "cc.Vec3", @@ -1746,7 +2022,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "0dfhq38CpI66kNkgxQ7j+3" }, { "__type__": "cc.UITransform", @@ -1754,11 +2030,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 73 + "__id__": 97 }, "_enabled": true, "__prefab": { - "__id__": 75 + "__id__": 99 }, "_contentSize": { "__type__": "cc.Size", @@ -1770,7 +2046,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "921n8ykChDCaXrbfZrxqhQ" }, { "__type__": "cc.CompPrefabInfo", @@ -1782,11 +2058,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 73 + "__id__": 97 }, "_enabled": true, "__prefab": { - "__id__": 77 + "__id__": 101 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1815,7 +2091,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "acjaAQQ49IBLielwGgDdoy" }, { "__type__": "cc.CompPrefabInfo", @@ -1827,11 +2103,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 73 + "__id__": 97 }, "_enabled": true, "__prefab": { - "__id__": 79 + "__id__": 103 }, "_alignFlags": 16, "_target": null, @@ -1851,12 +2127,51 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "45pI+9X09OmJyW/qlkWubs" }, { "__type__": "cc.CompPrefabInfo", "fileId": "7k+hXCUA9Zz40vJBSRAR4Q" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 97 + }, + "_enabled": true, + "__prefab": { + "__id__": 105 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "07A3JV2DlPKZ9eE1R9WxnU" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "nIppdPpj3SBG8x4OeocIDA" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 97 + }, + "_enabled": true, + "__prefab": { + "__id__": 107 + }, + "_id": "5dJsdxk7FLg6DSs0eS+Au9" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "0gZqF2arO6fU0BCrhcCGbw" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1880,7 +2195,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 82 + "__id__": 110 }, "_contentSize": { "__type__": "cc.Size", @@ -1892,7 +2207,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "e8R1LzSZVK7IJ4NOK2zvqb" }, { "__type__": "cc.CompPrefabInfo", @@ -1908,7 +2223,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 84 + "__id__": 112 }, "_alignFlags": 45, "_target": null, @@ -1928,7 +2243,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "aec7Ql7YZAgoKGIuZ1bbz+" }, { "__type__": "cc.CompPrefabInfo", @@ -1957,7 +2272,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 87 + "__id__": 115 }, "_contentSize": { "__type__": "cc.Size", @@ -1969,7 +2284,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "6fe8CaRA5NkYjsLLy3Us67" }, { "__type__": "cc.CompPrefabInfo", @@ -1985,7 +2300,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 89 + "__id__": 117 }, "_alignFlags": 45, "_target": null, @@ -2005,7 +2320,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "4brClsWu1Gb5ahxP1ze4cX" }, { "__type__": "cc.CompPrefabInfo", @@ -2021,6 +2336,7 @@ }, "fileId": "ThsJnP6HZZXePBbpuoV+sw", "instance": null, - "targetOverrides": null + "targetOverrides": null, + "nestedPrefabInstanceRoots": null } ] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab deleted file mode 100644 index bfac281..0000000 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab +++ /dev/null @@ -1,6762 +0,0 @@ -[ - { - "__type__": "cc.Prefab", - "_name": "Layer9_Feedback", - "_objFlags": 0, - "__editorExtras__": {}, - "_native": "", - "data": { - "__id__": 1 - }, - "optimizationPolicy": 0, - "persistent": false - }, - { - "__type__": "cc.Node", - "_name": "Layer9_Feedback", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": null, - "_children": [ - { - "__id__": 2 - }, - { - "__id__": 64 - }, - { - "__id__": 206 - }, - { - "__id__": 252 - } - ], - "_active": true, - "_components": [ - { - "__id__": 298 - }, - { - "__id__": 300 - } - ], - "_prefab": { - "__id__": 302 - }, - "_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": "689Pkd0hVBQYJRbcFFJqMC" - }, - { - "__type__": "cc.Node", - "_name": "group-74", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 3 - }, - { - "__id__": 11 - }, - { - "__id__": 19 - }, - { - "__id__": 27 - }, - { - "__id__": 35 - }, - { - "__id__": 43 - }, - { - "__id__": 51 - } - ], - "_active": true, - "_components": [ - { - "__id__": 59 - }, - { - "__id__": 61 - } - ], - "_prefab": { - "__id__": 63 - }, - "_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": "6atJPwSxdB7ZILtcxnSAHy" - }, - { - "__type__": "cc.Node", - "_name": "反馈遮罩", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 4 - }, - { - "__id__": 6 - }, - { - "__id__": 8 - } - ], - "_prefab": { - "__id__": 10 - }, - "_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": "06cPtS159MjZhZdPJMzNZ7" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 3 - }, - "_enabled": true, - "__prefab": { - "__id__": 5 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "3bxJcTUXJHi6GsSKIS9ZeP" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "R5IvgD+XRnXx41TgCincMA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 3 - }, - "_enabled": true, - "__prefab": { - "__id__": 7 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@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": "a5VTpYbvBKX7hGB/kZCFMN" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "MY3xYvo0+mhLT8yZ25NKTw" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 3 - }, - "_enabled": true, - "__prefab": { - "__id__": 9 - }, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "69u0aFpdNMSJVmy3GeE/+G" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "UAwevIs/xiDvMOlefRCJNQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "qRydJuFFBUygDHeV4KymcQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "反馈背景", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 12 - }, - { - "__id__": 14 - }, - { - "__id__": 16 - } - ], - "_prefab": { - "__id__": 18 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 218, - "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": "327qDRQQ9DfIETXgrvrEBv" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 11 - }, - "_enabled": true, - "__prefab": { - "__id__": 13 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1200, - "height": 200 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "31OcBfD1JCwYMOwT+I0b38" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "b5bb2fGBo5slqCp7duCziQ" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 11 - }, - "_enabled": true, - "__prefab": { - "__id__": 15 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "3e7492cb-7ae1-4f56-806d-07096662d8fb@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "222K6/qtFExZPxrQh6bs2f" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "zJIart3dIfRHTID5vhne5w" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 11 - }, - "_enabled": true, - "__prefab": { - "__id__": 17 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "32CbrfSsNOGIodZez9TrmF" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "9tYaYF6d4X/74rud4gLgQg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "CpWhEOU0Ohdh4/otgLNzew", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "反馈提交", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 20 - }, - { - "__id__": 22 - }, - { - "__id__": 24 - } - ], - "_prefab": { - "__id__": 26 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 497.5, - "y": 214, - "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": "00fK7OG2hNyaauq4CUQzGn" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 19 - }, - "_enabled": true, - "__prefab": { - "__id__": 21 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 135, - "height": 80 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "bcC1RIiLRL/7aALh1pOFVk" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "jNt74Ol9B1IHXSnxsJcV8Q" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 19 - }, - "_enabled": true, - "__prefab": { - "__id__": 23 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "6bc626b8-d480-4ba8-92ac-0f95e21761af@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": "53egZBVGdFBZfju/SKlbmH" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "xzO9yxKx4adjZHE0NweSfg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 19 - }, - "_enabled": true, - "__prefab": { - "__id__": 25 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 497.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "136YfzpQdCPYLacsz6sGNb" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "lmehedahgjuyO94v6wqUIw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "z0X+QlHGYq4z0Z/ULn5H2A", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "反馈输入内容", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 28 - }, - { - "__id__": 30 - }, - { - "__id__": 32 - } - ], - "_prefab": { - "__id__": 34 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -77, - "y": 214, - "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": "3ePxkBr9dDI7T4obnddQ7a" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 27 - }, - "_enabled": true, - "__prefab": { - "__id__": 29 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 0, - "height": 25.2 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "d1jQ0t/05Lv5D8dk/qg/M8" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "E75pHd64Xv0WhAy3ejjAHQ" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 27 - }, - "_enabled": true, - "__prefab": { - "__id__": 31 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 20, - "_overflow": 0, - "_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": "7dWKHRJsFKipTUBf0fgpUq" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "O0/pb62+3twGbC9d7rYAvg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 27 - }, - "_enabled": true, - "__prefab": { - "__id__": 33 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -77, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "cbB9FWP/hAM7sPx9YRFonJ" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "aYu4OUcEB7pKqgUvMjy/Bg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "2MNUHp6Jt1pPM93sLKHm0w", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3266", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 36 - }, - { - "__id__": 38 - }, - { - "__id__": 40 - } - ], - "_prefab": { - "__id__": 42 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 494.5, - "y": 176, - "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": "dcofmlo2RNl7rKD3lRhzcC" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 35 - }, - "_enabled": true, - "__prefab": { - "__id__": 37 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 135, - "height": 80 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "26t0bKhoNE7qrPMbgVuLtM" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "S9s3Vxi1vhO3OV6D43/w3w" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 35 - }, - "_enabled": true, - "__prefab": { - "__id__": 39 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "a933d0bf-d0bc-43e0-b475-0d9d8a16262b@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": "28hbykYtRK5bzUCs8YC/Ba" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "4JNd6cVFMfXSG9zvP4ZAOQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 35 - }, - "_enabled": true, - "__prefab": { - "__id__": 41 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 494.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "20jEOBZDJGNZb4oaTNuYrm" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "L8xhL9vivD4DC7EDJHtJDg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "RfTyCcmpP+8tkBCOWllmlg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3265", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 44 - }, - { - "__id__": 46 - }, - { - "__id__": 48 - } - ], - "_prefab": { - "__id__": 50 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 494.5, - "y": 258, - "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": "f2RmWVwwZNZ7DDxzG1dpYk" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 45 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 135, - "height": 80 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "7bRKOv6wdL+a4tYnEAgzRI" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "5bCnBdp3X9JGuNHL07B2yw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 47 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "fdc6b65e-e526-49d2-aa05-eb4d891174bb@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": "39L+hYqHVHn47bqfwEyCbC" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "HpPs/WnRn8G28M0noxuZiQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 49 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 494.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "bbwFpKeEZIP51NMS7msPwV" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "HQDIFcKta8VGbXnhH3ANxw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "H2CS83TUPxwd47FtFQ0UmQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "反馈标题", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 52 - }, - { - "__id__": 54 - }, - { - "__id__": 56 - } - ], - "_prefab": { - "__id__": 58 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 23.5, - "y": 483, - "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": "26PQvJZKpEY68HoZZnVmUl" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 51 - }, - "_enabled": true, - "__prefab": { - "__id__": 53 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 181, - "height": 70 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "a65sUTm9dCN5gEmw09x49O" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "XZxMeU/B43EWAn2YN35edQ" - }, - { - "__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__": "02d0a481-5261-4d50-ad04-2695547afe37@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": "96ZgH4C8dDDKTmsT9S/C4p" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "TN1d1c4IOzUeiwHjiHGUNg" - }, - { - "__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": 23.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "67y1hHAcxE0pC/F2iWsD07" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "CAxvd/4bCGse5s4VZnby6A" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "uNjwB9l3YB8tWK5jDFBPrA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": { - "__id__": 60 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "e32tZhLh5JDIoeWusgFeyt" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "2L/vzOpLBNUF+NW7YgeKBw" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": { - "__id__": 62 - }, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "a0uUSkonVFWpPDtNNVhugI" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "V8dJveYtIef2a866IT0w5w" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "qa/bXBGBOTih/C9NYSLsYw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "group-12", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 65 - }, - { - "__id__": 73 - }, - { - "__id__": 81 - }, - { - "__id__": 89 - }, - { - "__id__": 97 - }, - { - "__id__": 105 - }, - { - "__id__": 113 - }, - { - "__id__": 121 - }, - { - "__id__": 129 - }, - { - "__id__": 137 - }, - { - "__id__": 145 - }, - { - "__id__": 153 - }, - { - "__id__": 161 - }, - { - "__id__": 169 - }, - { - "__id__": 177 - }, - { - "__id__": 185 - }, - { - "__id__": 193 - } - ], - "_active": true, - "_components": [ - { - "__id__": 201 - }, - { - "__id__": 203 - } - ], - "_prefab": { - "__id__": 205 - }, - "_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": "71Iy0D/QpKMauuP+HU0KoW" - }, - { - "__type__": "cc.Node", - "_name": "通知遮罩", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 66 - }, - { - "__id__": 68 - }, - { - "__id__": 70 - } - ], - "_prefab": { - "__id__": 72 - }, - "_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": "b8PdP6cHNMU6eIr5Ucv16l" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 65 - }, - "_enabled": true, - "__prefab": { - "__id__": 67 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "eajEIL6VdF3oq7YKr2tmPO" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "jTjSc6ikVjd243DptWnwZA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 65 - }, - "_enabled": true, - "__prefab": { - "__id__": 69 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@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": "0dHPh31MpFErNqk971yFzf" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Myd6Mdc2d45ZYzZHHJAs2Q" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 65 - }, - "_enabled": true, - "__prefab": { - "__id__": 71 - }, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "23OhMi2gxJzZrqYSjaAkbA" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "+HR25dNgw+46DOpLCuDEIA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "rLczaWc4MWU/UE7eePwCBQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "通知背景", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 74 - }, - { - "__id__": 76 - }, - { - "__id__": 78 - } - ], - "_prefab": { - "__id__": 80 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0.5, - "y": -0.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": "82tKti+X1MbI7zJQFURkqe" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 73 - }, - "_enabled": true, - "__prefab": { - "__id__": 75 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1029, - "height": 603 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "5eHndrDXlM77Rbp3w0+Vmf" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "bO+PJtBQFam47nkQVq4X+w" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 73 - }, - "_enabled": true, - "__prefab": { - "__id__": 77 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "03393c45-1e39-42f5-b287-ea6fb574279c@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "50+6GO1tFDxLofT97xd6v6" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "SVH1QaDL2ANn6z73gJCw1w" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 73 - }, - "_enabled": true, - "__prefab": { - "__id__": 79 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "c3qFK4lSFJtILq0b9DUlws" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "sToaFwNsUEod5T/l8DzWnA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "8j5kQbpSzQcvgDLqy5Hhzw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit404", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 82 - }, - { - "__id__": 84 - }, - { - "__id__": 86 - } - ], - "_prefab": { - "__id__": 88 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -207, - "y": 167, - "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": "7405iSYT5NnZA+5jG4QgBi" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 81 - }, - "_enabled": true, - "__prefab": { - "__id__": 83 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 50, - "height": 50 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "04l1rm1fdEiYNVgGKq3zTc" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "O6ZpbJVJNTnnWfdUTU2wuw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 81 - }, - "_enabled": true, - "__prefab": { - "__id__": 85 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "dcc4530b-344b-40b6-8fbb-9fa35db6a289@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": "02fOjYg61ETbmKvMdv0fKM" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "1DU/7PWzXgp3HL4IklXtWQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 81 - }, - "_enabled": true, - "__prefab": { - "__id__": 87 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -207, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "e1TU688H5HVZ75Oeqw0SL5" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "QeZhPWf28UXy/TP+g3nshQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "xZXl7IIh/A64iZ3nhimx3A", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "通知内容", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 90 - }, - { - "__id__": 92 - }, - { - "__id__": 94 - } - ], - "_prefab": { - "__id__": 96 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -219.5, - "y": 178, - "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": "0bLZPYYHtHHbli1GXPWSri" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 89 - }, - "_enabled": true, - "__prefab": { - "__id__": 91 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 16, - "height": 20.16 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "517k5FyldOn7PY6aTSIKyk" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "gcvickHGHPQm/xSBtzg4bQ" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 89 - }, - "_enabled": true, - "__prefab": { - "__id__": 93 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 102, - "g": 41, - "b": 16, - "a": 255 - }, - "_string": "消", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 16, - "_fontSize": 16, - "_fontFamily": "Arial", - "_lineHeight": 16, - "_overflow": 0, - "_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": "dd6DMza0pCvYbUT58QplJj" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "jNoEW8APMg6QVii0ZsBTvA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 89 - }, - "_enabled": true, - "__prefab": { - "__id__": 95 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -219.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "adZ5nT/09PA5zcJ9mUNaME" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "SkVi6qIBeH+FT3o0OrhpuQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "AVtoVdcl7OCotGEhUe+nLg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit576", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 98 - }, - { - "__id__": 100 - }, - { - "__id__": 102 - } - ], - "_prefab": { - "__id__": 104 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 350, - "y": -239, - "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": "55K04Pl+1HP7UHIdv07bUs" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 97 - }, - "_enabled": true, - "__prefab": { - "__id__": 99 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 250, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "6cpaE6ETRJt4x0qBhDB1FO" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "oryuMHXxxcN/hOHR9cgK+A" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 97 - }, - "_enabled": true, - "__prefab": { - "__id__": 101 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "ac88bed1-d8b6-460d-9a1f-93d021c7b3f4@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": "465Rnfw8pBkYxi7skhBZas" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "P62Af8oAYHHDVFTVhe6zYA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 97 - }, - "_enabled": true, - "__prefab": { - "__id__": 103 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 350, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "f8GLWVmYxI7I6uNc1W3nxt" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "mI5NF0pKJ8T6guI6m48dhg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "wjGro6Vg43JcrEbGigp0eQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3102", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 106 - }, - { - "__id__": 108 - }, - { - "__id__": 110 - } - ], - "_prefab": { - "__id__": 112 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 101, - "y": -31, - "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": "51XQqxjhVNI4a8iAx2eer8" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 105 - }, - "_enabled": true, - "__prefab": { - "__id__": 107 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 752, - "height": 470 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "81IUvx3pZP0prE4zacK5nz" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "+rMinUbMpI4Ygx8WtnvdSw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 105 - }, - "_enabled": true, - "__prefab": { - "__id__": 109 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "200f41c0-4ecf-4ad7-8864-877306a46fd1@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": "86LZWJVg9Hj55B/ZD2c3Or" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "rzAAHeWiesIqtpv0gunScQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 105 - }, - "_enabled": true, - "__prefab": { - "__id__": 111 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 101, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "b14+5HJE1O0JdMWJOUPKh9" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "LaV0Dxzt+oYFyMok63CzVQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "DkvXoGoAV1kVEihDiOsd0A", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3105", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 114 - }, - { - "__id__": 116 - }, - { - "__id__": 118 - } - ], - "_prefab": { - "__id__": 120 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -22, - "y": -766, - "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": "40Xb0G5UdCfaB1YtcID2Pd" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 113 - }, - "_enabled": true, - "__prefab": { - "__id__": 115 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 770, - "height": 400 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "ecogHNgOxISb+3rlwWSc6W" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "biq7eAKo4K4+ns64FbG/xA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 113 - }, - "_enabled": true, - "__prefab": { - "__id__": 117 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "cb2429ec-da7a-421e-8635-9d8e9599dfd7@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": "67txmcBp1AZ4r/h0b4ZAmy" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "LBlstXePJimbm1hkDIGPpA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 113 - }, - "_enabled": true, - "__prefab": { - "__id__": 119 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -22, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "3dVXz7wrZAxIHnq8WWe0Tv" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "vtQ9r6tE11eSoq5HOSYOhw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "4nr4jTYKFd75UKbIsS4m/g", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3106", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 122 - }, - { - "__id__": 124 - }, - { - "__id__": 126 - } - ], - "_prefab": { - "__id__": 128 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -6, - "y": -782, - "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": "50OPYMNwRGeYiApAt/02K1" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 121 - }, - "_enabled": true, - "__prefab": { - "__id__": 123 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 770, - "height": 400 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "e6QNRTZwpLzYB0ZkSgZ1av" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "BPyXFMdPs9m9oP4GCdvEaA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 121 - }, - "_enabled": true, - "__prefab": { - "__id__": 125 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "dbeaba27-ba73-4fd0-9a5d-ae1dd8e1af1a@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": "4dFV/gE1xB75kDkpA1guRI" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "FdmXX3bguT9w6NBTze3+mg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 121 - }, - "_enabled": true, - "__prefab": { - "__id__": 127 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -6, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "34d1LM6N1Ns5/5wldjZGLK" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "0se+HHy/6tVc7MxtkYYHVA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "Cj+eqENMU2dh3oMtvuRwrQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "通知标题", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 130 - }, - { - "__id__": 132 - }, - { - "__id__": 134 - } - ], - "_prefab": { - "__id__": 136 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -122, - "y": 587, - "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": "5bppP5XixK6bGppLgJPV9s" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 129 - }, - "_enabled": true, - "__prefab": { - "__id__": 131 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 510, - "height": 138 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "fc/6mBMeVH56OtXI1QqWrJ" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "S5qs9szuMI47lMNQQsHDGA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 129 - }, - "_enabled": true, - "__prefab": { - "__id__": 133 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "05398908-866f-4605-b419-4a06ef63248a@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": "391vilz2lG4ZKg0mBZFnCI" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "79jb9zXianCbyoglnzrkhQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 129 - }, - "_enabled": true, - "__prefab": { - "__id__": 135 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -122, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "9aSc6v8QVFULCZqKAVxy9/" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "2r8V/D6VCyz9l2ukU+fkZA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "9Py8tFFY69pK/wYy+2cqiA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "通知关闭", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 138 - }, - { - "__id__": 140 - }, - { - "__id__": 142 - } - ], - "_prefab": { - "__id__": 144 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 220.5, - "y": 452, - "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": "61F/9NQApJmb3LoWYphMwD" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 137 - }, - "_enabled": true, - "__prefab": { - "__id__": 139 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 83, - "height": 84 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "8e+inAo5NKrZSCaEV4KxDt" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "8M76wxkG3hsHJHZhbUgvXA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 137 - }, - "_enabled": true, - "__prefab": { - "__id__": 141 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "eedce6d7-3ea6-434c-83e1-87d9a04b2bad@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": "fdPOr+aHRB/L6TS68Q2Wk9" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "06fP8pp7iy2AyrXEd2fDOA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 137 - }, - "_enabled": true, - "__prefab": { - "__id__": 143 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 220.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "72anWJu0pAqa9aJoOanZ5f" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "uB5bw9zTiyp4zKkhcdFfcA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "7cczemhO3aGzmlXN5891Og", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "通知标题内容", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 146 - }, - { - "__id__": 148 - }, - { - "__id__": 150 - } - ], - "_prefab": { - "__id__": 152 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0.5, - "y": 255, - "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": "75SH+Q9oFCUoMtTeu9/W6V" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 145 - }, - "_enabled": true, - "__prefab": { - "__id__": 147 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 403, - "height": 76 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "4c3pkQEG1Nw7TvlHeFSeC0" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "ulMXdG4fkADtK7a6f4JAvw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 145 - }, - "_enabled": true, - "__prefab": { - "__id__": 149 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "5febca8f-13b3-4ea3-997c-0f4534ba499d@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": "aeYvwXekZCO6b48Fs9JcJM" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "VnaRB97L/IofeB46fgdPTw" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 145 - }, - "_enabled": true, - "__prefab": { - "__id__": 151 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "1fVc50v8ZNzKLWpcwPKpcm" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "sBw8eZzN3wkDTxl+ZkN4cQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "QMjNU3Vk6QVhMOmkL/F5IQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit570", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 154 - }, - { - "__id__": 156 - }, - { - "__id__": 158 - } - ], - "_prefab": { - "__id__": 160 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -376, - "y": 154, - "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": "726Op8IjBG0apZDp1JNntl" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 153 - }, - "_enabled": true, - "__prefab": { - "__id__": 155 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 236, - "height": 100 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "d7PQBHR6xPPZ204ME/nwJL" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "3FhH9gwtlr61UlvAznLUHw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 153 - }, - "_enabled": true, - "__prefab": { - "__id__": 157 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "a42d050f-b023-4580-9444-36552fa546e3@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": "1d1eqlMNRD7LIP39PwHD9e" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "nY7KqaXGtVYwo/pkYHZOTg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 153 - }, - "_enabled": true, - "__prefab": { - "__id__": 159 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -376, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "dfNJs/11FN0I8VyFjMc6+T" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "XVThnqBHJmYEVJ/awzlaSA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "5RK0mu6ZyB5FDvhvUDnQRQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit571", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 162 - }, - { - "__id__": 164 - }, - { - "__id__": 166 - } - ], - "_prefab": { - "__id__": 168 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -376, - "y": 62, - "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": "8d9wovnDtFvoFilTrKtfIi" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 161 - }, - "_enabled": true, - "__prefab": { - "__id__": 163 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 236, - "height": 100 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "f5aPuD6yNG56KssQpnBrsT" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "rYTdc110CVVoG+TbKmHsfA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 161 - }, - "_enabled": true, - "__prefab": { - "__id__": 165 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "a42d050f-b023-4580-9444-36552fa546e3@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": "bbjaBL1c1LRY0aq88H7Ev4" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "xisoQg9hf9gk/mE6a2xDIg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 161 - }, - "_enabled": true, - "__prefab": { - "__id__": 167 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -376, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "a3A1/+42JBA5ODf/f96/DL" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "SMgIgYZSRpOIaINpbVbgzw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "UkjMf2Ux5rHaDMT3nZH48g", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit572", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 170 - }, - { - "__id__": 172 - }, - { - "__id__": 174 - } - ], - "_prefab": { - "__id__": 176 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -376, - "y": -30, - "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": "51QjrLUBVKgKX1Xn/5QglG" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 169 - }, - "_enabled": true, - "__prefab": { - "__id__": 171 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 236, - "height": 100 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "e4aiGg7l5AVIZsBYN2Iw5r" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "ofWiYe+RUWmW/emn75jkUw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 169 - }, - "_enabled": true, - "__prefab": { - "__id__": 173 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "a42d050f-b023-4580-9444-36552fa546e3@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": "57DpXcL3xDyoe39xc1cIx3" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "e6VdAzf460VdxAkyBTvsmQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 169 - }, - "_enabled": true, - "__prefab": { - "__id__": 175 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -376, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "91xKmJ+5RL/qbxsOng4knV" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "s+XjyGoeWBJowHUGHYFBSA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "TRhBtH5Dx96mBwFq39CcKw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit573", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 178 - }, - { - "__id__": 180 - }, - { - "__id__": 182 - } - ], - "_prefab": { - "__id__": 184 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -379.5, - "y": 156, - "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": "d2A1d206pFb6UGBuMFEU5I" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 177 - }, - "_enabled": true, - "__prefab": { - "__id__": 179 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 125, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "85BfW3YIlA7Y/MGhx3BB1U" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "r1gXFFdnCxOm0TRBpQy/mg" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 177 - }, - "_enabled": true, - "__prefab": { - "__id__": 181 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "87d01b20-33bc-45b6-99cc-d22412f6e82f@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": "9e8quVqIdIZoUYlEvNqneE" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "rEV6LSEcQXMl8VQ8Nts3Bg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 177 - }, - "_enabled": true, - "__prefab": { - "__id__": 183 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -379.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "afe32x5IZBXofPOWDWJ5Tp" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "D+u3dR5f2UaGMosPbPCSCg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "3j+6hyNRsIhIXQvkI2Ytxg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit574", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 186 - }, - { - "__id__": 188 - }, - { - "__id__": 190 - } - ], - "_prefab": { - "__id__": 192 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -379.5, - "y": 63, - "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": "a3iS8wrVhJTpxLS8xNNnWA" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 185 - }, - "_enabled": true, - "__prefab": { - "__id__": 187 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 125, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "49hyQn64pDQ7Qt5iSd5NwR" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "oJA5XsnW3qRD3mvqvF/gHQ" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 185 - }, - "_enabled": true, - "__prefab": { - "__id__": 189 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "9fd0b3df-4b98-428c-b2d3-fe3b467321b9@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": "54Ofr0Y15IRIKd3CrlEMDX" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "B1565nNx3F1RKluHABmezg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 185 - }, - "_enabled": true, - "__prefab": { - "__id__": 191 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -379.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "bdDnbGfgVKXoJKRwHBJMhm" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "TIxxzBGUjEO37OOsboeu2w" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "8RlvUWDhlGJKQ0odRO0QZQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit575", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 64 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 194 - }, - { - "__id__": 196 - }, - { - "__id__": 198 - } - ], - "_prefab": { - "__id__": 200 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -379.5, - "y": -28, - "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": "a3P5/QAj5Fc50p2wuD65Af" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 193 - }, - "_enabled": true, - "__prefab": { - "__id__": 195 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 125, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "3fW5+xaaVKOLZh88iWKA2T" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "JZ+OF8cwOnNz5IIlU63bNA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 193 - }, - "_enabled": true, - "__prefab": { - "__id__": 197 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "a972c966-3dc9-4396-8935-d2583f5ed499@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": "66zC1LBIFNdaLwe9Li4pZl" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "BT+TbPoInGL+m8AtGLlYOQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 193 - }, - "_enabled": true, - "__prefab": { - "__id__": 199 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -379.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "3bhj2/maJO5r2YADFE0HJD" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "DrvAs/ubM171yTnd2j+pNQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "wnSFuDE2y/oyfmKgHPfLVQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 64 - }, - "_enabled": true, - "__prefab": { - "__id__": 202 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "0dnWz8YXlM+JIV+h5mMG36" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "ERSzc8l9mvi/wusVMsF4dQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 64 - }, - "_enabled": true, - "__prefab": { - "__id__": 204 - }, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "08O7NcipVPQr23K+8AFMKp" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Cr6b9HuyKgkKHjZnv//56A" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "Owl2bbtFiiydQM0wdvsnEg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "group-42", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 207 - }, - { - "__id__": 215 - }, - { - "__id__": 223 - }, - { - "__id__": 231 - }, - { - "__id__": 239 - } - ], - "_active": true, - "_components": [ - { - "__id__": 247 - }, - { - "__id__": 249 - } - ], - "_prefab": { - "__id__": 251 - }, - "_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": "f5fvFkLJtCjZPhM2QJKt40" - }, - { - "__type__": "cc.Node", - "_name": "二维码遮罩", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 206 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 208 - }, - { - "__id__": 210 - }, - { - "__id__": 212 - } - ], - "_prefab": { - "__id__": 214 - }, - "_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": "12cw+WQEFLZaBfbCwKZENd" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 207 - }, - "_enabled": true, - "__prefab": { - "__id__": 209 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "904A1tMPFJppt7NuLy/4wG" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "HlEIe1/sTnt43f4BhzaSUA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 207 - }, - "_enabled": true, - "__prefab": { - "__id__": 211 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@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": "04Nz4+VRRJSLAfm0YoVAUy" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "lN/2s+Xi5j35LA3gqslKag" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 207 - }, - "_enabled": true, - "__prefab": { - "__id__": 213 - }, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "3c0rMBnF5K+Lk8TBpVczn2" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "6h4XS7BU0sajRVnXb3UHvg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "Dr1TE+y3UY95WdM0ZgyNsA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit342", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 206 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 216 - }, - { - "__id__": 218 - }, - { - "__id__": 220 - } - ], - "_prefab": { - "__id__": 222 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0.5, - "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": "f9l8ziv6hGjqMyIl7m+Ira" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 215 - }, - "_enabled": true, - "__prefab": { - "__id__": 217 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 955, - "height": 640 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "2eSh3wLGBGfL6a2l+dcUX2" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "F6ERWqkA92p3nEOQj3SxBQ" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 215 - }, - "_enabled": true, - "__prefab": { - "__id__": 219 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "dfdcc5f5-bf77-42a6-8e2e-f8462bc51edc@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "92cqtUUJdBI7KiO8fCPcDA" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "0IPn3dGUrJFEL9XjTsLKBw" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 215 - }, - "_enabled": true, - "__prefab": { - "__id__": 221 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "24Nu3BHA1B56YbaPfobCqw" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "NZiexs+J7eeNqrhIHPGQfw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "AJXHkIJo2ZBX1vYtqN95Vw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit343", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 206 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 224 - }, - { - "__id__": 226 - }, - { - "__id__": 228 - } - ], - "_prefab": { - "__id__": 230 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 115.5, - "y": -49.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": "e4t5EloMtGj79IJstbRmJ2" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 223 - }, - "_enabled": true, - "__prefab": { - "__id__": 225 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 321, - "height": 287 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "44YiTyfshM+LWye9mrAsX6" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "asn7G/JFow1wQVxu9m67LA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 223 - }, - "_enabled": true, - "__prefab": { - "__id__": 227 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "06403876-e8f4-48fd-ad50-9bbefe0c5d0a@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": "adJtKldedIirxfJbhnQpG+" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "tp56aGuZD77gYI0O8cTTtg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 223 - }, - "_enabled": true, - "__prefab": { - "__id__": 229 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 115.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "07W6YfXAlNLJCFXpaSvIf5" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "AvGiAKGIfBXHAQ+SXUmhbA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "ibFfKqVRH8Rgf62l6sDdWQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit344", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 206 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 232 - }, - { - "__id__": 234 - }, - { - "__id__": 236 - } - ], - "_prefab": { - "__id__": 238 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -293.5, - "y": -476.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": "e5/JZsnMdA255f+nb61TVb" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 231 - }, - "_enabled": true, - "__prefab": { - "__id__": 233 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 371, - "height": 51 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "78TpsCNtNA3pBwwr+svOf2" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "kDQvlUgf8GFlbqweZnbdTA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 231 - }, - "_enabled": true, - "__prefab": { - "__id__": 235 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "0c0c1c72-96f5-4c54-a199-0df6189c762c@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": "a8X7eaF7JDobKDpQ7fnTN+" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "CNr0yxiyfZXVc8bcRhsCgw" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 231 - }, - "_enabled": true, - "__prefab": { - "__id__": 237 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -293.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "40p/OOGzJEs6kvN4GF+8LO" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "GafHAU8h3Vgh9Rj1NBeDlQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "S0L/u1Syb1R21/7IQBYDhg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit345", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 206 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 240 - }, - { - "__id__": 242 - }, - { - "__id__": 244 - } - ], - "_prefab": { - "__id__": 246 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 115.5, - "y": -280, - "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": "b9KQuMXENLIqEo6TNJU/NV" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 239 - }, - "_enabled": true, - "__prefab": { - "__id__": 241 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 315, - "height": 74 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "51kLRVoI5OpKC9dLcOvIMW" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Uubls3G/twLToFssdAYobQ" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 239 - }, - "_enabled": true, - "__prefab": { - "__id__": 243 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "b1efcc9a-2999-4c92-8d68-d7462c4bca2e@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": "455U7zIB1JtriuzCTAoSC2" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "xW6BdgetTBpoKtBU2QFjbw" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 239 - }, - "_enabled": true, - "__prefab": { - "__id__": 245 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 115.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "15CiCTcNFPJLdzpStX/tnd" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "XNUokxwlP4HWTZF0ocY7CA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "k/BwuOnnY1nS7MyyOtUxdQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 206 - }, - "_enabled": true, - "__prefab": { - "__id__": 248 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "09YgMFt5RLFrMk/f7hQEmr" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "DnLqgash8Cofi+2F3HntaA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 206 - }, - "_enabled": true, - "__prefab": { - "__id__": 250 - }, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "4fv/A7ULBJk7WTzJRvDOBq" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "e/oSaSJnWs4wTPtJ43zBRA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "r3I+UqjEPwGxkPmtB1rEJA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "group-98", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 253 - }, - { - "__id__": 261 - }, - { - "__id__": 269 - }, - { - "__id__": 277 - }, - { - "__id__": 285 - } - ], - "_active": true, - "_components": [ - { - "__id__": 293 - }, - { - "__id__": 295 - } - ], - "_prefab": { - "__id__": 297 - }, - "_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": "0bmyCY+8JG4aBrPpKVPZg1" - }, - { - "__type__": "cc.Node", - "_name": "Spirit103", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 252 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 254 - }, - { - "__id__": 256 - }, - { - "__id__": 258 - } - ], - "_prefab": { - "__id__": 260 - }, - "_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": "c60Hid7PNNJanVRd5rpMmB" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 253 - }, - "_enabled": true, - "__prefab": { - "__id__": 255 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "b3f5CzWLlMuKO9t8apLqcT" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "lZ+V3GELUWCzNZkRRlotbw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 253 - }, - "_enabled": true, - "__prefab": { - "__id__": 257 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@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": "1eZrcc9yRL6LSCOabQTiHp" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "scaNElJtNBQx87XbAvhMEg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 253 - }, - "_enabled": true, - "__prefab": { - "__id__": 259 - }, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "72IZ0VpgZMraqotfngc9Eb" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "XbrQfpoxZ2+Jz7J/bD4NRg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "aSVlZ28n9y7vRhmZTho/yw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "房间列表背景", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 252 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 262 - }, - { - "__id__": 264 - }, - { - "__id__": 266 - } - ], - "_prefab": { - "__id__": 268 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -500, - "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": "a4oMmkx7VJaLJCHD3MX9yk" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 261 - }, - "_enabled": true, - "__prefab": { - "__id__": 263 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 280, - "height": 530 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "18P+UdO9BJ0bBHErpnaXmx" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "WkQdAD6KmTH7rfEynCpkrw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 261 - }, - "_enabled": true, - "__prefab": { - "__id__": 265 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "083b1c88-4376-4aea-b08c-12eed59f94df@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "95Au0ge3JOHp/YJPaegsvG" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "syEPZAhd+Jn1Sz865FroVA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 261 - }, - "_enabled": true, - "__prefab": { - "__id__": 267 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -500, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "cfRFdb+bRL9IFnpRBuRhu1" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "K2idFInjlyNnIHQVa+GufA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "QOTc8qKeWX9j7tD8erLR8w", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit100", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 252 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 270 - }, - { - "__id__": 272 - }, - { - "__id__": 274 - } - ], - "_prefab": { - "__id__": 276 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -794, - "y": 80, - "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": "deepSEUQ5IR7T0fRtRSIhl" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 269 - }, - "_enabled": true, - "__prefab": { - "__id__": 271 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 264, - "height": 80 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "7aExkH8ZxMqqmTBsUHod51" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "c9Cs3H8yy7d7MLXZpXrDmA" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 269 - }, - "_enabled": true, - "__prefab": { - "__id__": 273 - }, - "_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": "eaD5NQm9JI/YP2nvnny9Bm" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Xw3IrSWe0gBUCADQeEvn2Q" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 269 - }, - "_enabled": true, - "__prefab": { - "__id__": 275 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -794, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "93lK80cUVAaLd8DX4tOlk7" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "S3yqapqX9rj5WMwVRcs6zw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "PgfmXiXqx70ULuZesS6Sxg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit101", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 252 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 278 - }, - { - "__id__": 280 - }, - { - "__id__": 282 - } - ], - "_prefab": { - "__id__": 284 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -783, - "y": 237, - "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": "a28uWgl9pHFacPoY8mS6iz" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 277 - }, - "_enabled": true, - "__prefab": { - "__id__": 279 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 50 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "4egxNuQshCYpmRUosI3UIh" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "YkwFhmhpSYDSE/D6nebSug" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 277 - }, - "_enabled": true, - "__prefab": { - "__id__": 281 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@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": "e48xB+fZxCSqgFXlDTdjTv" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "cMWGtDyY018mn8+ebIS8DA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 277 - }, - "_enabled": true, - "__prefab": { - "__id__": 283 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -783, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "3aS2h2WEBNnrSFPwNXKE9a" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "IWawtncNyCLcqFkJgu7CxQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "LJvl3Ic38xbXvCwvYTHgTg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit102", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 252 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 286 - }, - { - "__id__": 288 - }, - { - "__id__": 290 - } - ], - "_prefab": { - "__id__": 292 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -755, - "y": 82.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": "79sttZYQNGsLanBfSdPmfd" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 285 - }, - "_enabled": true, - "__prefab": { - "__id__": 287 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 14, - "height": 19 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "93XqjMB9dN7YVtbIsWrZzQ" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "TJ1Wo2umlGc+hIKeYAqK4w" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 285 - }, - "_enabled": true, - "__prefab": { - "__id__": 289 - }, - "_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": "0b1aMnF1lGCo2vqrE4LO+B" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "2HhA5sSqqnofzqiv/6E/yg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 285 - }, - "_enabled": true, - "__prefab": { - "__id__": 291 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -755, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "d8LsauAk1PEapliJWqYuf+" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Gknv8cYn6SmQzZplKYC6cQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "wQomu/UsVlLMKvFGSR+c5g", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 252 - }, - "_enabled": true, - "__prefab": { - "__id__": 294 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "fbJeQ/GKVBh7R4fYajQCiY" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "fCB4k1+pLMAOzo9JWwmTrA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 252 - }, - "_enabled": true, - "__prefab": { - "__id__": 296 - }, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "55mvmnSdZJN6hPrpTqzt9d" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "ocFui9qQYP0ESFVJI0X4kQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "oayjfiUxtrEur+837YQ3Og", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 1 - }, - "_enabled": true, - "__prefab": { - "__id__": 299 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "69bys5w1NG5qgIfvO+/CjW" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "IFVM+3CEtaxjz5jh45+U3g" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 1 - }, - "_enabled": true, - "__prefab": { - "__id__": 301 - }, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "f3WN7ujeFPuLXqXJDb7GD5" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "5/sa/Uv5pRqAaz3RFFedRA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "V1CY5PkwEMa64h57ppK4Yw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - } -] diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Login.prefab similarity index 99% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Login.prefab index ced8f1d..6cae605 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Login.prefab @@ -1,7 +1,7 @@ [ { "__type__": "cc.Prefab", - "_name": "Login_Layer", + "_name": "Login", "_objFlags": 0, "__editorExtras__": {}, "_native": "", @@ -13,7 +13,7 @@ }, { "__type__": "cc.Node", - "_name": "Login_Layer", + "_name": "Login", "_objFlags": 0, "__editorExtras__": {}, "_parent": null, diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Login.prefab.meta similarity index 82% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab.meta rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Login.prefab.meta index 1530042..22aa978 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Login.prefab.meta @@ -1,14 +1,14 @@ -{ - "ver": "1.1.50", - "importer": "prefab", - "imported": true, - "uuid": "b209cefb-b381-4c7c-b765-94a5ab75a89c", - "files": [ - ".json" - ], - "subMetas": {}, - "userData": { - "syncNodeName": "Login_Layer", - "hasIcon": false - } -} +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "b209cefb-b381-4c7c-b765-94a5ab75a89c", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "Login", + "hasIcon": false + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/MainMenu.prefab similarity index 74% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/MainMenu.prefab index 35ea90f..2b99553 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/MainMenu.prefab @@ -1,7 +1,7 @@ [ { "__type__": "cc.Prefab", - "_name": "Layer4_MainMenu", + "_name": "MainMenu", "_objFlags": 0, "__editorExtras__": {}, "_native": "", @@ -13,7 +13,7 @@ }, { "__type__": "cc.Node", - "_name": "Layer4_MainMenu", + "_name": "MainMenu", "_objFlags": 0, "__editorExtras__": {}, "_parent": null, @@ -25,14 +25,14 @@ "_active": true, "_components": [ { - "__id__": 270 + "__id__": 340 }, { - "__id__": 272 + "__id__": 342 } ], "_prefab": { - "__id__": 274 + "__id__": 344 }, "_lpos": { "__type__": "cc.Vec3", @@ -61,7 +61,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "bfcfiALa9FK5kXVYNrGQeb" }, { "__type__": "cc.Node", @@ -79,46 +79,34 @@ "__id__": 11 }, { - "__id__": 19 + "__id__": 23 }, { - "__id__": 27 - }, - { - "__id__": 35 + "__id__": 31 }, { "__id__": 43 }, { - "__id__": 51 - }, - { - "__id__": 59 + "__id__": 55 }, { "__id__": 67 }, { - "__id__": 75 + "__id__": 79 }, { - "__id__": 83 + "__id__": 87 }, { - "__id__": 91 + "__id__": 95 }, { - "__id__": 99 + "__id__": 103 }, { - "__id__": 107 - }, - { - "__id__": 115 - }, - { - "__id__": 129 + "__id__": 121 }, { "__id__": 137 @@ -126,9 +114,6 @@ { "__id__": 145 }, - { - "__id__": 153 - }, { "__id__": 161 }, @@ -136,10 +121,7 @@ "__id__": 169 }, { - "__id__": 177 - }, - { - "__id__": 185 + "__id__": 181 }, { "__id__": 193 @@ -150,9 +132,6 @@ { "__id__": 209 }, - { - "__id__": 217 - }, { "__id__": 225 }, @@ -160,26 +139,20 @@ "__id__": 233 }, { - "__id__": 241 - }, - { - "__id__": 249 - }, - { - "__id__": 257 + "__id__": 267 } ], "_active": true, "_components": [ { - "__id__": 265 + "__id__": 335 }, { - "__id__": 267 + "__id__": 337 } ], "_prefab": { - "__id__": 269 + "__id__": 339 }, "_lpos": { "__type__": "cc.Vec3", @@ -208,7 +181,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "48DvKWwblL6LRW1O+Pj5KO" }, { "__type__": "cc.Node", @@ -261,7 +234,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "c3F6i4m31Fjao3UlENsce1" }, { "__type__": "cc.UITransform", @@ -285,11 +258,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "f9Xwbd+tZGdIHCHj5r+/xC" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "UcKe3BtLQzwz17M57/jH8A" + "fileId": "DF6MQffxXtwPts4p798eeg" }, { "__type__": "cc.Sprite", @@ -330,11 +303,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "97g7fqR/BJgbK2J0Js9Ww/" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "79ttjnpZYgSikmz06eKDNw" + "fileId": "KD8MUX3m4EzeesP9HQDxvw" }, { "__type__": "cc.Widget", @@ -366,11 +339,11 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "b6JRoOw3dN4YxzIGLOMt57" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "XXV3YK5hfEaPAkVaycSGjg" + "fileId": "aSOstybL/p3tgLISNDS31g" }, { "__type__": "cc.PrefabInfo", @@ -380,182 +353,7 @@ "asset": { "__id__": 0 }, - "fileId": "9sLbpUXGpNeN1JMbKAa8aw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit679", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 12 - }, - { - "__id__": 14 - }, - { - "__id__": 16 - } - ], - "_prefab": { - "__id__": 18 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -310.5, - "y": -28.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": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 11 - }, - "_enabled": true, - "__prefab": { - "__id__": 13 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 337, - "height": 441 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "ReCTwmD1IFTn8dQqjurEiw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 11 - }, - "_enabled": true, - "__prefab": { - "__id__": 15 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "4f725497-383a-4e19-bc2c-4fd55017042b@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": "MOGaPpKFutkLxQEHrBXqwQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 11 - }, - "_enabled": true, - "__prefab": { - "__id__": 17 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -310.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": "3qKYEPYEzg1gojUwZ6Qnhw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "uBkXU2htVD6YXG6th0Dimw", + "fileId": "o9dnH0GsD6NkDHJCRY8SEw", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -571,18 +369,24 @@ "_children": [], "_active": true, "_components": [ + { + "__id__": 12 + }, + { + "__id__": 14 + }, + { + "__id__": 16 + }, + { + "__id__": 18 + }, { "__id__": 20 - }, - { - "__id__": 22 - }, - { - "__id__": 24 } ], "_prefab": { - "__id__": 26 + "__id__": 22 }, "_lpos": { "__type__": "cc.Vec3", @@ -611,7 +415,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "2dVc2oDzBP1b7Ej0h3nyVu" }, { "__type__": "cc.UITransform", @@ -619,11 +423,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 11 }, "_enabled": true, "__prefab": { - "__id__": 21 + "__id__": 13 }, "_contentSize": { "__type__": "cc.Size", @@ -635,11 +439,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "57FG6kuqtBqbjC4c33NWRB" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "siZZMubYB/DWGLcot/5ghg" + "fileId": "1v1Ps0XP4Nb1wBMpO2WCWg" }, { "__type__": "cc.Sprite", @@ -647,11 +451,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 11 }, "_enabled": true, "__prefab": { - "__id__": 23 + "__id__": 15 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -680,11 +484,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "96/rDCyuFDjZvHkx5+4Lih" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "p23cqjDua0SQlXjEgHcymA" + "fileId": "/C+VpB8vjPazZ3uUjGPXiA" }, { "__type__": "cc.Widget", @@ -692,11 +496,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 11 }, "_enabled": true, "__prefab": { - "__id__": 25 + "__id__": 17 }, "_alignFlags": 16, "_target": null, @@ -716,11 +520,50 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "efLggYIstJHbFEgf3Tt0bP" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "AjzVA2qiyHZ47qxyqXJFqw" + "fileId": "VSs773EGlv5Fl4QOFF+uLw" + }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 19 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "fe/MO8jkNK5rYB6pkwWILu" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "x2Eq+vIQ4p0KPf9/2R4CsA" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 21 + }, + "_id": "6a1Vv1gbVN+6S/s/F2gpgx" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "HIScqI4ogn5PkW+HPVUXow" }, { "__type__": "cc.PrefabInfo", @@ -730,7 +573,7 @@ "asset": { "__id__": 0 }, - "fileId": "VGGeBByID65SqMxOnomKFA", + "fileId": "BfEfu441aDjFEyL7qQWEaA", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -746,18 +589,18 @@ "_children": [], "_active": true, "_components": [ + { + "__id__": 24 + }, + { + "__id__": 26 + }, { "__id__": 28 - }, - { - "__id__": 30 - }, - { - "__id__": 32 } ], "_prefab": { - "__id__": 34 + "__id__": 30 }, "_lpos": { "__type__": "cc.Vec3", @@ -786,7 +629,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "00Kps4661OaZuN4r2eI2NN" }, { "__type__": "cc.UITransform", @@ -794,11 +637,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 23 }, "_enabled": true, "__prefab": { - "__id__": 29 + "__id__": 25 }, "_contentSize": { "__type__": "cc.Size", @@ -810,11 +653,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "9bJ3b4CT1CXLt9ecCJI3Rc" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "00QUqwN82yD2DMGqF4r99A" + "fileId": "9EFwzDlO8SorPl6V6TLaxg" }, { "__type__": "cc.Label", @@ -822,11 +665,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 23 }, "_enabled": true, "__prefab": { - "__id__": 31 + "__id__": 27 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -878,11 +721,11 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "" + "_id": "e2/V5BphlA7YbO2t7M0CbR" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "G9rnJdlDeO61keQ+hl/E2Q" + "fileId": "gFS+WwN7jVCIPr+J7UWDfw" }, { "__type__": "cc.Widget", @@ -890,11 +733,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 23 }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 29 }, "_alignFlags": 16, "_target": null, @@ -914,11 +757,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "e0FGQyEWBMpq8+6XgtgpQA" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "za6AiRcRnD73Ia75Gw9UTw" + "fileId": "Frd1rtknohMCrCAK4WC7pw" }, { "__type__": "cc.PrefabInfo", @@ -928,7 +771,7 @@ "asset": { "__id__": 0 }, - "fileId": "FoQeB2nUiacGzW00Kn/INg", + "fileId": "+rzWjpuIRdjHAt711CMcdA", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -944,6 +787,12 @@ "_children": [], "_active": true, "_components": [ + { + "__id__": 32 + }, + { + "__id__": 34 + }, { "__id__": 36 }, @@ -984,7 +833,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "86Y7+xsFRGPKXYPolt+/nd" }, { "__type__": "cc.UITransform", @@ -992,11 +841,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 37 + "__id__": 33 }, "_contentSize": { "__type__": "cc.Size", @@ -1008,11 +857,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "e5ZeVFIzNKYpUVHOX9JP+q" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "Q++IzOCWeMUogSWLIYAdog" + "fileId": "QHvd/+VJJfa+DmpLmNRu4w" }, { "__type__": "cc.Sprite", @@ -1020,11 +869,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 39 + "__id__": 35 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1053,11 +902,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "76ZlI8lvpCoLeM9x4plGUw" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "BFY5zY2HC7E/XFyTEmGWYA" + "fileId": "sjDk2pV4QdxGlbpabS50pg" }, { "__type__": "cc.Widget", @@ -1065,11 +914,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 31 }, "_enabled": true, "__prefab": { - "__id__": 41 + "__id__": 37 }, "_alignFlags": 16, "_target": null, @@ -1089,11 +938,50 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "a5RT6uU7VDPIXjhLrzIiLt" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "5oB209tXKT5BFSiF22l3lA" + "fileId": "quGLMcub1zxJYpwr9eTn/g" + }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 31 + }, + "_enabled": true, + "__prefab": { + "__id__": 39 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "e3g07bfEdNOa61V82TTdaQ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "QSCM4wFdWiSMnrCOnPMrqA" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 31 + }, + "_enabled": true, + "__prefab": { + "__id__": 41 + }, + "_id": "b09zK+qBZBr4Jzy3u176YS" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "EpjdlKcGsqxPYZ2DBOmS2Q" }, { "__type__": "cc.PrefabInfo", @@ -1103,182 +991,7 @@ "asset": { "__id__": 0 }, - "fileId": "D9pt1CLT0FH1qGEUr2JHDg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit479", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 44 - }, - { - "__id__": 46 - }, - { - "__id__": 48 - } - ], - "_prefab": { - "__id__": 50 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -256.5, - "y": 239, - "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__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 45 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 55, - "height": 50 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "gGc84vmj7Y3irG2+RkNmoQ" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 47 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "151439f3-c9bd-4fb2-97b5-5afc295d76e8@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": "UoX+MqpCtliZxdeNMCjVdg" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 43 - }, - "_enabled": true, - "__prefab": { - "__id__": 49 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -256.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": "OCnu8SE6PmMFo/SWedDYrg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "WLusHqMO7lZLq6VFIn16Sg", + "fileId": "IwkMaC4sQzF4Bt+MFRkWjQ", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -1294,18 +1007,24 @@ "_children": [], "_active": true, "_components": [ + { + "__id__": 44 + }, + { + "__id__": 46 + }, + { + "__id__": 48 + }, + { + "__id__": 50 + }, { "__id__": 52 - }, - { - "__id__": 54 - }, - { - "__id__": 56 } ], "_prefab": { - "__id__": 58 + "__id__": 54 }, "_lpos": { "__type__": "cc.Vec3", @@ -1334,7 +1053,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "baQEON6flIBby8R8D1ae+E" }, { "__type__": "cc.UITransform", @@ -1342,11 +1061,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 43 }, "_enabled": true, "__prefab": { - "__id__": 53 + "__id__": 45 }, "_contentSize": { "__type__": "cc.Size", @@ -1358,11 +1077,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "6fiB/NZZhIZpvXxxWbCmsF" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "vafGIma6PE1pUlAWyBzLwA" + "fileId": "aFQ+jvdyAirnguNeiu1mxw" }, { "__type__": "cc.Sprite", @@ -1370,11 +1089,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 43 }, "_enabled": true, "__prefab": { - "__id__": 55 + "__id__": 47 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1403,11 +1122,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "66dsnv5jpMWbJnUAhHCPOa" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "uJchI3uhz23UDRhHTHDUvw" + "fileId": "hQvqZjHAgvijeLtACvRN6A" }, { "__type__": "cc.Widget", @@ -1415,11 +1134,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 43 }, "_enabled": true, "__prefab": { - "__id__": 57 + "__id__": 49 }, "_alignFlags": 16, "_target": null, @@ -1439,11 +1158,50 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "957v3dlFJMb7J5+JoN96bW" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "N7ojzue5lRUykQ3akyMjYQ" + "fileId": "xdFLb5cLYz+ZoIbFPT7acQ" + }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 51 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "20DZ0TprxM1orJ856Jvnn5" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "pnQlrSDHICm19ULR2Lqf0Q" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 53 + }, + "_id": "6dIdRmJNVMaYcGSYcQT8uH" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "q5LuskqU/2K+RkJrTb46lw" }, { "__type__": "cc.PrefabInfo", @@ -1453,7 +1211,7 @@ "asset": { "__id__": 0 }, - "fileId": "j2pIkf+6WZA64mI0mwkVww", + "fileId": "bEw55ldxoyzJOyxUlYwu3Q", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -1469,6 +1227,12 @@ "_children": [], "_active": true, "_components": [ + { + "__id__": 56 + }, + { + "__id__": 58 + }, { "__id__": 60 }, @@ -1509,7 +1273,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "7bWstWVBpBsb/68gvIAqQM" }, { "__type__": "cc.UITransform", @@ -1517,11 +1281,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 55 }, "_enabled": true, "__prefab": { - "__id__": 61 + "__id__": 57 }, "_contentSize": { "__type__": "cc.Size", @@ -1533,11 +1297,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "81rmbPTINJholPY0b8o9d4" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "+F5wpAex3klzyX5L5mlfLQ" + "fileId": "etk4ArHCBM4stXj8y2EHPg" }, { "__type__": "cc.Sprite", @@ -1545,11 +1309,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 55 }, "_enabled": true, "__prefab": { - "__id__": 63 + "__id__": 59 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1578,11 +1342,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "79YoFjbWVLqZkouG5Yd/KQ" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "Z7Fpqzc4G+PYJFE2XY9cVg" + "fileId": "H12sU9DkRWXDLLnALBn8tg" }, { "__type__": "cc.Widget", @@ -1590,11 +1354,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 55 }, "_enabled": true, "__prefab": { - "__id__": 65 + "__id__": 61 }, "_alignFlags": 16, "_target": null, @@ -1614,11 +1378,50 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "422iTpHtlLdLLqpDFUSeuq" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "dIsSySKiM8Q+mCkFShZbiA" + "fileId": "Jmbl1PauFTXDYPxBr1h4rQ" + }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 55 + }, + "_enabled": true, + "__prefab": { + "__id__": 63 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "04Tyb8iNdLVoygqZ5fQAJ/" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "bcr/lKRuIgR86ACYmzuIVw" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 55 + }, + "_enabled": true, + "__prefab": { + "__id__": 65 + }, + "_id": "45i7wLkVpLB5tvwNgVxE+Z" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "EsgddgKrqwN9dAHQvXcZ+A" }, { "__type__": "cc.PrefabInfo", @@ -1628,7 +1431,7 @@ "asset": { "__id__": 0 }, - "fileId": "g7cQBsANamQvYPyCyMFKHw", + "fileId": "9pR7y50vX6IwsZHPp3pJ0g", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -1652,10 +1455,16 @@ }, { "__id__": 72 + }, + { + "__id__": 74 + }, + { + "__id__": 76 } ], "_prefab": { - "__id__": 74 + "__id__": 78 }, "_lpos": { "__type__": "cc.Vec3", @@ -1684,7 +1493,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "e6qcM0rP5M4qTPD2scG/TC" }, { "__type__": "cc.UITransform", @@ -1708,11 +1517,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "88VOovVq9MTbLBz2LIWt/m" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "0QDiORipl/kv4kRNMT0FqA" + "fileId": "XJH4xwEn6sJmC7VKqacCoA" }, { "__type__": "cc.Sprite", @@ -1753,11 +1562,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "53aPNgtslPyKpdbEMxSp+c" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "voVYmEM7ZFegDTJnLHzsLQ" + "fileId": "VbA09S2dFKv+ax0bxqROUQ" }, { "__type__": "cc.Widget", @@ -1789,186 +1598,50 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "5fLuJq47ZEBZMtwQCZKO1h" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "H3lX7KPAjDWlNcidYSQJJA" + "fileId": "J8czmPdy8k98fL3KlCAw2w" }, { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "T85+25jo4FeBiqoyHNIJ8w", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit219", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 76 - }, - { - "__id__": 78 - }, - { - "__id__": 80 - } - ], - "_prefab": { - "__id__": 82 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 92.5, - "y": 237.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": "" - }, - { - "__type__": "cc.UITransform", + "__type__": "cc.Animation", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { + "__id__": 67 + }, + "_enabled": true, + "__prefab": { "__id__": 75 }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "1ajxJpP+FKu6t+15UuToRo" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "aKz5k9Zk4K4/2GkWsrwjKA" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 67 + }, "_enabled": true, "__prefab": { "__id__": 77 }, - "_contentSize": { - "__type__": "cc.Size", - "width": 645, - "height": 39 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" + "_id": "bbwvcphhZIuYZ++TZ/83XS" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "+Xs+B0TgomG6olakfrJFaw" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 75 - }, - "_enabled": true, - "__prefab": { - "__id__": 79 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "df4db047-ac71-4afd-b73d-e608abadc355@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": "YoubCBemRJYyVqZ4WR/0OA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 75 - }, - "_enabled": true, - "__prefab": { - "__id__": 81 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 92.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": "nCFxNavoEG8iGKoDZgcKIA" + "fileId": "Cqcd0sBDdD+B0IUHqtN/Tw" }, { "__type__": "cc.PrefabInfo", @@ -1978,205 +1651,7 @@ "asset": { "__id__": 0 }, - "fileId": "gp2hrjAdl6yPe+WI1CotJg", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit220", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 84 - }, - { - "__id__": 86 - }, - { - "__id__": 88 - } - ], - "_prefab": { - "__id__": 90 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -126, - "y": 239, - "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__": 83 - }, - "_enabled": true, - "__prefab": { - "__id__": 85 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 63.97470587327075, - "height": 20.16 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "mt8ZoK05FxxpcHwl7wDKtA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 83 - }, - "_enabled": true, - "__prefab": { - "__id__": 87 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "公告文字", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 16.866666666666667, - "_fontSize": 16, - "_fontFamily": "Arial", - "_lineHeight": 16, - "_overflow": 0, - "_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": "AkI3SM6iQFtLCxT1j75Zpw" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 83 - }, - "_enabled": true, - "__prefab": { - "__id__": 89 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -126, - "_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": "dhfhAGUuXNHoCJJ/Hotd8Q" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "jScJPaT8yapqkTA1XA4+kQ", + "fileId": "tPLZL9oTmT8c6L4LCZbJlg", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -2193,17 +1668,17 @@ "_active": true, "_components": [ { - "__id__": 92 + "__id__": 80 }, { - "__id__": 94 + "__id__": 82 }, { - "__id__": 96 + "__id__": 84 } ], "_prefab": { - "__id__": 98 + "__id__": 86 }, "_lpos": { "__type__": "cc.Vec3", @@ -2232,7 +1707,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "09wtwF6J1Eo5Es5B2A+wju" }, { "__type__": "cc.UITransform", @@ -2240,11 +1715,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 79 }, "_enabled": true, "__prefab": { - "__id__": 93 + "__id__": 81 }, "_contentSize": { "__type__": "cc.Size", @@ -2256,11 +1731,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "2auGHU7r9NBqp8ZwIxrMk0" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "Zbc8NKaZtQ74qHDCy0EKGw" + "fileId": "FmUc/q3mqxhEQU87BnR0XQ" }, { "__type__": "cc.Sprite", @@ -2268,11 +1743,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 79 }, "_enabled": true, "__prefab": { - "__id__": 95 + "__id__": 83 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2301,11 +1776,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "e5lz6rxWBJSKExlNyhYcPq" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "U8icO/fp/Ee4FHDjOq+5Fw" + "fileId": "iZO1F13GcZ4/UNbflDgAgg" }, { "__type__": "cc.Widget", @@ -2313,11 +1788,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 79 }, "_enabled": true, "__prefab": { - "__id__": 97 + "__id__": 85 }, "_alignFlags": 16, "_target": null, @@ -2337,11 +1812,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "12ZoonbJlBh63AEBaY1x0R" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "5RPKOHxA+Oi1FUYtDMnFMQ" + "fileId": "wKrO4jyoBWBhNCQSvmEwTw" }, { "__type__": "cc.PrefabInfo", @@ -2351,7 +1826,7 @@ "asset": { "__id__": 0 }, - "fileId": "hiRw92YHp2gv7ilALozUAQ", + "fileId": "8AqX36BBTYVsRv5DKCReZg", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -2368,17 +1843,17 @@ "_active": true, "_components": [ { - "__id__": 100 + "__id__": 88 }, { - "__id__": 102 + "__id__": 90 }, { - "__id__": 104 + "__id__": 92 } ], "_prefab": { - "__id__": 106 + "__id__": 94 }, "_lpos": { "__type__": "cc.Vec3", @@ -2407,7 +1882,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "522MlpggpG45oizBwSn6zC" }, { "__type__": "cc.UITransform", @@ -2415,11 +1890,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 87 }, "_enabled": true, "__prefab": { - "__id__": 101 + "__id__": 89 }, "_contentSize": { "__type__": "cc.Size", @@ -2431,11 +1906,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "31qqMWFh1NKZbhpIg/oMv3" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "uCWLBfGCub4l9nxCP2+iXA" + "fileId": "/vHgeNj+jzA59nd3tPcCYA" }, { "__type__": "cc.Label", @@ -2443,11 +1918,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 87 }, "_enabled": true, "__prefab": { - "__id__": 103 + "__id__": 91 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2499,11 +1974,11 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "" + "_id": "3auxTtwWxIkodQwnd6P7n1" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "yRgncryogPP81JYO0Lr8fg" + "fileId": "aywOjYm7lzCdUcLjbyzhTQ" }, { "__type__": "cc.Widget", @@ -2511,11 +1986,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 87 }, "_enabled": true, "__prefab": { - "__id__": 105 + "__id__": 93 }, "_alignFlags": 16, "_target": null, @@ -2535,11 +2010,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "19DcnwqP1JbK7NSYFEE4aP" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "lJFgbErURiCuH72BPmcp7A" + "fileId": "EaFS5ilRvK9b1Y73w8YyRQ" }, { "__type__": "cc.PrefabInfo", @@ -2549,7 +2024,7 @@ "asset": { "__id__": 0 }, - "fileId": "hwBz+OwmbPMV+/z2Jkh3Jg", + "fileId": "aPFxiX7AtnWJM1sdjLVa5A", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -2566,17 +2041,17 @@ "_active": true, "_components": [ { - "__id__": 108 + "__id__": 96 }, { - "__id__": 110 + "__id__": 98 }, { - "__id__": 112 + "__id__": 100 } ], "_prefab": { - "__id__": 114 + "__id__": 102 }, "_lpos": { "__type__": "cc.Vec3", @@ -2605,7 +2080,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "6fPtLQPotMAq8P02Vc+pdg" }, { "__type__": "cc.UITransform", @@ -2613,11 +2088,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 95 }, "_enabled": true, "__prefab": { - "__id__": 109 + "__id__": 97 }, "_contentSize": { "__type__": "cc.Size", @@ -2629,11 +2104,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "84A9jJlydLr6tSTAj56/o5" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "Bxt6sBN4x1NjtN9cI4sbCg" + "fileId": "aa/G5iowQchhX2HQjuIQ5g" }, { "__type__": "cc.Label", @@ -2641,11 +2116,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 95 }, "_enabled": true, "__prefab": { - "__id__": 111 + "__id__": 99 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2697,11 +2172,11 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "" + "_id": "1b3oQNPs5A5LeFwhvmlpPh" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "KmkXAkIIHI5fCu4zUMB2bA" + "fileId": "Li9c3qf38ZVSMCuMAndjCQ" }, { "__type__": "cc.Widget", @@ -2709,11 +2184,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 95 }, "_enabled": true, "__prefab": { - "__id__": 113 + "__id__": 101 }, "_alignFlags": 16, "_target": null, @@ -2733,11 +2208,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "3dy7v6/J9OvYlpy5rp5HFd" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "8MGIA/egSM1G5fHMFoEovw" + "fileId": "WiOvTz4lnN6hQ3B+3X9+iA" }, { "__type__": "cc.PrefabInfo", @@ -2747,7 +2222,7 @@ "asset": { "__id__": 0 }, - "fileId": "VsfgAdtyn0HUPYhqPcyhdQ", + "fileId": "uWaopCPysSopWobXdfazyA", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -2762,23 +2237,29 @@ }, "_children": [ { - "__id__": 116 + "__id__": 104 } ], "_active": true, "_components": [ { - "__id__": 122 + "__id__": 110 }, { - "__id__": 124 + "__id__": 112 }, { - "__id__": 126 + "__id__": 114 + }, + { + "__id__": 116 + }, + { + "__id__": 118 } ], "_prefab": { - "__id__": 128 + "__id__": 120 }, "_lpos": { "__type__": "cc.Vec3", @@ -2807,7 +2288,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "ec0aJyx0dIYZ2AjEfBYcvC" }, { "__type__": "cc.Node", @@ -2815,20 +2296,20 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 115 + "__id__": 103 }, "_children": [], "_active": true, "_components": [ { - "__id__": 117 + "__id__": 105 }, { - "__id__": 119 + "__id__": 107 } ], "_prefab": { - "__id__": 121 + "__id__": 109 }, "_lpos": { "__type__": "cc.Vec3", @@ -2857,7 +2338,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "bcEwYbm7NCrrZSiW1mVmyE" }, { "__type__": "cc.UITransform", @@ -2865,11 +2346,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 116 + "__id__": 104 }, "_enabled": true, "__prefab": { - "__id__": 118 + "__id__": 106 }, "_contentSize": { "__type__": "cc.Size", @@ -2881,11 +2362,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "4bm2WhsxlIJ7jBTUpfhQV0" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "37maSCaKxIoLCssBseTFhx" + "fileId": "183F2jAIyBqzx5uWv/Ylbw" }, { "__type__": "cc.Sprite", @@ -2893,11 +2374,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 116 + "__id__": 104 }, "_enabled": true, "__prefab": { - "__id__": 120 + "__id__": 108 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2926,11 +2407,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "0f2lLHdxFCkYtAcvv+XyUv" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "2dHwfuvlpKt7eTGUk7rzmF" + "fileId": "t9bEUT9aeZ5SDAbo1S+Oww" }, { "__type__": "cc.PrefabInfo", @@ -2940,7 +2421,7 @@ "asset": { "__id__": 0 }, - "fileId": "c5VTTNPHFGvY67MrFWsjzi", + "fileId": "2ZzHNWZyBNUKIBQniqXwxA", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -2951,11 +2432,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 103 }, "_enabled": true, "__prefab": { - "__id__": 123 + "__id__": 111 }, "_contentSize": { "__type__": "cc.Size", @@ -2967,11 +2448,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "ab4OWZRftNDZ4coU4ivdL/" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "RSC+9fTc7NeYewOskAZw0Q" + "fileId": "3QmUlKD/Jb9C9q/LMkGB9A" }, { "__type__": "cc.Sprite", @@ -2979,11 +2460,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 103 }, "_enabled": true, "__prefab": { - "__id__": 125 + "__id__": 113 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3012,11 +2493,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "84GOOaS1VAI7qWI1ULJd7Q" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "wntDJfRdjXyTdFvzWIUZOw" + "fileId": "Tk9yRFQK+rbWBqQTlpwoGQ" }, { "__type__": "cc.Widget", @@ -3024,11 +2505,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 103 }, "_enabled": true, "__prefab": { - "__id__": 127 + "__id__": 115 }, "_alignFlags": 16, "_target": null, @@ -3048,11 +2529,50 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "c8YdlqEN9HLKSb1F4Qcxar" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "ysPXaJJC5V8hY8YMpnafeA" + "fileId": "JzIJ6ArTU9rKLORXbFmNYw" + }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 103 + }, + "_enabled": true, + "__prefab": { + "__id__": 117 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "32VDeXWmRKcoYXWkpocLpE" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "DHAVt5xiYtGOCfkki/2/aA" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 103 + }, + "_enabled": true, + "__prefab": { + "__id__": 119 + }, + "_id": "12zJNk1ZhIQJwhE44MRySB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "hnenYorizoMdZK5WjYy6BQ" }, { "__type__": "cc.PrefabInfo", @@ -3062,7 +2582,7 @@ "asset": { "__id__": 0 }, - "fileId": "/uX4bLyfZcl6Zv+X/ikqrQ", + "fileId": "KUnHrMMYynYW+b/jVjoE7A", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -3075,7 +2595,11 @@ "_parent": { "__id__": 2 }, - "_children": [], + "_children": [ + { + "__id__": 122 + } + ], "_active": true, "_components": [ { @@ -3118,7 +2642,60 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "52KgwHvlxN5b1bZ0KDe7Qa" + }, + { + "__type__": "cc.Node", + "_name": "Locked", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 121 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 123 + }, + { + "__id__": 125 + }, + { + "__id__": 127 + } + ], + "_prefab": { + "__id__": 129 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 21.239, + "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": "c7G3yUm4tGVa1kHWLVdCZ/" }, { "__type__": "cc.UITransform", @@ -3126,7 +2703,129 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 129 + "__id__": 122 + }, + "_enabled": true, + "__prefab": { + "__id__": 124 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 230, + "height": 230 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "8dKVDWGZpAArsw6GQnhwEx" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "VkH4eeXc8MwEO2RcXBXxuQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 122 + }, + "_enabled": true, + "__prefab": { + "__id__": 126 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "81b7998e-2a7b-4ee5-a7fa-a34c6dbaecb8@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "b0VwQ3tuZM/rVq3a0lI/B9" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "XBMtpSwit1gJv3qNjRCqZA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 122 + }, + "_enabled": true, + "__prefab": { + "__id__": 128 + }, + "_alignFlags": 18, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 21.239, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "d7D8M/fhJBWqcQGrSiMool" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "+/1eAsba1az7z23eeR8vzQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "NfYuM+DmkMUyTb+pX73UMA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 121 }, "_enabled": true, "__prefab": { @@ -3142,11 +2841,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "48mrY/ghlO140Pir2QSHVI" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "ACTWs57QdVFAmSX1B/R7HA" + "fileId": "+bGC8HtxzMk4qRCa0Kk/fQ" }, { "__type__": "cc.Sprite", @@ -3154,7 +2853,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 129 + "__id__": 121 }, "_enabled": true, "__prefab": { @@ -3187,11 +2886,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "a1fSM3c9FEiqBkC95MZjzQ" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "3t6jIAr11lT8rlqo9znHzg" + "fileId": "vhnLKYEJjY/BhByK2KpkvQ" }, { "__type__": "cc.Widget", @@ -3199,7 +2898,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 129 + "__id__": 121 }, "_enabled": true, "__prefab": { @@ -3223,11 +2922,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "7bMzaIbH9AOqPACyQEnpSf" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "aXMqcvBi4ZOLQ/18dACMSA" + "fileId": "gzHVlQXA3sU6PTcUbfPn4A" }, { "__type__": "cc.PrefabInfo", @@ -3237,7 +2936,7 @@ "asset": { "__id__": 0 }, - "fileId": "LNWwMN3XplHnGQTmTrcMaw", + "fileId": "6WB+BNMpFZNSk4BsMq3Upw", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -3293,7 +2992,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "88Bumvj5pOwqOTWASrUW3q" }, { "__type__": "cc.UITransform", @@ -3317,11 +3016,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "03Tj+nR7ZCprg/Cr5+bs1Q" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "NYVco2O90VRw5oWOu63S6Q" + "fileId": "XBg1DHxFZTr4kuZ/6qEULw" }, { "__type__": "cc.Label", @@ -3385,11 +3084,11 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "" + "_id": "16BrBPIj1P57jnumJD2jx2" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "S79W1IX43OKEVTjhb8t7CQ" + "fileId": "Aw9CR7wYsAhhcpLdpOa6OA" }, { "__type__": "cc.Widget", @@ -3421,11 +3120,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "b5G83PnLhJLL9r2wiwHGgC" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "HmuFyOizOcsXthd1wHzJ7Q" + "fileId": "S1M2dKw6aGvF+4qnGzXelg" }, { "__type__": "cc.PrefabInfo", @@ -3435,7 +3134,7 @@ "asset": { "__id__": 0 }, - "fileId": "0F3NtCGLPwH/CriROchEDw", + "fileId": "N4XdoIX+a/B4CGlMvqQ1TQ", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -3448,21 +3147,25 @@ "_parent": { "__id__": 2 }, - "_children": [], + "_children": [ + { + "__id__": 146 + } + ], "_active": true, "_components": [ { - "__id__": 146 + "__id__": 154 }, { - "__id__": 148 + "__id__": 156 }, { - "__id__": 150 + "__id__": 158 } ], "_prefab": { - "__id__": 152 + "__id__": 160 }, "_lpos": { "__type__": "cc.Vec3", @@ -3491,7 +3194,182 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "9cf8upQXdMt57ySK/8LjXe" + }, + { + "__type__": "cc.Node", + "_name": "Locked", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 145 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 147 + }, + { + "__id__": 149 + }, + { + "__id__": 151 + } + ], + "_prefab": { + "__id__": 153 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 18.879, + "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": "b6rgWHSjVPj45Q3F8oFe+r" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 146 + }, + "_enabled": true, + "__prefab": { + "__id__": 148 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 230, + "height": 230 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "b14ZpXEHdD3qlpUI8eZsDX" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "MJAMaRaOzqHnLCBoSXZ15g" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 146 + }, + "_enabled": true, + "__prefab": { + "__id__": 150 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "81b7998e-2a7b-4ee5-a7fa-a34c6dbaecb8@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "camq4HDUFPZrI0x5IUZpOH" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "upp18/yElc7fTy3UD53ScQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 146 + }, + "_enabled": true, + "__prefab": { + "__id__": 152 + }, + "_alignFlags": 18, + "_target": null, + "_left": 49.5, + "_right": 49.5, + "_top": -3, + "_bottom": -3, + "_horizontalCenter": 18.879, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 230, + "_originalHeight": 230, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "cadARrjh9GE75aCasQ+IP3" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "9zXPfnz7UMMD/PehOSk0vQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "HmjfXLpX59liI6ImvjTtGw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null }, { "__type__": "cc.UITransform", @@ -3503,7 +3381,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 147 + "__id__": 155 }, "_contentSize": { "__type__": "cc.Size", @@ -3515,11 +3393,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "33lFai18ZJTZngxYhJ1QH/" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "2i4KqcSbqVEb9mXzuZvUgg" + "fileId": "1TlH40oqP0DQpzQ7iW4ThA" }, { "__type__": "cc.Sprite", @@ -3531,7 +3409,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 149 + "__id__": 157 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3560,11 +3438,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "c5Vee7MbpBfpnE1sLDyjX5" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "ODBIm47R77rFP/riWeBwsQ" + "fileId": "+kw/xcUmSyzVCe2M+qCoGw" }, { "__type__": "cc.Widget", @@ -3576,7 +3454,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 151 + "__id__": 159 }, "_alignFlags": 16, "_target": null, @@ -3596,11 +3474,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "8bHSCwsYlHjpEr2RmRJnMS" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "gMO8D7+OLU7adeFc26EgWQ" + "fileId": "lR78aRXHH2ZHCjysX7zYpg" }, { "__type__": "cc.PrefabInfo", @@ -3610,7 +3488,7 @@ "asset": { "__id__": 0 }, - "fileId": "I9m9g8uu3TD1Ps4phkkEaw", + "fileId": "jOmBmMRJlNU5PXmIyim9Lg", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -3627,17 +3505,17 @@ "_active": true, "_components": [ { - "__id__": 154 + "__id__": 162 }, { - "__id__": 156 + "__id__": 164 }, { - "__id__": 158 + "__id__": 166 } ], "_prefab": { - "__id__": 160 + "__id__": 168 }, "_lpos": { "__type__": "cc.Vec3", @@ -3666,7 +3544,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "13Mt1ZN7xHGYCAHKqxXj27" }, { "__type__": "cc.UITransform", @@ -3674,11 +3552,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 153 + "__id__": 161 }, "_enabled": true, "__prefab": { - "__id__": 155 + "__id__": 163 }, "_contentSize": { "__type__": "cc.Size", @@ -3690,11 +3568,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "576p/spxxAXJp2hLHG8HlE" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "ZhFR+WxamtB9q7N3/OXsew" + "fileId": "LFbApFqkYK6KoA5dzDFNMQ" }, { "__type__": "cc.Sprite", @@ -3702,11 +3580,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 153 + "__id__": 161 }, "_enabled": true, "__prefab": { - "__id__": 157 + "__id__": 165 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3735,11 +3613,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "5bWoe3gwBAlrGRhSQWwH2K" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "bMHXF4z5N6beCzs4UoZ3nA" + "fileId": "iD7z85ewBvm+SYMszFf2Eg" }, { "__type__": "cc.Widget", @@ -3747,11 +3625,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 153 + "__id__": 161 }, "_enabled": true, "__prefab": { - "__id__": 159 + "__id__": 167 }, "_alignFlags": 8, "_target": null, @@ -3771,11 +3649,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "7dYshVLYNIm6MXtN7TZX9W" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "N461jKfywNw0AclS/TUCgw" + "fileId": "Q+9Bdq91EK9zQsHdt+O3kQ" }, { "__type__": "cc.PrefabInfo", @@ -3785,7 +3663,7 @@ "asset": { "__id__": 0 }, - "fileId": "XZssDZJpPggNION3Lxqn0A", + "fileId": "mmWja326HWlEoemuPcv2Dw", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -3802,17 +3680,23 @@ "_active": true, "_components": [ { - "__id__": 162 + "__id__": 170 }, { - "__id__": 164 + "__id__": 172 }, { - "__id__": 166 + "__id__": 174 + }, + { + "__id__": 176 + }, + { + "__id__": 178 } ], "_prefab": { - "__id__": 168 + "__id__": 180 }, "_lpos": { "__type__": "cc.Vec3", @@ -3841,7 +3725,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "58BLnKGT5Lb5CF0W2FIACB" }, { "__type__": "cc.UITransform", @@ -3849,11 +3733,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 161 + "__id__": 169 }, "_enabled": true, "__prefab": { - "__id__": 163 + "__id__": 171 }, "_contentSize": { "__type__": "cc.Size", @@ -3865,11 +3749,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "63Wruj2bBGAoEqv770adWT" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "w7bIN5Vn45gnL7U7Y1uGWg" + "fileId": "T5NvoRUJYoIKf4UuRbP0Eg" }, { "__type__": "cc.Sprite", @@ -3877,11 +3761,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 161 + "__id__": 169 }, "_enabled": true, "__prefab": { - "__id__": 165 + "__id__": 173 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3910,11 +3794,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "7fXuFQvSVEj6haOvpGQM20" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "5moNdKGYm34hnu7vexRaGA" + "fileId": "HC6vHWUWD7ZPujuWf8+2qg" }, { "__type__": "cc.Widget", @@ -3922,11 +3806,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 161 + "__id__": 169 }, "_enabled": true, "__prefab": { - "__id__": 167 + "__id__": 175 }, "_alignFlags": 16, "_target": null, @@ -3946,11 +3830,50 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "54uBJu06dPeYM7kLaZ77A9" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "rkCjgpRZJ2QMZHM+pWc7cA" + "fileId": "mygJzPJ7OdRCcfAwGVGG6w" + }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 169 + }, + "_enabled": true, + "__prefab": { + "__id__": 177 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "4fNLr+eTBL/qh4OCPHHOgQ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "veibsoza12GHS9cjK+7HOw" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 169 + }, + "_enabled": true, + "__prefab": { + "__id__": 179 + }, + "_id": "7bTi7XLSpO9YLdAJjcYnjE" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ITunJzBN1wXIfnugVH7WjQ" }, { "__type__": "cc.PrefabInfo", @@ -3960,7 +3883,7 @@ "asset": { "__id__": 0 }, - "fileId": "getMsCv6fFyAnpbQ+prPsw", + "fileId": "l0FJ3bmeGk8vj7xKViLbkA", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -3977,17 +3900,23 @@ "_active": true, "_components": [ { - "__id__": 170 + "__id__": 182 }, { - "__id__": 172 + "__id__": 184 }, { - "__id__": 174 + "__id__": 186 + }, + { + "__id__": 188 + }, + { + "__id__": 190 } ], "_prefab": { - "__id__": 176 + "__id__": 192 }, "_lpos": { "__type__": "cc.Vec3", @@ -4016,7 +3945,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "2dEvJNtkNBeY3pVCdGYyAp" }, { "__type__": "cc.UITransform", @@ -4024,11 +3953,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 169 + "__id__": 181 }, "_enabled": true, "__prefab": { - "__id__": 171 + "__id__": 183 }, "_contentSize": { "__type__": "cc.Size", @@ -4040,11 +3969,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "50eqQjvshKPqsVBSbEuatb" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "SvqNolChxKBDiyvJ6OXGPQ" + "fileId": "QHJ6olB4rqvCblK95FM07g" }, { "__type__": "cc.Sprite", @@ -4052,11 +3981,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 169 + "__id__": 181 }, "_enabled": true, "__prefab": { - "__id__": 173 + "__id__": 185 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -4085,11 +4014,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "b5upA1RcdH9aiqv1KE+koZ" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "SKDLUS6gHggCE83Z3//dDA" + "fileId": "/eqBt+sQz+RtAhv1ZfBZ3Q" }, { "__type__": "cc.Widget", @@ -4097,11 +4026,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 169 + "__id__": 181 }, "_enabled": true, "__prefab": { - "__id__": 175 + "__id__": 187 }, "_alignFlags": 32, "_target": null, @@ -4121,11 +4050,50 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "b97n77VOtERKVK1l1qphJM" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "/Tz3robGTkPI0fjFggWtbg" + "fileId": "+pOtHuRahG18iu7zioj3aw" + }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 181 + }, + "_enabled": true, + "__prefab": { + "__id__": 189 + }, + "playOnLoad": false, + "_clips": [], + "_defaultClip": null, + "_id": "d6w+aYVJVJX7sM14GwRCiw" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "/viiwd8uZlq9sNjxb5j5ig" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 181 + }, + "_enabled": true, + "__prefab": { + "__id__": 191 + }, + "_id": "f66AMrybxCraqDE+SQBuO1" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "H/0MydVR3EC0KF6n1hcx5A" }, { "__type__": "cc.PrefabInfo", @@ -4135,7 +4103,7 @@ "asset": { "__id__": 0 }, - "fileId": "MzYzoEsUxfwa3y20F623Bg", + "fileId": "1mMJDSklnd6bRnnr8OGF2A", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -4152,17 +4120,17 @@ "_active": true, "_components": [ { - "__id__": 178 + "__id__": 194 }, { - "__id__": 180 + "__id__": 196 }, { - "__id__": 182 + "__id__": 198 } ], "_prefab": { - "__id__": 184 + "__id__": 200 }, "_lpos": { "__type__": "cc.Vec3", @@ -4191,7 +4159,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "3994iVzz1O47OlPIjNsAre" }, { "__type__": "cc.UITransform", @@ -4199,11 +4167,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 177 + "__id__": 193 }, "_enabled": true, "__prefab": { - "__id__": 179 + "__id__": 195 }, "_contentSize": { "__type__": "cc.Size", @@ -4215,11 +4183,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "eaHlTbn7RHy5QkBljUpiEx" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "fRShftQKthpBxTuZnq/rCQ" + "fileId": "zRSp8Ew5mtRLi5huSlkt2A" }, { "__type__": "cc.Sprite", @@ -4227,11 +4195,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 177 + "__id__": 193 }, "_enabled": true, "__prefab": { - "__id__": 181 + "__id__": 197 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -4260,11 +4228,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "213vJLRaJIba99tJS56l9q" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "ibEbpYO/gJFcVU/o2tHDDA" + "fileId": "8i5dSvsx3lMlXVjkCO2idQ" }, { "__type__": "cc.Widget", @@ -4272,11 +4240,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 177 + "__id__": 193 }, "_enabled": true, "__prefab": { - "__id__": 183 + "__id__": 199 }, "_alignFlags": 16, "_target": null, @@ -4296,11 +4264,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "01569+t8pH16pvDOZ1AM2Q" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "VILItkPt1chG1iXu453Yow" + "fileId": "MDtMfiEj7x57grlbFw89+g" }, { "__type__": "cc.PrefabInfo", @@ -4310,7 +4278,7 @@ "asset": { "__id__": 0 }, - "fileId": "ZC1g1m37R5Jb6jT5aAsYkw", + "fileId": "Qls8fA6+5tv/mORsAyVmHw", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -4327,17 +4295,17 @@ "_active": true, "_components": [ { - "__id__": 186 + "__id__": 202 }, { - "__id__": 188 + "__id__": 204 }, { - "__id__": 190 + "__id__": 206 } ], "_prefab": { - "__id__": 192 + "__id__": 208 }, "_lpos": { "__type__": "cc.Vec3", @@ -4366,7 +4334,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "e6kwOfOmtIAJETuHtP0A4U" }, { "__type__": "cc.UITransform", @@ -4374,11 +4342,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 185 + "__id__": 201 }, "_enabled": true, "__prefab": { - "__id__": 187 + "__id__": 203 }, "_contentSize": { "__type__": "cc.Size", @@ -4390,11 +4358,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "7bCsfMh1lCX47UQS/uazqI" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "27qdhVVdoxGaicVVQ8BgTw" + "fileId": "0MAOLcZeyV1mSjMi7PhI/w" }, { "__type__": "cc.Sprite", @@ -4402,11 +4370,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 185 + "__id__": 201 }, "_enabled": true, "__prefab": { - "__id__": 189 + "__id__": 205 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -4435,11 +4403,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "19wfqEpOdHVaYAIjrsj1gp" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "YxdVgZxtJiA+8RKu84jOxw" + "fileId": "xA6tRiw8M3Ciir4ObhDaiQ" }, { "__type__": "cc.Widget", @@ -4447,11 +4415,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 185 + "__id__": 201 }, "_enabled": true, "__prefab": { - "__id__": 191 + "__id__": 207 }, "_alignFlags": 8, "_target": null, @@ -4471,11 +4439,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "98U2VDJMZD6YLP2hjFZNPr" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "2WFUhR7cp8l2GUOWjXadfw" + "fileId": "9n/4TZ6dteeASbXx6negwA" }, { "__type__": "cc.PrefabInfo", @@ -4485,591 +4453,24 @@ "asset": { "__id__": 0 }, - "fileId": "J7X2Pndo8Myc5UMAZaqknw", + "fileId": "DYuTdwabj7ue7Q0IWIbKGw", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null }, { "__type__": "cc.Node", - "_name": "Spirit16", + "_name": "Spirit3139", "_objFlags": 0, "__editorExtras__": {}, "_parent": { "__id__": 2 }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 194 - }, - { - "__id__": 196 - }, - { - "__id__": 198 - } - ], - "_prefab": { - "__id__": 200 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -311, - "y": -185.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": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 193 - }, - "_enabled": true, - "__prefab": { - "__id__": 195 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 258, - "height": 21 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "BBHGmc9izClQLQjZ4iKfVQ" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 193 - }, - "_enabled": true, - "__prefab": { - "__id__": 197 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "f76d6d83-942a-4511-abdd-b12e14aba51d@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_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": "B2lKPSgzQU99vGAmgDOPjA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 193 - }, - "_enabled": true, - "__prefab": { - "__id__": 199 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -311, - "_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": "PbDUj4dzMvZDsQFXiRKilQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "moprYT5KQ02JYnfTOalSbA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit178", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 202 - }, - { - "__id__": 204 - }, - { - "__id__": 206 - } - ], - "_prefab": { - "__id__": 208 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -361.5, - "y": -185, - "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__": 201 - }, - "_enabled": true, - "__prefab": { - "__id__": 203 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 59.20561176043725, - "height": 16.38 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "hg5+9PiBK1paVp1wnq0/7A" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 201 - }, - "_enabled": true, - "__prefab": { - "__id__": 205 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 233, - "g": 210, - "b": 7, - "a": 255 - }, - "_string": "客服QQ:", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 13.704166666666667, - "_fontSize": 13, - "_fontFamily": "Arial", - "_lineHeight": 13, - "_overflow": 0, - "_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": "34zVLcZpRQO4eTx928YojQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 201 - }, - "_enabled": true, - "__prefab": { - "__id__": 207 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -361.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": "DjVmlRbospuEP1bJGeNY6Q" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "0xF4vjl1T0u6sFfjzTphqw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit179", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ + "_children": [ { "__id__": 210 - }, - { - "__id__": 212 - }, - { - "__id__": 214 } ], - "_prefab": { - "__id__": 216 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -361.5, - "y": -211, - "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__": 209 - }, - "_enabled": true, - "__prefab": { - "__id__": 211 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 59.20561176043725, - "height": 16.38 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "pQyukIBbL5OrFFxFe7SzBg" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 209 - }, - "_enabled": true, - "__prefab": { - "__id__": 213 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 233, - "g": 210, - "b": 7, - "a": 255 - }, - "_string": "客服QQ:", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 13.704166666666667, - "_fontSize": 13, - "_fontFamily": "Arial", - "_lineHeight": 13, - "_overflow": 0, - "_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": "Ld1Pjd3CE4VekxG/XQSx/Q" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 209 - }, - "_enabled": true, - "__prefab": { - "__id__": 215 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -361.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": "gCJsl4L8O9kzVAIlRdZBsw" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "t2S77673bF6GNCOa7J/Xtw", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit193", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], "_active": true, "_components": [ { @@ -5085,798 +4486,6 @@ "_prefab": { "__id__": 224 }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -431, - "y": -185, - "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__": 217 - }, - "_enabled": true, - "__prefab": { - "__id__": 219 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 25.04353036993577, - "height": 13.86 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "DwXEkShuzDcTlOgPgvgQ5w" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 217 - }, - "_enabled": true, - "__prefab": { - "__id__": 221 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "手机:", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 11.595833333333333, - "_fontSize": 11, - "_fontFamily": "Arial", - "_lineHeight": 11, - "_overflow": 0, - "_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": "q62C7SpZiJ7GBEabW4vNYQ" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 217 - }, - "_enabled": true, - "__prefab": { - "__id__": 223 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -431, - "_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": "Ppj9o4tLz0PM6xF/nALX/w" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "x/+IDMr8qnP8oyzHGjUvpA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit194", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 226 - }, - { - "__id__": 228 - }, - { - "__id__": 230 - } - ], - "_prefab": { - "__id__": 232 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -431, - "y": -211, - "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__": 225 - }, - "_enabled": true, - "__prefab": { - "__id__": 227 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 25.04353036993577, - "height": 13.86 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "LT2MQ6lTF90dAV30u+42Ag" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 225 - }, - "_enabled": true, - "__prefab": { - "__id__": 229 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "微信:", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 11.595833333333333, - "_fontSize": 11, - "_fontFamily": "Arial", - "_lineHeight": 11, - "_overflow": 0, - "_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": "Nr3uYp2C4IuCm/Z/HJH2IA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 225 - }, - "_enabled": true, - "__prefab": { - "__id__": 231 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -431, - "_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": "v2UC/OdXrp2NV5K1ViMyZQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "9wURYbwLwxktYeT1b0XCig", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit180", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 234 - }, - { - "__id__": 236 - }, - { - "__id__": 238 - } - ], - "_prefab": { - "__id__": 240 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -435, - "y": -159, - "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__": 233 - }, - "_enabled": true, - "__prefab": { - "__id__": 235 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 20.15827646368577, - "height": 13.86 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "P42aWnCOgDDwKmcrmnMUCA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 233 - }, - "_enabled": true, - "__prefab": { - "__id__": 237 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "QQ:", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 11.595833333333333, - "_fontSize": 11, - "_fontFamily": "Arial", - "_lineHeight": 11, - "_overflow": 0, - "_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": "d3pOvxbZVMFhDnft7Om2MA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 233 - }, - "_enabled": true, - "__prefab": { - "__id__": 239 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -435, - "_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": "G37hT44WKnatc2FdInGOhQ" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "AuexaWL3S2YRdXxEU7GJJA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit682", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 242 - }, - { - "__id__": 244 - }, - { - "__id__": 246 - } - ], - "_prefab": { - "__id__": 248 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -391, - "y": -159, - "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__": 241 - }, - "_enabled": true, - "__prefab": { - "__id__": 243 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 7.227773553297925, - "height": 16.38 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "cxXxQSmSANBKvIHPWn7dXQ" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 241 - }, - "_enabled": true, - "__prefab": { - "__id__": 245 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 233, - "g": 210, - "b": 7, - "a": 255 - }, - "_string": "a", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 13.704166666666667, - "_fontSize": 13, - "_fontFamily": "Arial", - "_lineHeight": 13, - "_overflow": 0, - "_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": "3VLUGbzfGLi8dCjb897kEA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 241 - }, - "_enabled": true, - "__prefab": { - "__id__": 247 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -391, - "_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": "2US7r7wLq+4h3LZw5c1KOg" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "VM3S08RYIaXlVlCPpVc5SA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Spirit3139", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 250 - }, - { - "__id__": 252 - }, - { - "__id__": 254 - } - ], - "_prefab": { - "__id__": 256 - }, "_lpos": { "__type__": "cc.Vec3", "x": 327, @@ -5904,7 +4513,60 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "e4sdJtGpZIKa3cG0RIoyDw" + }, + { + "__type__": "cc.Node", + "_name": "Locked", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 209 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 211 + }, + { + "__id__": 213 + }, + { + "__id__": 215 + } + ], + "_prefab": { + "__id__": 217 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 9.44, + "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": "22HtTCx6dKcZzZAj3Lpjvj" }, { "__type__": "cc.UITransform", @@ -5912,11 +4574,133 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 249 + "__id__": 210 }, "_enabled": true, "__prefab": { - "__id__": 251 + "__id__": 212 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 103.746, + "height": 103.746 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "a4sV5UYPtHLqENs/TX21tt" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "SvcBAjprs35J8vDuuQ8FYA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 210 + }, + "_enabled": true, + "__prefab": { + "__id__": 214 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "81b7998e-2a7b-4ee5-a7fa-a34c6dbaecb8@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": "86qTtkvNVMzLpJL4K1uE1O" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ZTRtDtyTpvNgfGK/gBEegQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 210 + }, + "_enabled": true, + "__prefab": { + "__id__": 216 + }, + "_alignFlags": 18, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 9.44, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "7e76a3tddCXIERzq7lkPdx" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "LUcJDYQghEusDNCkTD9Ofw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "2hWocvehW7vb8qmNIfMDKQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 209 + }, + "_enabled": true, + "__prefab": { + "__id__": 219 }, "_contentSize": { "__type__": "cc.Size", @@ -5928,11 +4712,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "46HtReRjFOPa8a54TaN3qJ" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "wqXUbACakNhg2VVXCeNFRg" + "fileId": "Esj6wk2lvWs5DCkfTSl9KQ" }, { "__type__": "cc.Sprite", @@ -5940,11 +4724,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 249 + "__id__": 209 }, "_enabled": true, "__prefab": { - "__id__": 253 + "__id__": 221 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -5973,11 +4757,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "09cTWwL2RMmJFTvkUJN9SX" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "IlVKTk+HA/+okoXA9Nyu2w" + "fileId": "ILwn2WA5vbP9o+hX9eeAQw" }, { "__type__": "cc.Widget", @@ -5985,11 +4769,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 249 + "__id__": 209 }, "_enabled": true, "__prefab": { - "__id__": 255 + "__id__": 223 }, "_alignFlags": 16, "_target": null, @@ -6009,11 +4793,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "07Isvsv95GXrYPUDEc7Hof" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "5O74ZnN70jN/Ehe9sL0CJw" + "fileId": "2+57rKMKQwIl4CD4JjgnkA" }, { "__type__": "cc.PrefabInfo", @@ -6023,7 +4807,7 @@ "asset": { "__id__": 0 }, - "fileId": "bJRsdh2OzDQN631/tyjqWw", + "fileId": "KpdGyL3b/8lSHLDNu1T41A", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -6040,17 +4824,17 @@ "_active": true, "_components": [ { - "__id__": 258 + "__id__": 226 }, { - "__id__": 260 + "__id__": 228 }, { - "__id__": 262 + "__id__": 230 } ], "_prefab": { - "__id__": 264 + "__id__": 232 }, "_lpos": { "__type__": "cc.Vec3", @@ -6079,7 +4863,7 @@ "y": 0, "z": 0 }, - "_id": "" + "_id": "796Jgq4pJEGbA6aONBP46M" }, { "__type__": "cc.UITransform", @@ -6087,11 +4871,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 257 + "__id__": 225 }, "_enabled": true, "__prefab": { - "__id__": 259 + "__id__": 227 }, "_contentSize": { "__type__": "cc.Size", @@ -6103,11 +4887,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "a9WSxfM7FGG45eVOMLb0B+" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "CtsC574c+od/Us36PDRShg" + "fileId": "GvzdJMFtyt+B3yTkp9ZR9A" }, { "__type__": "cc.Sprite", @@ -6115,11 +4899,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 257 + "__id__": 225 }, "_enabled": true, "__prefab": { - "__id__": 261 + "__id__": 229 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -6148,11 +4932,11 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "" + "_id": "a2qgEBj2JPk7YFYYMcw4jS" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "c06kNXSB7KEfvxAsBt5uXw" + "fileId": "zMprx9O/aGanNNAry8NFQA" }, { "__type__": "cc.Widget", @@ -6160,11 +4944,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 257 + "__id__": 225 }, "_enabled": true, "__prefab": { - "__id__": 263 + "__id__": 231 }, "_alignFlags": 16, "_target": null, @@ -6184,11 +4968,11 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "faWAg+fd1DKIJlrNMJh2Kt" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "F9TKYJSgGO5gci9vFTdsgQ" + "fileId": "E5xgdn3CCnSkrOUQfb94mw" }, { "__type__": "cc.PrefabInfo", @@ -6198,38 +4982,202 @@ "asset": { "__id__": 0 }, - "fileId": "ehoW4ZsAwUtESf1K8L7Bxg", + "fileId": "r/rPNhMQ4v/5PWkvfqjctQ", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null }, + { + "__type__": "cc.Node", + "_name": "Announcement", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 234 + }, + { + "__id__": 246 + }, + { + "__id__": 254 + } + ], + "_active": true, + "_components": [ + { + "__id__": 262 + }, + { + "__id__": 264 + } + ], + "_prefab": { + "__id__": 266 + }, + "_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": "d1iW60VAJCop5Bih4KnW+/" + }, + { + "__type__": "cc.Node", + "_name": "Spirit479", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 233 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 235 + }, + { + "__id__": 237 + }, + { + "__id__": 239 + }, + { + "__id__": 241 + }, + { + "__id__": 243 + } + ], + "_prefab": { + "__id__": 245 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -256.5, + "y": 239, + "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": "5540eJ1utPPLizmQlCPYk/" + }, { "__type__": "cc.UITransform", "_name": "", "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 2 + "__id__": 234 }, "_enabled": true, "__prefab": { - "__id__": 266 + "__id__": 236 }, "_contentSize": { "__type__": "cc.Size", - "width": 1600, - "height": 720 + "width": 55, + "height": 50 }, "_anchorPoint": { "__type__": "cc.Vec2", "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "047cJmjZtK4IYLTODnf3M1" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "wo/ntgIyQw3vzlsvkJuGzw" + "fileId": "0N4DT/25FKgbA8EoOSbWxg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 234 + }, + "_enabled": true, + "__prefab": { + "__id__": 238 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "151439f3-c9bd-4fb2-97b5-5afc295d76e8@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": "38qYP4rAFKwot2nBnEcczn" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "kuSeCRukxYVBnVXca7BkzQ" }, { "__type__": "cc.Widget", @@ -6237,19 +5185,19 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 2 + "__id__": 234 }, "_enabled": true, "__prefab": { - "__id__": 268 + "__id__": 240 }, - "_alignFlags": 45, + "_alignFlags": 16, "_target": null, "_left": 0, "_right": 0, "_top": 0, "_bottom": 0, - "_horizontalCenter": 0, + "_horizontalCenter": -256.5, "_verticalCenter": 0, "_isAbsLeft": true, "_isAbsRight": true, @@ -6257,15 +5205,62 @@ "_isAbsBottom": true, "_isAbsHorizontalCenter": true, "_isAbsVerticalCenter": true, - "_originalWidth": 1600, - "_originalHeight": 720, + "_originalWidth": 0, + "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "81s2z6YsJPDrONZOcW9+1j" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "lzAtBJZracwEzq8wsA7rJA" + "fileId": "djRloiFPFOGR8p/GU2Z3vw" + }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 234 + }, + "_enabled": true, + "__prefab": { + "__id__": 242 + }, + "playOnLoad": false, + "_clips": [ + { + "__uuid__": "aed03d13-bce1-42a0-9d23-ebd829d9ee55", + "__expectedType__": "cc.AnimationClip" + } + ], + "_defaultClip": { + "__uuid__": "aed03d13-bce1-42a0-9d23-ebd829d9ee55", + "__expectedType__": "cc.AnimationClip" + }, + "_id": "68Uthlxk1Oib1x2mfu9sQ5" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "7am13D0gUBHRfLHfM8EC9Q" + }, + { + "__type__": "6062eFrLeFCz5GlDvzzi64B", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 234 + }, + "_enabled": true, + "__prefab": { + "__id__": 244 + }, + "_id": "56XScc+01EMqkPYeVpg2pT" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "RAeUzLczb9LuPlCiN7atFQ" }, { "__type__": "cc.PrefabInfo", @@ -6275,7 +5270,380 @@ "asset": { "__id__": 0 }, - "fileId": "GtLDiP69mx24quKIqiFCzg", + "fileId": "YebAjjbaAaLyrTTiHMmEPA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit219", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 233 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 247 + }, + { + "__id__": 249 + }, + { + "__id__": 251 + } + ], + "_prefab": { + "__id__": 253 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 92.5, + "y": 237.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": "57zAwkScVNpIpDZ5sNxGW/" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 246 + }, + "_enabled": true, + "__prefab": { + "__id__": 248 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 645, + "height": 39 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "78iwZWAslCuIMWTb+gQwGM" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "FE9jHR2QC+fdJiy7oAbihg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 246 + }, + "_enabled": true, + "__prefab": { + "__id__": 250 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "df4db047-ac71-4afd-b73d-e608abadc355@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": "8aj5pfeAFD3YWgNRqgSuuP" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "/XbRcKSl8vWd7HO/Mdf9OA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 246 + }, + "_enabled": true, + "__prefab": { + "__id__": 252 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 92.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "cdNORtLZNMRI7ItP1Z0q6h" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ue78eJU/N729WB3fHG3WUg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "r2Qbl7b2Df7O7naO7XNqPA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit220", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 233 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 255 + }, + { + "__id__": 257 + }, + { + "__id__": 259 + } + ], + "_prefab": { + "__id__": 261 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -126, + "y": 239, + "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": "8eazy4rjdDDJce1SGqaA40" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 254 + }, + "_enabled": true, + "__prefab": { + "__id__": 256 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 79.98730082756917, + "height": 25.200000000000003 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "20T8VgWvlP4pkH4r0ykUpQ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "znEXlSr2Phs5bqUu/Ia9aQ" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 254 + }, + "_enabled": true, + "__prefab": { + "__id__": 258 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "公告文字", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 21.083333333333336, + "_fontSize": 20, + "_fontFamily": "Arial", + "_lineHeight": 20, + "_overflow": 0, + "_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": "acHrU26NlItq6Bs7prATnw" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "mIRvZxGJAzGCK66xNgDcBA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 254 + }, + "_enabled": true, + "__prefab": { + "__id__": 260 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -126, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "ebxoDOkmJM2qW5mJIwssEX" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "qQVtegLjzVcNCSjAKcNA4w" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "WhWgYPth20rNQFX7p/6/MA", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -6286,12 +5654,1723 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { + "__id__": 233 + }, + "_enabled": true, + "__prefab": { + "__id__": 263 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 100, + "height": 100 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "5cnhQNLK1KZJ8CBNRTq26u" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "1YzSUI3sxx09Cb2hTWz/TQ" + }, + { + "__type__": "4b459GrMHpAYZaQcQK5lqOu", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 233 + }, + "_enabled": true, + "__prefab": { + "__id__": 265 + }, + "_id": "22Seb0EE9H/7ivhG5SxB+R" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "BCcVdj7wvJyJorx+SagiZA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { "__id__": 1 }, + "asset": { + "__id__": 0 + }, + "fileId": "DUIiXxKPsrqjkXu7LxBSAA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "VipRank", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 268 + } + ], + "_active": true, + "_components": [ + { + "__id__": 332 + } + ], + "_prefab": { + "__id__": 334 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -306.726, + "y": -28.974, + "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": "d6nXuLY3NOPKwlWzy1aNUP" + }, + { + "__type__": "cc.Node", + "_name": "Spirit679", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 267 + }, + "_children": [ + { + "__id__": 269 + }, + { + "__id__": 277 + }, + { + "__id__": 285 + }, + { + "__id__": 293 + }, + { + "__id__": 301 + }, + { + "__id__": 309 + }, + { + "__id__": 317 + } + ], + "_active": true, + "_components": [ + { + "__id__": 325 + }, + { + "__id__": 327 + }, + { + "__id__": 329 + } + ], + "_prefab": { + "__id__": 331 + }, + "_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": "efglmDjsRC6b+67YmjuLvV" + }, + { + "__type__": "cc.Node", + "_name": "Spirit180", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 268 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 270 + }, + { + "__id__": 272 + }, + { + "__id__": 274 + } + ], + "_prefab": { + "__id__": 276 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -144.452, + "y": -130.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": "93UiEozsVJ47n994Tv1uLd" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 269 + }, "_enabled": true, "__prefab": { "__id__": 271 }, + "_contentSize": { + "__type__": "cc.Size", + "width": 27.49807003458498, + "height": 18.900000000000002 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0.5 + }, + "_id": "3dXZHxDO9KPZmY6DvRU7LF" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "E/9X63K+6MvnTB/uAu5m6w" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 269 + }, + "_enabled": true, + "__prefab": { + "__id__": 273 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "QQ:", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 15.8125, + "_fontSize": 15, + "_fontFamily": "Arial", + "_lineHeight": 15, + "_overflow": 0, + "_enableWrapText": false, + "_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": "efPgsNH6hJIbnKKhISvL6x" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "rrWRLHBUQoecpTEsiiEZLg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 269 + }, + "_enabled": true, + "__prefab": { + "__id__": 275 + }, + "_alignFlags": 8, + "_target": null, + "_left": 24.047999999999988, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_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": "91f+blVxRHkZbjpqsAPy9m" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "eCYzgOCmQSN+eoRS+m3f8w" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "zDQWDnYQ7RPpER1sVfiyxQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit682", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 268 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 278 + }, + { + "__id__": 280 + }, + { + "__id__": 282 + } + ], + "_prefab": { + "__id__": 284 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -70.60147674005681, + "y": -130.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": "a8TsNpbAVIsIBDveP6msPa" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 277 + }, + "_enabled": true, + "__prefab": { + "__id__": 279 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 8.340953480113637, + "height": 18.900000000000002 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0.5 + }, + "_id": "ddeBKXupBFH6KYrTfJeX/N" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "x2wI9bobB63g/LZkC2lpDw" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 277 + }, + "_enabled": true, + "__prefab": { + "__id__": 281 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 233, + "g": 210, + "b": 7, + "a": 255 + }, + "_string": "a", + "_horizontalAlign": 0, + "_verticalAlign": 1, + "_actualFontSize": 15.8125, + "_fontSize": 15, + "_fontFamily": "Arial", + "_lineHeight": 15, + "_overflow": 0, + "_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": "b51ugTgrdKnb3/dspGukOP" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "QRLaxsZoApArT3rHpdOPng" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 277 + }, + "_enabled": true, + "__prefab": { + "__id__": 283 + }, + "_alignFlags": 8, + "_target": null, + "_left": 97.89852325994319, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -371.115, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "82qRZsT3JDJaZrhTFYjC/2" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "XcG+D2efav4fCBx89kBQtg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "YJ6anIljvs9IcuXCQADA6Q", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit194", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 268 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 286 + }, + { + "__id__": 288 + }, + { + "__id__": 290 + } + ], + "_prefab": { + "__id__": 292 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -146.152, + "y": -182.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": "1e1hSRQUtPZYhvEYqqdNMp" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 285 + }, + "_enabled": true, + "__prefab": { + "__id__": 287 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 34.16206691576087, + "height": 18.900000000000002 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0.5 + }, + "_id": "4bY0+OwDFJQLZ9h1jEK5IB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "p8D+bG4/Bg990WIDaZA12Q" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 285 + }, + "_enabled": true, + "__prefab": { + "__id__": 289 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "微信:", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 15.8125, + "_fontSize": 15, + "_fontFamily": "Arial", + "_lineHeight": 15, + "_overflow": 0, + "_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": "28oUzKPvVK8LYglbqydqCX" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Syjsui7vXkNKUAhfVftchw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 285 + }, + "_enabled": true, + "__prefab": { + "__id__": 291 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -129.07096654211955, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "766XKyAY5I1qIZiAeqmTpT" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "HXUJp60hVw9mdWpgy5UcaQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "NcNwEhUAWmBLpHgC9pVTMg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit179", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 268 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 294 + }, + { + "__id__": 296 + }, + { + "__id__": 298 + } + ], + "_prefab": { + "__id__": 300 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -70.66006691576086, + "y": -182.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": "c15bPExnVIDop2jb3Y+X5b" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 293 + }, + "_enabled": true, + "__prefab": { + "__id__": 295 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 68.32413383152173, + "height": 18.900000000000002 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0.5 + }, + "_id": "e4U/McgjtCGKNcXZFwsMlV" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "bPp0uMe867tXaxJLofp4yg" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 293 + }, + "_enabled": true, + "__prefab": { + "__id__": 297 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 233, + "g": 210, + "b": 7, + "a": 255 + }, + "_string": "客服QQ:", + "_horizontalAlign": 0, + "_verticalAlign": 1, + "_actualFontSize": 15.8125, + "_fontSize": 15, + "_fontFamily": "Arial", + "_lineHeight": 15, + "_overflow": 0, + "_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": "8b+cg2TQFJIp3RNfC0qjRc" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "qVaXg9jaYfpC4S2S587Mjw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 293 + }, + "_enabled": true, + "__prefab": { + "__id__": 299 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -36.498, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "2c/07zafpEHaRAbninp4Fq" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "MwM/AvQ8bU3ErEVvAZHt9Q" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "zBZm2sfPXJFo5ySGFMfwgg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit16", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 268 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 302 + }, + { + "__id__": 304 + }, + { + "__id__": 306 + } + ], + "_prefab": { + "__id__": 308 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -1.276, + "y": -157, + "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": "1cfM0kpdBMTZp4ZqZUvMPs" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 301 + }, + "_enabled": true, + "__prefab": { + "__id__": 303 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 258, + "height": 21 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "7fbsWbkLJNV4fVB6JTrDov" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "9ZWcKUnhqWToWLfibZ+p0g" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 301 + }, + "_enabled": true, + "__prefab": { + "__id__": 305 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "f76d6d83-942a-4511-abdd-b12e14aba51d@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "8bloDwNOVFu7v+VXYdhazg" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "fE71JdpKKntkMy1Xx6ysNQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 301 + }, + "_enabled": true, + "__prefab": { + "__id__": 307 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -1.276, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "5dC/pHjwpPlYpkJHGKYLWn" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "DK+DtWF6pZLz+nO9ELGjlg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "yVjOh9fYWLdR7fW9nH2fUQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit193", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 268 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 310 + }, + { + "__id__": 312 + }, + { + "__id__": 314 + } + ], + "_prefab": { + "__id__": 316 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -146.152, + "y": -156.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": "5aDxzRI4NOXK8mMi3uxH7Q" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 309 + }, + "_enabled": true, + "__prefab": { + "__id__": 311 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 34.16206691576087, + "height": 18.900000000000002 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0.5 + }, + "_id": "01Y3Y0Z2VN/qGPXqBByQiV" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "qC+TdK9aYdkcFIhdzpFTRQ" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 309 + }, + "_enabled": true, + "__prefab": { + "__id__": 313 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "手机:", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 15.8125, + "_fontSize": 15, + "_fontFamily": "Arial", + "_lineHeight": 15, + "_overflow": 0, + "_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": "e0VAdkfklMC5QBcEQIOPFb" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "AK2a3YwYASlhMD+1yw/jYw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 309 + }, + "_enabled": true, + "__prefab": { + "__id__": 315 + }, + "_alignFlags": 8, + "_target": null, + "_left": 22.348000000000013, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -121.276, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "2fCvYSq8tIBYdOoWX+bzNC" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "vnKKeTPRiSU93QD0fh4AvA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "P84G8DeL+ftopCC83hat3w", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit178", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 268 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 318 + }, + { + "__id__": 320 + }, + { + "__id__": 322 + } + ], + "_prefab": { + "__id__": 324 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -70.66006691576086, + "y": -156.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": "36sRl88xhMfJcLzjw4ZqyR" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 317 + }, + "_enabled": true, + "__prefab": { + "__id__": 319 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 68.32413383152173, + "height": 18.900000000000002 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0.5 + }, + "_id": "70ByQNRr5Nq5Sp2oKo2A4L" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "8YGCNnklgyn8S0Rdym6APw" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 317 + }, + "_enabled": true, + "__prefab": { + "__id__": 321 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 233, + "g": 210, + "b": 7, + "a": 255 + }, + "_string": "客服QQ:", + "_horizontalAlign": 0, + "_verticalAlign": 1, + "_actualFontSize": 15.8125, + "_fontSize": 15, + "_fontFamily": "Arial", + "_lineHeight": 15, + "_overflow": 0, + "_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": "a8llOkYVtEIYwGI8u6SHSy" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "95C1g0lOqBu60GuGQq+48w" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 317 + }, + "_enabled": true, + "__prefab": { + "__id__": 323 + }, + "_alignFlags": 8, + "_target": null, + "_left": 97.83993308423914, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -51.776, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "a8IbY30VFFmaTPj79eVkJo" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "HCl1fG8ZodCnZMhNuhPTxw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "xwTyXQjD28iO5G6o6UAZGA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 268 + }, + "_enabled": true, + "__prefab": { + "__id__": 326 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 337, + "height": 441 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "70aTUmPbFGoZoysADct+N2" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "K26QMF0LtSgWwze39ENXxA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 268 + }, + "_enabled": true, + "__prefab": { + "__id__": 328 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "4f725497-383a-4e19-bc2c-4fd55017042b@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": "84FP4jKphANLPbIGINoyFu" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Qkxk0I3lHRWdCS4EYCc3nQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 268 + }, + "_enabled": true, + "__prefab": { + "__id__": 330 + }, + "_alignFlags": 18, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "e2x9zv7jtJI6+7HmKZGsRw" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "NmLzenWdxQ+3t3ltYuLU0g" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "tFvCTbd0mJSXxyZaH1hTsw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 267 + }, + "_enabled": true, + "__prefab": { + "__id__": 333 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 337, + "height": 441 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "a2rh3cJrVAFopAVbgKfhAa" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ccTW0Dpz9DSp7n8bXgCoPr" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "f7NUkyDzJMUpAQtrktRhi2", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 336 + }, "_contentSize": { "__type__": "cc.Size", "width": 1600, @@ -6302,11 +7381,11 @@ "x": 0.5, "y": 0.5 }, - "_id": "" + "_id": "66DQgJlOtHB42aJzAyj/95" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "bqgHupBeDADJ0fge/36M6Q" + "fileId": "NBM/xCopHqA6XWh8D1ZkoQ" }, { "__type__": "cc.Widget", @@ -6314,11 +7393,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 1 + "__id__": 2 }, "_enabled": true, "__prefab": { - "__id__": 273 + "__id__": 338 }, "_alignFlags": 45, "_target": null, @@ -6338,11 +7417,11 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "" + "_id": "88jpG90N5P24IZyIT550mA" }, { "__type__": "cc.CompPrefabInfo", - "fileId": "DzbgXs+gRz28XRgps9gqtg" + "fileId": "VKdaq4u/6XzfTh/ak72lKA" }, { "__type__": "cc.PrefabInfo", @@ -6352,8 +7431,86 @@ "asset": { "__id__": 0 }, - "fileId": "7jUKcK7VW+9kPygBBDS+LQ", + "fileId": "zw7ZfUwJ1PUFiWrKegPgjw", "instance": null, - "targetOverrides": null + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 341 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "4csm9XSiBGGp37/nXtudNk" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "uATs2vlRVQmzMtddWMoE9w" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 343 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "2epmQ2aKVPLLMXlB6lIreG" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "9C1DAFuML096Da2SLg610Q" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "LjUveEwenO1n+prtkd0haw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null } ] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/MainMenu.prefab.meta similarity index 82% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab.meta rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/MainMenu.prefab.meta index e7ef96f..bbf0798 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/MainMenu.prefab.meta @@ -8,6 +8,6 @@ ], "subMetas": {}, "userData": { - "syncNodeName": "Layer4_MainMenu" + "syncNodeName": "MainMenu" } } diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/NoticeBoard.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/NoticeBoard.prefab new file mode 100644 index 0000000..41ea820 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/NoticeBoard.prefab @@ -0,0 +1,3793 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "NoticeBoard", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "persistent": false + }, + { + "__type__": "cc.Node", + "_name": "NoticeBoard", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [ + { + "__id__": 160 + }, + { + "__id__": 162 + } + ], + "_prefab": { + "__id__": 164 + }, + "_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": "group-12", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 11 + }, + { + "__id__": 19 + }, + { + "__id__": 27 + }, + { + "__id__": 35 + }, + { + "__id__": 43 + }, + { + "__id__": 51 + }, + { + "__id__": 59 + }, + { + "__id__": 67 + }, + { + "__id__": 75 + }, + { + "__id__": 83 + }, + { + "__id__": 91 + } + ], + "_active": true, + "_components": [ + { + "__id__": 155 + }, + { + "__id__": 157 + } + ], + "_prefab": { + "__id__": 159 + }, + "_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": "通知遮罩", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + }, + { + "__id__": 6 + }, + { + "__id__": 8 + } + ], + "_prefab": { + "__id__": 10 + }, + "_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.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 5 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "EPMF0GOTHbKJgvTnWDMRFA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 7 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@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": "8oEFZxpb3xW1DoLVRE/zUw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 9 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ikqfuDncyNz0/cFX0XPm8Q" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "8h87cbctLjyBF70v+sB6Mw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "通知背景", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 12 + }, + { + "__id__": 14 + }, + { + "__id__": 16 + } + ], + "_prefab": { + "__id__": 18 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.5, + "y": -0.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": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 13 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1029, + "height": 603 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "RBj4x/UwWuuxCPJa40YsOg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 15 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "03393c45-1e39-42f5-b287-ea6fb574279c@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_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": "m7ozNg6Hz+rFkl/hykyViQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 17 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0.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": "ik7JaAugNKu7u0dVx9Lj+Q" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "agb5xMSwJvlFDmsK89xSdw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit404", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 20 + }, + { + "__id__": 22 + }, + { + "__id__": 24 + } + ], + "_prefab": { + "__id__": 26 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -207, + "y": 167, + "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__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 21 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 50, + "height": 50 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "XrNkZATGX4Tm8vymhDK1Tw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 23 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "dcc4530b-344b-40b6-8fbb-9fa35db6a289@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": "Cn+dLznX7FvUsTQCDHpVFw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 25 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -207, + "_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": "siavzBU3aNSfH4ZvmUaMZw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "dEunpBed0P6KhyLI1TTGFQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "通知内容", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 28 + }, + { + "__id__": 30 + }, + { + "__id__": 32 + } + ], + "_prefab": { + "__id__": 34 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -229.49841260344616, + "y": 178, + "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__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 29 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 19.996825206892293, + "height": 25.200000000000003 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "OdLALqJTcoPBq+rUnWxp2g" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 31 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 102, + "g": 41, + "b": 16, + "a": 255 + }, + "_string": "消", + "_horizontalAlign": 0, + "_verticalAlign": 1, + "_actualFontSize": 21.083333333333336, + "_fontSize": 20, + "_fontFamily": "Arial", + "_lineHeight": 20, + "_overflow": 0, + "_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": "OkRZ1lFmNitcbLYiptlS6A" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 33 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -219.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": "HEeBawZPiQKl9SV5wu6Vnw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "LmHyiZvKpsSmf0QBEEiTNg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit576", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 36 + }, + { + "__id__": 38 + }, + { + "__id__": 40 + } + ], + "_prefab": { + "__id__": 42 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 350, + "y": -239, + "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__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 37 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 250, + "height": 40 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "BECQQUcRwQAinULAj7X9Ew" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 39 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "ac88bed1-d8b6-460d-9a1f-93d021c7b3f4@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": "+qvL9ulfH0OJ6Z1FGDl7hA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 41 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 350, + "_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": "fdpm1qAMWKon0MEVrvqO5A" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "+iBs5DhhhmHdWvUqK3Y5sA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3102", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 44 + }, + { + "__id__": 46 + }, + { + "__id__": 48 + } + ], + "_prefab": { + "__id__": 50 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 101, + "y": -825.665, + "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__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 45 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 752, + "height": 470 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "BX6bL7zQDPCMjzriHMk0Ww" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 47 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "200f41c0-4ecf-4ad7-8864-877306a46fd1@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": "BASmkn8Lmzr+4Daz24wXww" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 49 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 101, + "_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": "3U1xFoS0jXJLk93OA7RJIQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "iOzlf8ak9Q/LaePkxajF8w", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3105", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 52 + }, + { + "__id__": 54 + }, + { + "__id__": 56 + } + ], + "_prefab": { + "__id__": 58 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -22, + "y": -766, + "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": 770, + "height": 400 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "eOc4bIbeCNHATKTIYqvT3Q" + }, + { + "__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__": "cb2429ec-da7a-421e-8635-9d8e9599dfd7@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": "kNC8yF0QamIokshHjWBl3Q" + }, + { + "__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": -22, + "_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": "RDAdkGLK6IsER48XazcIxQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "HjukamjtEFRDMMlOag5wvQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3106", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 60 + }, + { + "__id__": 62 + }, + { + "__id__": 64 + } + ], + "_prefab": { + "__id__": 66 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -6, + "y": -782, + "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__": 59 + }, + "_enabled": true, + "__prefab": { + "__id__": 61 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 770, + "height": 400 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "GJuyi0bWcweNfvOplc1N5g" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 59 + }, + "_enabled": true, + "__prefab": { + "__id__": 63 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "dbeaba27-ba73-4fd0-9a5d-ae1dd8e1af1a@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": "KtQDpdsaORQcLgOGklALrg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 59 + }, + "_enabled": true, + "__prefab": { + "__id__": 65 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -6, + "_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": "hC4OMzcWASqqoDQ6oZEodA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "5utpCoTVli18mdwOdx6sew", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "通知标题", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 68 + }, + { + "__id__": 70 + }, + { + "__id__": 72 + } + ], + "_prefab": { + "__id__": 74 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -122, + "y": 587, + "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__": 67 + }, + "_enabled": true, + "__prefab": { + "__id__": 69 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 510, + "height": 138 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "fkiMnNIlu30YMtOjlPg40A" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 67 + }, + "_enabled": true, + "__prefab": { + "__id__": 71 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "05398908-866f-4605-b419-4a06ef63248a@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": "pIpnHnYsTf3U7oXRtgr98A" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 67 + }, + "_enabled": true, + "__prefab": { + "__id__": 73 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -122, + "_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": "P/ETTjjI49zRJuO/nhqxPA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "WiHaWAz3fxLsPYXZoUrZpw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "通知关闭", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 76 + }, + { + "__id__": 78 + }, + { + "__id__": 80 + } + ], + "_prefab": { + "__id__": 82 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 474.971, + "y": 266.727, + "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__": 75 + }, + "_enabled": true, + "__prefab": { + "__id__": 77 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 83, + "height": 84 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "U39SxCZZm9p/DjLl1jkMyg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 75 + }, + "_enabled": true, + "__prefab": { + "__id__": 79 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "eedce6d7-3ea6-434c-83e1-87d9a04b2bad@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": "Oee93/IyD3OHo6JiJIllcA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 75 + }, + "_enabled": true, + "__prefab": { + "__id__": 81 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 474.971, + "_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": "vwuocmZijhdfSP0SqqtOgA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "smm5SzMTqRCL92ZlFrHPAQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "通知标题内容", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 84 + }, + { + "__id__": 86 + }, + { + "__id__": 88 + } + ], + "_prefab": { + "__id__": 90 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.5, + "y": 255, + "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__": 83 + }, + "_enabled": true, + "__prefab": { + "__id__": 85 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 403, + "height": 76 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "IFhWsGfDJNkh43MFb6Eu3A" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 83 + }, + "_enabled": true, + "__prefab": { + "__id__": 87 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "5febca8f-13b3-4ea3-997c-0f4534ba499d@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": "i4XvmgGGNLc1kgO97/zHog" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 83 + }, + "_enabled": true, + "__prefab": { + "__id__": 89 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0.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": "knrkvbS4w7mh47LmNgnZIg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "dEVg4d4WH+TEgq6BR1nPJA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "NoticeTabs", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 92 + }, + { + "__id__": 110 + }, + { + "__id__": 128 + } + ], + "_active": true, + "_components": [ + { + "__id__": 146 + }, + { + "__id__": 148 + }, + { + "__id__": 150 + }, + { + "__id__": 152 + } + ], + "_prefab": { + "__id__": 154 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -376, + "y": 200, + "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": "NoticeTab1", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 91 + }, + "_children": [ + { + "__id__": 93 + }, + { + "__id__": 99 + } + ], + "_active": true, + "_components": [ + { + "__id__": 105 + }, + { + "__id__": 107 + } + ], + "_prefab": { + "__id__": 109 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": -46, + "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": "Spirit570", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 92 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 94 + }, + { + "__id__": 96 + } + ], + "_prefab": { + "__id__": 98 + }, + "_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.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 93 + }, + "_enabled": true, + "__prefab": { + "__id__": 95 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 236, + "height": 100 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "IvEfcplxC+1pX9fzAQUoPA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 93 + }, + "_enabled": true, + "__prefab": { + "__id__": 97 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "7e19cefc-cd45-46b7-bef9-3a0b01fee076@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": "jHN0VqQ02NDJ1bPQxsL0DQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "EwWyWffG/09VgcSx5S3Jvg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit573", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 92 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 100 + }, + { + "__id__": 102 + } + ], + "_prefab": { + "__id__": 104 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -3.5, + "y": 2, + "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__": 99 + }, + "_enabled": true, + "__prefab": { + "__id__": 101 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 125, + "height": 40 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "1C2Z3mC5xnuBlYGTf9lBMw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 99 + }, + "_enabled": true, + "__prefab": { + "__id__": 103 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "e17f3cb4-8509-4fc4-91bc-940d79100d28@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": "K0m4mSTVN6ZFbVSqLvte0w" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "NFgOxneHHqj4DCbANibCDA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 92 + }, + "_enabled": true, + "__prefab": { + "__id__": 106 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 236, + "height": 92 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "nyXxeCzMVUO+3EAGRjpZpQ" + }, + { + "__type__": "cc.Toggle", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 92 + }, + "_enabled": true, + "__prefab": { + "__id__": 108 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "checkEvents": [], + "_isChecked": true, + "_checkMark": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Z5XBr8tnMML5efSJDgi7ZA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "o8A4WxIjFwsXQI+yZ7a6Ew", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "NoticeTab2", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 91 + }, + "_children": [ + { + "__id__": 111 + }, + { + "__id__": 117 + } + ], + "_active": true, + "_components": [ + { + "__id__": 123 + }, + { + "__id__": 125 + } + ], + "_prefab": { + "__id__": 127 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": -138, + "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": "Spirit571", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 110 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 112 + }, + { + "__id__": 114 + } + ], + "_prefab": { + "__id__": 116 + }, + "_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.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 111 + }, + "_enabled": true, + "__prefab": { + "__id__": 113 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 236, + "height": 100 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "6ez4ZP6JNuRq5FUBoVLJxQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 111 + }, + "_enabled": true, + "__prefab": { + "__id__": 115 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "a42d050f-b023-4580-9444-36552fa546e3@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": "Bx0iCT1oyvPMpzMSefa5xQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "L6e0DLZ6zcJ6wspu8AC/Cg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit574", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 110 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 118 + }, + { + "__id__": 120 + } + ], + "_prefab": { + "__id__": 122 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -3.5, + "y": 2, + "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__": 117 + }, + "_enabled": true, + "__prefab": { + "__id__": 119 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 125, + "height": 40 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "mQlYjAU0owe8X8Bl8Nq/xQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 117 + }, + "_enabled": true, + "__prefab": { + "__id__": 121 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "9fd0b3df-4b98-428c-b2d3-fe3b467321b9@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": "xLoQ/q99exnRNY1xPyvZOw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "Ns4j54/37SBxXLK1jnvqlw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 110 + }, + "_enabled": true, + "__prefab": { + "__id__": 124 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 236, + "height": 92 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "FwsD+VuVOyBXueXhKfaf7Q" + }, + { + "__type__": "cc.Toggle", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 110 + }, + "_enabled": true, + "__prefab": { + "__id__": 126 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "checkEvents": [], + "_isChecked": false, + "_checkMark": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "APO7f313HzYxkXD9QYjB+w" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3XrCraiLlzuCaKiKrME8kA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "NoticeTab3", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 91 + }, + "_children": [ + { + "__id__": 129 + }, + { + "__id__": 135 + } + ], + "_active": true, + "_components": [ + { + "__id__": 141 + }, + { + "__id__": 143 + } + ], + "_prefab": { + "__id__": 145 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": -230, + "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": "Spirit572", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 128 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 130 + }, + { + "__id__": 132 + } + ], + "_prefab": { + "__id__": 134 + }, + "_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.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 129 + }, + "_enabled": true, + "__prefab": { + "__id__": 131 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 236, + "height": 100 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "GAeg+oaXOmXcM1D9QV4DRQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 129 + }, + "_enabled": true, + "__prefab": { + "__id__": 133 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "a42d050f-b023-4580-9444-36552fa546e3@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": "VvOWtroImcD8yLde74Sxzw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9Uf+bpSVaP4cKEvQ233JIg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit575", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 128 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 136 + }, + { + "__id__": 138 + } + ], + "_prefab": { + "__id__": 140 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -3.5, + "y": 2, + "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__": 135 + }, + "_enabled": true, + "__prefab": { + "__id__": 137 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 125, + "height": 40 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "uaidJwNt1lLnp8L8/Npmrw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 135 + }, + "_enabled": true, + "__prefab": { + "__id__": 139 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "a972c966-3dc9-4396-8935-d2583f5ed499@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": "RcNr/HXIyHHpimmz6SK1Rg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "VAhCMyZCv5jQMPpiDy2W/g", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 128 + }, + "_enabled": true, + "__prefab": { + "__id__": 142 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 236, + "height": 92 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "hvYo/Mtdm/NvB0CYc1139w" + }, + { + "__type__": "cc.Toggle", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 128 + }, + "_enabled": true, + "__prefab": { + "__id__": 144 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "checkEvents": [], + "_isChecked": false, + "_checkMark": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2q0azEjKkoPAWGBw3QYBGg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "8l9M6lvYTO4Nx3Gyc1EsdA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 91 + }, + "_enabled": true, + "__prefab": { + "__id__": 147 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 236, + "height": 276 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 1 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "3lFqMKNUAGfNxoAXVQJajg" + }, + { + "__type__": "cc.Layout", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 91 + }, + "_enabled": true, + "__prefab": { + "__id__": 149 + }, + "_resizeMode": 1, + "_layoutType": 2, + "_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": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "u5dBE7laDznZ2DwzBN+5IA" + }, + { + "__type__": "cc.ToggleContainer", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 91 + }, + "_enabled": true, + "__prefab": { + "__id__": 151 + }, + "_allowSwitchOff": false, + "checkEvents": [], + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "PkzVx1vsnlLgoqSwR3v4eg" + }, + { + "__type__": "fb24dXpumxPj6farY5YiKC2", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 91 + }, + "_enabled": true, + "__prefab": { + "__id__": 153 + }, + "tabs": [ + { + "__id__": 107 + }, + { + "__id__": 125 + }, + { + "__id__": 143 + } + ], + "backgrounds": [ + { + "__id__": 96 + }, + { + "__id__": 114 + }, + { + "__id__": 132 + } + ], + "titles": [ + { + "__id__": 102 + }, + { + "__id__": 120 + }, + { + "__id__": 138 + } + ], + "normalBackground": { + "__uuid__": "a42d050f-b023-4580-9444-36552fa546e3@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "selectedBackground": { + "__uuid__": "7e19cefc-cd45-46b7-bef9-3a0b01fee076@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "normalTitles": [ + { + "__uuid__": "87d01b20-33bc-45b6-99cc-d22412f6e82f@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "9fd0b3df-4b98-428c-b2d3-fe3b467321b9@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "a972c966-3dc9-4396-8935-d2583f5ed499@f9941", + "__expectedType__": "cc.SpriteFrame" + } + ], + "selectedTitles": [ + { + "__uuid__": "e17f3cb4-8509-4fc4-91bc-940d79100d28@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "5d1ce63f-bcbe-4827-a77a-6296a0b058a8@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "b0f58d48-14db-4347-a2e5-8486861bc772@f9941", + "__expectedType__": "cc.SpriteFrame" + } + ], + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "RIXmWOnIo461W/zXquQ9JA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "eeozh+gJoh/1Dw3dz7tUrg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 156 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "1zHqoWPpjKckaHrP3Mhx0A" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 158 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "yQQc9D2jGphQPfmLmHisig" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "gvRZfwHA7qVys1sMX4y7Ow", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 161 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "iBK6VooCS7eh117So8NH9A" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 163 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "GNcz3L0H0oizNjyEo2LoiA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "OQHEWV5xZ33jhw0n3rvtPg", + "instance": null, + "targetOverrides": null + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/NoticeBoard.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/NoticeBoard.prefab.meta new file mode 100644 index 0000000..be87c40 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/NoticeBoard.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "e7a05100-7de4-4935-896e-a59061377b87", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "NoticeBoard" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/OwnedRoomList.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/OwnedRoomList.prefab new file mode 100644 index 0000000..a4d6f09 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/OwnedRoomList.prefab @@ -0,0 +1,1002 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "OwnedRoomList", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "persistent": false + }, + { + "__type__": "cc.Node", + "_name": "OwnedRoomList", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [ + { + "__id__": 42 + }, + { + "__id__": 44 + }, + { + "__id__": 46 + } + ], + "_prefab": { + "__id__": 48 + }, + "_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": "86QtcZs6tBiKzKkP4KeA4P" + }, + { + "__type__": "cc.Node", + "_name": "group-98", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 11 + } + ], + "_active": true, + "_components": [ + { + "__id__": 37 + }, + { + "__id__": 39 + } + ], + "_prefab": { + "__id__": 41 + }, + "_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": "a4npETFN5H5ZsixLA1uuer" + }, + { + "__type__": "cc.Node", + "_name": "Spirit103", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + }, + { + "__id__": 6 + }, + { + "__id__": 8 + } + ], + "_prefab": { + "__id__": 10 + }, + "_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": "deUs4VXHFCWZVhhLa/O//g" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 5 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "d80FFb//RCOb3XUQ1PcG4X" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "/ANP2WZI+9LSPfCF4a+YXw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 7 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@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": "0e8tKqAINB/4T4RdIJkQ9D" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "N92ZL93BKA/RxUyIH/1+Sg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 9 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "285mHWvGVJ7oPTH7iS5gbK" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Cerx1AIl69bfqKyKEaTqkg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "t6nnuitm1DaBKL0b6a5hyA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "房间列表背景", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 12 + } + ], + "_active": true, + "_components": [ + { + "__id__": 28 + }, + { + "__id__": 30 + }, + { + "__id__": 32 + }, + { + "__id__": 34 + } + ], + "_prefab": { + "__id__": 36 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -660, + "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": "fc1YIn3eFHlqy5tSyiUcs+" + }, + { + "__type__": "cc.Node", + "_name": "RoomScrollView", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 11 + }, + "_children": [ + { + "__id__": 13 + } + ], + "_active": true, + "_components": [ + { + "__id__": 19 + }, + { + "__id__": 21 + }, + { + "__id__": 23 + }, + { + "__id__": 25 + } + ], + "_prefab": { + "__id__": 27 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": -22.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": "123Eo1vA1Ej7YVEtw49PnM" + }, + { + "__type__": "cc.Node", + "_name": "Content", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 12 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 14 + }, + { + "__id__": 16 + } + ], + "_prefab": { + "__id__": 18 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 232.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": "30itkUkmNI7ZFeS02wden9" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 13 + }, + "_enabled": true, + "__prefab": { + "__id__": 15 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 264, + "height": 0 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 1 + }, + "_id": "27i87ISy1DkoWWKIweGmi8" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "sm5VyF6W2+2X3BEvpHiicA" + }, + { + "__type__": "cc.Layout", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 13 + }, + "_enabled": true, + "__prefab": { + "__id__": 17 + }, + "_resizeMode": 1, + "_layoutType": 2, + "_cellSize": { + "__type__": "cc.Size", + "width": 40, + "height": 40 + }, + "_startAxis": 0, + "_paddingLeft": 0, + "_paddingRight": 0, + "_paddingTop": 0, + "_paddingBottom": 0, + "_spacingX": 0, + "_spacingY": 5, + "_verticalDirection": 1, + "_horizontalDirection": 0, + "_constraint": 0, + "_constraintNum": 2, + "_affectedByScale": false, + "_isAlign": false, + "_id": "dfQW6xI7RCs4/093KI3SwN" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "pDzfa0sVDspE7hpvla5oSg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "CqjwnB9y2RSfzZLOh02cPQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 12 + }, + "_enabled": true, + "__prefab": { + "__id__": 20 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 264, + "height": 465 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "48qped3PhCFLX3LXh/3SPb" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Rsh1WE+OAjz/ZiS9+aAx4Q" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 12 + }, + "_enabled": true, + "__prefab": { + "__id__": 22 + }, + "_alignFlags": 45, + "_target": null, + "_left": 8, + "_right": 8, + "_top": 55, + "_bottom": 10, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 264, + "_originalHeight": 465, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "b8VphA/PJO9YewJtkthMAr" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "D1L8Z7/QesavvIULAsupjw" + }, + { + "__type__": "cc.Mask", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 12 + }, + "_enabled": true, + "__prefab": { + "__id__": 24 + }, + "_type": 0, + "_inverted": false, + "_segments": 64, + "_alphaThreshold": 0.1, + "_id": "77MVkeVetK3JQitdyy3M7f" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "En/dZsf5jWIYaaX5KUvOxA" + }, + { + "__type__": "cc.ScrollView", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 12 + }, + "_enabled": true, + "__prefab": { + "__id__": 26 + }, + "bounceDuration": 1, + "brake": 0.5, + "elastic": true, + "inertia": true, + "horizontal": false, + "vertical": true, + "cancelInnerEvents": true, + "scrollEvents": [], + "_content": { + "__id__": 13 + }, + "_horizontalScrollBar": null, + "_verticalScrollBar": null, + "_id": "05O+813idECqgzqxctxD6k" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "smtKEKnBWmauuLBcFq9Pvg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9wgk6GNjQiFNDSahnr8UDQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 29 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 280, + "height": 530 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "0e/ZB15BdLgr5wo9Xi936E" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "CmE0GWPyaUTWgts+l8xqAw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 31 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "083b1c88-4376-4aea-b08c-12eed59f94df@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "19a/iAQD5Jz4U24fLLjb3J" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "KU3VIBuNCDCqMw0wzq4Kag" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 33 + }, + "_alignFlags": 8, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -599.891, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "bekLntEjJERK8S3itrxVvk" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "o9lcTQ8FOuLHZN4BtEprRw" + }, + { + "__type__": "cc.BlockInputEvents", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 35 + }, + "_id": "a8uICjTE9A65KMzhJiy0tI" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "DrMZJXfKubH+bd39EGXkFA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "xhv5NtjmtC6G6XFN7ZZUcA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 38 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "acGfKWdRdCQalfYhLRgpo2" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "lAp+9aK3i2T3D2ekVQCDgw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 40 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "b9HLmV/Y5Cfq/+FvPHcuBt" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "qyAZt2BMI3NOmF/ghWKDag" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "2TFkFBOdkCYAzbp5CPmFvw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 43 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "9dxA8OayRDGLv0P2CR5av2" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "5jyQyhdlADhSCm/MLPMPpg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 45 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "77Dkz6V/5EsISDnkxqOqbC" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "YdJ1LAdpANp7wtYSpNScOA" + }, + { + "__type__": "08da0myI7dJxqPcSJ+ZHNot", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 47 + }, + "itemPrefab": { + "__uuid__": "67117675-9f6d-4def-a67c-db0e9fb19b98", + "__expectedType__": "cc.Prefab" + }, + "content": { + "__id__": 13 + }, + "scroll": { + "__id__": 25 + }, + "_id": "35VX9kw8BEpIxyXUvrx09R" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "DDjIwnW02Botd3HpoXpZaQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "Srz6RS6L4sNpMPurmzLtwQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/OwnedRoomList.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/OwnedRoomList.prefab.meta new file mode 100644 index 0000000..e6b1db4 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/OwnedRoomList.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "260ea2dc-696c-4a65-a2fd-109dd794bb45", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "OwnedRoomList" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/QrCode.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/QrCode.prefab new file mode 100644 index 0000000..284ae49 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/QrCode.prefab @@ -0,0 +1,1161 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "QrCode", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "persistent": false + }, + { + "__type__": "cc.Node", + "_name": "QrCode", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [ + { + "__id__": 48 + }, + { + "__id__": 50 + } + ], + "_prefab": { + "__id__": 52 + }, + "_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": "37eQOfQtVGg7GsgQMcmK4A" + }, + { + "__type__": "cc.Node", + "_name": "group-42", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 11 + }, + { + "__id__": 19 + }, + { + "__id__": 27 + }, + { + "__id__": 35 + } + ], + "_active": true, + "_components": [ + { + "__id__": 43 + }, + { + "__id__": 45 + } + ], + "_prefab": { + "__id__": 47 + }, + "_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": "20OVWRCEpHYIsmGzjo9rET" + }, + { + "__type__": "cc.Node", + "_name": "二维码遮罩", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + }, + { + "__id__": 6 + }, + { + "__id__": 8 + } + ], + "_prefab": { + "__id__": 10 + }, + "_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": "47p+C5uq5KJq60s2lYk9qP" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 5 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "ae1l2Ew31HLKyH+hqVBDAN" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "hLXDF4cE2aUV18UQJRYy0g" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 7 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@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": "3f7/XhScdNjYQkkI9oh2uh" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "M0iOmSxsSR9QHQLiEnEnYQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 9 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "69K708ZOxFRInJAo6/LCHt" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "s8cuJhI4Wx2a5ImwTsO75w" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "sMLGjoJf3QkaOFFeV0+jOA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit342", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 12 + }, + { + "__id__": 14 + }, + { + "__id__": 16 + } + ], + "_prefab": { + "__id__": 18 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.5, + "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": "cdoVnpJppJ9Ju36LxzUusq" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 13 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 955, + "height": 640 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "f9r00bx5JAR4vuo0qeuSne" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "4rpeOVZDCsgyay2Gor6LpQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 15 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "dfdcc5f5-bf77-42a6-8e2e-f8462bc51edc@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "d77kD8mf5I3ILJH6gXJH84" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "vxJsE0CjqSbWQnZCL+2Ngw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 17 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "27TqERiaBGmqmuDJp9PSqA" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "YmKzcfYfgJ1My4LIg+CrgA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "qZTGcctpokOq9cGDS/qEoQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit343", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 20 + }, + { + "__id__": 22 + }, + { + "__id__": 24 + } + ], + "_prefab": { + "__id__": 26 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 115.5, + "y": -49.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": "ed5mLWCKpAN4hdwMjzymxR" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 21 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 321, + "height": 287 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "e3SrGQgr1AdqSEVnd/KP1d" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "R4iFyO+zMgpiBjVoi6oQMw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 23 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "06403876-e8f4-48fd-ad50-9bbefe0c5d0a@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": "580hot385Odri9PXhN5S8I" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "OKIjKmFwKxsOSUem3Scr5Q" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 25 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 115.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "a9hcMkaMZO1ppK9VQictOf" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "R8W7SyXTEPmD9SNwwuhUrQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "cMoUiFrz6z1kcjviOh4Rlg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit344", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 28 + }, + { + "__id__": 30 + }, + { + "__id__": 32 + } + ], + "_prefab": { + "__id__": 34 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -293.5, + "y": -476.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": "8fhNXi0CBKcIDYoOLCY9t0" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 29 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 371, + "height": 51 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "e1KF4ztFtMrpLrs2EOyBqU" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "lNb0lLoreT8aImzv4sDCnQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 31 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "0c0c1c72-96f5-4c54-a199-0df6189c762c@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": "68L6hxmPdHAqUGyeBR6o65" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "npV4AxMcjNLh4D7Vc+Ay7g" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 33 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -293.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "60btD2WjVL/7u4oGr9XG7W" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "TOK7fUmS/Z/WPrPYLASLdA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "r4w4kaa6HxdApLiM4khTiw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit345", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 36 + }, + { + "__id__": 38 + }, + { + "__id__": 40 + } + ], + "_prefab": { + "__id__": 42 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 115.5, + "y": -280, + "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": "19yg5JSV1DqJn5a51sXgz3" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 37 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 315, + "height": 74 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "f9RcBkPgxOW69Az540rDnB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "kUvCZr/G/BdWhQzv/gsA3w" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 39 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "b1efcc9a-2999-4c92-8d68-d7462c4bca2e@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": "2892YiHNBOSq7dBPimDL91" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "kR+L5R0tKUAv/nLbCCJUcQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 41 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 115.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "aeWhW2A4lBw5lFI99NJO1S" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "RClNEntCvmPfUiQZj14WCw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "A30hxDhkSreEJ/8UinByKg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 44 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "11pB+C7pFMIYXXNLmdo2ee" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "BUuD1uSweLcdl/QQgEBpmg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 46 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "dbvcgz2iREfJC8Bb/XwMqQ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "/QSwfTaX37aOexPNUHYKtg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3Dt3BHMakJFBNGeBg45bpw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 49 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "16xIl44yZMppdacX276+YF" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "7/3povXuZxdiD9o5JnUMGw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 51 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "69QWKq0BFA1a/b3tbTm9vb" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "6cquxyc7LGgiguwJuynUrA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "yE2WhSK9g+Q3InLmcyjB9w", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/QrCode.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/QrCode.prefab.meta new file mode 100644 index 0000000..82b7d81 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/QrCode.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "95ee9b79-8c30-4f54-a2cf-19d02083cd28", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "QrCode" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/RedeemCode.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/RedeemCode.prefab new file mode 100644 index 0000000..b512840 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/RedeemCode.prefab @@ -0,0 +1,1540 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "RedeemCode", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "persistent": false + }, + { + "__type__": "cc.Node", + "_name": "RedeemCode", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [ + { + "__id__": 64 + }, + { + "__id__": 66 + } + ], + "_prefab": { + "__id__": 68 + }, + "_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": "94cqrxny5CM4IOSRgit4iK" + }, + { + "__type__": "cc.Node", + "_name": "group-74", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 11 + }, + { + "__id__": 19 + }, + { + "__id__": 27 + }, + { + "__id__": 35 + }, + { + "__id__": 43 + }, + { + "__id__": 51 + } + ], + "_active": true, + "_components": [ + { + "__id__": 59 + }, + { + "__id__": 61 + } + ], + "_prefab": { + "__id__": 63 + }, + "_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": "4fyihXsrtFEYpLNs6Oimry" + }, + { + "__type__": "cc.Node", + "_name": "反馈遮罩", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + }, + { + "__id__": 6 + }, + { + "__id__": 8 + } + ], + "_prefab": { + "__id__": 10 + }, + "_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": "dfNQEkx3RCCL9Gx9ifY2Zl" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 5 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "abGkupsV5C45D92hIYoy/M" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "YscqaYNchm1T/B6inwRagA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 7 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "eabf148a-fa6a-460a-be4f-8e586eb80e07@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": "e9/1VSAUZLSJMUeMngxsoy" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Wo19k4K1RNmLtcdcxexlvw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 9 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "feJHHvBbJCUrqxtIm8n4ls" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ZvhXmWfcbFn/QMNSmUhJEA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "oAW0raljLtxUBTM85qZD3g", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "反馈背景", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 12 + }, + { + "__id__": 14 + }, + { + "__id__": 16 + } + ], + "_prefab": { + "__id__": 18 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 218, + "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": "8f6PyvhZpESIAbzmNMlWrH" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 13 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1200, + "height": 200 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "2fBYa5UfhOxrimJIwNzDkq" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "w0zyUKVvjw6tnChrbUxysw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 15 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "3e7492cb-7ae1-4f56-806d-07096662d8fb@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "132DX6bRtLu6T2TEU+wWNS" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "L48XjYdcaFPZ9BT2WMxm5Q" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 17 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "87GDx56eJNc5Smk/oR+5Sp" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "pnuZM22SIKyeh4pV0tjtRw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "MDgmkp77gPFmIAFUV/BvqQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "反馈提交", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 20 + }, + { + "__id__": 22 + }, + { + "__id__": 24 + } + ], + "_prefab": { + "__id__": 26 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 497.5, + "y": 214, + "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": "b9HPp1HLBH3IQtHozk3mIF" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 21 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 135, + "height": 80 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "51iI0dIQ9OyIIuI2Jhl0K1" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "4W5MZpnBamyovbJSEmJvqg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 23 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "6bc626b8-d480-4ba8-92ac-0f95e21761af@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": "0b3XPh9ONHu5aUzyRr7aGa" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "MybaoUgQ7PMmKxqUcaPQcQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 25 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 497.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "25AcUBSyZJiIdwlhI0cNvo" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "0qEVumFDZKthLb+YS5mawg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "WlvuuJSTb/uWRKgrYBzf2g", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "反馈输入内容", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 28 + }, + { + "__id__": 30 + }, + { + "__id__": 32 + } + ], + "_prefab": { + "__id__": 34 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -77, + "y": 214, + "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": "e5E6Cumw9FoIATFd3Iqfgr" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 29 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 0, + "height": 25.2 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "fbqpIBrf9LD7C55k5KgAjI" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "rLI3tpZfDlefKjINlq8g7w" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 31 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_string": "", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 21.083333333333336, + "_fontSize": 20, + "_fontFamily": "Arial", + "_lineHeight": 20, + "_overflow": 0, + "_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": "28cc1vY7VAmplx30ONTZK9" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "l+OIJkcbIWkJDrQCgl6Lrg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 33 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -77, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "90F8ipeSpD6rj0pnFLCuvZ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "mV65hNTkxmw6GixltCj76g" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "gosxyT8W1w6vAHhTLg+62g", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3266", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 36 + }, + { + "__id__": 38 + }, + { + "__id__": 40 + } + ], + "_prefab": { + "__id__": 42 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 494.5, + "y": 176, + "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": "85Js4D84pH2qu/andCSq7o" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 37 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 135, + "height": 80 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "6eVKiYNmZODYMxw44nATKW" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "N/KwJNt3gX6BNmGM4Gw7CA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 39 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "a933d0bf-d0bc-43e0-b475-0d9d8a16262b@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": "fanplPeWVDMZ0kBp9ZUYoj" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "fpjpw/Bz4RBXeBRNlCh8bQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 41 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 494.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "fepT6q5INMFqbIsCf9cVdu" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "+tsXYPWsPfGXB9LEvaJRtw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "7CypavivZinRbFzlMLcQwg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3265", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 44 + }, + { + "__id__": 46 + }, + { + "__id__": 48 + } + ], + "_prefab": { + "__id__": 50 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 494.5, + "y": 258, + "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": "cegzIigP1Eor9ASQ452fPF" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 45 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 135, + "height": 80 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "66fsfmmRpO7JW1sZEXvcMs" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "xmNswzQ7wRuZWW104zSUCg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 47 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "fdc6b65e-e526-49d2-aa05-eb4d891174bb@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": "ff8oRGczlIVZ0QPCidoaDo" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "8W4AaFvDFVgDreFNt5REkw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 49 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 494.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "e2icrFmLZK7I5lPaYHdaVT" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "7t1DOD5+2n0/J4etHCU4aQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "v/15z+5HscpifG9yw+IAOQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "反馈标题", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 52 + }, + { + "__id__": 54 + }, + { + "__id__": 56 + } + ], + "_prefab": { + "__id__": 58 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 23.5, + "y": 483, + "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": "62TdZM2JlG0Y93ETTFbkzT" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 51 + }, + "_enabled": true, + "__prefab": { + "__id__": 53 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 181, + "height": 70 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "5d/5g33dVJk7Ar2I4Vzbo0" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "s42MQuAjtRUdHqoBKDCipw" + }, + { + "__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__": "02d0a481-5261-4d50-ad04-2695547afe37@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": "14LbCvASREW71GxHSZdjqO" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "THDyGya8gxZ6tulavJTCVg" + }, + { + "__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": 23.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "dcO7GlD91B/Z0mBCtKkVHW" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "H1kXX/1jMy3BURvsHQ2tQA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "1qjeHcqZTPoZ1U/XYFtVHA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 60 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "e3ORjF3rtPvb7FcA9os/C0" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "YW3d68yAP+RGqfLKokZP+A" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 62 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "4dV7CZ9nBGF7DFIjcGy3PQ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "n+xIY+Z6X/RBZtjVwq2vIg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9MbWsE4D9mvr28wYz4TW3w", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 65 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "a5UefiOnBMNqCvvLgjVqco" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Om3SCPuFM3WMxyD5ihS40A" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 67 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "14nbXCGm1OOYgt/26e+S0t" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "78ybgnW1Sjaf7jse9Pb+WQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "QKtac/wR7kXoIwqx8F30XQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/RedeemCode.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/RedeemCode.prefab.meta new file mode 100644 index 0000000..f182f08 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/RedeemCode.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "ada34847-93c7-48f2-bb0a-5c1db4b112f6", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "RedeemCode" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/ResourceLoading.prefab similarity index 82% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/ResourceLoading.prefab index 0403efb..a326d41 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/ResourceLoading.prefab @@ -1,7 +1,7 @@ [ { "__type__": "cc.Prefab", - "_name": "Layer1_Logo", + "_name": "ResourceLoading", "_objFlags": 0, "__editorExtras__": {}, "_native": "", @@ -13,22 +13,19 @@ }, { "__type__": "cc.Node", - "_name": "Layer1_Logo", + "_name": "ResourceLoading", "_objFlags": 0, "__editorExtras__": {}, "_parent": null, "_children": [ { "__id__": 2 - }, - { - "__id__": 50 } ], "_active": true, "_components": [], "_prefab": { - "__id__": 60 + "__id__": 50 }, "_lpos": { "__type__": "cc.Vec3", @@ -1130,241 +1127,6 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, - { - "__type__": "cc.Node", - "_name": "group-32", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 51 - } - ], - "_active": true, - "_components": [], - "_prefab": { - "__id__": 59 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.Node", - "_name": "Spirit221", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 50 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 52 - }, - { - "__id__": 54 - }, - { - "__id__": 56 - } - ], - "_prefab": { - "__id__": 58 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": -1612.5, - "y": 1099.952, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 51 - }, - "_enabled": true, - "__prefab": { - "__id__": 53 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 63, - "height": 63 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "Y8tq/cDuhVvttPn+x9+bww" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 51 - }, - "_enabled": true, - "__prefab": { - "__id__": 55 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "974bf7e8-69fd-49ff-95fa-ed7ddb8d6a14@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 0, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "t+vRkZQc/kDtmwdvT1STKA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 51 - }, - "_enabled": true, - "__prefab": { - "__id__": 57 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -1612.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "K2+PudUKVOrBYR09Lp3sAA" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "J1IKV6k3EB5NkpQVaZVxiA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "JccA+NmPM7lVqq5kAuMdjQ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, { "__type__": "cc.PrefabInfo", "root": { diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/ResourceLoading.prefab.meta similarity index 82% rename from cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab.meta rename to cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/ResourceLoading.prefab.meta index 6d4eeb7..b9bf1ad 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/ResourceLoading.prefab.meta @@ -8,6 +8,6 @@ ], "subMetas": {}, "userData": { - "syncNodeName": "Layer1_Logo" + "syncNodeName": "ResourceLoading" } } diff --git a/cocoscreator_projects/YouleNexus/assets/games.meta b/cocoscreator_projects/YouleNexus/assets/games.meta new file mode 100644 index 0000000..43722f7 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "bfaf200b-f8f5-4fcb-b9e3-7e4e831177ce", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/games/template.meta b/cocoscreator_projects/YouleNexus/assets/games/template.meta new file mode 100644 index 0000000..ec2f48a --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "c593ddf6-0173-48e8-96cb-dab1273e43d8", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/games/template/TemplateCreateRoomView.ts b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateCreateRoomView.ts new file mode 100644 index 0000000..8800406 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateCreateRoomView.ts @@ -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();} +} diff --git a/cocoscreator_projects/YouleNexus/assets/games/template/TemplateCreateRoomView.ts.meta b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateCreateRoomView.ts.meta new file mode 100644 index 0000000..b274c45 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateCreateRoomView.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "ebbef7d9-7bda-4c61-9b0a-1ee56e7817fb", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoom.prefab b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoom.prefab new file mode 100644 index 0000000..f115219 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoom.prefab @@ -0,0 +1,3213 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "TemplateRoom", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "persistent": false + }, + { + "__type__": "cc.Node", + "_name": "TemplateRoom", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + }, + { + "__id__": 96 + }, + { + "__id__": 102 + }, + { + "__id__": 108 + }, + { + "__id__": 114 + }, + { + "__id__": 122 + } + ], + "_active": true, + "_components": [ + { + "__id__": 130 + }, + { + "__id__": 132 + }, + { + "__id__": 134 + } + ], + "_prefab": { + "__id__": 136 + }, + "_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": "69YMwMFQFDvqf2TlyrtDDs" + }, + { + "__type__": "cc.Node", + "_name": "group-21", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 11 + }, + { + "__id__": 19 + }, + { + "__id__": 27 + }, + { + "__id__": 35 + }, + { + "__id__": 43 + }, + { + "__id__": 51 + }, + { + "__id__": 59 + }, + { + "__id__": 67 + }, + { + "__id__": 75 + }, + { + "__id__": 83 + } + ], + "_active": true, + "_components": [ + { + "__id__": 91 + }, + { + "__id__": 93 + } + ], + "_prefab": { + "__id__": 95 + }, + "_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": "5fuURzcehGZ6GghP+++fBn" + }, + { + "__type__": "cc.Node", + "_name": "Spirit149", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + }, + { + "__id__": 6 + }, + { + "__id__": 8 + } + ], + "_prefab": { + "__id__": 10 + }, + "_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": "06Tp5yKrlJoZ5wt6CvwXqb" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 5 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "5acXOx5s5PmovsOKuNJ3B9" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "+/OS2Q/5UoIklTLGOJ/zIg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 7 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "e1aa83b5-47de-4593-997d-debc3ceb2557@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": "15PrmKcLNO/Kz0CGc7o3YT" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "gbQfzNHcExfp5uZV/RIiYw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 9 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "b7M11G2hhOlZnuLJJUQSH0" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "dVFWlhsHlkDr9aUbmrpK9Q" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "S1+WR/nWxrU+TKt9e6badw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit284", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 12 + }, + { + "__id__": 14 + }, + { + "__id__": 16 + } + ], + "_prefab": { + "__id__": 18 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -420.5, + "y": 325, + "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": "cffAfcIrxDM58ln4+Uu8uK" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 13 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 247, + "height": 62 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "2fjYfhq5VAa4UD8Nvb4cAJ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "oSsbqOK1MzMKZOpalrCVmA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 15 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "64bfd3fc-4394-4c36-a9c2-aa7d3f50c5c7@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "9afKtWVSRF559J0xIJ9qzc" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "sb5h5noP7NZhcXdOIeZsLQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 17 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -420.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "57Qfkw8qlOfK4BWFiBbLsB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "0Fk6gA9/VAIgPsmnXFhbkw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "Gco1xZrPCkQ1cuSuVdRNWw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit156", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 20 + }, + { + "__id__": 22 + }, + { + "__id__": 24 + } + ], + "_prefab": { + "__id__": 26 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -397, + "y": 325, + "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": "b89rDhkwtI8aNb47scSHW2" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 21 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 4, + "height": 18 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "7ee4GzcI9PQYsALEp5WrzU" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "U0m8CXIsoMsiPtxy8h/M0A" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 23 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "7d193c00-a7ee-4d75-956c-8dd6f6300b56@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": "d17QQ4XjNJGKzrBaZseX9C" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "wwovzjTTxkmq/E6QdBXaxg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": { + "__id__": 25 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -397, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "f7GYNGk6lHIqWjrxXl0MOY" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "MnX2shseBow3CAeD6Rfeug" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "admM8nqjcjUOGv1RkCdtNg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit154", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 28 + }, + { + "__id__": 30 + }, + { + "__id__": 32 + } + ], + "_prefab": { + "__id__": 34 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -420, + "y": 325, + "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": "935o9iYrFD6JJQNIh2KCxt" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 29 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 30, + "height": 20 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "bbV3wvDwhNNI/zHyNXStev" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "bV0lE9c61pv4s7B2zBGRkg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 31 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "55fc2d29-a269-4025-b25a-afe69d92d238@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": "35E7oEwg5Cl6aBpY4iOCaS" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "13kxlhvAoyXgn0+iQoJa+g" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 33 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -420, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "1fIH9SNnVIib2AR7TBw3je" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "+XP8ISANORargmYAqB2+GQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "T/e4p03YqdohRS7CyoSCnw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit155", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 36 + }, + { + "__id__": 38 + }, + { + "__id__": 40 + } + ], + "_prefab": { + "__id__": 42 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -374, + "y": 325, + "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": "33zitfGlNA4JpcAH5t6cvY" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 37 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 30, + "height": 20 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "d22dGvDW1JZJe0EJ6d78SZ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "1Wj7Z8sK+q0ybMftqC/hkw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 39 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "55fc2d29-a269-4025-b25a-afe69d92d238@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": "02KjgDyO1MtYi6v9gQWAC2" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "FCrFK8ho4U6JYb9VIv23gw" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 35 + }, + "_enabled": true, + "__prefab": { + "__id__": 41 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -374, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "f18h8naktJoqlJE/IzI3mB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "vuURkFalHDZgNecJvx6Kfg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "cBG/Bu457UUI+9ULAn/jgA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit336", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 44 + }, + { + "__id__": 46 + }, + { + "__id__": 48 + } + ], + "_prefab": { + "__id__": 50 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -510.5, + "y": 325, + "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": "f77jtoStNFLrDpDahD7GCa" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 45 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 29, + "height": 22 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "7bTwI+PctKQJ/rvtj4u/6u" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "MVSydp6WW3pGoEAv/jdEZg" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 47 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "1ef090dc-6a2b-4a1d-b99a-8f84f04e0aa6@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": "a0RtFpYUpIXLhujD7nwkkS" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "KzthoIrgIgkMK30qBfohUg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 43 + }, + "_enabled": true, + "__prefab": { + "__id__": 49 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -510.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "b8N5tBQ7dIqJpgpmMOrIOL" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "JckeQ+kQcm1i1SyyCMTy3Q" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "LxlmMgNnM/QEVEsLTWG9CA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit337", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 52 + }, + { + "__id__": 54 + }, + { + "__id__": 56 + } + ], + "_prefab": { + "__id__": 58 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -463.5, + "y": 324.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": "df+Mldi/lIKKrp89h5F5B9" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 51 + }, + "_enabled": true, + "__prefab": { + "__id__": 53 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 49, + "height": 19 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "55ipflgUpHL64aUV8m5Wb1" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "eJyT4PqjR3EbMUlo1x4Glg" + }, + { + "__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__": "7e671255-a374-47e4-9264-fde41226b020@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": "4agfgSW1JDXqJoZMuBne8s" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "mGH0Dhz7Q22Ktsra/D7sdw" + }, + { + "__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": -463.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "f0yvOZqdFHXb0TrrhsQVRM" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "QVvC3+gj+32RilZh7Ng0cw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ABSkg199TlMA4rjluQtDww", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit338", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 60 + }, + { + "__id__": 62 + }, + { + "__id__": 64 + } + ], + "_prefab": { + "__id__": 66 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -463.5, + "y": 324.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": "c6e5GxaU9K+YFWixUpJdkq" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 59 + }, + "_enabled": true, + "__prefab": { + "__id__": 61 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 49, + "height": 19 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "89zX3Fzf9NDrixvINkpLEK" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "nDT2OyC8Xpqp0vWZ3bu5zQ" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 59 + }, + "_enabled": true, + "__prefab": { + "__id__": 63 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "7e671255-a374-47e4-9264-fde41226b020@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": "34hHD2qkhJLrcEn8yrhOeX" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Wn4YtOQll/jKF9AncBJJYQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 59 + }, + "_enabled": true, + "__prefab": { + "__id__": 65 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -463.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "c6ZpFhB0FM7K3yOeAQPkd3" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "bnXEnF+un7isafBgleKJ4A" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "vn9+QABegRBwXQr/kCfBlg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3141", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 68 + }, + { + "__id__": 70 + }, + { + "__id__": 72 + } + ], + "_prefab": { + "__id__": 74 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -411, + "y": 324.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": "55NSk3+TtKv65duKxn971O" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 67 + }, + "_enabled": true, + "__prefab": { + "__id__": 69 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 102, + "height": 21 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "2dgmCq5shDUIo2eoObyQYp" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "yUds9wr8D+O2b7CKsleKjA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 67 + }, + "_enabled": true, + "__prefab": { + "__id__": 71 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "7d88dcff-d1ce-4a0a-b24f-2f861d94b613@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": "b5fGFUD3pAeLpT6EQMczCT" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "5MsjYhHL7FteBj5qke17vg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 67 + }, + "_enabled": true, + "__prefab": { + "__id__": 73 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -411, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "a6EJfOvEJBdZz4OQSTpF/e" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2YeC26lDBBPE6REux647VQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "Fuhd2vJNnyPkcaejkcPRlA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit3142", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 76 + }, + { + "__id__": 78 + }, + { + "__id__": 80 + } + ], + "_prefab": { + "__id__": 82 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -494.5, + "y": 324.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": "1e6S8MHeJOYo1bqoHjoB3C" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 75 + }, + "_enabled": true, + "__prefab": { + "__id__": 77 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 59, + "height": 25 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "0bWdjNt0xP6JsR26f6aUyf" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "/HY/M9fzFW6lReD/VfQQwA" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 75 + }, + "_enabled": true, + "__prefab": { + "__id__": 79 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "13a32880-9854-4d79-b292-6cf00516d88c@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": "f6OOkA31ZNkodYxbgZVZsP" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "rLqYVgmgoDMh2Doc72tV2g" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 75 + }, + "_enabled": true, + "__prefab": { + "__id__": 81 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -494.5, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "6bbQyySrlHrK/R44BIrhkF" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "NY99JMBpZHMGikhYvu5yEw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3g78c8kJkzL2fcHpniotMg", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Spirit291", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 84 + }, + { + "__id__": 86 + }, + { + "__id__": 88 + } + ], + "_prefab": { + "__id__": 90 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -393, + "y": 418, + "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": "021/ZdfFpJRaLXAAiFOs1/" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 83 + }, + "_enabled": true, + "__prefab": { + "__id__": 85 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 63.1484375, + "height": 20.16 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "d2A6mM9hRGxqt9YspV+xkq" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "RQJOPB59fbHZnehrAtWZmQ" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 83 + }, + "_enabled": true, + "__prefab": { + "__id__": 87 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_string": "Spirit291", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 16, + "_fontSize": 16, + "_fontFamily": "Arial", + "_lineHeight": 16, + "_overflow": 0, + "_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": "46IzPrQgRDNqKiMkNrARKx" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "lGRKzMXVoFFNnMdBuggXrg" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 83 + }, + "_enabled": true, + "__prefab": { + "__id__": 89 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": -393, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "d7WWfBRwFHSJE+IL2JURXi" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "JAT5Jb7MTXhkPl3sFjbCSg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "A18tvmMgq6q8naHLDnt7rw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 92 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "ca/4U/c+ZBPLaRZ6RJtY96" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "PfkOe8y/X4vIsuat1pGx/g" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 94 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "039j+TOBpJSrlil5tifZAU" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "uPENW91nI2nKWmCBPiHlkQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "uC/MZwX5BdwMBtuzLPazlA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "RoomCode", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 97 + }, + { + "__id__": 99 + } + ], + "_prefab": { + "__id__": 101 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 275, + "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": "1a8b/KWSRMpZbrIiPI19t1" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 96 + }, + "_enabled": true, + "__prefab": { + "__id__": 98 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1000, + "height": 60 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "cbjzNpkvVADbTbhIoRZa0r" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "r9x9J3mL+CVOwhTiCaYmfg" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 96 + }, + "_enabled": true, + "__prefab": { + "__id__": 100 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "房间号", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 0, + "_fontSize": 36, + "_fontFamily": "Arial", + "_lineHeight": 46, + "_overflow": 0, + "_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": "f430E0/uFF9LhYDizDnBcB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "tBe/Bn3ixYiKlKPv/pDghw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "1RkDYUJPJJr1TnKroONltw", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Players", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 103 + }, + { + "__id__": 105 + } + ], + "_prefab": { + "__id__": 107 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 10, + "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": "14+V7bN7lGl7hMbiBPL/BP" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 102 + }, + "_enabled": true, + "__prefab": { + "__id__": 104 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1000, + "height": 440 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "287wYjuvtGhYqxEInn2/10" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "XyWBBCPQbcVslNemw17r0Q" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 102 + }, + "_enabled": true, + "__prefab": { + "__id__": 106 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "等待玩家加入", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 0, + "_fontSize": 28, + "_fontFamily": "Arial", + "_lineHeight": 38, + "_overflow": 0, + "_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": "54hAwHzHFOPbFzmNDPi8Sm" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Z4yVp4FXEkjdPNeAzc2xxA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "rVuBgMFG0Q9Qm28uzilqyA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "TemplateTitle", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 109 + }, + { + "__id__": 111 + } + ], + "_prefab": { + "__id__": 113 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 325, + "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": "17O7ah869OprguY8duvZCs" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 108 + }, + "_enabled": true, + "__prefab": { + "__id__": 110 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1000, + "height": 60 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "cfJj3cF+tPiL9xFdON6ZPZ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "JLkek99QSP8qQ1M17+6plQ" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 108 + }, + "_enabled": true, + "__prefab": { + "__id__": 112 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "友乐 · 房间", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 0, + "_fontSize": 24, + "_fontFamily": "Arial", + "_lineHeight": 34, + "_overflow": 0, + "_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": "04IhJATyxKdLddCCNG4DLr" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "uYZlHY8MzAvlet8FBL6t8g" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "74Kl2zT/VHw5WBzmX1hycQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Prepare", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 115 + }, + { + "__id__": 117 + }, + { + "__id__": 119 + } + ], + "_prefab": { + "__id__": 121 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -160, + "y": -285, + "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": "94NPIywmNNtZc+b7DBj49v" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 114 + }, + "_enabled": true, + "__prefab": { + "__id__": 116 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 230, + "height": 60 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "90P1oIlpxB/Iwd3CsYOLeY" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "bY9UYIxt3EhPJ1APYyOHQQ" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 114 + }, + "_enabled": true, + "__prefab": { + "__id__": 118 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "准备", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 0, + "_fontSize": 32, + "_fontFamily": "Arial", + "_lineHeight": 42, + "_overflow": 0, + "_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": "38L/NCFHdNkZ+jMs7x/npl" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "1ejIkIFqkv9siBskETj0tA" + }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 114 + }, + "_enabled": true, + "__prefab": { + "__id__": 120 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "a1dAlGn4xC1KrlrJAnCLPd" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "iA6xXljY+JP2RIwsXQjcrQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "CqH/MExKSIHiVxxGJORp0g", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.Node", + "_name": "Exit", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 123 + }, + { + "__id__": 125 + }, + { + "__id__": 127 + } + ], + "_prefab": { + "__id__": 129 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 160, + "y": -285, + "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": "25Aw4xmV9Dzr4xg7IMSkvD" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 122 + }, + "_enabled": true, + "__prefab": { + "__id__": 124 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 230, + "height": 60 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "6122Lk8JZG66FL8znx0HPp" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "sPsk+U69zaZW7CB/GWgecg" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 122 + }, + "_enabled": true, + "__prefab": { + "__id__": 126 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_string": "退出房间", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 0, + "_fontSize": 32, + "_fontFamily": "Arial", + "_lineHeight": 42, + "_overflow": 0, + "_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": "fcST5VgbJIvL3v4Pm7IZe6" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "fcmyviUOwGgGicB0501SjA" + }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 122 + }, + "_enabled": true, + "__prefab": { + "__id__": 128 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 0, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "d95LanHm5MrrlVjyOdBiZO" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "fe9DN7eJrx5YwwutpIrOXg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "Psop/BIz+vuFXzq/hVeO/g", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 131 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "16dhyQb3JAvalZLCnc2Gdl" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "tP6YI1fkRzHF/q0WgcUqrA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 133 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 1600, + "_originalHeight": 720, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "a7Jws3EOBEW7J35Dqz0Rem" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Rr2YAkAh4IcIqJHpFoRypg" + }, + { + "__type__": "e0dc1+IpxlLGZgnLPnkRSvp", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 135 + }, + "roomLabel": { + "__id__": 99 + }, + "playersLabel": { + "__id__": 105 + }, + "prepareButton": { + "__id__": 119 + }, + "exitButton": { + "__id__": 127 + }, + "_id": "e64rp8hu5LI5LLUjEud6Qw" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "PYELr83gfb8MDqgyC2SBvA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ekOJZ8eJJnq2aqdQF+QM8A", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoom.prefab.meta b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoom.prefab.meta new file mode 100644 index 0000000..dfae088 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoom.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.50", + "importer": "prefab", + "imported": true, + "uuid": "f4582832-23e2-4981-be26-08dfe832aa96", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "TemplateRoom" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoomView.ts b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoomView.ts new file mode 100644 index 0000000..ed2f586 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoomView.ts @@ -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|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();} +} diff --git a/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoomView.ts.meta b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoomView.ts.meta new file mode 100644 index 0000000..dafb05f --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template/TemplateRoomView.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "e0dc1f88-a719-4b19-9827-2cf9e4452be9", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/games/template/template-game.ts b/cocoscreator_projects/YouleNexus/assets/games/template/template-game.ts new file mode 100644 index 0000000..1d6d2a1 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template/template-game.ts @@ -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;}}; +} diff --git a/cocoscreator_projects/YouleNexus/assets/games/template/template-game.ts.meta b/cocoscreator_projects/YouleNexus/assets/games/template/template-game.ts.meta new file mode 100644 index 0000000..f5bd0cf --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template/template-game.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "c6a24a57-c9c3-4eda-9f82-73f1990703fb", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/games/template/template-room-rules.ts b/cocoscreator_projects/YouleNexus/assets/games/template/template-room-rules.ts new file mode 100644 index 0000000..e6e19ca --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template/template-room-rules.ts @@ -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; + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/games/template/template-room-rules.ts.meta b/cocoscreator_projects/YouleNexus/assets/games/template/template-room-rules.ts.meta new file mode 100644 index 0000000..446acc1 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/games/template/template-room-rules.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "0cad77f9-c126-4532-9ffb-70e650008a34", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt b/cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt index df73461..35792ef 100644 --- a/cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt +++ b/cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt @@ -1,1500 +1,1500 @@ -{ - "showmessage": "", - "visitor_server_tcp": "115.159.146.131:51088", - "visitor_server_http": "115.159.146.131:51089", - "isclose": 1, - "scrollmsg": "官方郑重声明:近期有不法之徒以片面的游戏得分数据欺骗玩家购买外挂,已有玩家上当受骗,在此官方郑重声明,本游戏绝无外挂,凡自称有外挂的都是骗子,切勿上当受骗!", - "noticemsg": "", - "menunotice": { - "advert0": { - "type": 0, - "imgUrl": "http://tsgames.daoqi88.cn/image/adver0_youle2.png", - "imgAdUrl": "", - "height": 470, - "msg": "", - "url": "" - }, - "advert1": { - "msg": "", - "url": "" - }, - "advert2": { - "msg": "", - "url": "" - } - }, - "Ads": "本游戏仅为娱乐休闲使用,房卡所有#游戏通用", - "roomCardName": "房卡", - "nameImgFrame_1": 1, - "starName": "金币", - "nameImgFrame_2": 2, - "shieldShowRebate": 1, - "WechatPublicNo_ewm": "", - "behaviorList": [ - [ - 3233, - "rankList" - ], - [ - 604, - "rankList" - ], - [ - 76, - "ewm" - ], - [ - 3232, - "vipRank" - ], - [ - 3231, - "vipRank" - ], - [ - 679, - "match" - ], - [ - 1719, - "match" - ] - ], - "managerUrl": "http://dlweb.daoqi88.cn/white_list.html", - "service_qq": "", - "rankList": 0, - "wareHouse": 0, - "betaText": "Beta", - "ramSeat": 1, - "payCSS": "", - "payJS": "", - "cdKey": 2, - "agentlist": [ - { - "agentid": "veRa0qrBf0df2K1G4de2tgfmVxB2jxpv", - "agentname": "进贤", - "scrollmsg": "聚友棋牌仅限娱乐,禁止赌博!一经发现立即封号。客服号改为:jyqp181999", - "showmessage": "", - "xxroomdeduct": 1, - "audio_domain":"gameaudio.daoqi88.cn", - "audio_bucket":"gameaudio", - "html_buyroomcard": 2, - "html_applysales": 2, - "sale_wechat": "jyqp181999", - "agentmode": 2, - "WechatPublicNo_ewm": "http://tsgames.daoqi88.cn/image_ewm/ewm_jinxian2.png", - "service_wechat": "jyqp181999", - "service_tel": "", - "url_topup": "http://dlwebv3/wan_purchase_new.html", - "url_download": "http://sharegame/gameshare.html", - "logimage2": "http://op0g0jeqa.bkt.clouddn.com/log_jinxian.png", - "h5shareurl": "http://h5game1/gameLobby920/index2.html", - "game_share_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", - "shareh5title": "进贤聚友棋牌", - "shareh5desc": "一起来嗨~", - "gameNoticeShow": 0, - "shareType":1, - "shareImgUrl":"https://tsgames.daoqi88.cn/image/match.png", - "hallNoticeShow": 0, - "gameHallConfig": { - "topImgUrl": "http://tsgames.daoqi88.cn/image_xgamehall/gamehalllogo.png" - }, - "menunotice": { - "advert0": { - "type": 0, - "imgUrl": "http://tsgames.daoqi88.cn/image/adver0_jinxian2.png", - "imgAdUrl": "", - "height": 470, - "msg": "进贤本地玩法8年老平台##绿色平台,仅供娱乐!绝无外挂!##客服微信:jyqp181999", - "url": "" - } - }, - "rebateConfig": { - "from": 3, - "value": 0, - "rebateCharm": 1 - }, - "gamelist": [{ - "gameid": "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO", - "gamename": "进贤聚友", - "game_server_tcp": "8.139.255.236:56072", - "game_server_http": "8.139.255.236:56073", - "game_version": 261, - "game_zip": "http://tsgames.daoqi88.cn/zip2/gamehall.zip", - "game_size": "4.61M", - "game_hall_dir": "gamehall", - "game_down_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", - "game_down_memo": "进贤聚友,让欢乐无限分享", - "hideSpid": [ - 478 - ], - - "channellist": [{ - "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", - "channelname": "进贤", - "ios_defdownload_marketid": 2, - "and_defdownload_marketid": 3, - "game_version": 0, - "game_zip": "", - "game_size": "", - "marketlist": [{ - "marketid": 1, - "marketname": "App Store", - "app_version": 1, - "app_download": "", - "app_size": "", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 2, - "marketname": "苹果企业签", - "app_version": 44, - "app_download": "itms-services://?action=download-manifest&url=https://daoqi.daoqi888.cn/plist/gamehall_jinxianv2.plist", - "app_size": "2.61M", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 3, - "marketname": "安卓", - "app_version": 50, - "app_download": "https://daoqi.daoqi888.cn/apk/gamehall_jinxianv2.apk", - "app_size": "6.61M" - }, - { - "marketid": 4, - "marketname": "鸿蒙", - "deviceLogin":true, - "app_version": 2, - "app_download": "https://daoqi.daoqi888.cn/harmonyos/gamehall_jinxian_harmonyosv2.apk", - "app_size": "6.61M" - } - ] - }] - }, - { - "gameid": "a52y0ajd106pKU1KIvF1zRii8h1gRem5", - "gamename": "三个老K", - "sharename": "【三个老K】", - "category": 1, - "game_server_tcp": "8.139.255.236:56084", - "game_server_http": "8.139.255.236:56085", - "game_version": 155, - "game_zip": "http://tsgames.daoqi88.cn/zip2/sangelaok.zip", - "game_size": "9.07M", - "game_hall_dir": "sangelaok", - "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/3glaoK.png", - "channellist": [{ - "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", - "channelname": "进贤", - "ios_defdownload_marketid": 2, - "and_defdownload_marketid": 3, - "game_version": 0, - "game_zip": "", - "game_size": "", - "marketlist": [{ - "marketid": 2, - "marketname": "苹果企业签", - "app_version": 0, - "app_download": "", - "app_size": "", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 3, - "marketname": "安卓", - "app_version": 0, - "app_download": "", - "app_size": "" - }, - { - "marketid": 4, - "marketname": "鸿蒙", - "deviceLogin":true, - "app_version": 1, - "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", - "app_size": "6.61M" - } - ] - }] - }, - { - "gameid": "rZDp0wfvC0ZaPT5iLqh2itGFsnJaDvzc", - "gamename": "进贤麻将", - "category": 2, - "game_server_tcp": "8.139.255.236:56056", - "game_server_http": "8.139.255.236:56057", - "sharename": "【进贤麻将】", - "game_version": 41, - "game_zip": "http://tsgames.daoqi88.cn/zip2/majiang_jx.zip", - "game_size": "10.59M", - "game_hall_dir": "majiang_jx", - "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/mj_jx.png", - "haverobot": 1, - "game_config": { - "test": 1, - "asetcount": [ - 8, - 16 - ], - "needroomcard": [ - [ - 1, - 3, - 3, - 3 - ], - [ - 2, - 6, - 6, - 6 - ] - ], - "needroomcard_joinroom": [ - [ - 1, - 3, - 3, - 3 - ], - [ - 2, - 6, - 6, - 6 - ] - ], - "Time_Quick": 70, - "Time_playcard": 60, - "Time_initial": 160 - }, - "system_room": [{ - "type": 2, - "title": "", - "roomtype": [ - 1, - 1, - 1, - 0, - 1, - 0, - 1, - 1, - 0, - 0, - 0, - 0, [ - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ], - 0, - 2, - 1, [ - 1, - 2, - 400 - ], - 0, - 2 - ], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_1.png", - "roomLeave": 1, - "removeRobot": 0 - }, - { - "type": 2, - "title": "", - "roomtype": [ - 1, - 1, - 1, - 0, - 1, - 0, - 3, - 1, - 0, - 0, - 0, - 0, [ - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ], - 0, - 2, - 1, [ - 1, - 2, - 400 - ], - 0, - 2 - ], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_2.png", - "roomLeave": 1, - "removeRobot": 0 - }, - { - "type": 2, - "title": "", - "roomtype": [ - 1, - 1, - 1, - 0, - 1, - 0, - 3, - 1, - 0, - 0, - 0, - 0, [ - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ], - 0, - 2, - 1, [ - 2, - 100, - 20000 - ], - 0, - 2 - ], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_3.png", - "removeRobot": 1 - }, - { - "type": 2, - "title": "", - "roomtype": [ - 1, - 1, - 1, - 0, - 1, - 0, - 3, - 1, - 0, - 0, - 0, - 0, [ - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ], - 0, - 2, - 1, [ - 3, - 1000, - 200000 - ], - 0, - 2 - ], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_4.png", - "removeRobot": 1 - } - ], - "channellist": [{ - "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", - "channelname": "进贤", - "ios_defdownload_marketid": 2, - "and_defdownload_marketid": 3, - "game_version": 0, - "game_zip": "", - "game_size": "", - "marketlist": [{ - "marketid": 2, - "marketname": "苹果企业签", - "app_version": 0, - "app_download": "", - "app_size": "", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 3, - "marketname": "安卓", - "app_version": 0, - "app_download": "", - "app_size": "" - }, - { - "marketid": 4, - "marketname": "鸿蒙", - "deviceLogin":true, - "app_version": 1, - "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", - "app_size": "6.61M" - } - ] - }] - }, - { - "gameid": "sJgp0FArn0olVc3yitp3xThV036fG7n2", - "gamename": "跑得快", - "sharename": "【跑得快】", - "category": 1, - "game_server_tcp": "8.149.128.168:56024", - "game_server_http": "8.149.128.168:56025", - "game_version": 54, - "game_zip": "http://tsgames.daoqi88.cn/zip2/pdk_card_client-jinxian.zip", - "game_size": "9.8M", - "game_hall_dir": "pdk_card_client-jinxian", - "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/paodk-jx.png", - "haverobot": 1, - "game_config": { - "test": 1, - "star_beishu": [ - 1, - 10 - ], - "star_xianzhi": [ - 100, - 1000 - ], - "star_chashuifei": 300, - "needroomcard": [ - [ - 1, - 4, - 3, - 2 - ], - [ - 2, - 8, - 6, - 4 - ] - ], - "needroomcard_joinroom": [ - [ - 1, - 4, - 3, - 2 - ], - [ - 2, - 8, - 6, - 4 - ] - ], - "asetcount": [ - 8, - 16 - ], - "Time_Quick": 70, - "Time_playcard": 60, - "Time_initial": 160, - "paytype": "房卡", - "horse": "买马", - "horse_mun": [ - 1, - 2, - 3, - 4 - ], - "move_x": 20 - }, - "system_room": [{ - "type": 2, - "title": "4人初级场 10张", - "roomtype": [ - 1, - 1, - 1, - 2, - 1, -1, - 1, -1, -1, -1, -1, - 0, [ - [ - [ - [ - 2, - 3 - ], - [ - 1, - 5 - ] - ], - [ - [ - 3, - 4 - ], - [ - 3, - 4 - ] - ], - [ - [ - 1, - 2 - ], - [ - 1, - 4 - ] - ] - ], - [ - 8, - 1, - 1, - 0 - ], - 1, - 1, [ - 2, - 3 - ] - ], - 0, - 2, - 1, [ - 1, - 2, - 400 - ], - 0, - 2 - ], - "removeRobot": 1 - }, - { - "type": 2, - "title": "4人中级场 10张", - "roomtype": [ - 1, - 1, - 1, - 2, - 1, -1, - 1, -1, -1, -1, -1, - 0, [ - [ - [ - [ - 2, - 3 - ], - [ - 1, - 5 - ] - ], - [ - [ - 3, - 4 - ], - [ - 3, - 4 - ] - ], - [ - [ - 1, - 2 - ], - [ - 1, - 4 - ] - ] - ], - [ - 8, - 1, - 1, - 0 - ], - 1, - 1, [ - 2, - 3 - ] - ], - 0, - 2, - 1, [ - 2, - 100, - 20000 - ], - 0, - 2 - ], - "removeRobot": 1 - }, - { - "type": 2, - "title": "4人高级场 10张", - "roomtype": [ - 1, - 1, - 1, - 2, - 1, -1, - 1, -1, -1, -1, -1, - 0, [ - [ - [ - [ - 2, - 3 - ], - [ - 1, - 5 - ] - ], - [ - [ - 3, - 4 - ], - [ - 3, - 4 - ] - ], - [ - [ - 1, - 2 - ], - [ - 1, - 4 - ] - ] - ], - [ - 8, - 1, - 1, - 0 - ], - 1, - 1, [ - 2, - 3 - ] - ], - 0, - 2, - 1, [ - 3, - 1000, - 200000 - ], - 0, - 2 - ], - "removeRobot": 1 - } - ], - "channellist": [{ - "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", - "channelname": "进贤", - "ios_defdownload_marketid": 2, - "and_defdownload_marketid": 3, - "game_version": 0, - "game_zip": "", - "game_size": "", - "marketlist": [{ - "marketid": 2, - "marketname": "苹果企业签", - "app_version": 0, - "app_download": "", - "app_size": "", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 3, - "marketname": "安卓", - "app_version": 0, - "app_download": "", - "app_size": "" - }, - { - "marketid": 4, - "marketname": "鸿蒙", - "deviceLogin":true, - "app_version": 1, - "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", - "app_size": "6.61M" - } - ] - }] - }, - { - "gameid": "6ujc0IfoL0t1zS7J2dZ1eFh2u2t9682b", - "gamename": "找朋友", - "isdebugger": 0, - "category": 1, - "sharename": "【找朋友】", - "game_server_tcp": "8.149.128.168:56010", - "game_server_http": "8.149.128.168:56011", - "game_version": 7, - "game_zip": "http://tsgames.daoqi88.cn/zip2/zpy.zip", - "game_size": "11.16M", - "game_hall_dir": "zpy", - "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/zhaopy.png", - "game_config": { - "buckle": [ - 2, - 4 - ], - "personal": [ - 1, - 2 - ], - "numbergames": [ - 4, - 8 - ], - "mode":1, - "direction": 0, - "jiesuan": 1, - "touxiang": 0 - }, - "channellist": [{ - "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", - "channelname": "进贤", - "ios_defdownload_marketid": 2, - "and_defdownload_marketid": 3, - "game_version": 0, - "game_zip": "", - "game_size": "", - "marketlist": [{ - "marketid": 2, - "marketname": "苹果企业签", - "app_version": 0, - "app_download": "", - "app_size": "", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 3, - "marketname": "安卓", - "app_version": 0, - "app_download": "", - "app_size": "" - }, - { - "marketid": 4, - "marketname": "鸿蒙", - "deviceLogin":true, - "app_version": 1, - "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", - "app_size": "6.61M" - } - ] - }] - }, - { - "gameid": "sJgp0FArn0olVc8yitp0xThV036fG7n0", - "gamename": "关牌", - "sharename": "【关牌】", - "category": 1, - "game_server_tcp": "8.149.128.168:56016", - "game_server_http": "8.149.128.168:56017", - "isdebugger": 0, - "game_version": 54, - "game_zip": "http://tsgames.daoqi88.cn/zip2/guanpai-jx.zip", - "game_size": "11.0M", - "game_hall_dir": "guanpai-jx", - "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/guanpai.png", - "game_config": { - "buckle": [ - 1, - 2 - ], - "personal": [ - 1, - 2 - ], - "numbergames": [ - 6, - 12 - ], - "tiaoshi": 0, - "people": [ - 2, - 3 - ], - "difen": 10, - "zha": [ - 5, - 10, - 20 - ], - "zidongchu": 0, - "beishu": [ - 1, - 100 - ], - "xianzhi": [ - 1000, - 50000 - ], - "chxz": [ - 0, - 100 - ] - }, - "system_room": [{ - "type": 2, - "title": "练习场", - "roomtype": [ - 1, - 2, - 1, - 2, - 1, - 2, [ - 1, - 200 - ] - ] - }, - { - "type": 2, - "title": "初级场", - "roomtype": [ - 1, - 2, - 1, - 2, - 1, - 2, [ - 10, - 2000 - ] - ] - }, - { - "type": 2, - "title": "中级场", - "roomtype": [ - 1, - 2, - 1, - 2, - 1, - 2, [ - 20, - 4000 - ] - ] - }, - { - "type": 2, - "title": "高级场", - "roomtype": [ - 1, - 2, - 1, - 2, - 1, - 2, [ - 50, - 10000 - ] - ] - } - ], - "channellist": [{ - "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", - "channelname": "进贤", - "ios_defdownload_marketid": 2, - "and_defdownload_marketid": 3, - "game_version": 0, - "game_zip": "", - "game_size": "", - "marketlist": [{ - "marketid": 2, - "marketname": "苹果企业签", - "app_version": 0, - "app_download": "", - "app_size": "", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 3, - "marketname": "安卓", - "app_version": 0, - "app_download": "", - "app_size": "" - }, - { - "marketid": 4, - "marketname": "鸿蒙", - "deviceLogin":true, - "app_version": 1, - "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", - "app_size": "6.61M" - } - ] - }] - }, - { - "gameid": "FDtj0pGhT0Evjd3efAS5wkIlamiTh2oa", - "gamename": "斗地主", - "sharename": "【斗地主】", - "category": 1, - "game_server_tcp": "8.149.128.168:56008", - "game_server_http": "8.149.128.168:56009", - "isdebugger": 0, - "game_version": 30, - "game_zip": "http://tsgames.daoqi88.cn/zip2/doudizhu.zip", - "game_size": "13.3M", - "game_hall_dir": "doudizhu", - "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/doudizhu.png", - "game_config": { - "jushu": [ - 6, - 12 - ], - "kouka": [ - [ - 1, - 2 - ], - [ - 2, - 4 - ] - ], - "dizhu": [ - 1, - 5, - 10 - ], - "gold": [ - 1, - 50, - 1, - 1000 - ], - "tg_time": [ - 700, - 700, - 700, - 2500 - ], - "process": [ - [ - 1, - 50, - 0 - ], - [ - 2, - 15000, - 0 - ], - [ - 3, - 5000, -1 - ] - ], - "moshi": [ - 0, - 1, - 0 - ], - "renshu": [ - 2, - 3, - 4 - ], - "paixing": [ - [ - 1, [ - 1, - 1 - ] - ], - [ - 1, [ - 2, - 1 - ] - ], - [ - 1, [ - 1, - 5 - ] - ], - [ - 1, [ - 3, - 1 - ] - ], - [ - 1, [ - 3, - 1 - ] - ], - [ - 1, [ - 3, - 1 - ] - ], - [ - 0, [ - 3, - 1 - ] - ], - [ - 1, [ - 4, - 1 - ] - ], - [ - 1, [ - 4, - 1 - ] - ], - [ - 1, [ - 2, - 3 - ] - ], - [ - 1, [ - 3, - 2 - ] - ], - [ - 2, [ - 4, - 1 - ] - ], - [ - 3, [ - 1, - 2 - ] - ], - [ - 0, [ - 5, - 1 - ] - ] - ], - "playtime": [ - 15, - 5, - 15 - ], - "card_len": [ - [ - 17, - 13 - ], - [ - 20, - 14 - ] - ], - "del_cards": [ - [ - 3, [ - 1, - 2, - 3, - 4 - ] - ], - [ - 4, [ - 1, - 2, - 3, - 4 - ] - ], - [ - 5, [ - 1, - 2, - 3, - 4 - ] - ] - ], - "dipai": [ - 1, - 3, - 14 - ] - }, - "channellist": [{ - "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", - "channelname": "进贤", - "game_version": 0, - "game_zip": "", - "game_size": "", - "ios_defdownload_marketid": 2, - "and_defdownload_marketid": 3, - "marketlist": [{ - "marketid": 2, - "marketname": "苹果企业签", - "app_version": 0, - "app_download": "", - "app_size": "", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 3, - "marketname": "安卓", - "app_version": 0, - "app_download": "", - "app_size": "" - }, - { - "marketid": 4, - "marketname": "鸿蒙", - "deviceLogin":true, - "app_version": 1, - "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", - "app_size": "6.61M" - } - ] - }] - } - ] - }, - { - "agentid": "1B2h0ccl205c390Y28m1Ajdplkuu4wgy", - "agentname": "测试", - "scrollmsg": "聚友棋牌仅限娱乐,禁止赌博!一经发现立即封号。客服号改为:jyqp181999", - "showmessage": "", - "xxroomdeduct": 1, - "audio_domain":"gameaudio.daoqi88.cn", - "audio_bucket":"gameaudio", - "html_buyroomcard": 2, - "html_applysales": 2, - "sale_wechat": "jyqp181999", - "agentmode": 2, - "WechatPublicNo_ewm": "http://tsgames.daoqi88.cn/image_ewm/ewm_jinxian2.png", - "service_wechat": "jyqp181999", - "service_tel": "", - "url_topup": "http://dlwebv3/wan_purchase_new.html", - "url_download": "http://sharegame/gameshare.html", - "logimage2": "http://op0g0jeqa.bkt.clouddn.com/log_jinxian.png", - "h5shareurl": "http://h5game1/gameLobby920/index2.html", - "game_share_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", - "shareh5title": "进贤聚友棋牌", - "shareh5desc": "一起来嗨~", - "gameNoticeShow": 0, - "shareType":1, - "showVisitor":1, - "shareImgUrl":"https://tsgames.daoqi88.cn/image/match.png", - "hallNoticeShow": 0, - "gameHallConfig": { - "topImgUrl": "http://tsgames.daoqi88.cn/image_xgamehall/gamehalllogo.png" - }, - "menunotice": { - "advert0": { - "type": 0, - "imgUrl": "http://tsgames.daoqi88.cn/image/adver0_jinxian2.png", - "imgAdUrl": "", - "height": 470, - "msg": " 【测试专用】 ##进贤本地玩法8年老平台##绿色平台,仅供娱乐!绝无外挂!##客服微信:jyqp181999", - "url": "" - } - }, - "rebateConfig": { - "from": 3, - "value": 0, - "rebateCharm": 1 - }, - "gamelist": [ - { - "gameid": "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO", - "gamename": "进贤聚友", - "game_server_tcp": "8.139.255.236:56072", - "game_server_http": "8.139.255.236:56073", - "game_version": 261, - "game_zip": "http://tsgames.daoqi88.cn/zip2/gamehall.zip", - "game_size": "4.61M", - "game_hall_dir": "gamehall", - "game_down_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", - "game_down_memo": "进贤聚友,让欢乐无限分享", - "hideSpid": [ - 478 - ], - - "channellist": [{ - "channelid": "aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG", - "channelname": "进贤", - "ios_defdownload_marketid": 2, - "and_defdownload_marketid": 3, - "game_version": 0, - "game_zip": "", - "game_size": "", - "marketlist": [{ - "marketid": 1, - "marketname": "App Store", - "app_version": 1, - "app_download": "", - "app_size": "", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 2, - "marketname": "苹果企业签", - "app_version": 45, - "app_download": "itms-services://?action=download-manifest&url=https://daoqi.daoqi888.cn/plist/gamehall_jinxianv2_test.plist", - "app_size": "2.61M", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 3, - "marketname": "安卓", - "app_version": 50, - "app_download": "https://daoqi.daoqi888.cn/apk/gamehall_jinxianv2.apk", - "app_size": "6.61M" - }, - { - "marketid": 4, - "marketname": "鸿蒙", - "deviceLogin":false, - "app_version": 2, - "app_download": "https://daoqi.daoqi888.cn/harmonyos/gamehall_jinxian_harmonyosv2.apk", - "app_size": "6.61M" - } - ] - }] - }, - { - "gameid": "jzWe0kzmN0tAcZ5MLks3rFWcxNecogyu", - "gamename": "新进贤麻将", - "category": 2, - "game_server_tcp": "8.139.255.236:56030", - "game_server_http": "8.139.255.236:56031", - "sharename": "【新进贤麻将】", - "game_version": 7, - "game_zip": "http://tsgames.daoqi88.cn/zip2/jinxianmahjong.zip", - "game_size": "10.59M", - "game_hall_dir": "jinxianmahjong", - "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/mj_jx.png", - "haverobot": 1, - "game_config": { }, - "system_room": [{ - "type": 2, - "title": "", - "roomtype": [], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_1.png", - "roomLeave": 1, - "removeRobot": 0 - }, - { - "type": 2, - "title": "", - "roomtype": [], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_2.png", - "roomLeave": 1, - "removeRobot": 0 - }, - { - "type": 2, - "title": "", - "roomtype": [], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_3.png", - "removeRobot": 1 - }, - { - "type": 2, - "title": "", - "roomtype": [], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_4.png", - "removeRobot": 1 - } - ], - "channellist": [{ - "channelid": "aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG", - "channelname": "进贤", - "ios_defdownload_marketid": 2, - "and_defdownload_marketid": 3, - "game_version": 0, - "game_zip": "", - "game_size": "", - "marketlist": [{ - "marketid": 2, - "marketname": "苹果企业签", - "app_version": 0, - "app_download": "", - "app_size": "", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 3, - "marketname": "安卓", - "app_version": 0, - "app_download": "", - "app_size": "" - }, - { - "marketid": 4, - "marketname": "鸿蒙", - "deviceLogin":false, - "app_version": 1, - "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", - "app_size": "6.61M" - } - ] - }] - }, - { - "gameid": "J9r4022t80pxlb01zOK76DN1WD4QewFl", - "gamename": "二七王", - "category": 2, - "game_server_tcp": "8.139.255.236:56030", - "game_server_http": "8.139.255.236:56031", - "sharename": "【二七王】", - "game_version": 1, - "game_zip": "http://tsgames.daoqi88.cn/zip2/jinxianmahjong.zip", - "game_size": "10.59M", - "game_hall_dir": "jinxianmahjong", - "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/mj_jx.png", - "haverobot": 1, - "game_config": { }, - "system_room": [{ - "type": 2, - "title": "", - "roomtype": [], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_1.png", - "roomLeave": 1, - "removeRobot": 0 - }, - { - "type": 2, - "title": "", - "roomtype": [], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_2.png", - "roomLeave": 1, - "removeRobot": 0 - }, - { - "type": 2, - "title": "", - "roomtype": [], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_3.png", - "removeRobot": 1 - }, - { - "type": 2, - "title": "", - "roomtype": [], - "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_4.png", - "removeRobot": 1 - } - ], - "channellist": [{ - "channelid": "aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG", - "channelname": "进贤", - "ios_defdownload_marketid": 2, - "and_defdownload_marketid": 3, - "game_version": 0, - "game_zip": "", - "game_size": "", - "marketlist": [{ - "marketid": 2, - "marketname": "苹果企业签", - "app_version": 0, - "app_download": "", - "app_size": "", - "game_version": 0, - "game_zip": "", - "game_size": "" - }, - { - "marketid": 3, - "marketname": "安卓", - "app_version": 0, - "app_download": "", - "app_size": "" - }, - { - "marketid": 4, - "marketname": "鸿蒙", - "deviceLogin":false, - "app_version": 1, - "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", - "app_size": "6.61M" - } - ] - }] - } - ] - } - ] +{ + "showmessage": "", + "visitor_server_tcp": "115.159.146.131:51088", + "visitor_server_http": "115.159.146.131:51089", + "isclose": 1, + "scrollmsg": "官方郑重声明:近期有不法之徒以片面的游戏得分数据欺骗玩家购买外挂,已有玩家上当受骗,在此官方郑重声明,本游戏绝无外挂,凡自称有外挂的都是骗子,切勿上当受骗!", + "noticemsg": "", + "menunotice": { + "advert0": { + "type": 0, + "imgUrl": "http://tsgames.daoqi88.cn/image/adver0_youle2.png", + "imgAdUrl": "", + "height": 470, + "msg": "", + "url": "" + }, + "advert1": { + "msg": "", + "url": "" + }, + "advert2": { + "msg": "", + "url": "" + } + }, + "Ads": "本游戏仅为娱乐休闲使用,房卡所有#游戏通用", + "roomCardName": "房卡", + "nameImgFrame_1": 1, + "starName": "金币", + "nameImgFrame_2": 2, + "shieldShowRebate": 1, + "WechatPublicNo_ewm": "", + "behaviorList": [ + [ + 3233, + "rankList" + ], + [ + 604, + "rankList" + ], + [ + 76, + "ewm" + ], + [ + 3232, + "vipRank" + ], + [ + 3231, + "vipRank" + ], + [ + 679, + "match" + ], + [ + 1719, + "match" + ] + ], + "managerUrl": "http://dlweb.daoqi88.cn/white_list.html", + "service_qq": "", + "rankList": 0, + "wareHouse": 0, + "betaText": "Beta", + "ramSeat": 1, + "payCSS": "", + "payJS": "", + "cdKey": 2, + "agentlist": [ + { + "agentid": "veRa0qrBf0df2K1G4de2tgfmVxB2jxpv", + "agentname": "进贤", + "scrollmsg": "聚友棋牌仅限娱乐,禁止赌博!一经发现立即封号。客服号改为:jyqp181999", + "showmessage": "", + "xxroomdeduct": 1, + "audio_domain":"gameaudio.daoqi88.cn", + "audio_bucket":"gameaudio", + "html_buyroomcard": 2, + "html_applysales": 2, + "sale_wechat": "jyqp181999", + "agentmode": 2, + "WechatPublicNo_ewm": "http://tsgames.daoqi88.cn/image_ewm/ewm_jinxian2.png", + "service_wechat": "jyqp181999", + "service_tel": "", + "url_topup": "http://dlwebv3/wan_purchase_new.html", + "url_download": "http://sharegame/gameshare.html", + "logimage2": "http://op0g0jeqa.bkt.clouddn.com/log_jinxian.png", + "h5shareurl": "http://h5game1/gameLobby920/index2.html", + "game_share_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", + "shareh5title": "进贤聚友棋牌", + "shareh5desc": "一起来嗨~", + "gameNoticeShow": 0, + "shareType":1, + "shareImgUrl":"https://tsgames.daoqi88.cn/image/match.png", + "hallNoticeShow": 0, + "gameHallConfig": { + "topImgUrl": "http://tsgames.daoqi88.cn/image_xgamehall/gamehalllogo.png" + }, + "menunotice": { + "advert0": { + "type": 0, + "imgUrl": "http://tsgames.daoqi88.cn/image/adver0_jinxian2.png", + "imgAdUrl": "", + "height": 470, + "msg": "进贤本地玩法8年老平台##绿色平台,仅供娱乐!绝无外挂!##客服微信:jyqp181999", + "url": "" + } + }, + "rebateConfig": { + "from": 3, + "value": 0, + "rebateCharm": 1 + }, + "gamelist": [{ + "gameid": "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO", + "gamename": "进贤聚友", + "game_server_tcp": "8.139.255.236:56072", + "game_server_http": "8.139.255.236:56073", + "game_version": 261, + "game_zip": "http://tsgames.daoqi88.cn/zip2/gamehall.zip", + "game_size": "4.61M", + "game_hall_dir": "gamehall", + "game_down_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", + "game_down_memo": "进贤聚友,让欢乐无限分享", + "hideSpid": [ + 478 + ], + + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 1, + "marketname": "App Store", + "app_version": 1, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 44, + "app_download": "itms-services://?action=download-manifest&url=https://daoqi.daoqi888.cn/plist/gamehall_jinxianv2.plist", + "app_size": "2.61M", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 50, + "app_download": "https://daoqi.daoqi888.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 2, + "app_download": "https://daoqi.daoqi888.cn/harmonyos/gamehall_jinxian_harmonyosv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "a52y0ajd106pKU1KIvF1zRii8h1gRem5", + "gamename": "三个老K", + "sharename": "【三个老K】", + "category": 1, + "game_server_tcp": "8.139.255.236:56084", + "game_server_http": "8.139.255.236:56085", + "game_version": 155, + "game_zip": "http://tsgames.daoqi88.cn/zip2/sangelaok.zip", + "game_size": "9.07M", + "game_hall_dir": "sangelaok", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/3glaoK.png", + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "rZDp0wfvC0ZaPT5iLqh2itGFsnJaDvzc", + "gamename": "进贤麻将", + "category": 2, + "game_server_tcp": "8.139.255.236:56056", + "game_server_http": "8.139.255.236:56057", + "sharename": "【进贤麻将】", + "game_version": 41, + "game_zip": "http://tsgames.daoqi88.cn/zip2/majiang_jx.zip", + "game_size": "10.59M", + "game_hall_dir": "majiang_jx", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/mj_jx.png", + "haverobot": 1, + "game_config": { + "test": 1, + "asetcount": [ + 8, + 16 + ], + "needroomcard": [ + [ + 1, + 3, + 3, + 3 + ], + [ + 2, + 6, + 6, + 6 + ] + ], + "needroomcard_joinroom": [ + [ + 1, + 3, + 3, + 3 + ], + [ + 2, + 6, + 6, + 6 + ] + ], + "Time_Quick": 70, + "Time_playcard": 60, + "Time_initial": 160 + }, + "system_room": [{ + "type": 2, + "title": "", + "roomtype": [ + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 0, [ + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + 0, + 2, + 1, [ + 1, + 2, + 400 + ], + 0, + 2 + ], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_1.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [ + 1, + 1, + 1, + 0, + 1, + 0, + 3, + 1, + 0, + 0, + 0, + 0, [ + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + 0, + 2, + 1, [ + 1, + 2, + 400 + ], + 0, + 2 + ], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_2.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [ + 1, + 1, + 1, + 0, + 1, + 0, + 3, + 1, + 0, + 0, + 0, + 0, [ + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + 0, + 2, + 1, [ + 2, + 100, + 20000 + ], + 0, + 2 + ], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_3.png", + "removeRobot": 1 + }, + { + "type": 2, + "title": "", + "roomtype": [ + 1, + 1, + 1, + 0, + 1, + 0, + 3, + 1, + 0, + 0, + 0, + 0, [ + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + 0, + 2, + 1, [ + 3, + 1000, + 200000 + ], + 0, + 2 + ], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_4.png", + "removeRobot": 1 + } + ], + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "sJgp0FArn0olVc3yitp3xThV036fG7n2", + "gamename": "跑得快", + "sharename": "【跑得快】", + "category": 1, + "game_server_tcp": "8.149.128.168:56024", + "game_server_http": "8.149.128.168:56025", + "game_version": 54, + "game_zip": "http://tsgames.daoqi88.cn/zip2/pdk_card_client-jinxian.zip", + "game_size": "9.8M", + "game_hall_dir": "pdk_card_client-jinxian", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/paodk-jx.png", + "haverobot": 1, + "game_config": { + "test": 1, + "star_beishu": [ + 1, + 10 + ], + "star_xianzhi": [ + 100, + 1000 + ], + "star_chashuifei": 300, + "needroomcard": [ + [ + 1, + 4, + 3, + 2 + ], + [ + 2, + 8, + 6, + 4 + ] + ], + "needroomcard_joinroom": [ + [ + 1, + 4, + 3, + 2 + ], + [ + 2, + 8, + 6, + 4 + ] + ], + "asetcount": [ + 8, + 16 + ], + "Time_Quick": 70, + "Time_playcard": 60, + "Time_initial": 160, + "paytype": "房卡", + "horse": "买马", + "horse_mun": [ + 1, + 2, + 3, + 4 + ], + "move_x": 20 + }, + "system_room": [{ + "type": 2, + "title": "4人初级场 10张", + "roomtype": [ + 1, + 1, + 1, + 2, + 1, -1, + 1, -1, -1, -1, -1, + 0, [ + [ + [ + [ + 2, + 3 + ], + [ + 1, + 5 + ] + ], + [ + [ + 3, + 4 + ], + [ + 3, + 4 + ] + ], + [ + [ + 1, + 2 + ], + [ + 1, + 4 + ] + ] + ], + [ + 8, + 1, + 1, + 0 + ], + 1, + 1, [ + 2, + 3 + ] + ], + 0, + 2, + 1, [ + 1, + 2, + 400 + ], + 0, + 2 + ], + "removeRobot": 1 + }, + { + "type": 2, + "title": "4人中级场 10张", + "roomtype": [ + 1, + 1, + 1, + 2, + 1, -1, + 1, -1, -1, -1, -1, + 0, [ + [ + [ + [ + 2, + 3 + ], + [ + 1, + 5 + ] + ], + [ + [ + 3, + 4 + ], + [ + 3, + 4 + ] + ], + [ + [ + 1, + 2 + ], + [ + 1, + 4 + ] + ] + ], + [ + 8, + 1, + 1, + 0 + ], + 1, + 1, [ + 2, + 3 + ] + ], + 0, + 2, + 1, [ + 2, + 100, + 20000 + ], + 0, + 2 + ], + "removeRobot": 1 + }, + { + "type": 2, + "title": "4人高级场 10张", + "roomtype": [ + 1, + 1, + 1, + 2, + 1, -1, + 1, -1, -1, -1, -1, + 0, [ + [ + [ + [ + 2, + 3 + ], + [ + 1, + 5 + ] + ], + [ + [ + 3, + 4 + ], + [ + 3, + 4 + ] + ], + [ + [ + 1, + 2 + ], + [ + 1, + 4 + ] + ] + ], + [ + 8, + 1, + 1, + 0 + ], + 1, + 1, [ + 2, + 3 + ] + ], + 0, + 2, + 1, [ + 3, + 1000, + 200000 + ], + 0, + 2 + ], + "removeRobot": 1 + } + ], + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "6ujc0IfoL0t1zS7J2dZ1eFh2u2t9682b", + "gamename": "找朋友", + "isdebugger": 0, + "category": 1, + "sharename": "【找朋友】", + "game_server_tcp": "8.149.128.168:56010", + "game_server_http": "8.149.128.168:56011", + "game_version": 7, + "game_zip": "http://tsgames.daoqi88.cn/zip2/zpy.zip", + "game_size": "11.16M", + "game_hall_dir": "zpy", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/zhaopy.png", + "game_config": { + "buckle": [ + 2, + 4 + ], + "personal": [ + 1, + 2 + ], + "numbergames": [ + 4, + 8 + ], + "mode":1, + "direction": 0, + "jiesuan": 1, + "touxiang": 0 + }, + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "sJgp0FArn0olVc8yitp0xThV036fG7n0", + "gamename": "关牌", + "sharename": "【关牌】", + "category": 1, + "game_server_tcp": "8.149.128.168:56016", + "game_server_http": "8.149.128.168:56017", + "isdebugger": 0, + "game_version": 54, + "game_zip": "http://tsgames.daoqi88.cn/zip2/guanpai-jx.zip", + "game_size": "11.0M", + "game_hall_dir": "guanpai-jx", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/guanpai.png", + "game_config": { + "buckle": [ + 1, + 2 + ], + "personal": [ + 1, + 2 + ], + "numbergames": [ + 6, + 12 + ], + "tiaoshi": 0, + "people": [ + 2, + 3 + ], + "difen": 10, + "zha": [ + 5, + 10, + 20 + ], + "zidongchu": 0, + "beishu": [ + 1, + 100 + ], + "xianzhi": [ + 1000, + 50000 + ], + "chxz": [ + 0, + 100 + ] + }, + "system_room": [{ + "type": 2, + "title": "练习场", + "roomtype": [ + 1, + 2, + 1, + 2, + 1, + 2, [ + 1, + 200 + ] + ] + }, + { + "type": 2, + "title": "初级场", + "roomtype": [ + 1, + 2, + 1, + 2, + 1, + 2, [ + 10, + 2000 + ] + ] + }, + { + "type": 2, + "title": "中级场", + "roomtype": [ + 1, + 2, + 1, + 2, + 1, + 2, [ + 20, + 4000 + ] + ] + }, + { + "type": 2, + "title": "高级场", + "roomtype": [ + 1, + 2, + 1, + 2, + 1, + 2, [ + 50, + 10000 + ] + ] + } + ], + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "FDtj0pGhT0Evjd3efAS5wkIlamiTh2oa", + "gamename": "斗地主", + "sharename": "【斗地主】", + "category": 1, + "game_server_tcp": "8.149.128.168:56008", + "game_server_http": "8.149.128.168:56009", + "isdebugger": 0, + "game_version": 30, + "game_zip": "http://tsgames.daoqi88.cn/zip2/doudizhu.zip", + "game_size": "13.3M", + "game_hall_dir": "doudizhu", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/doudizhu.png", + "game_config": { + "jushu": [ + 6, + 12 + ], + "kouka": [ + [ + 1, + 2 + ], + [ + 2, + 4 + ] + ], + "dizhu": [ + 1, + 5, + 10 + ], + "gold": [ + 1, + 50, + 1, + 1000 + ], + "tg_time": [ + 700, + 700, + 700, + 2500 + ], + "process": [ + [ + 1, + 50, + 0 + ], + [ + 2, + 15000, + 0 + ], + [ + 3, + 5000, -1 + ] + ], + "moshi": [ + 0, + 1, + 0 + ], + "renshu": [ + 2, + 3, + 4 + ], + "paixing": [ + [ + 1, [ + 1, + 1 + ] + ], + [ + 1, [ + 2, + 1 + ] + ], + [ + 1, [ + 1, + 5 + ] + ], + [ + 1, [ + 3, + 1 + ] + ], + [ + 1, [ + 3, + 1 + ] + ], + [ + 1, [ + 3, + 1 + ] + ], + [ + 0, [ + 3, + 1 + ] + ], + [ + 1, [ + 4, + 1 + ] + ], + [ + 1, [ + 4, + 1 + ] + ], + [ + 1, [ + 2, + 3 + ] + ], + [ + 1, [ + 3, + 2 + ] + ], + [ + 2, [ + 4, + 1 + ] + ], + [ + 3, [ + 1, + 2 + ] + ], + [ + 0, [ + 5, + 1 + ] + ] + ], + "playtime": [ + 15, + 5, + 15 + ], + "card_len": [ + [ + 17, + 13 + ], + [ + 20, + 14 + ] + ], + "del_cards": [ + [ + 3, [ + 1, + 2, + 3, + 4 + ] + ], + [ + 4, [ + 1, + 2, + 3, + 4 + ] + ], + [ + 5, [ + 1, + 2, + 3, + 4 + ] + ] + ], + "dipai": [ + 1, + 3, + 14 + ] + }, + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "game_version": 0, + "game_zip": "", + "game_size": "", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + } + ] + }, + { + "agentid": "1B2h0ccl205c390Y28m1Ajdplkuu4wgy", + "agentname": "测试", + "scrollmsg": "聚友棋牌仅限娱乐,禁止赌博!一经发现立即封号。客服号改为:jyqp181999", + "showmessage": "", + "xxroomdeduct": 1, + "audio_domain":"gameaudio.daoqi88.cn", + "audio_bucket":"gameaudio", + "html_buyroomcard": 2, + "html_applysales": 2, + "sale_wechat": "jyqp181999", + "agentmode": 2, + "WechatPublicNo_ewm": "http://tsgames.daoqi88.cn/image_ewm/ewm_jinxian2.png", + "service_wechat": "jyqp181999", + "service_tel": "", + "url_topup": "http://dlwebv3/wan_purchase_new.html", + "url_download": "http://sharegame/gameshare.html", + "logimage2": "http://op0g0jeqa.bkt.clouddn.com/log_jinxian.png", + "h5shareurl": "http://h5game1/gameLobby920/index2.html", + "game_share_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", + "shareh5title": "进贤聚友棋牌", + "shareh5desc": "一起来嗨~", + "gameNoticeShow": 0, + "shareType":1, + "showVisitor":0, + "shareImgUrl":"https://tsgames.daoqi88.cn/image/match.png", + "hallNoticeShow": 0, + "gameHallConfig": { + "topImgUrl": "http://tsgames.daoqi88.cn/image_xgamehall/gamehalllogo.png" + }, + "menunotice": { + "advert0": { + "type": 0, + "imgUrl": "http://tsgames.daoqi88.cn/image/adver0_jinxian2.png", + "imgAdUrl": "", + "height": 470, + "msg": " 【测试专用】 ##进贤本地玩法8年老平台##绿色平台,仅供娱乐!绝无外挂!##客服微信:jyqp181999", + "url": "" + } + }, + "rebateConfig": { + "from": 3, + "value": 0, + "rebateCharm": 1 + }, + "gamelist": [ + { + "gameid": "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO", + "gamename": "进贤聚友", + "game_server_tcp": "8.139.255.236:56072", + "game_server_http": "8.139.255.236:56073", + "game_version": 261, + "game_zip": "http://tsgames.daoqi88.cn/zip2/gamehall.zip", + "game_size": "4.61M", + "game_hall_dir": "gamehall", + "game_down_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", + "game_down_memo": "进贤聚友,让欢乐无限分享", + "hideSpid": [ + 478 + ], + + "channellist": [{ + "channelid": "aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 1, + "marketname": "App Store", + "app_version": 1, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 45, + "app_download": "itms-services://?action=download-manifest&url=https://daoqi.daoqi888.cn/plist/gamehall_jinxianv2_test.plist", + "app_size": "2.61M", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 50, + "app_download": "https://daoqi.daoqi888.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":false, + "app_version": 2, + "app_download": "https://daoqi.daoqi888.cn/harmonyos/gamehall_jinxian_harmonyosv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "jzWe0kzmN0tAcZ5MLks3rFWcxNecogyu", + "gamename": "新进贤麻将", + "category": 2, + "game_server_tcp": "8.139.255.236:56030", + "game_server_http": "8.139.255.236:56031", + "sharename": "【新进贤麻将】", + "game_version": 7, + "game_zip": "http://tsgames.daoqi88.cn/zip2/jinxianmahjong.zip", + "game_size": "10.59M", + "game_hall_dir": "jinxianmahjong", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/mj_jx.png", + "haverobot": 1, + "game_config": { }, + "system_room": [{ + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_1.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_2.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_3.png", + "removeRobot": 1 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_4.png", + "removeRobot": 1 + } + ], + "channellist": [{ + "channelid": "aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":false, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "J9r4022t80pxlb01zOK76DN1WD4QewFl", + "gamename": "二七王", + "category": 2, + "game_server_tcp": "8.139.255.236:56030", + "game_server_http": "8.139.255.236:56031", + "sharename": "【二七王】", + "game_version": 1, + "game_zip": "http://tsgames.daoqi88.cn/zip2/jinxianmahjong.zip", + "game_size": "10.59M", + "game_hall_dir": "jinxianmahjong", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/mj_jx.png", + "haverobot": 1, + "game_config": { }, + "system_room": [{ + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_1.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_2.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_3.png", + "removeRobot": 1 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_4.png", + "removeRobot": 1 + } + ], + "channellist": [{ + "channelid": "aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":false, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + } + ] + } + ] } \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene index 403c240..013a53c 100644 --- a/cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene +++ b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene @@ -52,7 +52,7 @@ }, "autoReleaseAssets": false, "_globals": { - "__id__": 5 + "__id__": 6 }, "_id": "1c10c2ee-adc2-496f-9005-d3e0e6ebbb56" }, @@ -72,6 +72,9 @@ }, { "__id__": 4 + }, + { + "__id__": 5 } ], "_prefab": null, @@ -182,8 +185,24 @@ "__uuid__": "e1b53f04-3e2f-4127-9a3f-f629bed6905e", "__expectedType__": "cc.Prefab" }, - "notices": { - "__uuid__": "b13f487a-451e-41ce-a810-1fab0d52248f", + "redeemCode": { + "__uuid__": "ada34847-93c7-48f2-bb0a-5c1db4b112f6", + "__expectedType__": "cc.Prefab" + }, + "qrCode": { + "__uuid__": "95ee9b79-8c30-4f54-a2cf-19d02083cd28", + "__expectedType__": "cc.Prefab" + }, + "noticeBoard": { + "__uuid__": "e7a05100-7de4-4935-896e-a59061377b87", + "__expectedType__": "cc.Prefab" + }, + "ownedRoomList": { + "__uuid__": "260ea2dc-696c-4a65-a2fd-109dd794bb45", + "__expectedType__": "cc.Prefab" + }, + "invite": { + "__uuid__": "a3db6daa-a98e-4a73-b0f7-cd122bdb11c5", "__expectedType__": "cc.Prefab" }, "profile": { @@ -206,6 +225,100 @@ "__uuid__": "f4af0cc2-2514-4bd5-a0de-7a3dbb32438b", "__expectedType__": "cc.Prefab" }, + "rank": { + "__uuid__": "3eafe9c2-475b-4c81-b71a-37ffa2717c71", + "__expectedType__": "cc.Prefab" + }, + "rankItem": { + "__uuid__": "5925d1d2-f4b2-44c1-a37a-fb2bcdcf664e", + "__expectedType__": "cc.Prefab" + }, + "rankMedals": [ + { + "__uuid__": "fb86ab44-8140-4d8e-82f1-84ae4b8f3216@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "10653735-f46a-4e0f-906f-dacb093dad23@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "87749e95-72aa-435e-93bb-1f41553922f0@f9941", + "__expectedType__": "cc.SpriteFrame" + } + ], + "rankTitle": { + "__uuid__": "ecb18612-2066-42d2-8fa0-c0161124cf1e@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "vipRankTitle": { + "__uuid__": "97b4bd7f-8716-497f-a375-d9c841cf34e9@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "ownedRoomDigits": [ + { + "__uuid__": "9d49bb70-608e-46d2-8672-5afb78bbd8d7@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "e71157d8-1261-4917-bac1-1b48ab5b6ab9@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "bade804f-d16a-46b6-8746-600ff6b93cce@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "dae7297a-cb1a-49d7-98a9-a3008a8afc7a@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "f1365825-aa47-4efe-936c-81284b0d5e1f@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "b4d68ae8-0799-47c1-8b30-26b2daed2d59@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "be16e577-8add-4c32-a56b-948a52e0180b@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "2fd355d7-493d-474c-b753-676c7ee9e50d@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "3c1709f3-a24f-4a9e-814c-04bc1441da55@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "4dd36de5-fadb-4852-8334-e9b5834a3957@f9941", + "__expectedType__": "cc.SpriteFrame" + } + ], + "rankSymbols": [ + { + "__uuid__": "6b9ad554-bf4a-4493-82dd-92b4328cb8a4@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "2f1e97ec-1bad-4731-9563-d90ae5b93111@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "a1016858-72af-4a2c-8d8e-7098eb82a310@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "a6986c72-2bef-4f50-b215-86d4de943b68@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "df99cc0a-0b2a-4fdd-b271-7932b2bea237@f9941", + "__expectedType__": "cc.SpriteFrame" + } + ], "digits": [ { "__uuid__": "fc6e9a8e-0a6d-4555-bc3e-88fd427b9f3a@f9941", @@ -272,32 +385,56 @@ ], "_id": "abZXhqPblEq57+36N2j8xH" }, + { + "__type__": "ec19eRwzzZA6LQg5DFOX78H", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "createRoom": { + "__uuid__": "584dbb85-4acc-4bea-9d9b-e0cbeb8741da", + "__expectedType__": "cc.Prefab" + }, + "room": { + "__uuid__": "f4582832-23e2-4981-be26-08dfe832aa96", + "__expectedType__": "cc.Prefab" + }, + "roomScene": { + "__uuid__": "18f06ada-9d36-485c-9f44-556d6980ca7a", + "__expectedType__": "cc.SceneAsset" + }, + "_id": "c2sXHnthJJQoYDj00RAeBa" + }, { "__type__": "cc.SceneGlobals", "ambient": { - "__id__": 6 - }, - "shadows": { "__id__": 7 }, - "_skybox": { + "shadows": { "__id__": 8 }, - "fog": { + "_skybox": { "__id__": 9 }, - "octree": { + "fog": { "__id__": 10 }, - "skin": { + "octree": { "__id__": 11 }, - "lightProbeInfo": { + "skin": { "__id__": 12 }, - "postSettings": { + "lightProbeInfo": { "__id__": 13 }, + "postSettings": { + "__id__": 14 + }, "bakedWithStationaryMainLight": false, "bakedWithHighpLightmap": false }, diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/TemplateRoom.scene b/cocoscreator_projects/YouleNexus/assets/scenes/TemplateRoom.scene new file mode 100644 index 0000000..426cba4 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/TemplateRoom.scene @@ -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 + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/TemplateRoom.scene.meta b/cocoscreator_projects/YouleNexus/assets/scenes/TemplateRoom.scene.meta new file mode 100644 index 0000000..7970c85 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/TemplateRoom.scene.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.1.50", + "importer": "scene", + "imported": true, + "uuid": "18f06ada-9d36-485c-9f44-556d6980ca7a", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/AnnouncementView.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/AnnouncementView.ts new file mode 100644 index 0000000..4d85e0b --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/AnnouncementView.ts @@ -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(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); + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/AnnouncementView.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/AnnouncementView.ts.meta new file mode 100644 index 0000000..220591c --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/AnnouncementView.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "4b4591ab-307a-4061-9690-7102b996a3ae", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/CreateRoomPage.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/CreateRoomPage.ts new file mode 100644 index 0000000..0e15c93 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/CreateRoomPage.ts @@ -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');} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/CreateRoomPage.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/CreateRoomPage.ts.meta new file mode 100644 index 0000000..23e21f3 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/CreateRoomPage.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "956dedf9-3650-418b-af8a-a755a26d991c", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/JoinRoomView.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/JoinRoomView.ts new file mode 100644 index 0000000..b76e5fa --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/JoinRoomView.ts @@ -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;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=ivoid)|null=null; + + bindPages(visible:readonly boolean[],select:(index:number)=>void):()=>void { + this.release?.(); + const count=this.tabs.length,container=this.getComponent(ToggleContainer),layout=this.getComponent(Layout); + if(count===0||visible.length!==count||!visible[0]||!container||container.allowSwitchOff||!layout||layout.type!==Layout.Type.VERTICAL) + throw Error('NoticeTabsView requires a vertical single-selection container and a visible first page'); + if(!this.normalBackground||!this.selectedBackground||[this.backgrounds,this.titles,this.normalTitles,this.selectedTitles].some(items=>items.length!==count||items.some(item=>!item))) + throw Error('NoticeTabsView requires complete serialized tab artwork'); + this.tabs.forEach((tab,index)=>{if(tab.node.parent!==this.node)throw Error('Notice tabs must be direct ToggleContainer children');tab.node.active=visible[index];}); + this.tabs[0].setIsCheckedWithoutNotify(true); + this.tabs.slice(1).forEach(tab=>tab.setIsCheckedWithoutNotify(false)); + const paint=()=>this.tabs.forEach((tab,index)=>{ + this.backgrounds[index].spriteFrame=tab.isChecked?this.selectedBackground:this.normalBackground; + this.titles[index].spriteFrame=tab.isChecked?this.selectedTitles[index]:this.normalTitles[index]; + }); + const handlers=this.tabs.map((tab,index)=>()=>{paint();if(tab.isChecked)select(index);}); + this.tabs.forEach((tab,index)=>tab.node.on(Toggle.EventType.TOGGLE,handlers[index])); + const release=()=>{this.tabs.forEach((tab,index)=>tab.node.off(Toggle.EventType.TOGGLE,handlers[index]));if(this.release===release)this.release=null;}; + this.release=release;paint();layout.updateLayout(true);select(0); + return release; + } + onDestroy():void{this.release?.();} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/NoticeTabsView.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/NoticeTabsView.ts.meta new file mode 100644 index 0000000..edc2b20 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/NoticeTabsView.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "fb24d5e9-ba6c-4f8f-a7da-ad8e5888a0b6", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalSwitchAnimation.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalSwitchAnimation.ts new file mode 100644 index 0000000..a1da8a5 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalSwitchAnimation.ts @@ -0,0 +1,63 @@ +import { _decorator, Animation, AnimationClip, AnimationState, Component, Sprite, SpriteFrame } from 'cc'; +const { ccclass, disallowMultiple, property, requireComponent } = _decorator; + +/** Optional transition clips decorate a state supplied by the switch's owner. + * Empty on/off clips mean an immediate sprite change. Clips never change business state. + */ +@ccclass('OptionalSwitchAnimation') +@disallowMultiple +@requireComponent([Animation, Sprite]) +export class OptionalSwitchAnimation extends Component { + @property({type:AnimationClip,displayName:'开启片段',tooltip:'可选;开启时播放一次,完成后恢复开启状态图片。'}) + onClip:AnimationClip|null=null; + @property({type:AnimationClip,displayName:'关闭片段',tooltip:'可选;关闭时播放一次,完成后恢复关闭状态图片。'}) + offClip:AnimationClip|null=null; + private stateFrame:SpriteFrame|null=null; + private playing:AnimationState|null=null; + + private get animation():Animation { + const animation=this.getComponent(Animation); + if(!animation)throw Error(`${this.node.name} requires Animation`); + return animation; + } + + onLoad():void { this.animation.on(Animation.EventType.FINISHED,this.finished,this); } + + setState(on:boolean,frame:SpriteFrame,animate:boolean):void { + if(!frame)throw Error(`${this.node.name} requires a switch state SpriteFrame`); + this.playing=null; + const animation=this.animation; + animation.stop(); + this.stateFrame=frame; + this.restoreFrame(); + const clip=on?this.onClip:this.offClip; + if(!animate||!clip||!this.enabledInHierarchy)return; + const state=animation.addClip(clip); + state.wrapMode=AnimationClip.WrapMode.Normal; + state.repeatCount=1; + this.playing=state; + animation.play(clip.name); + } + + private finished(_type:Animation.EventType,state:AnimationState):void { + // A stopped/replaced transition may have queued an event in the previous frame. + if(state!==this.playing||state.isPlaying)return; + this.playing=null; + this.restoreFrame(); + } + + private restoreFrame():void { + if(!this.stateFrame)return; // No state has been supplied yet; leave the authored sprite intact. + const sprite=this.getComponent(Sprite); + if(!sprite)throw Error(`${this.node.name} requires Sprite`); + sprite.spriteFrame=this.stateFrame; + } + + onDisable():void { + this.playing=null; + this.animation.stop(); + this.restoreFrame(); + } + + onDestroy():void { this.animation.off(Animation.EventType.FINISHED,this.finished,this); } +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalSwitchAnimation.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalSwitchAnimation.ts.meta new file mode 100644 index 0000000..f24bccc --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalSwitchAnimation.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "3ba9b2d6-3770-42ea-8a7c-56542d25e288", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalUiAnimation.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalUiAnimation.ts new file mode 100644 index 0000000..2ecfcee --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalUiAnimation.ts @@ -0,0 +1,23 @@ +import { _decorator, Animation, Component } from 'cc'; +const { ccclass, disallowMultiple, requireComponent } = _decorator; + +/** Animation.defaultClip is optional: empty means the authored static appearance. + * The clip owns its duration and wrap mode; visibility owns playback lifetime. + */ +@ccclass('OptionalUiAnimation') +@disallowMultiple +@requireComponent(Animation) +export class OptionalUiAnimation extends Component { + private get animation(): Animation { + const animation=this.getComponent(Animation); + if(!animation)throw Error(`${this.node.name} requires Animation`); + return animation; + } + + onEnable(): void { + const animation=this.animation; + if(animation.defaultClip)animation.play(animation.defaultClip.name); + } + + onDisable(): void { this.animation.stop(); } +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalUiAnimation.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalUiAnimation.ts.meta new file mode 100644 index 0000000..7e7f28c --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OptionalUiAnimation.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "6062e16b-2de1-42cf-91a5-0efcf38bae01", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomItemView.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomItemView.ts new file mode 100644 index 0000000..09c3318 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomItemView.ts @@ -0,0 +1,21 @@ +import {_decorator,Component,Layout,Node,Sprite,SpriteFrame,UITransform,instantiate} from 'cc'; +const {ccclass,property}=_decorator; + +@ccclass('OwnerRoomItemView') +export class OwnerRoomItemView extends Component { + @property(Sprite) digitTemplate:Sprite|null=null; + @property(Node) digitsRoot:Node|null=null; + render(code:number,frames:readonly SpriteFrame[]):void { + if(!Number.isSafeInteger(code)||code<=0)throw Error('Invalid owned room code'); + if(frames.length!==10||frames.some(frame=>!frame))throw Error('Owner room requires ten digit frames'); + if(!this.digitTemplate||!this.digitsRoot)throw Error('OwnerRoomItemView requires digit template and container'); + const layout=this.digitsRoot.getComponent(Layout),area=this.digitsRoot.getComponent(UITransform); + if(!layout||!area)throw Error('Owner room digits require Layout and UITransform'); + for(const child of [...this.digitsRoot.children]){child.removeFromParent();child.destroy();} + for(const char of String(code)){ + const digit=instantiate(this.digitTemplate.node);digit.parent=this.digitsRoot;digit.active=true; + digit.getComponent(Sprite)!.spriteFrame=frames[Number(char)]; + } + layout.updateLayout(true); + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomItemView.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomItemView.ts.meta new file mode 100644 index 0000000..a05f6f8 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomItemView.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "e40a5810-2a03-40e9-aff2-348909665b03", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomListView.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomListView.ts new file mode 100644 index 0000000..90c7edd --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomListView.ts @@ -0,0 +1,28 @@ +import {_decorator,Component,Layout,Node,Prefab,ScrollView,SpriteFrame,UITransform,instantiate} from 'cc'; +import {OwnerRoomItemView} from './OwnerRoomItemView.ts'; +const {ccclass,property}=_decorator; + +@ccclass('OwnerRoomListView') +export class OwnerRoomListView extends Component { + @property(Prefab) itemPrefab:Prefab|null=null; + @property(Node) content:Node|null=null; + @property(ScrollView) scroll:ScrollView|null=null; + render(codes:readonly number[],frames:readonly SpriteFrame[]):Array<{node:Node;code:number}> { + if(!this.itemPrefab||!this.content||!this.scroll)throw Error('OwnerRoomListView requires item prefab, content and ScrollView'); + const layout=this.content.getComponent(Layout); + if(!layout||layout.type!==Layout.Type.VERTICAL||layout.resizeMode!==Layout.ResizeMode.CONTAINER||this.scroll.content!==this.content) + throw Error('OwnerRoomListView requires a vertical scrolling content Layout'); + for(const child of [...this.content.children]){child.removeFromParent();child.destroy();} + const rows=codes.map(code=>{ + const node=instantiate(this.itemPrefab!);node.parent=this.content;node.name=`OwnerRoom${code}`; + const item=node.getComponent(OwnerRoomItemView);if(!item)throw Error('Owner room prefab requires OwnerRoomItemView'); + item.render(code,frames);return {node,code}; + }); + // Empty vertical Layout can produce a negative height from its inter-item spacing. + layout.enabled=rows.length>0; + if(rows.length===0)this.content.getComponent(UITransform)!.height=0; + else layout.updateLayout(true); + this.scroll.stopAutoScroll();this.scroll.scrollToTop(0); + return rows; + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomListView.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomListView.ts.meta new file mode 100644 index 0000000..722d17f --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/OwnerRoomListView.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "08da09b2-23b7-49c6-a3dc-489f991cda2d", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts index aa9448f..a92558a 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts @@ -1,350 +1,443 @@ -import { _decorator, assetManager, Component, Director, Label, Node, Prefab, ProgressBar, SceneAsset, - UITransform, Widget, director, instantiate, isValid } from 'cc'; -import { resolveRuntimeConfig, type RuntimeConfig } from '../../framework/config/runtime-config.ts'; -import { CocosLocalConfigFetcher } from './local-config-fetcher.ts'; -import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts'; -import { resolveLaunchWaitTime } from '../../framework/config/remote-config.ts'; -import { resolveLoginPolicy, type LoginPolicy } from '../../framework/presentation/login-policy.ts'; -import { LoginAccounts, type LoginNativeSettings } from '../../framework/adapters/native/login-accounts.ts'; -import {VipRoomHistory} from '../../framework/adapters/native/vip-room-history.ts'; -import {vipCode} from '../../framework/protocol/contracts/vip-room.ts'; -import { WebViewJavascriptBridgeAdapter, type WebViewJavascriptBridgeEnvironment } from '../../framework/adapters/native/webview-javascript-bridge.ts'; -import { PlatformRuntime } from '../../framework/platform/runtime.ts'; -import { WireClient } from '../../framework/net/wire-client.ts'; -import { CocosWebSocketTransport } from '../../framework/net/cocos-transport.ts'; -import type { DeviceLoginMode, LoginAccountIdentity, LoginDeviceSnapshot } from '../../framework/protocol/contracts/login-contract.ts'; -import { createLoginStageGameEntry } from './login-stage-game-entry.ts'; -import { CocosLogoCountdownView, createLogoCountdown } from './logo-countdown.ts'; -import { requireNode } from './node-bindings.ts'; -import { SceneFlow } from './scene-flow.ts'; -import { LoginPanel } from './login-panel.ts'; -import { LobbyAssets } from './LobbyAssets.ts'; -import { LobbyPanel } from './lobby-panel.ts'; -import { logNativeCall } from '../../framework/core/native-call-log.ts'; -import type { NativeCallName } from '../../framework/adapters/native/native-contracts.ts'; -import { resolveLobbyPolicy } from '../../framework/presentation/lobby-model.ts'; -import { openLobbyUrl, resolveLobbyLaunchMode } from '../../framework/adapters/native/lobby-links.ts'; -import { resolveLobbyMenuConfig, LOBBY_UNAVAILABLE, type LobbyContext } from '../../framework/presentation/lobby-menu.ts'; -import { FeedbackPanels } from './feedback-panels.ts'; +import { _decorator, assetManager, Component, Director, Label, Node, Prefab, ProgressBar, SceneAsset, + UITransform, Widget, director, instantiate, isValid } from 'cc'; +import { resolveRuntimeConfig, type RuntimeConfig } from '../../framework/config/runtime-config.ts'; +import { CocosLocalConfigFetcher } from './local-config-fetcher.ts'; +import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts'; +import { resolveLaunchWaitTime } from '../../framework/config/remote-config.ts'; +import { resolveLoginPolicy, type LoginPolicy } from '../../framework/presentation/login-policy.ts'; +import { LoginAccounts, type LoginNativeSettings } from '../../framework/adapters/native/login-accounts.ts'; +import {OwnedRoomHistory} from '../../framework/adapters/native/owned-room-history.ts'; +import {VipRoomHistory} from '../../framework/adapters/native/vip-room-history.ts'; +import {vipCode} from '../../framework/protocol/contracts/vip-room.ts'; +import { WebViewJavascriptBridgeAdapter, type WebViewJavascriptBridgeEnvironment } from '../../framework/adapters/native/webview-javascript-bridge.ts'; +import { PlatformRuntime } from '../../framework/platform/runtime.ts'; +import { WireClient } from '../../framework/net/wire-client.ts'; +import { CocosWebSocketTransport } from '../../framework/net/cocos-transport.ts'; +import type { DeviceLoginMode, LoginAccountIdentity, LoginDeviceSnapshot } from '../../framework/protocol/contracts/login-contract.ts'; +import {createTemplateGame} from '../../games/template/template-game.ts'; +import {TemplateRoomView} from '../../games/template/TemplateRoomView.ts'; +import {SubgameAssets} from './SubgameAssets.ts'; +import { CocosLogoCountdownView, createLogoCountdown } from './logo-countdown.ts'; +import { requireNode } from './node-bindings.ts'; +import { SceneFlow } from './scene-flow.ts'; +import { LoginPanel } from './login-panel.ts'; +import { LobbyAssets } from './LobbyAssets.ts'; +import { LobbyPanel } from './lobby-panel.ts'; +import { logNativeCall } from '../../framework/core/native-call-log.ts'; +import type { NativeCallName } from '../../framework/adapters/native/native-contracts.ts'; +import { resolveLobbyPolicy } from '../../framework/presentation/lobby-model.ts'; +import { openLobbyUrl, resolveLobbyLaunchMode } from '../../framework/adapters/native/lobby-links.ts'; +import { resolveLobbyMenuConfig, LOBBY_UNAVAILABLE, type LobbyContext } from '../../framework/presentation/lobby-menu.ts'; +import { FeedbackPanels } from './feedback-panels.ts'; import { isLoginResponse } from '../../framework/protocol/first-slice-routes.ts'; - -const {ccclass,property}=_decorator; -function required(value:T|null|undefined,name:string):T{if(value==null)throw Error(`PlatformStartup missing ${name}`);return value;} - -/** One persistent owner for config/socket/account; scenes own their own visual trees. */ -@ccclass('PlatformStartup') -export class PlatformStartup extends Component { - @property(SceneAsset) loadingScene:SceneAsset|null=null; - @property(SceneAsset) loginScene:SceneAsset|null=null; - @property(SceneAsset) lobbyScene:SceneAsset|null=null; - @property(Prefab) loadingPrefab:Prefab|null=null; - @property(Prefab) loginPrefab:Prefab|null=null; - @property(Prefab) protocolPrefab:Prefab|null=null; - @property(Prefab) phonePrefab:Prefab|null=null; - @property(Prefab) tipsPrefab:Prefab|null=null; - @property(Prefab) waitPrefab:Prefab|null=null; - @property(Prefab) reconnectPrefab:Prefab|null=null; - @property(Prefab) kickPrefab:Prefab|null=null; - private feedback:FeedbackPanels|null=null; - private waitingReply=false; - private reconnecting=false; +import {readInviteClipboard,readClipboardText} from '../../framework/adapters/native/invite-clipboard.ts'; + +const {ccclass,property}=_decorator; +function required(value:T|null|undefined,name:string):T{if(value==null)throw Error(`PlatformStartup missing ${name}`);return value;} + +/** One persistent owner for config/socket/account; scenes own their own visual trees. */ +@ccclass('PlatformStartup') +export class PlatformStartup extends Component { + @property(SceneAsset) loadingScene:SceneAsset|null=null; + @property(SceneAsset) loginScene:SceneAsset|null=null; + @property(SceneAsset) lobbyScene:SceneAsset|null=null; + @property(Prefab) loadingPrefab:Prefab|null=null; + @property(Prefab) loginPrefab:Prefab|null=null; + @property(Prefab) protocolPrefab:Prefab|null=null; + @property(Prefab) phonePrefab:Prefab|null=null; + @property(Prefab) tipsPrefab:Prefab|null=null; + @property(Prefab) waitPrefab:Prefab|null=null; + @property(Prefab) reconnectPrefab:Prefab|null=null; + @property(Prefab) kickPrefab:Prefab|null=null; + private feedback:FeedbackPanels|null=null; + private waitingReply=false; + private reconnecting=false; private runtime:PlatformRuntime|null=null; - private config:RuntimeConfig|null=null; - private policy:LoginPolicy|null=null; - private accounts:LoginAccounts|null=null; - private panel:LoginPanel|null=null; - private lobby:LobbyPanel|null=null; - private lobbyContext:LobbyContext|null=null; - private openExternal:((url:string)=>void)|null=null; - private bridge:WebViewJavascriptBridgeAdapter|null=null; - private restoreShareLogin:(()=>void)|null=null; - private restoreLocation:(()=>void)|null=null; - private startLocation:(()=>void)|null=null; - private location:unknown=null; - private loginResponse:unknown=null; - private unsubscribe:(()=>void)|null=null; - private countdown:ReturnType|null=null; - private countdownView:CocosLogoCountdownView|null=null; - private progress:ProgressBar|null=null; - private status:Label|null=null; - private device:LoginDeviceSnapshot|null=null; - private busy=false; - private terminal=false; - private returningToLogin=false; - private readonly events:Array<{kind:string;data:unknown}>=[]; - private readonly flow=new SceneFlow(name=>new Promise((resolve,reject)=>{ - if(!director.loadScene(name,error=>error?reject(error):resolve()))reject(Error(`Cannot load scene ${name}`)); - })); - - /** 常驻服务按加载场景、配置与桥接、连接、登录场景顺序启动;场景仅拥有自己的 UI。 */ - async start():Promise{ - try{ - required(this.loadingScene,'loadingScene');required(this.loginScene,'loginScene'); - required(this.lobbyScene,'lobbyScene');required(this.getComponent(LobbyAssets),'LobbyAssets'); - required(this.loadingPrefab,'loadingPrefab');required(this.loginPrefab,'loginPrefab'); - required(this.protocolPrefab,'protocolPrefab');required(this.phonePrefab,'phonePrefab'); - director.addPersistRootNode(this.node); - await this.flow.enter(this.loadingScene!.name,()=>this.bindLoading()); - if(this.terminal)return; - const injected=window as unknown as {settings?:LoginNativeSettings & {startlocation(type:number):void};app_version?:number|string;app_appversion?:number|string}; - const bridgeHost=injected.app_version!==undefined&&Number(injected.app_version)!==0; - const native=bridgeHost?{ - getcompareCode:()=>Number(injected.app_appversion), - // uAgent_3 passes the original argument, not its unused local tp variable. - accreditlogin:(provider:number)=>required(this.bridge,'native bridge').call('accreditlogin',provider===2?undefined:provider,()=>{}), - }:injected.settings; - this.config=await resolveRuntimeConfig({hostKind:bridgeHost?'uAgent_3':native?'native-settings':'h5',win:window as any, - search:window.location.search,fetcher:new HttpConfigFetcher(),localFetcher:new CocosLocalConfigFetcher()}); - if(this.terminal)return; - this.policy=resolveLoginPolicy(this.config.remoteConfig); - this.openExternal=url=>openLobbyUrl(url,bridgeHost?{kind:'bridge',call:(name,data)=>required(this.bridge,'native bridge').call(name,data,()=>{})}: - injected.settings?{kind:'settings',browser:target=>(injected.settings as unknown as {browser(url:string):void}).browser(target)}: - {kind:'h5',open:target=>{window.open(target,'_blank','noopener');}},Date.now); - const loginConfig={source:this.config.source,gameserver:this.config.gameserver,identity:this.config.identity, - showVisitor:this.policy.showVisitor,deviceLogin:this.policy.deviceLogin,qqLogin:this.policy.qqLogin}; - this.events.push({kind:'login-config',data:loginConfig}); - if(this.config.isDebugger)console.info('[PlatformStartup] login-config',JSON.stringify(loginConfig)); - this.accounts=new LoginAccounts({storage:window.localStorage,settings:native,random:Math.random,now:Date.now, - gameid:this.config.identity.gameid,agentid:this.config.identity.agentid,policy:this.policy, - submit:(account,mode)=>this.login(account,mode),tip:text=>this.tip(text)}); - this.lobbyContext={versionState:this.accounts.versionState, - launchMode:resolveLobbyLaunchMode(window.location.search,bridgeHost,(window as any).app_Launchtype)}; - const previous=(window as any).sharelogin; - const callback=(openid:unknown,headimgurl:unknown,nickname:unknown,sex:unknown,city:unknown,Province:unknown,unionid:unknown)=>{ - if(!this.terminal)this.acceptNative({openid,headimgurl,nickname,sex,city,Province,unionid},'global'); - }; - (window as any).sharelogin=callback; - this.restoreShareLogin=()=>{if((window as any).sharelogin===callback){if(previous===undefined)delete (window as any).sharelogin;else (window as any).sharelogin=previous;}}; - const oldLocation=(window as any).getlocationinfo; - const locationCallback=(data:unknown)=>{ - if(this.terminal)return; - try{ - const value=typeof data==='string'?JSON.parse(data):data; - if(value===null||typeof value!=='object'||Array.isArray(value))throw Error('Invalid getlocationinfo payload'); - this.location=value; - }catch(error){this.tip(error instanceof Error?error.message:String(error));} - }; - (window as any).getlocationinfo=locationCallback; - this.restoreLocation=()=>{if((window as any).getlocationinfo===locationCallback){if(oldLocation===undefined)delete (window as any).getlocationinfo;else (window as any).getlocationinfo=oldLocation;}}; - if(native){ - this.bridge=new WebViewJavascriptBridgeAdapter(window as unknown as WebViewJavascriptBridgeEnvironment); - this.bridge.register('sharelogin',data=>this.acceptNative(data,'bridge')); - this.bridge.register('getlocationinfo',locationCallback); - this.startLocation=()=>{if(bridgeHost)required(this.bridge,'native bridge').call('startlocation',2,()=>{}); - else {logNativeCall('settings','startlocation',[2],true);required(injected.settings,'settings').startlocation(2);}}; - }else{ - this.startLocation=()=>logNativeCall('settings','startlocation',[2],false); - } - required(this.countdown,'countdown').start(resolveLaunchWaitTime(this.config.remoteConfig)); - await this.startSession(true); - }catch(error){this.fail(error);} - } - /** 退出只重建账号会话;配置、原生桥和已加载资源由常驻组件保留。 */ + private game:ReturnType|null=null; + private roomPending:'create_room'|'self_join_room'|null=null; + private roomTimer:ReturnType|null=null; + private roomReceipt:{rpc:string;data:Record}|null=null; + private config:RuntimeConfig|null=null; + private policy:LoginPolicy|null=null; + private accounts:LoginAccounts|null=null; + private panel:LoginPanel|null=null; + private lobby:LobbyPanel|null=null; + private lobbyContext:LobbyContext|null=null; + private openExternal:((url:string)=>void)|null=null; + private bridge:WebViewJavascriptBridgeAdapter|null=null; + private restoreShareLogin:(()=>void)|null=null; + private restoreLocation:(()=>void)|null=null; + private startLocation:(()=>void)|null=null; + private location:unknown=null; + private loginResponse:unknown=null; + private unsubscribe:(()=>void)|null=null; + private countdown:ReturnType|null=null; + private countdownView:CocosLogoCountdownView|null=null; + private progress:ProgressBar|null=null; + private status:Label|null=null; + private device:LoginDeviceSnapshot|null=null; + private busy=false; + private terminal=false; + private returningToLogin=false; + private readonly events:Array<{kind:string;data:unknown}>=[]; + private readonly flow=new SceneFlow(name=>new Promise((resolve,reject)=>{ + if(!director.loadScene(name,error=>error?reject(error):resolve()))reject(Error(`Cannot load scene ${name}`)); + })); + + /** 常驻服务按加载场景、配置与桥接、连接、登录场景顺序启动;场景仅拥有自己的 UI。 */ + async start():Promise{ + try{ + required(this.loadingScene,'loadingScene');required(this.loginScene,'loginScene'); + required(this.lobbyScene,'lobbyScene');required(this.getComponent(LobbyAssets),'LobbyAssets'); + required(this.loadingPrefab,'loadingPrefab');required(this.loginPrefab,'loginPrefab'); + required(this.protocolPrefab,'protocolPrefab');required(this.phonePrefab,'phonePrefab'); + director.addPersistRootNode(this.node); + await this.flow.enter(this.loadingScene!.name,()=>this.bindLoading()); + if(this.terminal)return; + const injected=window as unknown as {settings?:LoginNativeSettings & {startlocation(type:number):void};app_version?:number|string;app_appversion?:number|string}; + const bridgeHost=injected.app_version!==undefined&&Number(injected.app_version)!==0; + const native=bridgeHost?{ + getcompareCode:()=>Number(injected.app_appversion), + // uAgent_3 passes the original argument, not its unused local tp variable. + accreditlogin:(provider:number)=>required(this.bridge,'native bridge').call('accreditlogin',provider===2?undefined:provider,()=>{}), + }:injected.settings; + this.config=await resolveRuntimeConfig({hostKind:bridgeHost?'uAgent_3':native?'native-settings':'h5',win:window as any, + search:window.location.search,fetcher:new HttpConfigFetcher(),localFetcher:new CocosLocalConfigFetcher()}); + if(this.terminal)return; + this.policy=resolveLoginPolicy(this.config.remoteConfig); + this.openExternal=url=>openLobbyUrl(url,bridgeHost?{kind:'bridge',call:(name,data)=>required(this.bridge,'native bridge').call(name,data,()=>{})}: + injected.settings?{kind:'settings',browser:target=>(injected.settings as unknown as {browser(url:string):void}).browser(target)}: + {kind:'h5',open:target=>{window.open(target,'_blank','noopener');}},Date.now); + const loginConfig={source:this.config.source,gameserver:this.config.gameserver,identity:this.config.identity, + showVisitor:this.policy.showVisitor,deviceLogin:this.policy.deviceLogin,qqLogin:this.policy.qqLogin}; + this.events.push({kind:'login-config',data:loginConfig}); + if(this.config.isDebugger)console.info('[PlatformStartup] login-config',JSON.stringify(loginConfig)); + this.accounts=new LoginAccounts({storage:window.localStorage,settings:native,random:Math.random,now:Date.now, + gameid:this.config.identity.gameid,agentid:this.config.identity.agentid,policy:this.policy, + submit:(account,mode)=>this.login(account,mode),tip:text=>this.tip(text)}); + this.lobbyContext={versionState:this.accounts.versionState, + launchMode:resolveLobbyLaunchMode(window.location.search,bridgeHost,(window as any).app_Launchtype)}; + const previous=(window as any).sharelogin; + const callback=(openid:unknown,headimgurl:unknown,nickname:unknown,sex:unknown,city:unknown,Province:unknown,unionid:unknown)=>{ + if(!this.terminal)this.acceptNative({openid,headimgurl,nickname,sex,city,Province,unionid},'global'); + }; + (window as any).sharelogin=callback; + this.restoreShareLogin=()=>{if((window as any).sharelogin===callback){if(previous===undefined)delete (window as any).sharelogin;else (window as any).sharelogin=previous;}}; + const oldLocation=(window as any).getlocationinfo; + const locationCallback=(data:unknown)=>{ + if(this.terminal)return; + try{ + const value=typeof data==='string'?JSON.parse(data):data; + if(value===null||typeof value!=='object'||Array.isArray(value))throw Error('Invalid getlocationinfo payload'); + this.location=value; + }catch(error){this.tip(error instanceof Error?error.message:String(error));} + }; + (window as any).getlocationinfo=locationCallback; + this.restoreLocation=()=>{if((window as any).getlocationinfo===locationCallback){if(oldLocation===undefined)delete (window as any).getlocationinfo;else (window as any).getlocationinfo=oldLocation;}}; + if(native){ + this.bridge=new WebViewJavascriptBridgeAdapter(window as unknown as WebViewJavascriptBridgeEnvironment); + this.bridge.register('sharelogin',data=>this.acceptNative(data,'bridge')); + this.bridge.register('getlocationinfo',locationCallback); + this.startLocation=()=>{if(bridgeHost)required(this.bridge,'native bridge').call('startlocation',2,()=>{}); + else {logNativeCall('settings','startlocation',[2],true);required(injected.settings,'settings').startlocation(2);}}; + }else{ + this.startLocation=()=>logNativeCall('settings','startlocation',[2],false); + } + required(this.countdown,'countdown').start(resolveLaunchWaitTime(this.config.remoteConfig)); + await this.startSession(true); + }catch(error){this.fail(error);} + } + /** 退出只重建账号会话;配置、原生桥和已加载资源由常驻组件保留。 */ private async startSession(firstStart:boolean):Promise{ - this.runtime=new PlatformRuntime({gameEntry:createLoginStageGameEntry(required(this.config,'config').identity.gameid,event=>this.events.push(event)), - resolveRuntimeConfig:async()=>required(this.config,'config'), - createWireClient:config=>{ - const wire=new WireClient({servers:config.servers,transportFactory:()=>new CocosWebSocketTransport()}); + const identity=required(this.config,'config').identity; + this.game=createTemplateGame(identity.gameid); + this.runtime=new PlatformRuntime({gameEntry:this.game.entry, + roomHistory:new OwnedRoomHistory(window.localStorage,identity.gameid,identity.agentid), + resolveRuntimeConfig:async()=>required(this.config,'config'), + createWireClient:config=>{ + const wire=new WireClient({servers:config.servers,transportFactory:()=>new CocosWebSocketTransport()}); wire.subscribe(event=>{this.events.push({kind:'wire',data:event.type}); - if(event.type==='message'&&(isLoginResponse(event.message)||(event.message.route==='agent'&&event.message.rpc==='self_join_room'))){ - const data=event.message.data as Record; - if(data.state===0&&data.shortcode){const id=config.identity; - new VipRoomHistory(window.localStorage,id.gameid,id.agentid).add(vipCode(data.shortcode,'$.data.shortcode')); + if(event.type==='message')this.lobby?.requestReceived(event.message); + if(event.type==='message'&&event.message.route==='agent'&&(event.message.rpc==='create_room'||event.message.rpc==='self_join_room')){ + this.roomReceipt={rpc:event.message.rpc,data:event.message.data as Record};this.endRoomRequest(); + } + if(event.type==='close'||event.type==='slow'||event.type==='reconnecting'){this.endRoomRequest();this.lobby?.cancelRoomPage();} + if(event.type==='close'||event.type==='slow'||event.type==='reconnecting'){this.lobby?.cancelTaskRequest();this.lobby?.cancelRankRequest();} + if(event.type==='message'&&(isLoginResponse(event.message)||(event.message.route==='agent'&&(event.message.rpc==='self_join_room'||event.message.rpc==='create_room')))){ + const data=event.message.data as Record; + if(data.state===0&&data.shortcode){const id=config.identity; + new VipRoomHistory(window.localStorage,id.gameid,id.agentid).add(vipCode(data.shortcode,'$.data.shortcode')); + } + } + if(event.type==='open'&&this.reconnecting){ + this.reconnecting=false;this.feedback?.reconnect(false); + this.waitingReply=this.busy||this.runtime!.state.players.selfPlayerId!==null; + this.feedback?.loading(this.waitingReply); + } + if(event.type==='message'&&isLoginResponse(event.message)){ + this.loginResponse=event.message.data; + this.waitingReply=false;this.reconnecting=false; + this.feedback?.loading(false);this.feedback?.reconnect(false); + } + });return wire; + }, + scene:{showLoading:()=>{},showLogin:()=>{void this.showLogin().catch(error=>this.fail(error));}, + showLobby:()=>{void this.showLobby().catch(error=>this.fail(error));}, + showRoom:()=>{void this.showRoom().catch(error=>this.fail(error));}, + showReconnect:()=>{this.waitingReply=false;this.feedback?.loading(false);this.reconnecting=true;this.feedback?.reconnect(true);}, + showTips:(message,time,source)=>{if(source==='show_message'){this.endRoomRequest();this.lobby?.cancelTaskRequest();this.lobby?.cancelInviteRequest();this.lobby?.cancelRankRequest();}this.tip(message,time);}, + showKicked:data=>{ + const packet=data as {msg?:string;fromOther?:unknown;gameid?:unknown}; + const text=packet.msg||`网络出现异常!${packet.fromOther?(packet.gameid==='G2hw0ubng0zcoI0r4mx3H2yr4GejidwO'?2:1):0}#(请重启游戏)`; + this.fail(Error(text)); + }, + showServerDenial:outcome=>{ + this.busy=false;this.panel?.setBusy(false); + // Desk.login explicitly defines an empty/missing msg as 登录失败. + if(outcome.rpc==='player_login'){ + this.loginResponse=null; + const message=outcome.data.msg; + if(message!=null&&typeof message!=='string')throw Error('player_login.msg must be a string'); + this.tip(message||'登录失败'); + }else{ + this.endRoomRequest();this.roomReceipt=null; + if(Number(outcome.data.showerror)===1){ + if(typeof outcome.data.error!=='string')throw Error(`${outcome.rpc}.error must be a string`); + this.tip(outcome.data.error); } } - if(event.type==='open'&&this.reconnecting){ - this.reconnecting=false;this.feedback?.reconnect(false); - this.waitingReply=this.busy||this.runtime!.state.players.selfPlayerId!==null; - this.feedback?.loading(this.waitingReply); - } - if(event.type==='message'&&isLoginResponse(event.message)){ - this.loginResponse=event.message.data; - this.waitingReply=false;this.reconnecting=false; - this.feedback?.loading(false);this.feedback?.reconnect(false); - } - });return wire; - }, - scene:{showLoading:()=>{},showLogin:()=>{void this.showLogin().catch(error=>this.fail(error));}, - showLobby:()=>{void this.showLobby().catch(error=>this.fail(error));}, - showRoom:()=>{this.saveAcceptedLogin();this.fail(Error('当前入口尚未接入子游戏场景'));}, - showReconnect:()=>{this.waitingReply=false;this.feedback?.loading(false);this.reconnecting=true;this.feedback?.reconnect(true);}, - showTips:(message,time,source)=>{if(source==='show_message')this.lobby?.cancelTaskRequest();this.tip(message,time);}, - showKicked:data=>{ - const packet=data as {msg?:string;fromOther?:unknown;gameid?:unknown}; - const text=packet.msg||`网络出现异常!${packet.fromOther?(packet.gameid==='G2hw0ubng0zcoI0r4mx3H2yr4GejidwO'?2:1):0}#(请重启游戏)`; - this.fail(Error(text)); - }, - showServerDenial:outcome=>{ - this.busy=false;this.panel?.setBusy(false); - // Desk.login explicitly defines an empty/missing msg as 登录失败. - if(outcome.rpc==='player_login'){ - this.loginResponse=null; - const message=outcome.data.msg; - if(message!=null&&typeof message!=='string')throw Error('player_login.msg must be a string'); - this.tip(message||'登录失败'); - }else this.tip(JSON.stringify(outcome.data)); - }, - showFatal:error=>this.fail(error)}, - loadResources:()=>firstStart?this.loadLoginResources():Promise.resolve(), - waitForMinimumDisplay:()=>firstStart?required(this.countdown,'countdown').promise:Promise.resolve(), - getLoginDeviceSnapshot:()=>required(this.device,'prepared login device'), - }); - this.unsubscribe=this.runtime.subscribeState(state=>{ - if(firstStart)this.countdown?.socketReady(state.app.phase==='connected'||state.app.phase==='logged-in'); - this.lobby?.render(state); - },error=>this.fail(error)); - await this.runtime.start(); - } - private canvas():Node{return required(director.getScene(),'scene').getChildByName('Canvas') ?? (()=>{throw Error('Scene requires Canvas');})();} - private createStatus():void{ - const canvas=this.canvas();const node=new Node('Status');node.parent=canvas;node.layer=canvas.layer; - node.setPosition(0,315,0);node.addComponent(UITransform).setContentSize(1200,60); - this.status=node.addComponent(Label);this.status.string='';this.status.fontSize=24;this.status.lineHeight=28; - this.feedback?.dispose(); - this.feedback=new FeedbackPanels(canvas,{tips:required(this.tipsPrefab,'tipsPrefab'),loading:required(this.waitPrefab,'waitPrefab'), - reconnect:required(this.reconnectPrefab,'reconnectPrefab'),kick:required(this.kickPrefab,'kickPrefab')}); - this.feedback.loading(this.waitingReply);this.feedback.reconnect(this.reconnecting); - } - private async bindLoading():Promise{ - const logo=instantiate(required(this.loadingPrefab,'loadingPrefab'));logo.parent=this.canvas();logo.setPosition(0,0,0); - this.createStatus();requireNode(logo,'group-32').active=false; - this.progress=required(requireNode(logo,'group-1/Spirit254').getComponent(ProgressBar),'Logo ProgressBar'); - const labels=logo.getComponentsInChildren(Label).filter(label=>label.node.name==='Spirit3104'); - if(labels.length!==1)throw Error('Logo requires one Spirit3104'); - this.countdownView=new CocosLogoCountdownView(requireNode(logo,'group-1/Spirit3103'),labels[0].node,()=>this.countdown?.skip()); - this.countdown=createLogoCountdown({now:()=>performance.now(),schedule:(callback,ms)=>{const id=window.setTimeout(callback,ms);return ()=>window.clearTimeout(id);}, - render:state=>this.countdownView?.render(state)}); - await new Promise(resolve=>director.once(Director.EVENT_AFTER_DRAW,resolve)); - this.events.push({kind:'scene',data:'Loading'}); - } - private async loadLoginResources():Promise{ - const prefab=required(this.loginPrefab,'loginPrefab'); - await new Promise((resolve,reject)=>assetManager.loadAny(prefab.uuid,(done,total)=>{ - if(!this.terminal&&this.progress&&total>0)this.progress.progress=done/total; - },error=>error?reject(error):resolve())); - if(this.terminal)throw Error('Startup stopped'); - this.countdown?.resourcesReady();required(this.progress,'progress').node.active=false; - } - /** 完成真实场景切换后再绑定按钮和尝试缓存登录;弹窗挂在独立 PopupRoot。 */ - private async showLogin():Promise{ - this.countdownView?.dispose();this.countdownView=null; - await this.flow.enter(required(this.loginScene,'loginScene').name,()=>{ - const root=instantiate(required(this.loginPrefab,'loginPrefab'));root.parent=this.canvas();root.setPosition(0,0,0); - const popup=new Node('PopupRoot');popup.parent=this.canvas();popup.layer=root.layer;popup.addComponent(UITransform).setContentSize(1600,720); - this.createStatus();this.panel=new LoginPanel(root,popup,required(this.protocolPrefab,'protocolPrefab'),required(this.phonePrefab,'phonePrefab'), - required(this.accounts,'accounts'),required(this.policy,'policy'),message=>this.tip(message), - ()=>!this.busy&&!this.terminal&&this.runtime?.state.app.phase==='connected'); - const version=required(requireNode(root,'group-2/版本显示').getComponent(Label),'version Label'); - // Beta 展示格式使用当前渠道版本号;只改变显示,不改变登录协议中的数值版本。 - version.string=`v${required(this.config,'config').identity.version}.0.0 Beta`; - // Label 自动尺寸更新后重新计算左上角 Widget,避免长版本文字向屏幕外扩展。 - required(version.node.getComponent(UITransform),'version UITransform').setAnchorPoint(0,1); - version.updateRenderData(true); - const versionWidget=required(version.node.getComponent(Widget),'version Widget'); - versionWidget.target=this.canvas(); - versionWidget.updateAlignment(); - this.events.push({kind:'scene',data:'Login'}); - // Legacy location acquisition is independent of login. Keep the source null - // until a real callback arrives; report host failure without stopping the socket. - try{this.startLocation?.();}catch(error){this.tip(`定位接口调用失败:${error instanceof Error?error.message:String(error)}`);} - if(this.accounts!.cached())this.accounts!.autoLogin(); - }); - } - private acceptNative(data:unknown,channel:'global'|'bridge'):void{ - if(this.terminal)return; - try{required(this.accounts,'accounts').acceptNative(data,channel);}catch(error){this.tip(error instanceof Error?error.message:String(error));} - } - private saveAcceptedLogin():void{ - if(this.loginResponse!==null){ - required(this.accounts,'accounts').loginSucceeded(required(this.loginResponse,'login response'),required(this.device,'prepared login device').deviceLogin); - this.loginResponse=null; - this.busy=false; - } - } - private async showLobby():Promise{ - if(this.terminal)return; - this.saveAcceptedLogin(); - this.panel?.dispose();this.panel=null; - await this.flow.enterOnce(required(this.lobbyScene,'lobbyScene').name,()=>{ - this.createStatus(); - const popup=new Node('PopupRoot');popup.parent=this.canvas();popup.layer=this.canvas().layer; - popup.addComponent(UITransform).setContentSize(1600,720); - this.lobby=new LobbyPanel(this.canvas(),popup,required(this.getComponent(LobbyAssets),'LobbyAssets'), - required(this.config,'config'),resolveLobbyPolicy(required(this.config,'config').remoteConfig), - required(this.runtime,'runtime').state,message=>this.tip(message),()=>this.logout(),()=>this.location,url=>required(this.openExternal,'browser host')(url), - resolveLobbyMenuConfig(required(this.config,'config').remoteConfig),required(this.lobbyContext,'lobby context'),action=>{ - this.events.push({kind:'lobby-action',data:action}); + }, + showFatal:error=>this.fail(error)}, + loadResources:()=>firstStart?this.loadLoginResources():Promise.resolve(), + waitForMinimumDisplay:()=>firstStart?required(this.countdown,'countdown').promise:Promise.resolve(), + getLoginDeviceSnapshot:()=>required(this.device,'prepared login device'), + }); + this.unsubscribe=this.runtime.subscribeState(state=>{ + if(firstStart)this.countdown?.socketReady(state.app.phase==='connected'||state.app.phase==='logged-in'); + this.lobby?.render(state); + },error=>this.fail(error)); + await this.runtime.start(); + } + private canvas():Node{return required(director.getScene(),'scene').getChildByName('Canvas') ?? (()=>{throw Error('Scene requires Canvas');})();} + private createStatus():void{ + const canvas=this.canvas();const node=new Node('Status');node.parent=canvas;node.layer=canvas.layer; + node.setPosition(0,315,0);node.addComponent(UITransform).setContentSize(1200,60); + this.status=node.addComponent(Label);this.status.string='';this.status.fontSize=24;this.status.lineHeight=28; + this.feedback?.dispose(); + this.feedback=new FeedbackPanels(canvas,{tips:required(this.tipsPrefab,'tipsPrefab'),loading:required(this.waitPrefab,'waitPrefab'), + reconnect:required(this.reconnectPrefab,'reconnectPrefab'),kick:required(this.kickPrefab,'kickPrefab')}); + this.feedback.loading(this.waitingReply);this.feedback.reconnect(this.reconnecting); + } + private async bindLoading():Promise{ + const logo=instantiate(required(this.loadingPrefab,'loadingPrefab'));logo.parent=this.canvas();logo.setPosition(0,0,0); + this.createStatus(); + this.progress=required(requireNode(logo,'group-1/Spirit254').getComponent(ProgressBar),'Logo ProgressBar'); + const labels=logo.getComponentsInChildren(Label).filter(label=>label.node.name==='Spirit3104'); + if(labels.length!==1)throw Error('Logo requires one Spirit3104'); + this.countdownView=new CocosLogoCountdownView(requireNode(logo,'group-1/Spirit3103'),labels[0].node,()=>this.countdown?.skip()); + this.countdown=createLogoCountdown({now:()=>performance.now(),schedule:(callback,ms)=>{const id=window.setTimeout(callback,ms);return ()=>window.clearTimeout(id);}, + render:state=>this.countdownView?.render(state)}); + await new Promise(resolve=>director.once(Director.EVENT_AFTER_DRAW,resolve)); + this.events.push({kind:'scene',data:'Loading'}); + } + private async loadLoginResources():Promise{ + const prefab=required(this.loginPrefab,'loginPrefab'); + await new Promise((resolve,reject)=>assetManager.loadAny(prefab.uuid,(done,total)=>{ + if(!this.terminal&&this.progress&&total>0)this.progress.progress=done/total; + },error=>error?reject(error):resolve())); + if(this.terminal)throw Error('Startup stopped'); + this.countdown?.resourcesReady();required(this.progress,'progress').node.active=false; + } + /** 完成真实场景切换后再绑定按钮和尝试缓存登录;弹窗挂在独立 PopupRoot。 */ + private async showLogin():Promise{ + this.countdownView?.dispose();this.countdownView=null; + await this.flow.enter(required(this.loginScene,'loginScene').name,()=>{ + const root=instantiate(required(this.loginPrefab,'loginPrefab'));root.parent=this.canvas();root.setPosition(0,0,0); + const popup=new Node('PopupRoot');popup.parent=this.canvas();popup.layer=root.layer;popup.addComponent(UITransform).setContentSize(1600,720); + this.createStatus();this.panel=new LoginPanel(root,popup,required(this.protocolPrefab,'protocolPrefab'),required(this.phonePrefab,'phonePrefab'), + required(this.accounts,'accounts'),required(this.policy,'policy'),message=>this.tip(message), + ()=>!this.busy&&!this.terminal&&this.runtime?.state.app.phase==='connected'); + const version=required(requireNode(root,'group-2/版本显示').getComponent(Label),'version Label'); + // Beta 展示格式使用当前渠道版本号;只改变显示,不改变登录协议中的数值版本。 + version.string=`v${required(this.config,'config').identity.version}.0.0 Beta`; + // Label 自动尺寸更新后重新计算左上角 Widget,避免长版本文字向屏幕外扩展。 + required(version.node.getComponent(UITransform),'version UITransform').setAnchorPoint(0,1); + version.updateRenderData(true); + const versionWidget=required(version.node.getComponent(Widget),'version Widget'); + versionWidget.target=this.canvas(); + versionWidget.updateAlignment(); + this.events.push({kind:'scene',data:'Login'}); + // Legacy location acquisition is independent of login. Keep the source null + // until a real callback arrives; report host failure without stopping the socket. + try{this.startLocation?.();}catch(error){this.tip(`定位接口调用失败:${error instanceof Error?error.message:String(error)}`);} + if(this.accounts!.cached())this.accounts!.autoLogin(); + }); + } + private acceptNative(data:unknown,channel:'global'|'bridge'):void{ + if(this.terminal)return; + try{required(this.accounts,'accounts').acceptNative(data,channel);}catch(error){this.tip(error instanceof Error?error.message:String(error));} + } + private saveAcceptedLogin():void{ + if(this.loginResponse!==null){ + required(this.accounts,'accounts').loginSucceeded(required(this.loginResponse,'login response'),required(this.device,'prepared login device').deviceLogin); + this.loginResponse=null; + this.busy=false; + } + } + private async showLobby():Promise{ + if(this.terminal)return; + this.saveAcceptedLogin(); + this.panel?.dispose();this.panel=null; + await this.flow.enterOnce(required(this.lobbyScene,'lobbyScene').name,()=>{ + this.createStatus(); + const popup=new Node('PopupRoot');popup.parent=this.canvas();popup.layer=this.canvas().layer; + popup.addComponent(UITransform).setContentSize(1600,720); + this.lobby=new LobbyPanel(this.canvas(),popup,required(this.getComponent(LobbyAssets),'LobbyAssets'), + required(this.config,'config'),resolveLobbyPolicy(required(this.config,'config').remoteConfig), + required(this.runtime,'runtime').state,message=>this.tip(message),()=>this.logout(),()=>this.location,url=>required(this.openExternal,'browser host')(url), + resolveLobbyMenuConfig(required(this.config,'config').remoteConfig),required(this.lobbyContext,'lobby context'),action=>{ + this.events.push({kind:'lobby-action',data:action}); if(action==='tasks'){required(this.runtime,'runtime').getPlayerTasks();return;} - this.tip(required(LOBBY_UNAVAILABLE[action],`lobby action handler ${action}`)); - },taskid=>required(this.runtime,'runtime').claimTask(taskid),(name,data)=>this.callNative(name,data),{ - request:codes=>required(this.runtime,'runtime').getVipRooms(codes), - join:code=>required(this.runtime,'runtime').joinVipRoom(code,this.location), + if(action==='create-room'){required(this.lobby,'lobby').openCreate();return;} + this.tip(required(LOBBY_UNAVAILABLE[action],`lobby action handler ${action}`)); + },taskid=>required(this.runtime,'runtime').claimTask(taskid),(name,data)=>this.callNative(name,data),{ + request:codes=>required(this.runtime,'runtime').getVipRooms(codes), + join:code=>this.requestRoom('self_join_room',()=>required(this.runtime,'runtime').joinVipRoom(code,this.location)), describe:()=> '子游戏规则描述尚未接入', + },{ + query:()=>required(this.runtime,'runtime').getPlayerInvitecode(), + bind:code=>required(this.runtime,'runtime').bindInvitecode(code), + paste:()=>this.pasteInvitecode(), + },{ + rank:()=>required(this.runtime,'runtime').getTreasureList(), + loading:show=>required(this.feedback,'feedback').loading(show), + vipRank:limit=>required(this.runtime,'runtime').getVipRankList(limit), + joinOwned:code=>this.requestRoom('self_join_room',()=>required(this.runtime,'runtime').joinOwnedRoom(code,this.location)), + redeem:cardno=>required(this.runtime,'runtime').redeemCard(cardno), + paste:()=>this.pasteInvitecode(true), + room:{createPrefab:required(required(this.getComponent(SubgameAssets),'SubgameAssets').createRoom,'subgame create prefab'), + join:roomcode=>this.requestRoom('self_join_room',()=>required(this.runtime,'runtime').joinRoom({roomcode,...this.roomEnvironment()})), + create:roomtype=>this.requestRoom('create_room',()=>required(this.runtime,'runtime').createRoom({roomtype,...this.roomEnvironment()})), + busy:()=>this.roomPending!==null,close:()=>this.feedback?.loading(false)}, }); - popup.setSiblingIndex(this.canvas().children.length-1); - required(this.status,'status').node.setSiblingIndex(this.canvas().children.length-1); - this.events.push({kind:'scene',data:'Lobby'}); - }); + popup.setSiblingIndex(this.canvas().children.length-1); + required(this.status,'status').node.setSiblingIndex(this.canvas().children.length-1); + this.events.push({kind:'scene',data:'Lobby'}); + }); this.tip(''); } - private callNative(name:string,data:Record):void { - const win=window as unknown as {settings?:Record;app_version?:number|string}; - if(win.app_version!==undefined&&Number(win.app_version)!==0){ - const bridge=this.bridge; - if(bridge?.isReady()){bridge.call(name as NativeCallName,data,()=>{});return;} - logNativeCall('WVJB',name,[data],false);return; + private roomEnvironment():{ip:string;location:unknown}{ + const state=required(this.runtime,'runtime').state; + const self=required(state.players.entities[required(state.players.selfPlayerId,'self player id')],'self player'); + if(typeof self.ip!=='string')throw Error('Authenticated player is missing ip'); + return {ip:self.ip,location:this.location}; + } + private requestRoom(rpc:'create_room'|'self_join_room',send:()=>void):void{ + if(this.roomPending!==null)return; + const state=required(this.runtime,'runtime').state; + if(state.app.phase!=='logged-in'||state.room.kind!=='outside')throw Error('当前连接状态无法进入房间'); + this.roomPending=rpc;this.lobby?.setRoomBusy(true);this.feedback?.loading(true); + this.roomTimer=setTimeout(()=>{this.endRoomRequest();this.tip('房间请求超时,请重试');},15000); + try{send();}catch(error){this.endRoomRequest();throw error;} + } + private endRoomRequest():void{ + if(this.roomTimer!==null)clearTimeout(this.roomTimer);this.roomTimer=null; + if(this.roomPending!==null){this.roomPending=null;this.lobby?.setRoomBusy(false);this.feedback?.loading(false);} + } + private async showRoom():Promise{ + if(this.terminal)return; + this.saveAcceptedLogin();this.endRoomRequest(); + // This callback runs only after the runtime accepted and committed the packet. + const receipt=this.roomReceipt;this.roomReceipt=null; + if(receipt&&Number(receipt.data.state)===0){ + const identity=required(this.config,'config').identity; + if(receipt.rpc==='create_room')window.localStorage.setItem('tsgame_roomtype_'+identity.gameid+identity.agentid,JSON.stringify({data:receipt.data.roomtype})); + if(receipt.rpc==='self_join_room'&&receipt.data.shortcode)window.localStorage.setItem('tsgame_shortcode_'+identity.gameid+identity.agentid,String(receipt.data.shortcode)); } - const args=name==='friendsSharetypeUrlToptitleDescript'? - [data.sharefriend,data.type,data.webpageUrl,data.title,data.description]:[data]; - const method=win.settings?.[name]; - logNativeCall('settings',name,args,typeof method==='function'); - if(typeof method==='function')Reflect.apply(method,win.settings,args); + this.panel?.dispose();this.panel=null;this.lobby?.dispose();this.lobby=null; + const assets=required(this.getComponent(SubgameAssets),'SubgameAssets'); + await this.flow.enter(required(assets.roomScene,'subgame room scene').name,()=>{ + if(required(this.runtime,'runtime').state.room.kind!=='inside')return; + const root=instantiate(required(assets.room,'subgame room prefab'));root.parent=this.canvas();root.setPosition(0,0,0); + const view=required(root.getComponent(TemplateRoomView),'TemplateRoomView'); + root.on('room-action-error',(error:unknown)=>this.tip(error instanceof Error?error.message:String(error))); + root.on('room-action-timeout',()=>this.tip('房间操作超时,请重试')); + this.createStatus();required(this.game,'template game').mount(view);this.events.push({kind:'scene',data:'Room'}); + }); } + private async pasteInvitecode(raw=false):Promise{ + const readText=raw?readClipboardText:readInviteClipboard; + const win=window as unknown as {settings?:{gamepastetext?:()=>unknown};app_version?:number|string}; + if(win.app_version!==undefined&&Number(win.app_version)!==0){ + const bridge=required(this.bridge,'clipboard bridge'); + return readText({kind:'bridge',call:(name,data,callback)=>bridge.call(name,data,callback)}); + } + if(win.settings){ + const read=required(win.settings.gamepastetext,'settings.gamepastetext'); + return readText({kind:'settings',read:()=>read.call(win.settings)}); + } + if(!navigator.clipboard?.readText)throw Error('当前环境无法读取剪贴板,请手动输入'); + return readText({kind:'browser',read:()=>navigator.clipboard.readText()}); + } + private callNative(name:string,data:Record):void { + const win=window as unknown as {settings?:Record;app_version?:number|string}; + if(win.app_version!==undefined&&Number(win.app_version)!==0){ + const bridge=this.bridge; + if(bridge?.isReady()){bridge.call(name as NativeCallName,data,()=>{});return;} + logNativeCall('WVJB',name,[data],false);return; + } + const args=name==='friendsSharetypeUrlToptitleDescript'? + [data.sharefriend,data.type,data.webpageUrl,data.title,data.description]:[data]; + const method=win.settings?.[name]; + logNativeCall('settings',name,args,typeof method==='function'); + if(typeof method==='function')Reflect.apply(method,win.settings,args); + } private logout():void{ - if(this.returningToLogin||this.terminal)return; + if(this.returningToLogin||this.terminal)return; this.returningToLogin=true; - required(this.accounts,'accounts').logout(); - this.unsubscribe?.();this.unsubscribe=null; - this.lobby?.dispose();this.lobby=null; - this.panel?.dispose();this.panel=null; - this.runtime?.stop();this.runtime=null; - this.waitingReply=false;this.reconnecting=false;this.feedback?.loading(false);this.feedback?.reconnect(false);this.feedback?.closeTips(); - this.busy=false;this.device=null;this.loginResponse=null; - void this.startSession(false).catch(error=>this.fail(error)).finally(()=>{this.returningToLogin=false;}); - } - /** 所有登录最终收口:再次校验协议与就绪状态,保存本次模式供登录响应使用。 */ - private login(account:LoginAccountIdentity,mode:DeviceLoginMode):void{ - const runtime=required(this.runtime,'runtime'); - if(this.busy||this.terminal)return; - if(!required(this.accounts,'accounts').requireAgreement())return; - if(!runtime.ready)throw Error('登录资源或连接尚未就绪'); - const storage=window.localStorage; - let machineid=storage.getItem('tsgame_machineId_'); - if(machineid===null){machineid=`${Date.now()}-${1000+Math.floor(Math.random()*8999)}`;storage.setItem('tsgame_machineId_',machineid);} - const host=window as unknown as {returnCitySN?:{ip:string;province:string;city:string}}; - const city=host.returnCitySN; - if(city!==undefined&&(!city||typeof city.ip!=='string'||typeof city.province!=='string'||typeof city.city!=='string'))throw Error('Invalid returnCitySN'); - this.device={location:this.location,machineid,machineroom:'',deviceLogin:mode,loginPlayerId:{enabled:false},...(city?{ip:city.ip}:{})}; - this.busy=true;this.panel?.setBusy(true); - this.waitingReply=true;this.feedback?.loading(true); - try{runtime.login(city?{...account,province:city.province,city:city.city}:account);} - catch(error){this.busy=false;this.waitingReply=false;this.feedback?.loading(false);this.panel?.setBusy(false);throw error;} - } - private tip(text:string,time=3000):void{ - if(text)this.feedback?.showTips(text,time); - if(this.status&&isValid(this.status))this.status.string=''; - } - private fail(error:unknown):void{ - console.error('[PlatformStartup]',error); - try{this.feedback?.kick(error instanceof Error?error.message:String(error));} - finally{this.release();} - } - /** 幂等释放计时器、订阅和宿主回调,阻止销毁后的迟到消息重新激活流程。 */ + this.endRoomRequest();this.roomReceipt=null; + required(this.accounts,'accounts').logout(); + this.unsubscribe?.();this.unsubscribe=null; + this.lobby?.dispose();this.lobby=null; + this.panel?.dispose();this.panel=null; + this.runtime?.stop();this.runtime=null; + this.waitingReply=false;this.reconnecting=false;this.feedback?.loading(false);this.feedback?.reconnect(false);this.feedback?.closeTips(); + this.busy=false;this.device=null;this.loginResponse=null; + void this.startSession(false).catch(error=>this.fail(error)).finally(()=>{this.returningToLogin=false;}); + } + /** 所有登录最终收口:再次校验协议与就绪状态,保存本次模式供登录响应使用。 */ + private login(account:LoginAccountIdentity,mode:DeviceLoginMode):void{ + const runtime=required(this.runtime,'runtime'); + if(this.busy||this.terminal)return; + if(!required(this.accounts,'accounts').requireAgreement())return; + if(!runtime.ready)throw Error('登录资源或连接尚未就绪'); + const storage=window.localStorage; + let machineid=storage.getItem('tsgame_machineId_'); + if(machineid===null){machineid=`${Date.now()}-${1000+Math.floor(Math.random()*8999)}`;storage.setItem('tsgame_machineId_',machineid);} + const host=window as unknown as {returnCitySN?:{ip:string;province:string;city:string}}; + const city=host.returnCitySN; + if(city!==undefined&&(!city||typeof city.ip!=='string'||typeof city.province!=='string'||typeof city.city!=='string'))throw Error('Invalid returnCitySN'); + this.device={location:this.location,machineid,machineroom:'',deviceLogin:mode,loginPlayerId:{enabled:false},...(city?{ip:city.ip}:{})}; + this.busy=true;this.panel?.setBusy(true); + this.waitingReply=true;this.feedback?.loading(true); + try{runtime.login(city?{...account,province:city.province,city:city.city}:account);} + catch(error){this.busy=false;this.waitingReply=false;this.feedback?.loading(false);this.panel?.setBusy(false);throw error;} + } + private tip(text:string,time=3000):void{ + if(text)this.feedback?.showTips(text,time); + if(this.status&&isValid(this.status))this.status.string=''; + } + private fail(error:unknown):void{ + console.error('[PlatformStartup]',error); + try{this.feedback?.kick(error instanceof Error?error.message:String(error));} + finally{this.release();} + } + /** 幂等释放计时器、订阅和宿主回调,阻止销毁后的迟到消息重新激活流程。 */ private release():void{ - if(this.terminal)return;this.terminal=true;this.flow.dispose();this.panel?.dispose();this.lobby?.dispose(); - this.countdown?.cancel();this.countdownView?.dispose();this.unsubscribe?.();this.bridge?.dispose(); - this.restoreShareLogin?.();this.restoreLocation?.();this.runtime?.stop(); - } - onDestroy():void{this.release();this.feedback?.dispose();} - getDiagnostics(){return {events:[...this.events],ready:this.runtime?.ready,busy:this.busy,terminal:this.terminal};} -} + if(this.terminal)return;this.terminal=true;this.flow.dispose();this.panel?.dispose();this.lobby?.dispose(); + this.endRoomRequest();this.countdown?.cancel();this.countdownView?.dispose();this.unsubscribe?.();this.bridge?.dispose(); + this.restoreShareLogin?.();this.restoreLocation?.();this.runtime?.stop(); + } + onDestroy():void{this.release();this.feedback?.dispose();} + getDiagnostics(){return {events:[...this.events],ready:this.runtime?.ready,busy:this.busy,terminal:this.terminal};} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/ProfileInfoListView.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/ProfileInfoListView.ts new file mode 100644 index 0000000..59e90d4 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/ProfileInfoListView.ts @@ -0,0 +1,41 @@ +import {_decorator,Component,Label,Layout,Node,Prefab,ScrollView,UITransform,Vec2,instantiate} from 'cc'; +const {ccclass,property}=_decorator; +function required(value:T|null,name:string):T{if(value===null)throw Error(`ProfileInfoListView requires ${name}`);return value;} + +/** The prefab owns typography, spacing and viewport dimensions. Data owns the row list. */ +@ccclass('ProfileInfoListView') +export class ProfileInfoListView extends Component { + @property(Prefab) textItemPrefab:Prefab|null=null; + @property(Prefab) authPrefab:Prefab|null=null; + @property(Node) content:Node|null=null; + @property(ScrollView) scroll:ScrollView|null=null; + @property(Label) signature:Label|null=null; + @property(UITransform) signatureBox:UITransform|null=null; + private rows:Label[]=[]; + private signatureMinimumHeight:number|null=null; + + render(texts:readonly string[],sign:string):void { + const content=required(this.content,'content'),scroll=required(this.scroll,'ScrollView'); + const signature=required(this.signature,'signature'),box=required(this.signatureBox,'signature background'); + const item=required(this.textItemPrefab,'text item prefab'),layout=required(content.getComponent(Layout),'vertical Layout'); + if(layout.type!==Layout.Type.VERTICAL||layout.resizeMode!==Layout.ResizeMode.CONTAINER)throw Error('ProfileInfoListView requires a vertical container Layout'); + if(this.signatureMinimumHeight===null)this.signatureMinimumHeight=box.height; + const offset=scroll.getScrollOffset(); + while(this.rows.length>texts.length){const row=this.rows.pop()!;row.node.removeFromParent();row.node.destroy();} + while(this.rows.length{ + label.node.setSiblingIndex(index);label.string=texts[index];label.updateRenderData(true); + }); + signature.string=sign;signature.updateRenderData(true); + const textArea=required(signature.getComponent(UITransform),'signature UITransform'); + // The authored text inset is symmetric; the serialized background sets the minimum height. + box.height=Math.max(this.signatureMinimumHeight,textArea.height-2*signature.node.position.y); + box.node.setSiblingIndex(this.rows.length); + layout.updateLayout(true); + scroll.stopAutoScroll(); + scroll.scrollToOffset(new Vec2(0,Math.max(0,Math.min(offset.y,scroll.getMaxScrollOffset().y))),0); + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/ProfileInfoListView.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/ProfileInfoListView.ts.meta new file mode 100644 index 0000000..85db403 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/ProfileInfoListView.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "b1570b17-a411-4b15-a66f-256d865d61e5", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/RankListView.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/RankListView.ts new file mode 100644 index 0000000..f7302f6 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/RankListView.ts @@ -0,0 +1,58 @@ +import {assetManager,Button,ImageAsset,Label,Layout,Node,Prefab,ScrollView,Sprite,SpriteFrame,Texture2D,UITransform,instantiate,isValid} from 'cc'; +import type {RankEntry} from '../../framework/protocol/contracts/rank-list.ts'; +import {requireNode} from './node-bindings.ts'; +function need(v:T|null,name:string):T{if(v===null)throw Error(`Rank list missing ${name}`);return v;} + +/** Rows instantiate the editor-owned VipRankItem; ScrollView and Layout own geometry. */ +export class RankListView { + private disposed=false; + private generation=0; + private images:Array<{frame:SpriteFrame;texture:Texture2D}>=[]; + constructor(private readonly group:Node,entries:readonly RankEntry[],starName:string,medals:readonly SpriteFrame[],title:SpriteFrame,itemPrefab:Prefab,vip=false){ + if(entries.length===0)throw Error('Cannot open empty rank list'); + if(medals.length!==3)throw Error('Rank list requires three medal frames'); + const node=(id:number)=>requireNode(group,`Spirit${id}`); + need(node(3234).getComponent(Sprite),'title Sprite').spriteFrame=title; + node(609).active=false; + this.text(640,''); + const scrolls=group.getComponentsInChildren(ScrollView); + if(scrolls.length!==1)throw Error('Rank list requires one authored ScrollView'); + const scroll=scrolls[0],content=need(scroll.content,'ScrollView content'); + const layout=need(content.getComponent(Layout),'content Layout'); + if(layout.type!==Layout.Type.VERTICAL||layout.resizeMode!==Layout.ResizeMode.CONTAINER)throw Error('Rank content requires vertical container Layout'); + for(const child of [...content.children])child.destroy(); + const choose=(index:number)=>{ + const row=entries[index];this.text(636,row.name);this.text(637,`ID:${row.playerid}`);this.text(638,String(row.star)); + this.text(640,vip?'公告:\n'+(row.destwo?'('+row.destwo+')\n':'')+(row.notice||''):''); + this.text(641,row.signature?`个性签名:\n${row.signature}`:'个性签名:'); + const sig=need(node(641).getComponent(Label),'signature');sig.enableWrapText=true;sig.overflow=Label.Overflow.RESIZE_HEIGHT; + need(sig.node.getComponent(UITransform),'signature transform').width=400; + const token=++this.generation;this.avatar(node(634),row.headimg,()=>token===this.generation); + }; + entries.forEach((entry,index)=>{ + const row=instantiate(itemPrefab);row.name='VipRankItem'+(index+1);row.parent=content;row.active=true; + const itemNode=(id:number)=>requireNode(row,'Spirit'+id); + const medal=itemNode(629);medal.active=index<3; + if(index<3)need(medal.getComponent(Sprite),'medal').spriteFrame=medals[index]; + this.avatar(itemNode(630),entry.headimg,()=>true); + need(itemNode(632).getComponent(Label),'name').string=entry.name; + need(itemNode(633).getComponent(Label),'star').string=starName+':'+entry.star; + const button=row.getComponent(Button)||row.addComponent(Button);button.transition=Button.Transition.NONE; + row.on(Button.EventType.CLICK,()=>choose(index)); + }); + layout.updateLayout(); + choose(0);scroll.scrollToTop(0); + } + private text(id:number,value:string):void{need(requireNode(this.group,`Spirit${id}`).getComponent(Label),`Spirit${id} Label`).string=value;} + private avatar(node:Node,url:string,current:()=>boolean):void{ + const sprite=need(node.getComponent(Sprite),'avatar');sprite.spriteFrame=null;sprite.sizeMode=Sprite.SizeMode.CUSTOM; + if(!url)return; + assetManager.loadRemote(url,{ext:'.png'},(error,image)=>{ + if(this.disposed||!isValid(node)||!current())return; + if(error){this.group.emit('rank-image-error',error.message);return;} + const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame; + this.images.push({texture,frame}); + }); + } + dispose():void{this.disposed=true;for(const {frame,texture} of this.images){frame.destroy();texture.destroy();}this.images=[];} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/RankListView.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/RankListView.ts.meta new file mode 100644 index 0000000..841c6c1 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/RankListView.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "9186a6b3-f2f5-4b75-aeba-a958033141c3", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/SubgameAssets.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/SubgameAssets.ts new file mode 100644 index 0000000..38f1ab8 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/SubgameAssets.ts @@ -0,0 +1,9 @@ +import {_decorator,Component,Prefab,SceneAsset} from 'cc'; +const {ccclass,property}=_decorator; +/** App composition root: each game assigns its own pre-room page, room view and scene. */ +@ccclass('SubgameAssets') +export class SubgameAssets extends Component { + @property(Prefab) createRoom:Prefab|null=null; + @property(Prefab) room:Prefab|null=null; + @property(SceneAsset) roomScene:SceneAsset|null=null; +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/SubgameAssets.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/SubgameAssets.ts.meta new file mode 100644 index 0000000..64331f2 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/SubgameAssets.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "ec19e470-cf36-40e8-b420-e4314e5fbf07", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/feedback-panels.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/feedback-panels.ts index 7ef9465..9e7dbc5 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/feedback-panels.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/feedback-panels.ts @@ -1,66 +1,68 @@ -import {Animation, BlockInputEvents, Label, Node, Prefab, Tween, UITransform, Widget, instantiate, isValid, tween} from 'cc'; -import {requireNode} from './node-bindings.ts'; - -/** Original layers 612/614/615/616. Scene-independent state is owned by PlatformStartup. */ -export class FeedbackPanels { - private timer:ReturnType|null=null; - private nodes:Node[]=[]; - private tipNode:Node; - private loadingNode:Node; - private reconnectNode:Node; - private kickNode:Node; - private locked=false; - constructor(canvas:Node,assets:{tips:Prefab;loading:Prefab;reconnect:Prefab;kick:Prefab}){ - const mount=(asset:Prefab)=>{ - const n=instantiate(asset);n.parent=canvas;n.setPosition(0,0,0); - const w=n.getComponent(Widget);if(w){w.target=canvas;w.isAlignLeft=w.isAlignRight=w.isAlignTop=w.isAlignBottom=true;w.left=w.right=w.top=w.bottom=0;w.updateAlignment();} - if(!n.getComponent(BlockInputEvents))n.addComponent(BlockInputEvents); - n.active=false;this.nodes.push(n);return n; - }; - this.loadingNode=mount(assets.loading);this.reconnectNode=mount(assets.reconnect); - this.tipNode=mount(assets.tips);this.kickNode=mount(assets.kick); - requireNode(this.tipNode,'group-89').active=false; - this.tipNode.on(Node.EventType.TOUCH_END,()=>this.closeTips()); +import {Animation, BlockInputEvents, Label, Node, Prefab, UITransform, Widget, instantiate, isValid} from 'cc'; +import {requireNode} from './node-bindings.ts'; + +/** Original layers 612/614/615/616. Scene-independent state is owned by PlatformStartup. */ +export class FeedbackPanels { + private timer:ReturnType|null=null; + private nodes:Node[]=[]; + private tipNode:Node; + private loadingNode:Node; + private reconnectNode:Node; + private kickNode:Node; + private locked=false; + constructor(canvas:Node,assets:{tips:Prefab;loading:Prefab;reconnect:Prefab;kick:Prefab}){ + const mount=(asset:Prefab)=>{ + const n=instantiate(asset);n.parent=canvas;n.setPosition(0,0,0); + const w=n.getComponent(Widget);if(w){w.target=canvas;w.isAlignLeft=w.isAlignRight=w.isAlignTop=w.isAlignBottom=true;w.left=w.right=w.top=w.bottom=0;w.updateAlignment();} + if(!n.getComponent(BlockInputEvents))n.addComponent(BlockInputEvents); + n.active=false;this.nodes.push(n);return n; + }; + this.loadingNode=mount(assets.loading);this.reconnectNode=mount(assets.reconnect); + this.tipNode=mount(assets.tips);this.kickNode=mount(assets.kick); + requireNode(this.tipNode,'group-89').active=false; + this.tipNode.on(Node.EventType.TOUCH_END,()=>this.closeTips()); + } + private front(n:Node):void{n.setSiblingIndex(n.parent!.children.length-1);if(this.locked)this.kickNode.setSiblingIndex(this.kickNode.parent!.children.length-1);} + private text(root:Node,path:string,bgPath:string,message:string):void{ + const n=requireNode(root,path),label=n.getComponent(Label);if(!label)throw Error(`${path} requires Label`); + n.getComponent(Widget)!.enabled=false; + label.string=message.replace(/#/g,'\n');label.enableWrapText=false;label.overflow=Label.Overflow.NONE; + label.horizontalAlign=Label.HorizontalAlign.CENTER;label.verticalAlign=Label.VerticalAlign.CENTER; + label.updateRenderData(true); + const size=n.getComponent(UITransform)!; + if(size.width>1000){label.enableWrapText=true;label.overflow=Label.Overflow.RESIZE_HEIGHT;size.width=1000;label.updateRenderData(true);} + const bg=requireNode(root,bgPath);const w=bg.getComponent(Widget);if(w)w.enabled=false; + const height=Math.max(105,size.height+60); + bg.getComponent(UITransform)!.setContentSize(Math.max(570,size.width+100),height); + bg.setPosition(0,18,0);n.setPosition(0,18,0); + } + showTips(message:string,time=3000):void{ + if(this.locked||!message)return;this.closeTips(); + this.text(this.tipNode,'group-32/提示文字','group-32/提示背景',message); + this.tipNode.active=true;this.front(this.tipNode); + if(time>0)this.timer=setTimeout(()=>this.closeTips(),time); + } + closeTips():void{if(this.timer!==null)clearTimeout(this.timer);this.timer=null;if(isValid(this.tipNode))this.tipNode.active=false;} + loading(show:boolean):void{ + const animation=requireNode(this.loadingNode,'group-40/载入动画').getComponent(Animation); + if(!animation||!animation.defaultClip)throw Error('Loading requires serialized AnimationClip'); + this.loadingNode.active=show&&!this.locked; + if(this.loadingNode.active){this.front(this.loadingNode);if(!animation.getState(animation.defaultClip.name)?.isPlaying)animation.play();} + else animation.stop(); } - private front(n:Node):void{n.setSiblingIndex(n.parent!.children.length-1);if(this.locked)this.kickNode.setSiblingIndex(this.kickNode.parent!.children.length-1);} - private text(root:Node,path:string,bgPath:string,message:string):void{ - const n=requireNode(root,path),label=n.getComponent(Label);if(!label)throw Error(`${path} requires Label`); - n.getComponent(Widget)!.enabled=false; - label.string=message.replace(/#/g,'\n');label.enableWrapText=false;label.overflow=Label.Overflow.NONE; - label.horizontalAlign=Label.HorizontalAlign.CENTER;label.verticalAlign=Label.VerticalAlign.CENTER; - label.updateRenderData(true); - const size=n.getComponent(UITransform)!; - if(size.width>1000){label.enableWrapText=true;label.overflow=Label.Overflow.RESIZE_HEIGHT;size.width=1000;label.updateRenderData(true);} - const bg=requireNode(root,bgPath);const w=bg.getComponent(Widget);if(w)w.enabled=false; - const height=Math.max(105,size.height+60); - bg.getComponent(UITransform)!.setContentSize(Math.max(570,size.width+100),height); - bg.setPosition(0,18,0);n.setPosition(0,18,0); - } - showTips(message:string,time=3000):void{ - if(this.locked||!message)return;this.closeTips(); - this.text(this.tipNode,'group-32/提示文字','group-32/提示背景',message); - this.tipNode.active=true;this.front(this.tipNode); - if(time>0)this.timer=setTimeout(()=>this.closeTips(),time); - } - closeTips():void{if(this.timer!==null)clearTimeout(this.timer);this.timer=null;if(isValid(this.tipNode))this.tipNode.active=false;} - private spin(root:Node,path:string,visible:boolean):void{ - const n=requireNode(root,path);Tween.stopAllByTarget(n);root.active=visible&&!this.locked; - if(root.active){this.front(root);tween(n).by(1,{angle:-360}).repeatForever().start();} - } - loading(show:boolean):void{this.spin(this.loadingNode,'group-40/载入动画',show);} - reconnect(show:boolean):void{ - const animation=requireNode(this.reconnectNode,'group-35/重连动画').getComponent(Animation); - if(!animation||!animation.defaultClip)throw Error('Reconnect requires serialized AnimationClip'); - this.reconnectNode.active=show&&!this.locked; - if(this.reconnectNode.active){this.front(this.reconnectNode);animation.play();}else animation.stop(); - } - kick(message:string):void{ - this.loading(false);this.reconnect(false);this.closeTips();this.locked=true; - this.text(this.kickNode,'group-33/强制退出提示文字','group-33/强制下线提示底',message); - this.kickNode.active=true;this.front(this.kickNode); - } - dispose():void{ - this.closeTips();for(const n of this.nodes){if(!isValid(n))continue;for(const c of n.getComponentsInChildren(UITransform))Tween.stopAllByTarget(c.node);n.destroy();} - this.nodes=[]; - } -} + reconnect(show:boolean):void{ + const animation=requireNode(this.reconnectNode,'group-35/重连动画').getComponent(Animation); + if(!animation||!animation.defaultClip)throw Error('Reconnect requires serialized AnimationClip'); + this.reconnectNode.active=show&&!this.locked; + if(this.reconnectNode.active){this.front(this.reconnectNode);animation.play();}else animation.stop(); + } + kick(message:string):void{ + this.loading(false);this.reconnect(false);this.closeTips();this.locked=true; + this.text(this.kickNode,'group-33/强制退出提示文字','group-33/强制下线提示底',message); + this.kickNode.active=true;this.front(this.kickNode); + } + dispose():void{ + this.closeTips();for(const n of this.nodes){if(!isValid(n))continue;for(const animation of n.getComponentsInChildren(Animation))animation.stop();n.destroy();} + this.nodes=[]; + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/lobby-panel.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/lobby-panel.ts index 4f024ed..2d58a70 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/lobby-panel.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/lobby-panel.ts @@ -1,388 +1,624 @@ -import { assetManager, BlockInputEvents, Button, ImageAsset, Label, Mask, Node, Prefab, ScrollView, Sprite, SpriteFrame, Texture2D, UITransform, Widget, instantiate, isValid } from 'cc'; -import type { RuntimeConfig } from '../../framework/config/runtime-config.ts'; -import { selectLobbyModel, shouldShowNotice, type LobbyModel, type LobbyPolicy } from '../../framework/presentation/lobby-model.ts'; -import type { PlatformState } from '../../framework/platform/stores/platform-types.ts'; -import { LobbyAssets } from './LobbyAssets.ts'; +import { assetManager, BlockInputEvents, Button, EditBox, ImageAsset, Label, Mask, Node, Prefab, ScrollView, Sprite, SpriteFrame, Texture2D, UITransform, Widget, instantiate, isValid } from 'cc'; +import type { RuntimeConfig } from '../../framework/config/runtime-config.ts'; +import { selectLobbyModel, shouldShowNotice, type LobbyModel, type LobbyPolicy } from '../../framework/presentation/lobby-model.ts'; +import type { PlatformState } from '../../framework/platform/stores/platform-types.ts'; +import { LobbyAssets } from './LobbyAssets.ts'; import { TaskListView } from './TaskListView.ts'; -import {VipRoomListView} from './VipRoomListView.ts'; -import {VipRoomHistory} from '../../framework/adapters/native/vip-room-history.ts'; -import type {VipRoom,VipCode} from '../../framework/core/types/vip-room.ts'; -import { resolveSharePolicy,sharePayload } from '../../framework/presentation/share-model.ts'; -import type { TaskData } from '../../framework/core/types/task.ts'; -import { requireNode } from './node-bindings.ts'; -import { LOBBY_MENU_ACTIONS, lobbyMenuState, lobbyTrayState, type LobbyAction, type LobbyContext, type LobbyMenuConfig, type LobbyMenuState } from '../../framework/presentation/lobby-menu.ts'; - -function need(value:T|null|undefined,name:string):T {if(value==null)throw Error(`Lobby missing ${name}`);return value;} -const label=(root:Node,path:string)=>need(requireNode(root,path).getComponent(Label),`${path} Label`); - -/** Scene-local view. All identity/assets come from PlatformStore; configuration is resolved once. */ -export class LobbyPanel { - private releases:Array<()=>void>=[]; - private modalReleases:Array<()=>void>=[]; - private modal:Node|null=null; - private modalKind=''; - private disposed=false; - private model:LobbyModel; - private menu:Node; - private header:Node; - private noticeMenu:Node; - private avatarUrl:string|null=null; - private avatarFrame:SpriteFrame|null=null; - private avatarTexture:Texture2D|null=null; - private avatarGeneration=0; - private cardValue:number|null=null; - private beanValue:number|null=null; - private readonly menuNodes=new Map(); - private readonly taskTip:Node; - private taskData:readonly TaskData[]|undefined; - private taskPending:'list'|'award'|null=null; - private taskTimer:ReturnType|null=null; +import { AnnouncementView } from './AnnouncementView.ts'; +import { NoticeTabsView } from './NoticeTabsView.ts'; +import { OwnerRoomListView } from './OwnerRoomListView.ts'; +import { OptionalSwitchAnimation } from './OptionalSwitchAnimation.ts'; +import { ProfileInfoListView } from './ProfileInfoListView.ts'; +import { validateProfileAuth } from '../../framework/presentation/profile-auth.ts'; +import {RequestLoading} from '../../framework/presentation/request-loading.ts'; +import {JoinRoomView} from './JoinRoomView.ts'; +import {CreateRoomPage} from './CreateRoomPage.ts'; +import {RankListView} from './RankListView.ts'; +import {parseRankList,type RankEntry} from '../../framework/protocol/contracts/rank-list.ts'; +import {VipRoomListView} from './VipRoomListView.ts'; +import {OwnedRoomHistory} from '../../framework/adapters/native/owned-room-history.ts'; +import {VipRoomHistory} from '../../framework/adapters/native/vip-room-history.ts'; +import type {VipRoom,VipCode} from '../../framework/core/types/vip-room.ts'; +import { resolveSharePolicy,sharePayload,resolveQrCodePolicy } from '../../framework/presentation/share-model.ts'; +import type { TaskData } from '../../framework/core/types/task.ts'; +import { requireNode } from './node-bindings.ts'; +import { LOBBY_MENU_ACTIONS, lobbyLockedTip, lobbyMenuState, lobbyTrayState, type LobbyAction, type LobbyContext, type LobbyMenuConfig, type LobbyMenuState } from '../../framework/presentation/lobby-menu.ts'; + +function need(value:T|null|undefined,name:string):T {if(value==null)throw Error(`Lobby missing ${name}`);return value;} +const label=(root:Node,path:string)=>need(requireNode(root,path).getComponent(Label),`${path} Label`); + +/** Scene-local view. All identity/assets come from PlatformStore; configuration is resolved once. */ +export class LobbyPanel { + private releases:Array<()=>void>=[]; + private modalReleases:Array<()=>void>=[]; + private modal:Node|null=null; + private modalKind=''; + private disposed=false; + private model:LobbyModel; + private menu:Node; + private header:Node; + private noticeMenu:Node; + private avatarUrl:string|null=null; + private avatarFrame:SpriteFrame|null=null; + private avatarTexture:Texture2D|null=null; + private avatarGeneration=0; + private cardValue:number|null=null; + private beanValue:number|null=null; + private readonly menuNodes=new Map(); + private readonly taskTip:Node; + private taskData:readonly TaskData[]|undefined; + private readonly requestLoading:RequestLoading; + private taskPending:'list'|'award'|null=null; + private taskTimer:ReturnType|null=null; private vipData:readonly VipRoom[]|undefined; - constructor(private readonly canvas:Node,private readonly popup:Node,private readonly assets:LobbyAssets, - private readonly config:RuntimeConfig,private readonly policy:LobbyPolicy,state:PlatformState, - private readonly tip:(text:string)=>void,private readonly logout:()=>void, - private readonly location:()=>unknown,private readonly openUrl:(url:string)=>void, - private readonly menuConfig:LobbyMenuConfig,private readonly context:LobbyContext, - private readonly onAction:(action:LobbyAction)=>void, - private readonly claimTask:(taskid:number|string)=>void, - private readonly nativeCall:(name:string,data:Record)=>void, - private readonly vip?:{request:(codes:readonly VipCode[])=>void;join:(code:VipCode)=>void;describe:(roomtype:readonly unknown[])=>string}){ - this.model=selectLobbyModel(state); - if(assets.switches.length!==2)throw Error('LobbyAssets requires 2 switch frames'); - this.menu=this.add(assets.menu,canvas);this.header=this.add(assets.header,canvas);this.noticeMenu=this.add(assets.noticeMenu,canvas); - requireNode(this.header,'group-103').active=false; - requireNode(this.noticeMenu,'group-95').active=menuConfig.template.isGameHall; - for(const root of [this.menu,this.header,this.noticeMenu]){ - for(const node of root.getComponentsInChildren(UITransform).map(ui=>ui.node)){ - const match=/^Spirit(\d+)$/.exec(node.name); - if(match){const id=Number(match[1]);if(this.menuNodes.has(id))throw Error(`Duplicate lobby node ${id}`);this.menuNodes.set(id,node);} - } - } - this.menuNodes.set(10,requireNode(this.header,'group-3/购买房卡')); - this.menuNodes.set(9,requireNode(this.header,'group-3/PlayerInfo/头像框')); - // Original JumpMenuScene hides these transient/obsolete fields. - for(const id of [219,220,157,497])requireNode(this.menu,`group-3/Spirit${id}`).active=false; + private invitePending:'query'|'binding'|null=null; + private inviteTimer:ReturnType|null=null; + private inviteReceipt:PlatformState['inviteResponse']; + private rankData:readonly RankEntry[]|undefined; + private vipRankReceipt:PlatformState['vipRank']; + private rankPending:'rank'|'vip-rank'|null=null; + private rankTimer:ReturnType|null=null; + private roomPage:{setBusy(busy:boolean):void;close():void}|null=null; + constructor(private readonly canvas:Node,private readonly popup:Node,private readonly assets:LobbyAssets, + private readonly config:RuntimeConfig,private readonly policy:LobbyPolicy,state:PlatformState, + private readonly tip:(text:string)=>void,private readonly logout:()=>void, + private readonly location:()=>unknown,private readonly openUrl:(url:string)=>void, + private readonly menuConfig:LobbyMenuConfig,private readonly context:LobbyContext, + private readonly onAction:(action:LobbyAction)=>void, + private readonly claimTask:(taskid:number|string)=>void, + private readonly nativeCall:(name:string,data:Record)=>void, + private readonly vip?:{request:(codes:readonly VipCode[])=>void;join:(code:VipCode)=>void;describe:(roomtype:readonly unknown[])=>string}, + private readonly invite?:{query:()=>void;bind:(code:string)=>void;paste:()=>Promise}, + private readonly extras?:{rank:()=>void;redeem:(cardno:string)=>void;paste:()=>Promise;vipRank?:(limit:number)=>void;joinOwned?:(code:number)=>void;loading?:(show:boolean)=>void; + room?:{createPrefab:Prefab;join(code:number):void;create(roomtype:readonly unknown[]):void;busy():boolean;close():void}}){ + this.requestLoading=new RequestLoading(show=>need(this.extras?.loading,'request Loading UI')(show)); + this.model=selectLobbyModel(state); + if(assets.switches.length!==2)throw Error('LobbyAssets requires 2 switch frames'); + this.menu=this.add(assets.menu,canvas);this.header=this.add(assets.header,canvas);this.noticeMenu=this.add(assets.noticeMenu,canvas); + requireNode(this.header,'group-103').active=false; + requireNode(this.noticeMenu,'group-95').active=menuConfig.template.isGameHall; + for(const root of [this.menu,this.header,this.noticeMenu]){ + for(const node of root.getComponentsInChildren(UITransform).map(ui=>ui.node)){ + const match=/^Spirit(\d+)$/.exec(node.name); + if(match){const id=Number(match[1]);if(this.menuNodes.has(id))throw Error(`Duplicate lobby node ${id}`);this.menuNodes.set(id,node);} + } + } + this.menuNodes.set(10,requireNode(this.header,'group-3/购买房卡')); + this.menuNodes.set(9,requireNode(this.header,'group-3/PlayerInfo/头像框')); + // Original JumpMenuScene hides these transient/obsolete fields. + for(const id of [157,497])requireNode(this.menu,`group-3/Spirit${id}`).active=false; // updateNoticeBoard clears all old contact samples before rendering source values. - for(const id of [180,193,194,682,178,179,340]){label(this.menu,`group-3/Spirit${id}`).string='';requireNode(this.menu,`group-3/Spirit${id}`).active=false;} - label(this.menu,'group-3/Spirit681').string='客服信息'; - const pairs=[[180,682],[193,178],[194,179]]; - policy.contacts.forEach((contact,index)=>{ - const [title,value]=pairs[index];const titleLabel=label(this.menu,`group-3/Spirit${title}`),valueLabel=label(this.menu,`group-3/Spirit${value}`); - titleLabel.string=contact.label;valueLabel.string=contact.value;titleLabel.node.active=true;valueLabel.node.active=true; - }); - label(this.noticeMenu,'group-3/Spirit145').string=policy.scroll; - for(const [id,action] of Object.entries(LOBBY_MENU_ACTIONS))this.bind(need(this.menuNodes.get(Number(id)),`menu ${id}`),()=>this.dispatch(action)); - this.bind(requireNode(this.noticeMenu,'group-95/Spirit3122'),()=>{requireNode(this.noticeMenu,'group-95').active=false;}); + const vipRank=requireNode(this.menu,'group-3/VipRank/Spirit679'); + for(const id of [180,193,194,682,178,179]){label(vipRank,`Spirit${id}`).string='';requireNode(vipRank,`Spirit${id}`).active=false;} + label(this.menu,'group-3/Spirit340').string='';requireNode(this.menu,'group-3/Spirit340').active=false; + label(this.menu,'group-3/Spirit681').string='客服信息'; + const pairs=[[180,682],[193,178],[194,179]]; + policy.contacts.forEach((contact,index)=>{ + const [title,value]=pairs[index];const titleLabel=label(vipRank,`Spirit${title}`),valueLabel=label(vipRank,`Spirit${value}`); + titleLabel.string=contact.label;valueLabel.string=contact.value;titleLabel.node.active=true;valueLabel.node.active=true; + }); + label(this.noticeMenu,'group-3/Spirit145').string=policy.scroll; + for(const [id,action] of Object.entries(LOBBY_MENU_ACTIONS))this.bind(need(this.menuNodes.get(Number(id)),`menu ${id}`),()=>this.dispatch(action)); + this.bind(requireNode(this.noticeMenu,'group-95/Spirit3122'),()=>{requireNode(this.noticeMenu,'group-95').active=false;}); this.applyMenuState(lobbyMenuState(menuConfig,context)); - if(policy.contacts.length>0)requireNode(this.menu,'group-3/Spirit16').active=false; - this.taskTip=requireNode(this.menu,'group-3/Spirit17/Tip'); - need(this.taskTip.getComponent(Sprite),'Spirit17/Tip Sprite'); - this.render(state); - const key=`tsgame_gameNoteTime_${config.identity.gameid}${config.identity.agentid}`; - const previous=window.localStorage.getItem(key),time=previous===null?null:Number(previous); - if(time!==null&&!Number.isFinite(time))throw Error(`Invalid ${key}`); - const now=Date.now(); - if(context.versionState===0&&shouldShowNotice(policy.noticeHours,time,now,false)){ - if(policy.noticeHours>0)window.localStorage.setItem(key,String(now)); - this.openNotice(); + need(requireNode(this.menu,'group-3/Announcement').getComponent(AnnouncementView),'AnnouncementView').show(policy.scroll); + for(const [id,locked] of Object.entries(menuConfig.locked)){ + requireNode(this.menu,`group-3/Spirit${id}/Locked`).active=locked; } - } + if(policy.contacts.length>0)requireNode(vipRank,'Spirit16').active=false; + this.taskTip=requireNode(this.menu,'group-3/Spirit17/Tip'); + need(this.taskTip.getComponent(Sprite),'Spirit17/Tip Sprite'); + this.render(state); + const key=`tsgame_gameNoteTime_${config.identity.gameid}${config.identity.agentid}`; + const previous=window.localStorage.getItem(key),time=previous===null?null:Number(previous); + if(time!==null&&!Number.isFinite(time))throw Error(`Invalid ${key}`); + const now=Date.now(); + if(context.versionState===0&&shouldShowNotice(policy.noticeHours,time,now,false)){ + if(policy.noticeHours>0)window.localStorage.setItem(key,String(now)); + this.openNotice(); + } + } private dispatch(action:LobbyAction):void{ switch(action){ + case 'buy-roomcard':case 'buy-bean':return; // Payments are retired in the current business. + case 'my-rooms':this.openOwnedRooms();return; + case 'vip-rank':this.requestVipRank();return; + case 'world-rooms': + case 'match': + case 'official-rooms':{ + const text=lobbyLockedTip(this.menuConfig,action); + if(text!==null){this.tip(text);return;} + this.onAction(action);return; + } + case 'agent':this.requestInvite('query',()=>need(this.invite,'invite commands').query());return; case 'share':this.openShare();return; - case 'vip-rooms':this.openVipRooms();return; - case 'tasks':this.requestTask('list',()=>this.onAction('tasks'));return; - case 'settings':this.openSettings();return; - case 'profile':this.openProfile();return; + case 'qrcode':this.openQrCode();return; + case 'rank':this.requestRank();return; + case 'cdkey':this.openCdKey();return; + case 'vip-rooms':this.openVipRooms();return; + case 'tasks':this.requestTask('list',()=>this.onAction('tasks'));return; + case 'settings':this.openSettings();return; + case 'profile':this.openProfile();return; case 'join-room':this.openJoin();return; - case 'notice':this.openNotice();return; - case 'tray':{ - const tray=requireNode(this.noticeMenu,'group-95'); - if(tray.active){tray.active=false;return;} - if(this.context.versionState===1)return; - const state=lobbyTrayState(this.menuConfig,this.context); - const bg=requireNode(tray,'Spirit3121');bg.active=true; - need(bg.getComponent(UITransform),'tray background').width=150*Object.values(state.visible).filter(Boolean).length+30; - this.applyMenuState(state); - tray.active=true;return; - } - default:this.onAction(action); + case 'create-room':this.openCreate();return; + case 'notice':this.openNotice();return; + case 'tray':{ + const tray=requireNode(this.noticeMenu,'group-95'); + if(tray.active){tray.active=false;return;} + if(this.context.versionState===1)return; + const state=lobbyTrayState(this.menuConfig,this.context); + const bg=requireNode(tray,'Spirit3121');bg.active=true; + need(bg.getComponent(UITransform),'tray background').width=150*Object.values(state.visible).filter(Boolean).length+30; + this.applyMenuState(state); + tray.active=true;return; + } + default:this.onAction(action); + } + } + private applyMenuState(state:LobbyMenuState):void{ + // A projection includes nodes from the adjacent notice layer. Missing IDs belonging to other layers are intentional. + for(const [id,active] of Object.entries(state.visible)){const node=this.menuNodes.get(Number(id));if(node)node.active=active;} + for(const id of new Set([...Object.keys(state.left),...Object.keys(state.top)])){ + const node=this.menuNodes.get(Number(id));if(!node)continue; + const ui=need(node.getComponent(UITransform),`menu ${id} transform`); + const widget=node.getComponent(Widget); + // Original coordinates are top-left in a centered 1280x720 layout; retain the 1600x720 Cocos canvas. + const left=state.left[Number(id)],top=state.top[Number(id)]; + if(left!==undefined){ + const x=left-640+ui.width*ui.anchorX; + if(widget){widget.isAlignLeft=false;widget.isAlignRight=false;widget.isAlignHorizontalCenter=true;widget.horizontalCenter=x;} + node.setPosition(x,node.position.y,0); + } + if(top!==undefined){ + const y=360-top-ui.height*(1-ui.anchorY); + if(widget){widget.isAlignTop=false;widget.isAlignBottom=false;widget.isAlignVerticalCenter=true;widget.verticalCenter=y;} + node.setPosition(node.position.x,y,0); + } + if(widget)widget.updateAlignment(); + } + } + private add(prefab:Prefab|null,parent:Node):Node { + const root=instantiate(need(prefab,'prefab'));root.parent=parent;root.setPosition(0,0,0);root.active=true; + const widget=root.getComponent(Widget); + if(widget){widget.target=parent;widget.isAlignLeft=true;widget.isAlignRight=true;widget.isAlignTop=true;widget.isAlignBottom=true; + widget.left=0;widget.right=0;widget.top=0;widget.bottom=0;widget.updateAlignment();} + return root; + } + private bind(node:Node,action:()=>void,modal=false):void { + const button=node.getComponent(Button)??node.addComponent(Button); + button.transition=Button.Transition.NONE; + const listener=()=>{if(this.disposed||!node.activeInHierarchy||!button.interactable||(!modal&&this.modal!==null)||!this.model.canAct)return; + try{action();}catch(error){this.tip(error instanceof Error?error.message:String(error));}}; + node.on(Button.EventType.CLICK,listener); + (modal?this.modalReleases:this.releases).push(()=>{if(isValid(node))node.off(Button.EventType.CLICK,listener);}); + } + render(state:PlatformState):void { + if(this.disposed)return;this.model=selectLobbyModel(state); + label(this.header,'group-3/PlayerInfo/昵称').string=this.model.nickname; + label(this.header,'group-3/PlayerInfo/ID').string=`ID:${this.model.playerid}`; + if(this.cardValue!==this.model.roomcard){this.numbers('Spirit3161','Spirit287',this.model.roomcard);this.cardValue=this.model.roomcard;} + if(this.beanValue!==this.model.bean){this.numbers('Spirit3162','Spirit496',this.model.bean);this.beanValue=this.model.bean;} + // Task tip stays attached to its button and inherits its visibility. + this.taskTip.active=this.model.taskstate!==0; + if(!this.model.canAct){this.cancelTaskRequest();this.cancelInviteRequest();} + if(!this.model.canAct)this.cancelRankRequest(); + if(!this.model.canAct)this.cancelRoomPage(); + if(state.treasureList!==this.rankData){ + this.rankData=state.treasureList;const show=this.rankPending==='rank';if(show)this.cancelRankRequest(); + if(show&&this.model.canAct&&state.treasureList)this.openRank(state.treasureList); } - } - private applyMenuState(state:LobbyMenuState):void{ - // A projection includes nodes from the adjacent notice layer. Missing IDs belonging to other layers are intentional. - for(const [id,active] of Object.entries(state.visible)){const node=this.menuNodes.get(Number(id));if(node)node.active=active;} - for(const id of new Set([...Object.keys(state.left),...Object.keys(state.top)])){ - const node=this.menuNodes.get(Number(id));if(!node)continue; - const ui=need(node.getComponent(UITransform),`menu ${id} transform`); - const widget=node.getComponent(Widget); - // Original coordinates are top-left in a centered 1280x720 layout; retain the 1600x720 Cocos canvas. - const left=state.left[Number(id)],top=state.top[Number(id)]; - if(left!==undefined){ - const x=left-640+ui.width*ui.anchorX; - if(widget){widget.isAlignLeft=false;widget.isAlignRight=false;widget.isAlignHorizontalCenter=true;widget.horizontalCenter=x;} - node.setPosition(x,node.position.y,0); - } - if(top!==undefined){ - const y=360-top-ui.height*(1-ui.anchorY); - if(widget){widget.isAlignTop=false;widget.isAlignBottom=false;widget.isAlignVerticalCenter=true;widget.verticalCenter=y;} - node.setPosition(node.position.x,y,0); - } - if(widget)widget.updateAlignment(); + if(state.vipRank!==this.vipRankReceipt){ + this.vipRankReceipt=state.vipRank; + if(this.rankPending==='vip-rank'){this.cancelRankRequest();if(this.model.canAct&&state.vipRank?.entries)this.openRank(state.vipRank.entries,true);} } - } - private add(prefab:Prefab|null,parent:Node):Node { - const root=instantiate(need(prefab,'prefab'));root.parent=parent;root.setPosition(0,0,0);root.active=true; - const widget=root.getComponent(Widget); - if(widget){widget.target=parent;widget.isAlignLeft=true;widget.isAlignRight=true;widget.isAlignTop=true;widget.isAlignBottom=true; - widget.left=0;widget.right=0;widget.top=0;widget.bottom=0;widget.updateAlignment();} - return root; - } - private bind(node:Node,action:()=>void,modal=false):void { - const button=node.getComponent(Button)??node.addComponent(Button); - button.transition=Button.Transition.NONE; - const listener=()=>{if(this.disposed||!node.activeInHierarchy||!button.interactable||(!modal&&this.modal!==null)||!this.model.canAct)return; - try{action();}catch(error){this.tip(error instanceof Error?error.message:String(error));}}; - node.on(Button.EventType.CLICK,listener); - (modal?this.modalReleases:this.releases).push(()=>{if(isValid(node))node.off(Button.EventType.CLICK,listener);}); - } - render(state:PlatformState):void { - if(this.disposed)return;this.model=selectLobbyModel(state); - label(this.header,'group-3/PlayerInfo/昵称').string=this.model.nickname; - label(this.header,'group-3/PlayerInfo/ID').string=`ID:${this.model.playerid}`; - if(this.cardValue!==this.model.roomcard){this.numbers('Spirit3161','Spirit287',this.model.roomcard);this.cardValue=this.model.roomcard;} - if(this.beanValue!==this.model.bean){this.numbers('Spirit3162','Spirit496',this.model.bean);this.beanValue=this.model.bean;} - // Task tip stays attached to its button and inherits its visibility. - this.taskTip.active=this.model.taskstate!==0; - if(!this.model.canAct)this.cancelTaskRequest(); - if(state.tasks!==this.taskData){ - this.taskData=state.tasks; - const show=this.taskPending==='list'||this.modalKind==='tasks'; - this.cancelTaskRequest(); - if(show&&state.tasks&&this.model.canAct){this.tip('');this.openTasks(state.tasks);} + if(state.inviteResponse!==this.inviteReceipt){ + this.inviteReceipt=state.inviteResponse; + if(state.inviteResponse?.kind===this.invitePending){ + const open=this.invitePending==='query';this.cancelInviteRequest(); + if(open&&this.model.canAct){ + const player=need(state.players.entities[need(state.players.selfPlayerId,'playerid')],'player'); + if(player.invitecode===undefined)throw Error('Missing queried invitecode'); + this.openInvite(player.invitecode); + } + } } - this.updateAvatar(); - if(state.vipRooms!==this.vipData){this.vipData=state.vipRooms;if(this.modalKind==='vip-rooms'&&state.vipRooms)this.renderVipRooms(state.vipRooms);} - if(this.modalKind==='profile')this.renderProfile(); + if(state.tasks!==this.taskData){ + this.taskData=state.tasks; + const show=this.taskPending==='list'||this.modalKind==='tasks'; + this.cancelTaskRequest(); + if(show&&state.tasks&&this.model.canAct){this.tip('');this.openTasks(state.tasks);} + } + this.updateAvatar(); + if(state.vipRooms!==this.vipData){this.vipData=state.vipRooms;if(this.modalKind==='vip-rooms'&&state.vipRooms)this.renderVipRooms(state.vipRooms);} + if(this.modalKind==='profile')this.renderProfile(); + } + private numbers(name:string,background:string,value:number):void { + const root=requireNode(this.header,`group-3/${name}`),ui=need(root.getComponent(UITransform),name); + const numberLabel=label(root,'NumberLabel'); + numberLabel.string=String(value); + numberLabel.updateRenderData(true); + const width=need(numberLabel.node.getComponent(UITransform),'number label transform').width; + ui.setContentSize(width,28); + const bg=requireNode(this.header,`group-3/${background}`),bgUI=need(bg.getComponent(UITransform),background); + bgUI.width=width>119?width+55:155; + const widget=root.getComponent(Widget);if(widget)widget.enabled=false; + root.setPosition(bg.position.x+8,root.position.y,0); + } + private updateAvatar():void { + if(this.avatarUrl===this.model.avatar)return; + this.avatarUrl=this.model.avatar;const generation=++this.avatarGeneration; + const sprite=need(requireNode(this.header,'group-3/PlayerInfo/头像框/头像').getComponent(Sprite),'avatar'); + sprite.sizeMode=Sprite.SizeMode.CUSTOM; + need(sprite.node.getComponent(UITransform),'avatar transform').setContentSize(63,65); + sprite.spriteFrame=null; + this.avatarFrame?.destroy();this.avatarTexture?.destroy();this.avatarFrame=null;this.avatarTexture=null; + if(this.model.avatar==='')return; + assetManager.loadRemote(this.model.avatar,{ext:'.png'},(error,image)=>{ + if(this.disposed||generation!==this.avatarGeneration)return; + if(error){this.tip(`头像加载失败:${error.message}`);return;} + const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture; + this.avatarTexture=texture;this.avatarFrame=frame;sprite.spriteFrame=frame; + if(this.modalKind==='profile')this.renderProfile(); + }); + } + private open(prefab:Prefab|null,kind:string,group:string|null):Node { + this.close();requireNode(this.noticeMenu,'group-95').active=false; + const modal=this.add(prefab,this.popup);this.modal=modal;this.modalKind=kind; + if(group!==null)for(const child of modal.children)child.active=child.name===group; + const shield=new Node('ModalInputShield');shield.parent=modal;shield.layer=modal.layer;shield.setSiblingIndex(0); + shield.addComponent(UITransform).setContentSize(1600,720);shield.addComponent(BlockInputEvents); + return modal; + } + private openOwnedRooms():void { + const entries=new OwnedRoomHistory(window.localStorage,this.config.identity.gameid,this.config.identity.agentid).read(); + if(this.assets.ownedRoomDigits.length!==10)throw Error('Owned room requires ten digit frames'); + const modal=this.open(this.assets.ownedRoomList,'my-rooms','group-98'),group=requireNode(modal,'group-98'); + this.bind(requireNode(group,'Spirit103'),()=>this.close(),true); + const view=need(modal.getComponent(OwnerRoomListView),'OwnerRoomListView'); + for(const {node,code} of view.render(entries,this.assets.ownedRoomDigits)) + this.bind(node,()=>{this.close();need(this.extras?.joinOwned,'owned room command')(code);},true); } - private numbers(name:string,background:string,value:number):void { - const root=requireNode(this.header,`group-3/${name}`),ui=need(root.getComponent(UITransform),name); - const numberLabel=label(root,'NumberLabel'); - numberLabel.string=String(value); - numberLabel.updateRenderData(true); - const width=need(numberLabel.node.getComponent(UITransform),'number label transform').width; - ui.setContentSize(width,28); - const bg=requireNode(this.header,`group-3/${background}`),bgUI=need(bg.getComponent(UITransform),background); - bgUI.width=width>119?width+55:155; - const widget=root.getComponent(Widget);if(widget)widget.enabled=false; - root.setPosition(bg.position.x+8,root.position.y,0); + private requestVipRank():void { + if(this.rankPending)return; + this.rankPending='vip-rank'; + this.rankTimer=setTimeout(()=>{this.cancelRankRequest();this.tip('排行榜请求超时,请重试');},15000); + try{this.requestLoading.begin('getVipRankList');need(this.extras?.vipRank,'VIP rank command')(this.menuConfig.sys.vipRankLimit);}catch(error){this.cancelRankRequest();throw error;} } - private updateAvatar():void { - if(this.avatarUrl===this.model.avatar)return; - this.avatarUrl=this.model.avatar;const generation=++this.avatarGeneration; - const sprite=need(requireNode(this.header,'group-3/PlayerInfo/头像框/头像').getComponent(Sprite),'avatar'); - sprite.sizeMode=Sprite.SizeMode.CUSTOM; - need(sprite.node.getComponent(UITransform),'avatar transform').setContentSize(63,65); - sprite.spriteFrame=null; - this.avatarFrame?.destroy();this.avatarTexture?.destroy();this.avatarFrame=null;this.avatarTexture=null; - if(this.model.avatar==='')return; - assetManager.loadRemote(this.model.avatar,{ext:'.png'},(error,image)=>{ - if(this.disposed||generation!==this.avatarGeneration)return; - if(error){this.tip(`头像加载失败:${error.message}`);return;} - const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture; - this.avatarTexture=texture;this.avatarFrame=frame;sprite.spriteFrame=frame; - if(this.modalKind==='profile')this.renderProfile(); + private requestRank():void { + if(this.rankPending)return; + const mode=Number(this.menuConfig.hall.rankList); + if(mode===1){ + const raw=this.config.remoteConfig.getValue('rankList1'); + this.openRank(raw?parseRankList(raw,true):[]);return; + } + if(mode!==2)return; + this.rankPending='rank'; + this.rankTimer=setTimeout(()=>{this.cancelRankRequest();this.tip('排行榜请求超时,请重试');},15000); + try{need(this.extras,'rank commands').rank();}catch(error){this.cancelRankRequest();throw error;} + } + requestReceived(message:{route:string;rpc:string;data?:unknown}):void{this.requestLoading.received(message);} + cancelRankRequest():void { + this.requestLoading.cancel('getVipRankList'); + if(this.rankTimer!==null)clearTimeout(this.rankTimer); + this.rankTimer=null;this.rankPending=null; + } + private openRank(entries:readonly RankEntry[],vip=false):void { + requireNode(this.noticeMenu,'group-95').active=false; + if(entries.length===0)return; + // Legacy Utl.Config.ownerNote and Logic.setOwnerNote, scoped to game + agent. + const ownerNote=window.localStorage.getItem('tsgame_ownerNote_'+this.config.identity.gameid+this.config.identity.agentid); + if(!vip&&ownerNote&&this.menuConfig.hall.isReSetOwnerNote)entries=[{...entries[0],signature:ownerNote.replace(/#/g,'\n')},...entries.slice(1)]; + const rawFrame=this.config.remoteConfig.getValue('nameImgFrame_2'); + // Logic.getConfig_Succ explicitly defines frame 1 when this optional value is falsy. + const frame=rawFrame?Number(rawFrame):1; + if(!Number.isInteger(frame)||!this.assets.rankSymbols[frame-1])throw Error('Invalid nameImgFrame_2'); + const modal=this.open(this.assets.rank,vip?'vip-rank':'rank','group-84'),group=requireNode(modal,'group-84'); + need(requireNode(group,'Spirit639').getComponent(Sprite),'rank symbol').spriteFrame=this.assets.rankSymbols[frame-1]; + this.bind(requireNode(group,'Spirit605'),()=>this.close(),true); + requireNode(group,'Spirit605/Spirit606').addComponent(BlockInputEvents); + const onError=(message:string)=>this.tip(`排行榜头像加载失败:${message}`);group.on('rank-image-error',onError); + const view=new RankListView(group,entries,this.policy.starName,this.assets.rankMedals,need(vip?this.assets.vipRankTitle:this.assets.rankTitle,'rank title'),need(this.assets.rankItem,'rankItem'),vip); + this.modalReleases.push(()=>{group.off('rank-image-error',onError);view.dispose();}); + } + private openCdKey():void { + const modal=this.open(this.assets.redeemCode,'cdkey','group-74'),group=requireNode(modal,'group-74'); + this.bind(requireNode(group,'反馈遮罩'),()=>this.close(),true); + requireNode(group,'反馈背景').addComponent(BlockInputEvents); + requireNode(group,'反馈提交').active=false; + const text=label(group,'反馈输入内容');text.string='';text.overflow=Label.Overflow.CLAMP; + const w=text.node.getComponent(Widget);if(w)w.enabled=false; + const area=need(text.node.getComponent(UITransform),'card input transform');area.setContentSize(930,130);area.setAnchorPoint(0,1); + const input=new Node('CardInput');input.layer=group.layer;input.parent=group;input.setPosition(text.node.position); + input.addComponent(UITransform).setContentSize(930,130); + text.node.parent=input;text.node.setPosition(-465,65,0); + const edit=input.addComponent(EditBox);edit.textLabel=text;edit.inputMode=EditBox.InputMode.SINGLE_LINE; + edit.inputFlag=EditBox.InputFlag.DEFAULT;edit.maxLength=-1; + this.modalReleases.push(()=>{edit.blur();edit.string='';}); + let pasting=false; + this.bind(requireNode(group,'Spirit3265'),()=>{ + if(pasting)return;pasting=true; + void need(this.extras,'card clipboard').paste().then(value=>{if(this.modal===modal&&this.model.canAct&&value)edit.string=value;}) + .catch(error=>{if(this.modal===modal)this.tip(error instanceof Error?error.message:String(error));}).finally(()=>{pasting=false;}); + },true); + this.bind(requireNode(group,'Spirit3266'),()=>{ + const cardno=edit.string;if(cardno===''||/^[ ]+$/.test(cardno))return; + this.close();need(this.extras,'redemption commands').redeem(cardno); + },true); + } + private openQrCode():void { + const policy=resolveQrCodePolicy(this.config);if(!policy)return; + const modal=this.open(this.assets.qrCode,'qrcode','group-42'),group=requireNode(modal,'group-42'); + this.bind(requireNode(group,'二维码遮罩'),()=>this.close(),true); + requireNode(group,'Spirit342').addComponent(BlockInputEvents); + this.bind(requireNode(group,'Spirit345'),()=>this.nativeCall('friendsSharetypeUrlToptitleDescript',policy.share),true); + const sprite=need(requireNode(group,'Spirit343').getComponent(Sprite),'QR image Sprite'); + sprite.sizeMode=Sprite.SizeMode.CUSTOM;sprite.spriteFrame=null; + assetManager.loadRemote(policy.url,{ext:'.png'},(error,image)=>{ + if(this.disposed||this.modal!==modal||!isValid(sprite.node))return; + if(error){this.tip(`二维码图片加载失败:${error.message}`);return;} + const texture=new Texture2D();texture.image=image; + const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame; + this.modalReleases.push(()=>{sprite.spriteFrame=null;frame.destroy();texture.destroy();}); }); } - private open(prefab:Prefab|null,kind:string,group:string):Node { - this.close();requireNode(this.noticeMenu,'group-95').active=false; - const modal=this.add(prefab,this.popup);this.modal=modal;this.modalKind=kind; - for(const child of modal.children)child.active=child.name===group; - const shield=new Node('ModalInputShield');shield.parent=modal;shield.layer=modal.layer;shield.setSiblingIndex(0); - shield.addComponent(UITransform).setContentSize(1600,720);shield.addComponent(BlockInputEvents); - return modal; - } openProfile():void { - const modal=this.open(this.assets.profile,'profile','group-18'); - this.bind(requireNode(modal,'group-18/Spirit116'),()=>this.close(),true);this.renderProfile(); - this.bind(requireNode(modal,'group-18/Spirit484'),()=>this.tip('实名认证尚未接入'),true); - } - private renderProfile():void { - const modal=need(this.modal,'profile'); - const values:Record={196:this.model.nickname,197:`ID:${this.model.playerid}`,198:`IP:${need(this.model.ip,'player ip')}`, - 199:`积分:${this.model.score}`,510:`${this.policy.starName}:${this.model.bean}`,3152:'个性签名:',3157:this.model.sign}; - const location=this.location() as {errorCode?:number;province?:string;city?:string}|null; - values[200]=location?.errorCode===0?`${need(location.province,'location province')}-${need(location.city,'location city')}`:'获取定位失败!'; - for(const [id,text] of Object.entries(values))label(modal,`group-18/Spirit${id}`).string=text; - const avatar=need(requireNode(modal,'group-18/Spirit118').getComponent(Sprite),'profile avatar'); + const modal=this.open(this.assets.profile,'profile','group-18'); + this.bind(requireNode(modal,'group-18/Spirit116'),()=>this.close(),true);this.renderProfile(); + const view=need(modal.getComponent(ProfileInfoListView),'ProfileInfoListView'); + this.bind(requireNode(modal,'group-18/Spirit484'),()=>this.openAuth(need(view.authPrefab,'auth prefab')),true); + } + private renderProfile():void { + const modal=need(this.modal,'profile'); + const values=[this.model.nickname,`ID:${this.model.playerid}`,`IP:${need(this.model.ip,'player ip')}`, + `积分:${this.model.score}`,`${this.policy.starName}:${this.model.bean}`]; + const location=this.location() as {errorCode?:number;province?:string;city?:string}|null; + values.push(location?.errorCode===0?`${need(location.province,'location province')}-${need(location.city,'location city')}`:'获取定位失败!','个性签名:'); + need(modal.getComponent(ProfileInfoListView),'ProfileInfoListView').render(values,this.model.sign); + const avatar=need(requireNode(modal,'group-18/Spirit118').getComponent(Sprite),'profile avatar'); avatar.sizeMode=Sprite.SizeMode.CUSTOM;avatar.spriteFrame=this.avatarFrame; } - openSettings():void { - const modal=this.open(this.assets.settings,'settings','group-6'); - const group=requireNode(modal,'group-6'); - for(const name of ['申请解散','邀请好友'])requireNode(group,name).active=false; - this.bind(requireNode(group,'设置遮罩'),()=>this.close(),true); - this.bind(requireNode(group,'退出登录'),()=>{if(this.context.launchMode===1)this.tip('请返回游戏大厅退出登录');else this.logout();},true); - for(const [name,key] of [['音乐底',`tsgame_bgm_${this.config.identity.gameid}`],['音效底',`tsgame_sound_${this.config.identity.gameid}${this.config.identity.agentid}`]] as const){ - const sprite=need(requireNode(group,name).getComponent(Sprite),name); - const read=()=>{const raw=window.localStorage.getItem(key);if(raw===null){window.localStorage.setItem(key,'1');return true;} - if(raw!=='0'&&raw!=='1')throw Error(`Invalid ${key}`);return raw==='1';}; - const render=()=>{sprite.spriteFrame=this.assets.switches[read()?0:1];};render(); - this.bind(sprite.node,()=>{window.localStorage.setItem(key,read()?'0':'1');render();},true); - } - for(const name of ['Spirit357','Spirit388'])this.bind(requireNode(group,name),()=>this.tip('原生震动与语音开关尚未接入'),true); + private openAuth(prefab:Prefab):void { + const modal=this.open(prefab,'auth','group-57'),group=requireNode(modal,'group-57'); + const name=need(requireNode(group,'NameInput').getComponent(EditBox),'auth name EditBox'); + const id=need(requireNode(group,'IdInput').getComponent(EditBox),'auth ID EditBox'); + name.string='';id.string=''; + const blur=()=>{name.blur();id.blur();}; + this.modalReleases.push(blur); + this.bind(requireNode(group,'Spirit485'),()=>this.close(),true); + this.bind(requireNode(group,'Spirit493'),()=>{ + const result=validateProfileAuth(name.string,id.string); + if(result==='name'){this.tip('姓名不正确!');return;} + if(result==='id'){this.tip('身份证信息不正确!');return;} + this.close();this.tip('认证成功!'); + },true); } - openNotice():void { - const modal=this.open(this.assets.notices,'notice','group-12'),group=requireNode(modal,'group-12'); + openSettings():void { + const modal=this.open(this.assets.settings,'settings','group-6'); + const group=requireNode(modal,'group-6'); + for(const name of ['申请解散','邀请好友'])requireNode(group,name).active=false; + this.bind(requireNode(group,'设置遮罩'),()=>this.close(),true); + this.bind(requireNode(group,'退出登录'),()=>{if(this.context.launchMode===1)this.tip('请返回游戏大厅退出登录');else this.logout();},true); + for(const [name,key] of [['音乐底',`tsgame_bgm_${this.config.identity.gameid}`],['音效底',`tsgame_sound_${this.config.identity.gameid}${this.config.identity.agentid}`]] as const){ + const node=requireNode(group,name); + const animation=need(node.getComponent(OptionalSwitchAnimation),`${name} OptionalSwitchAnimation`); + const read=()=>{const raw=window.localStorage.getItem(key);if(raw===null){window.localStorage.setItem(key,'1');return true;} + if(raw!=='0'&&raw!=='1')throw Error(`Invalid ${key}`);return raw==='1';}; + const render=(animate:boolean)=>{const on=read();animation.setState(on,this.assets.switches[on?0:1],animate);};render(false); + this.bind(node,()=>{window.localStorage.setItem(key,read()?'0':'1');render(true);},true); + } + for(const name of ['Spirit357','Spirit388'])this.bind(requireNode(group,name),()=>this.tip('原生震动与语音开关尚未接入'),true); + } + openNotice():void { + const modal=this.open(this.assets.noticeBoard,'notice','group-12'),group=requireNode(modal,'group-12'); for(const name of ['通知遮罩','通知关闭'])this.bind(requireNode(group,name),()=>this.close(),true); - const close=requireNode(group,'通知关闭'); - const closeWidget=close.getComponent(Widget);if(closeWidget)closeWidget.enabled=false; - close.setPosition(475,264,0);close.active=true; - // Replace the old ifast clipping surface with a native masked ScrollView. - const viewport=new Node('NoticeViewport');viewport.parent=group;viewport.layer=group.layer;viewport.setPosition(125,-30,0); - viewport.addComponent(UITransform).setContentSize(725,450);viewport.addComponent(Mask); - const scroll=viewport.addComponent(ScrollView);scroll.horizontal=false; - const contentRoot=new Node('NoticeContent');contentRoot.parent=viewport;contentRoot.layer=group.layer; - const contentUI=contentRoot.addComponent(UITransform);contentUI.setAnchorPoint(0.5,1);contentUI.setContentSize(725,450);contentRoot.setPosition(0,225,0);scroll.content=contentRoot; - const contentLabel=label(group,'通知内容');const textWidget=contentLabel.node.getComponent(Widget);if(textWidget)textWidget.enabled=false; - contentLabel.node.parent=contentRoot;contentLabel.node.setPosition(-345,0,0); - const textUI=need(contentLabel.node.getComponent(UITransform),'notice text');textUI.setAnchorPoint(0,1);textUI.setContentSize(690,450); - contentLabel.overflow=Label.Overflow.RESIZE_HEIGHT;contentLabel.enableWrapText=true;contentLabel.horizontalAlign=Label.HorizontalAlign.LEFT;contentLabel.verticalAlign=Label.VerticalAlign.TOP; - const imageNode=requireNode(group,'Spirit3102');const imageWidget=imageNode.getComponent(Widget);if(imageWidget)imageWidget.enabled=false; - imageNode.parent=contentRoot;imageNode.setPosition(0,0,0);need(imageNode.getComponent(UITransform),'notice image transform').setAnchorPoint(0.5,1); - let pageGeneration=0; - let selectedPage=0; - let releaseImage=()=>{}; - this.modalReleases.push(()=>releaseImage()); - const link=requireNode(group,'Spirit576'); - this.bind(link,()=>this.openUrl(this.policy.notices[selectedPage].url),true); - this.bind(imageNode,()=>this.openUrl(this.policy.notices[selectedPage].url),true); - const show=(index:number)=>{ - selectedPage=index;releaseImage();releaseImage=()=>{}; - const generation=++pageGeneration; - const notice=this.policy.notices[index]; - for(const id of [404,3105,3106,576])requireNode(group,`Spirit${id}`).active=false; - link.active=notice.type===0&¬ice.url!==''; - imageNode.active=false; - const content=contentLabel;content.string=notice.text;content.node.active=notice.type===0; - content.updateRenderData(true);contentUI.height=Math.max(450,textUI.height);scroll.scrollToTop(0); - if(notice.type===1&¬ice.image){ - imageNode.active=true; - const sprite=need(imageNode.getComponent(Sprite),'notice image');sprite.spriteFrame=null; - assetManager.loadRemote(notice.image,{ext:'.png'},(error,image)=>{ - if(this.disposed||this.modal!==modal||generation!==pageGeneration||!isValid(imageNode))return; - if(error){this.tip(`公告图片加载失败:${error.message}`);return;} - const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.sizeMode=Sprite.SizeMode.CUSTOM;sprite.spriteFrame=frame; - need(imageNode.getComponent(UITransform),'notice image transform').setContentSize(725,notice.height);contentUI.height=Math.max(450,notice.height);scroll.scrollToTop(0); - releaseImage=()=>{sprite.spriteFrame=null;frame.destroy();texture.destroy();}; - }); - } - }; - for(let index=0;index<3;index++){ - const notice=this.policy.notices[index],visible=index===0||notice.text!==''||notice.image!==''; - for(const id of [570+index,573+index]){const button=requireNode(group,`Spirit${id}`);button.active=visible;this.bind(button,()=>show(index),true);} + // Decorative panel surfaces must consume hits instead of exposing the backdrop behind them. + for(const name of ['通知背景','通知标题','通知标题内容']){ + const surface=requireNode(group,name); + if(!surface.getComponent(BlockInputEvents))surface.addComponent(BlockInputEvents); } - show(0); - } + const close=requireNode(group,'通知关闭'); + const closeWidget=close.getComponent(Widget);if(closeWidget)closeWidget.enabled=false; + close.setPosition(475,264,0);close.active=true; + // Replace the old ifast clipping surface with a native masked ScrollView. + const viewport=new Node('NoticeViewport');viewport.parent=group;viewport.layer=group.layer;viewport.setPosition(125,-30,0); + viewport.addComponent(UITransform).setContentSize(725,450);viewport.addComponent(Mask); + const scroll=viewport.addComponent(ScrollView);scroll.horizontal=false; + const contentRoot=new Node('NoticeContent');contentRoot.parent=viewport;contentRoot.layer=group.layer; + const contentUI=contentRoot.addComponent(UITransform);contentUI.setAnchorPoint(0.5,1);contentUI.setContentSize(725,450);contentRoot.setPosition(0,225,0);scroll.content=contentRoot; + const contentLabel=label(group,'通知内容');const textWidget=contentLabel.node.getComponent(Widget);if(textWidget)textWidget.enabled=false; + contentLabel.node.parent=contentRoot;contentLabel.node.setPosition(-345,0,0); + const textUI=need(contentLabel.node.getComponent(UITransform),'notice text');textUI.setAnchorPoint(0,1);textUI.setContentSize(690,450); + contentLabel.overflow=Label.Overflow.RESIZE_HEIGHT;contentLabel.enableWrapText=true;contentLabel.horizontalAlign=Label.HorizontalAlign.LEFT;contentLabel.verticalAlign=Label.VerticalAlign.TOP; + const imageNode=requireNode(group,'Spirit3102');const imageWidget=imageNode.getComponent(Widget);if(imageWidget)imageWidget.enabled=false; + imageNode.parent=contentRoot;imageNode.setPosition(0,0,0);need(imageNode.getComponent(UITransform),'notice image transform').setAnchorPoint(0.5,1); + let pageGeneration=0; + let selectedPage=0; + let releaseImage=()=>{}; + this.modalReleases.push(()=>releaseImage()); + const link=requireNode(group,'Spirit576'); + this.bind(link,()=>this.openUrl(this.policy.notices[selectedPage].url),true); + this.bind(imageNode,()=>this.openUrl(this.policy.notices[selectedPage].url),true); + const show=(index:number)=>{ + selectedPage=index;releaseImage();releaseImage=()=>{}; + const generation=++pageGeneration; + const notice=this.policy.notices[index]; + for(const id of [404,3105,3106,576])requireNode(group,`Spirit${id}`).active=false; + link.active=notice.type===0&¬ice.url!==''; + imageNode.active=false; + const content=contentLabel;content.string=notice.text;content.node.active=notice.type===0; + content.updateRenderData(true);contentUI.height=Math.max(450,textUI.height);scroll.scrollToTop(0); + if(notice.type===1&¬ice.image){ + imageNode.active=true; + const sprite=need(imageNode.getComponent(Sprite),'notice image');sprite.spriteFrame=null; + assetManager.loadRemote(notice.image,{ext:'.png'},(error,image)=>{ + if(this.disposed||this.modal!==modal||generation!==pageGeneration||!isValid(imageNode))return; + if(error){this.tip(`公告图片加载失败:${error.message}`);return;} + const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.sizeMode=Sprite.SizeMode.CUSTOM;sprite.spriteFrame=frame; + need(imageNode.getComponent(UITransform),'notice image transform').setContentSize(725,notice.height);contentUI.height=Math.max(450,notice.height);scroll.scrollToTop(0); + releaseImage=()=>{sprite.spriteFrame=null;frame.destroy();texture.destroy();}; + }); + } + }; + const tabs=need(requireNode(group,'NoticeTabs').getComponent(NoticeTabsView),'NoticeTabsView'); + this.modalReleases.push(tabs.bindPages(this.policy.notices.map((notice,index)=>index===0||notice.text!==''||notice.image!==''),show)); + } openJoin():void { const modal=this.open(this.assets.join,'join','group-5'); - for(const id of [27,59])this.bind(requireNode(modal,`group-5/Spirit${id}`),()=>this.close(),true); - // Room entry needs the real game host before a room request can be sent safely. - for(const id of [28,29,30,31,32,33,34,35,36,37,38,39,40])this.bind(requireNode(modal,`group-5/Spirit${id}`),()=>this.tip('加入房间将在目标子游戏接入后启用'),true); - for(let id=236;id<=241;id++)requireNode(modal,`group-5/Spirit${id}`).active=false; + const commands=need(this.extras?.room,'room entry commands'); + const view=need(modal.getComponent(JoinRoomView),'JoinRoomView');this.roomPage=view; + const saved=window.localStorage.getItem('tsgame_shortcode_'+this.config.identity.gameid+this.config.identity.agentid); + const current=()=>this.modal===modal&&this.roomPage===view&&this.model.canAct; + view.open({initialCode:saved===null?'':saved,submit:code=>{if(current())commands.join(code);},cancel:()=>{if(this.modal===modal)this.close();}, + paste:()=>need(this.extras,'clipboard').paste(),reportError:text=>this.tip(text)}); + view.setBusy(commands.busy()); } - openShare():void { - const modal=this.open(this.assets.share,'share','group-11'),policy=resolveSharePolicy(this.config); - this.bind(requireNode(modal,'group-11/Spirit72'),()=>this.close(),true); - for(const [id,friend] of [[74,1],[75,2]] as const)this.bind(requireNode(modal,`group-11/Spirit${id}`),()=>{ - const data=sharePayload(policy,friend);if(data)this.nativeCall('friendsSharetypeUrlToptitleDescript',data); - this.close(); - },true); - if(policy.background){ - const node=requireNode(modal,'group-11/Spirit345'),sprite=need(node.getComponent(Sprite),'share background'); - assetManager.loadRemote(policy.background,{ext:'.png'},(error,image)=>{ - if(this.disposed||this.modal!==modal||!isValid(node))return; - if(error){this.tip(`分享背景加载失败:${error.message}`);return;} - const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame; - this.modalReleases.push(()=>{sprite.spriteFrame=null;frame.destroy();texture.destroy();}); - }); + openCreate():void { + const commands=need(this.extras?.room,'room entry commands'); + const modal=this.open(commands.createPrefab,'create',null); + const page=need(modal.getComponent(CreateRoomPage),'subgame CreateRoomPage');this.roomPage=page; + const text=window.localStorage.getItem('tsgame_roomtype_'+this.config.identity.gameid+this.config.identity.agentid); + let previous:unknown=null; + if(text!==null&&text!==''){ + const saved=JSON.parse(text);if(!saved||!Array.isArray(saved.data))throw Error('Invalid saved roomtype');previous=saved.data; } + const current=()=>this.modal===modal&&this.roomPage===page&&this.model.canAct; + page.open({previousRoomtype:previous,submit:roomtype=>{if(current())commands.create(roomtype);},cancel:()=>{if(this.modal===modal)this.close();},reportError:text=>{if(current())this.tip(text);}}); + page.setBusy(commands.busy()); } - private openVipRooms():void { - const modal=this.open(this.assets.header,'vip-rooms','group-103'),group=requireNode(modal,'group-103'); - this.menu.active=false;this.noticeMenu.active=false; - this.bind(requireNode(group,'Spirit3182'),()=>this.close(),true); - for(const id of [3190,3191])requireNode(group,`Spirit${id}`).active=false; - const advanced=need(this.model.advanced,'players.self.advanced'); - for(const id of advanced?[3170,3171]:[3183,3184,3194,3195,3196,3197,3198,3214,3215,3235])requireNode(group,`Spirit${id}`).active=false; - const view=need(modal.getComponent(VipRoomListView),'VipRoomListView'),port=need(this.vip,'VIP command port'); - view.configure(port.describe); - this.bind(requireNode(group,'Spirit3175'),()=>view.showRooms(),true); - this.bind(requireNode(group,'Spirit3184'),()=>{this.close();this.onAction('create-room');},true); - this.bind(requireNode(group,'Spirit3240'),()=>this.openJoin(),true); - this.bind(requireNode(group,'Spirit3231'),()=>this.onAction('vip-rank'),true); - this.bind(requireNode(group,'Spirit3199'),()=>this.tip(`房间 ${view.selectedCode}:短房号排行模块尚未接入`),true); - for(const id of [3194,3195,3214])this.bind(requireNode(group,`Spirit${id}`),()=>this.tip('黑名单管理模块尚未接入'),true); - for(const id of [3196,3197,3215])this.bind(requireNode(group,`Spirit${id}`),()=>this.tip('统一魅力值设置模块尚未接入'),true); - this.bind(requireNode(group,'Spirit3235'),()=>this.tip('好友管理模块尚未接入'),true); - if(advanced){ - label(group,'Spirit3198').string='(黑名单内玩家临时禁入房间,请尽快到后台处理好友关系)'; - requireNode(group,'Spirit3235').active=!!this.config.remoteConfig.getValue('managerUrl'); - } - const join=(code:VipCode)=>{if(this.model.canAct&&this.modal===modal){try{port.join(code);}catch(e){this.tip(e instanceof Error?e.message:String(e));}}}; - modal.on('vip-join',join);this.modalReleases.push(()=>modal.off('vip-join',join)); - const refresh=()=>{if(this.model.canAct&&this.modal===modal)port.request(this.vipHistory().read());}; - const raw=this.config.remoteConfig.getValue('vipFlushTime'); - // GameData.sysConfig defines the optional refresh setting as 10000 ms. - const delay=raw==null?10000:Number(raw);if(!Number.isFinite(delay)||delay<=0)throw Error('Invalid vipFlushTime'); - const timer=setInterval(()=>{try{if(this.vipHistory().read().length>0)refresh();}catch(e){this.tip(e instanceof Error?e.message:String(e));}},delay); - this.modalReleases.push(()=>clearInterval(timer)); - if(this.vipData)this.renderVipRooms(this.vipData); - refresh(); - if(!advanced){const wx=this.config.remoteConfig.getValue('vipWx');label(group,'Spirit3171').string=wx==null?'':String(wx);} - } - private vipHistory():VipRoomHistory {return new VipRoomHistory(window.localStorage,this.config.identity.gameid,this.config.identity.agentid);} - private renderVipRooms(rooms:readonly VipRoom[]):void {need(need(this.modal,'VIP modal').getComponent(VipRoomListView),'VipRoomListView').render(rooms,this.vipHistory().read());} - private requestTask(kind:'list'|'award',send:()=>void):void { - if(this.taskPending||!this.model.canAct)return; - this.taskPending=kind; - this.setTaskButtons(false); - this.taskTimer=setTimeout(()=>{this.cancelTaskRequest();this.tip('任务请求超时,请重试');},10000); - try{send();}catch(error){this.cancelTaskRequest();throw error;} - } - /** Server show_message and connection transitions release the local request guard. */ + setRoomBusy(busy:boolean):void{this.roomPage?.setBusy(busy);} + cancelRoomPage():void{if(this.roomPage)this.close();} + openShare():void { + const modal=this.open(this.assets.share,'share','group-11'),policy=resolveSharePolicy(this.config); + this.bind(requireNode(modal,'group-11/Spirit72'),()=>this.close(),true); + for(const [id,friend] of [[74,1],[75,2]] as const)this.bind(requireNode(modal,`group-11/Spirit${id}`),()=>{ + const data=sharePayload(policy,friend);if(data)this.nativeCall('friendsSharetypeUrlToptitleDescript',data); + this.close(); + },true); + if(policy.background){ + const node=requireNode(modal,'group-11/Spirit345'),sprite=need(node.getComponent(Sprite),'share background'); + assetManager.loadRemote(policy.background,{ext:'.png'},(error,image)=>{ + if(this.disposed||this.modal!==modal||!isValid(node))return; + if(error){this.tip(`分享背景加载失败:${error.message}`);return;} + const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame; + this.modalReleases.push(()=>{sprite.spriteFrame=null;frame.destroy();texture.destroy();}); + }); + } + } + private openVipRooms():void { + const modal=this.open(this.assets.header,'vip-rooms','group-103'),group=requireNode(modal,'group-103'); + this.menu.active=false;this.noticeMenu.active=false; + this.bind(requireNode(group,'Spirit3182'),()=>this.close(),true); + for(const id of [3190,3191])requireNode(group,`Spirit${id}`).active=false; + const advanced=need(this.model.advanced,'players.self.advanced'); + for(const id of advanced?[3170,3171]:[3183,3184,3194,3195,3196,3197,3198,3214,3215,3235])requireNode(group,`Spirit${id}`).active=false; + const view=need(modal.getComponent(VipRoomListView),'VipRoomListView'),port=need(this.vip,'VIP command port'); + view.configure(port.describe); + this.bind(requireNode(group,'Spirit3175'),()=>view.showRooms(),true); + this.bind(requireNode(group,'Spirit3184'),()=>this.openCreate(),true); + this.bind(requireNode(group,'Spirit3240'),()=>this.openJoin(),true); + this.bind(requireNode(group,'Spirit3231'),()=>this.requestVipRank(),true); + this.bind(requireNode(group,'Spirit3199'),()=>this.tip(`房间 ${view.selectedCode}:短房号排行模块尚未接入`),true); + for(const id of [3194,3195,3214])this.bind(requireNode(group,`Spirit${id}`),()=>this.tip('黑名单管理模块尚未接入'),true); + for(const id of [3196,3197,3215])this.bind(requireNode(group,`Spirit${id}`),()=>this.tip('统一魅力值设置模块尚未接入'),true); + this.bind(requireNode(group,'Spirit3235'),()=>this.tip('好友管理模块尚未接入'),true); + if(advanced){ + label(group,'Spirit3198').string='(黑名单内玩家临时禁入房间,请尽快到后台处理好友关系)'; + requireNode(group,'Spirit3235').active=!!this.config.remoteConfig.getValue('managerUrl'); + } + const join=(code:VipCode)=>{if(this.model.canAct&&this.modal===modal){try{port.join(code);}catch(e){this.tip(e instanceof Error?e.message:String(e));}}}; + modal.on('vip-join',join);this.modalReleases.push(()=>modal.off('vip-join',join)); + const refresh=()=>{if(this.model.canAct&&this.modal===modal)port.request(this.vipHistory().read());}; + const raw=this.config.remoteConfig.getValue('vipFlushTime'); + // GameData.sysConfig defines the optional refresh setting as 10000 ms. + const delay=raw==null?10000:Number(raw);if(!Number.isFinite(delay)||delay<=0)throw Error('Invalid vipFlushTime'); + const timer=setInterval(()=>{try{if(this.vipHistory().read().length>0)refresh();}catch(e){this.tip(e instanceof Error?e.message:String(e));}},delay); + this.modalReleases.push(()=>clearInterval(timer)); + if(this.vipData)this.renderVipRooms(this.vipData); + refresh(); + if(!advanced){const wx=this.config.remoteConfig.getValue('vipWx');label(group,'Spirit3171').string=wx==null?'':String(wx);} + } + private vipHistory():VipRoomHistory {return new VipRoomHistory(window.localStorage,this.config.identity.gameid,this.config.identity.agentid);} + private renderVipRooms(rooms:readonly VipRoom[]):void {need(need(this.modal,'VIP modal').getComponent(VipRoomListView),'VipRoomListView').render(rooms,this.vipHistory().read());} + private requestTask(kind:'list'|'award',send:()=>void):void { + if(this.taskPending||!this.model.canAct)return; + this.taskPending=kind; + this.setTaskButtons(false); + this.taskTimer=setTimeout(()=>{this.cancelTaskRequest();this.tip('任务请求超时,请重试');},10000); + try{this.requestLoading.begin(kind==='list'?'get_player_task':'get_task_award');send();}catch(error){this.cancelTaskRequest();throw error;} + } + /** Server show_message and connection transitions release the local request guard. */ cancelTaskRequest():void { - if(this.taskTimer!==null)clearTimeout(this.taskTimer); - this.taskTimer=null;this.taskPending=null;this.setTaskButtons(true); + this.requestLoading.cancel('get_player_task');this.requestLoading.cancel('get_task_award'); + if(this.taskTimer!==null)clearTimeout(this.taskTimer); + this.taskTimer=null;this.taskPending=null;this.setTaskButtons(true); + } + private setTaskButtons(enabled:boolean):void { + if(this.modalKind==='tasks'&&this.modal)for(const button of this.modal.getComponentsInChildren(Button)){ + if(button.node.name==='Spirit124'||button.node.name==='Spirit125')button.interactable=enabled; + } + } + private openTasks(tasks:readonly TaskData[]):void { + const modal=this.modalKind==='tasks'?need(this.modal,'tasks'):this.open(this.assets.tasks,'tasks','group-10'); + if(this.modalReleases.length===0){ + for(const id of [112,115])this.bind(requireNode(modal,`group-10/Spirit${id}`),()=>this.close(),true); + const claim=(taskid:number|string)=>{if(this.model.canAct&&!this.disposed&&this.modal===modal){ + try{this.requestTask('award',()=>this.claimTask(taskid));}catch(error){this.tip(error instanceof Error?error.message:String(error));} + }}; + const perform=()=>{if(this.model.canAct&&!this.taskPending&&this.modal===modal){this.close();this.openShare();}}; + modal.on('task-claim',claim);modal.on('task-do',perform); + this.modalReleases.push(()=>{modal.off('task-claim',claim);modal.off('task-do',perform);}); + } + need(modal.getComponent(TaskListView),'TaskListView').render(tasks,this.policy.roomCardName); + } + private requestInvite(kind:'query'|'binding',send:()=>void):void { + if(this.invitePending||!this.model.canAct||this.disposed)return; + this.invitePending=kind; + this.inviteTimer=setTimeout(()=>{this.cancelInviteRequest();this.tip('邀请码请求超时,请重试');},15000); + try{send();}catch(error){this.cancelInviteRequest();throw error;} } - private setTaskButtons(enabled:boolean):void { - if(this.modalKind==='tasks'&&this.modal)for(const button of this.modal.getComponentsInChildren(Button)){ - if(button.node.name==='Spirit124'||button.node.name==='Spirit125')button.interactable=enabled; - } + cancelInviteRequest():void { + if(this.inviteTimer!==null)clearTimeout(this.inviteTimer); + this.inviteTimer=null;this.invitePending=null; } - private openTasks(tasks:readonly TaskData[]):void { - const modal=this.modalKind==='tasks'?need(this.modal,'tasks'):this.open(this.assets.tasks,'tasks','group-10'); - if(this.modalReleases.length===0){ - for(const id of [112,115])this.bind(requireNode(modal,`group-10/Spirit${id}`),()=>this.close(),true); - const claim=(taskid:number|string)=>{if(this.model.canAct&&!this.disposed&&this.modal===modal){ - try{this.requestTask('award',()=>this.claimTask(taskid));}catch(error){this.tip(error instanceof Error?error.message:String(error));} - }}; - const perform=()=>{if(this.model.canAct&&!this.taskPending&&this.modal===modal){this.close();this.openShare();}}; - modal.on('task-claim',claim);modal.on('task-do',perform); - this.modalReleases.push(()=>{modal.off('task-claim',claim);modal.off('task-do',perform);}); - } - need(modal.getComponent(TaskListView),'TaskListView').render(tasks,this.policy.roomCardName); + private openInvite(code:string|null):void { + const modal=this.open(this.assets.invite,'invite','group-80'),group=requireNode(modal,'group-80'); + requireNode(this.noticeMenu,'group-95').active=false; + this.bind(requireNode(group,'Spirit362'),()=>this.close(),true); + const input=requireNode(group,'Spirit365/InputEditBox'),edit=need(input.getComponent(EditBox),'invite EditBox'); + if(!edit.textLabel||!edit.placeholderLabel)throw Error('Invite EditBox requires serialized labels'); + const bound=!!code; + for(const id of [365,366,367])requireNode(group,`Spirit${id}`).active=!bound; + const message=label(group,'Spirit368');message.node.active=bound; + message.string=bound?`您已绑定邀请码,所绑定邀请码为:${code}`:''; + edit.string=''; + if(bound)return; + const changed=()=>{edit.string=edit.string.replace(/[^0-9]/g,'');}; + input.on(EditBox.EventType.TEXT_CHANGED,changed);this.modalReleases.push(()=>input.off(EditBox.EventType.TEXT_CHANGED,changed)); + let pasting=false; + this.bind(requireNode(group,'Spirit366'),()=>{ + if(pasting)return;pasting=true; + void need(this.invite,'invite commands').paste().then(text=>{ + if(!this.disposed&&this.model.canAct&&this.modal===modal)edit.string=text; + }).catch(error=>{if(!this.disposed&&this.modal===modal)this.tip(error instanceof Error?error.message:String(error));}).finally(()=>{pasting=false;}); + },true); + this.bind(requireNode(group,'Spirit367'),()=>{ + const text=edit.string;edit.blur();this.close(); + this.requestInvite('binding',()=>need(this.invite,'invite commands').bind(text)); + },true); } close():void { - this.cancelTaskRequest(); - if(this.modalKind==='vip-rooms'){this.menu.active=true;this.noticeMenu.active=true;} - for(const release of this.modalReleases.splice(0))release(); - this.modal?.destroy();this.modal=null;this.modalKind=''; - } - dispose():void { - if(this.disposed)return;this.disposed=true;this.avatarGeneration++;this.close(); - for(const release of this.releases.splice(0))release(); - this.avatarFrame?.destroy();this.avatarTexture?.destroy(); - for(const root of [this.menu,this.header,this.noticeMenu])if(isValid(root))root.destroy(); - } -} + if(this.roomPage){this.roomPage.close();this.roomPage=null;this.extras?.room?.close();} + this.cancelRankRequest(); + this.cancelInviteRequest(); + this.cancelTaskRequest(); + if(this.modalKind==='vip-rooms'){this.menu.active=true;this.noticeMenu.active=true;} + for(const release of this.modalReleases.splice(0))release(); + this.modal?.destroy();this.modal=null;this.modalKind=''; + } + dispose():void { + if(this.disposed)return;this.disposed=true;this.avatarGeneration++;this.close(); + for(const release of this.releases.splice(0))release(); + this.avatarFrame?.destroy();this.avatarTexture?.destroy(); + for(const root of [this.menu,this.header,this.noticeMenu])if(isValid(root))root.destroy(); + } +} diff --git a/cocoscreator_projects/YouleNexus/out/legacy-migration/BackHall_Layer.prefab b/cocoscreator_projects/YouleNexus/out/legacy-migration/BackHall_Layer.prefab index d9a2811..058de3e 100644 --- a/cocoscreator_projects/YouleNexus/out/legacy-migration/BackHall_Layer.prefab +++ b/cocoscreator_projects/YouleNexus/out/legacy-migration/BackHall_Layer.prefab @@ -196,7 +196,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "UAhPXQ0pcgPLBhZGsoLhpk" + "fileId": "KmCbPcyLuSAnPSb4Ao1ZbH" }, { "__type__": "cc.ProgressBar", @@ -234,7 +234,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "X9jNlgxMuWTZWg3yko0Ef5" + "fileId": "1w6LAU60abbZ653JG2MhQY" }, { "__type__": "cc.Widget", @@ -266,7 +266,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "M8oETywDKfVtW03c6i5c8F" + "fileId": "6sH0fhb7cSe3CagtT31MHh" }, { "__type__": "cc.PrefabInfo", @@ -276,7 +276,7 @@ "asset": { "__id__": 0 }, - "fileId": "FkVgDsbQwHUPuTSOtY78jM", + "fileId": "NguEjs0OMrEiDszJkUp68U", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -307,7 +307,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "8oAPaJwl2mT0I9ETeTCJM0" + "fileId": "nWg4388SGHDRGLuhjT8OuJ" }, { "__type__": "cc.Widget", @@ -343,7 +343,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "0PbpjPqdut36m0p7vkdSWq" + "fileId": "hcCQOygYzxbGZ8BWFuNw8a" }, { "__type__": "cc.PrefabInfo", @@ -353,7 +353,7 @@ "asset": { "__id__": 0 }, - "fileId": "2jVxPppbazd1zjDELWSE6A", + "fileId": "jxRFiWtNSo2m42T7OBLQUK", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -384,7 +384,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "zY8k5HH8JOpzGTwlZH4ttW" + "fileId": "sGRA19LNm3KNKj8v8Iu2MI" }, { "__type__": "cc.Widget", @@ -420,7 +420,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "X0gZATQiaIriidWVTQdZoA" + "fileId": "MOcjiata5iz4gBYQ9VKk7r" }, { "__type__": "cc.PrefabInfo", @@ -430,7 +430,7 @@ "asset": { "__id__": 0 }, - "fileId": "1kL3d0xRmGSN3bpCDVKBf4", + "fileId": "uwhtXKAkXjgGj7x4Lkyif5", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null diff --git a/cocoscreator_projects/YouleNexus/out/legacy-migration/Protol_Layer.prefab b/cocoscreator_projects/YouleNexus/out/legacy-migration/Protol_Layer.prefab index 0661881..500ac0a 100644 --- a/cocoscreator_projects/YouleNexus/out/legacy-migration/Protol_Layer.prefab +++ b/cocoscreator_projects/YouleNexus/out/legacy-migration/Protol_Layer.prefab @@ -205,7 +205,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "DiD9kCtFwKiAHcCBsw91ox" + "fileId": "6QCalwmn12FDYzFvhkWjgp" }, { "__type__": "cc.Sprite", @@ -250,7 +250,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "MFwz5OTT5Pr2abCwd1LzmF" + "fileId": "4i6Mu1aRyE1YudHtgYsm0N" }, { "__type__": "cc.Widget", @@ -284,7 +284,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "zewbk9AykIRIGfsgqMdeov" + "fileId": "gYsQszfnj4K5zMiYpRyTic" }, { "__type__": "cc.PrefabInfo", @@ -294,7 +294,7 @@ "asset": { "__id__": 0 }, - "fileId": "99cDwVxXMUbpi7RwxkhnuC", + "fileId": "dieQbqOspfeWrBmXa2Cc85", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -378,7 +378,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "C8fBTsdpsoqBerU6buLHcB" + "fileId": "odvJ5gkJS0qrQi0kO6wPTU" }, { "__type__": "cc.Sprite", @@ -423,7 +423,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "UiKhB2jxsNfvLd57wDNuO6" + "fileId": "BQiVBaYuKYasRYP9YtJkZ7" }, { "__type__": "cc.Widget", @@ -455,7 +455,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "MVtGc8lHvLnSwPzXIIMVKS" + "fileId": "qtTcAU1UZi6oA9NNjGijAF" }, { "__type__": "cc.PrefabInfo", @@ -465,7 +465,7 @@ "asset": { "__id__": 0 }, - "fileId": "HPRQdpFIuUZVPFblNKz3y4", + "fileId": "r7roQZRsedT3Ubmlx5CyLM", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -549,7 +549,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "BtHLuUBkZ8WZvbxwHAE82H" + "fileId": "KjnhG1Zh8MHtXMUEooPM4e" }, { "__type__": "cc.Sprite", @@ -594,7 +594,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "Ky2NjkIVvDYNRZytwfZIXw" + "fileId": "zuv8HMjFUIrxDDOmdJ3Ehs" }, { "__type__": "cc.Widget", @@ -626,7 +626,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "NNdJ4Kj5iclMHNtHo18HI3" + "fileId": "J9tRhiDkq2ipS0G0rQonpB" }, { "__type__": "cc.PrefabInfo", @@ -636,7 +636,7 @@ "asset": { "__id__": 0 }, - "fileId": "f9sii87ZZEfCzqdyuoO0mr", + "fileId": "ZdVxC7WUVo8XqAJvBKSz6N", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -720,7 +720,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "hYdY9g8BWahqN9Sc7WNmKS" + "fileId": "xGuK9Fik8tSDugKQxZRclS" }, { "__type__": "cc.Sprite", @@ -765,7 +765,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "tu7H2RG3i8L2lEU3lEG9Bv" + "fileId": "QDPlk4h7vGgo0bp2av1eUn" }, { "__type__": "cc.Widget", @@ -797,7 +797,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "kOZ8nGFhuNT4YkXfky0rrX" + "fileId": "TlWIMNwIYHM0qxHQkheDpl" }, { "__type__": "cc.PrefabInfo", @@ -807,7 +807,7 @@ "asset": { "__id__": 0 }, - "fileId": "TZT7Z47HZ5nFQ4oeMWaUGt", + "fileId": "w6A6CaVzVT2SoSBT98sh23", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -838,7 +838,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "JfQyr8aYu8oOOJq2DU7T09" + "fileId": "7VFMjnKn9aBttmIlmZZlMU" }, { "__type__": "cc.Widget", @@ -874,7 +874,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "a6SP5fORymIDQ37aJo5IpF" + "fileId": "W2u01z74oLQxRBVP8cC6y0" }, { "__type__": "cc.PrefabInfo", @@ -884,7 +884,7 @@ "asset": { "__id__": 0 }, - "fileId": "8azfm7OwjX4ETglWDxCy6Q", + "fileId": "WCobwawmZrx8SS6FBosXZe", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -915,7 +915,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "KEccdWDJ1pyLt5u5rDgTEo" + "fileId": "gWSaqoabdpY4zuli1me5xB" }, { "__type__": "cc.Widget", @@ -951,7 +951,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "FGW4cAL6WnBt7XtmH1180z" + "fileId": "isLBoJvjorlJSa3FD6Bynr" }, { "__type__": "cc.PrefabInfo", @@ -961,7 +961,7 @@ "asset": { "__id__": 0 }, - "fileId": "55U58Rmn7SzArxrX69S6lj", + "fileId": "YblXWKKQwlkZJKKb8JN5cz", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null diff --git a/cocoscreator_projects/framework-tests/adapters/invite-clipboard.test.ts b/cocoscreator_projects/framework-tests/adapters/invite-clipboard.test.ts new file mode 100644 index 0000000..53e9c86 --- /dev/null +++ b/cocoscreator_projects/framework-tests/adapters/invite-clipboard.test.ts @@ -0,0 +1,19 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readInviteClipboard,readClipboardText} from '../../YouleNexus/assets/framework/adapters/native/invite-clipboard.ts'; +test('invite paste keeps native signatures and strips non-digits without truncating',async()=>{ + const calls:unknown[]=[]; + assert.equal(await readInviteClipboard({kind:'settings',read:(...args:unknown[])=>{calls.push(args);return '码00 1234567';}}),'001234567'); + assert.deepEqual(calls,[[]]); + assert.equal(await readInviteClipboard({kind:'bridge',call:(name,data,cb)=>{assert.equal(name,'gamepastetext');assert.equal(data,'');cb('00-12');}}),'0012'); + assert.equal(await readInviteClipboard({kind:'browser',read:async()=>'A123'}),'123'); + await assert.rejects(readInviteClipboard({kind:'settings',read:()=>null}),/clipboard/); +}); + +test('redemption paste preserves letters, punctuation, whitespace and leading zeroes',async()=>{ + for(const text of ['00Ab-12',' aB ','']){ + assert.equal(await readClipboardText({kind:'settings',read:()=>text}),text); + assert.equal(await readClipboardText({kind:'bridge',call:(name,data,cb)=>{assert.equal(name,'gamepastetext');assert.equal(data,'');cb(text);}}),text); + assert.equal(await readClipboardText({kind:'browser',read:async()=>text}),text); + } +}); diff --git a/cocoscreator_projects/framework-tests/adapters/owned-room-history.test.ts b/cocoscreator_projects/framework-tests/adapters/owned-room-history.test.ts new file mode 100644 index 0000000..05b6ebc --- /dev/null +++ b/cocoscreator_projects/framework-tests/adapters/owned-room-history.test.ts @@ -0,0 +1,14 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {OwnedRoomHistory} from '../../YouleNexus/assets/framework/adapters/native/owned-room-history.ts'; +test('owned rooms use legacy cache, order, and server outcome lifecycle',()=>{ + const data=new Map();const history=new OwnedRoomHistory({getItem:k=>data.get(k)??null,setItem:(k,v)=>{data.set(k,v);}},'g','a'); + assert.deepEqual(history.read(),[]);assert.equal(data.get('tsgame_roomlist_ga'),'{{}}'.replace('{{}}','{"data":[]}')); + history.exited({isowner:1,roomcode:'123456'},0);history.exited({isowner:1,roomcode:'654321'},0); + history.exited({isowner:1,roomcode:'123456'},0);assert.deepEqual(history.read(),[654321,123456]); + history.exited({isowner:0,roomcode:333333},0);history.exited({isowner:1,roomcode:333333},1);assert.equal(history.read().length,2); + history.joined({state:2,roomcode:123456});assert.equal(history.read().length,2); + history.joined({state:99,roomcode:123456});assert.deepEqual(history.read(),[654321]); + history.joined({state:0,isowner:1,roomcode:654321});assert.deepEqual(history.read(),[]); + data.set('tsgame_roomlist_ga','bad');assert.throws(()=>history.read()); +}); diff --git a/cocoscreator_projects/framework-tests/platform/commands.test.ts b/cocoscreator_projects/framework-tests/platform/commands.test.ts index 5bbc4aa..428cf12 100644 --- a/cocoscreator_projects/framework-tests/platform/commands.test.ts +++ b/cocoscreator_projects/framework-tests/platform/commands.test.ts @@ -1,223 +1,283 @@ -import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; -// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 -const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); -import { remoteFixture } from '../fixtures/remote-config.ts'; -import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; -import { test } from 'node:test'; -import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; -import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts'; -import { PlatformCommands } from '../../YouleNexus/assets/framework/platform/commands.ts'; -import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; -import { - parseLoginResponse, -} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts'; - -const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ - startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release', - isDebugger: false, - identity: Object.freeze({ - agentid: 'agent-from-config', - channelid: 'channel-from-config', - gameid: 'game-from-config', - marketid: 'market-from-config', - version: 41, - }), - servers: Object.freeze(['ws://fixture']), - source: 'remote', - gameserver: 'https://fixture/config', - ...remoteFixture(), -}); - -function fixture(name: string): { data: Record } { - return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')); -} - -function makeStore(location: 'outside' | 'inside', stage = 0, infinite = 0): PlatformStore { - const store = new PlatformStore(); - const source = fixture(location === 'inside' ? 'player-login-room.json' : 'player-login-success.json'); - if (location === 'inside') { - source.data.isbattle = stage; - source.data.infinite = infinite; - } - store.applyLoginSuccess(parseLoginResponse(source.data)); - return store; -} - +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); +import { remoteFixture } from '../fixtures/remote-config.ts'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { test } from 'node:test'; +import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; +import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts'; +import { PlatformCommands } from '../../YouleNexus/assets/framework/platform/commands.ts'; +import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; +import { + parseLoginResponse, +} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts'; + +const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ + startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release', + isDebugger: false, + identity: Object.freeze({ + agentid: 'agent-from-config', + channelid: 'channel-from-config', + gameid: 'game-from-config', + marketid: 'market-from-config', + version: 41, + }), + servers: Object.freeze(['ws://fixture']), + source: 'remote', + gameserver: 'https://fixture/config', + ...remoteFixture(), +}); + +function fixture(name: string): { data: Record } { + return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')); +} + +function makeStore(location: 'outside' | 'inside', stage = 0, infinite = 0): PlatformStore { + const store = new PlatformStore(); + const source = fixture(location === 'inside' ? 'player-login-room.json' : 'player-login-success.json'); + if (location === 'inside') { + source.data.isbattle = stage; + source.data.infinite = infinite; + } + store.applyLoginSuccess(parseLoginResponse(source.data)); + return store; +} + function makeCommands(store: PlatformStore): { - readonly commands: PlatformCommands; - readonly sent: OutboundEnvelope[]; -} { - const sent: OutboundEnvelope[] = []; - return { - commands: new PlatformCommands({ - runtimeConfig: RUNTIME_CONFIG, - store, - send: { send: (envelope) => { sent.push(envelope); } }, - }), - sent, - }; + readonly commands: PlatformCommands; + readonly sent: OutboundEnvelope[]; +} { + const sent: OutboundEnvelope[] = []; + return { + commands: new PlatformCommands({ + runtimeConfig: RUNTIME_CONFIG, + store, + send: { send: (envelope) => { sent.push(envelope); } }, + }), + sent, + }; } -test('VIP query and both entry forms use the exact agent protocol',()=>{ +test('invite query and binding preserve exact legacy fields and leading zeros',()=>{ const store=makeStore('outside'),{commands,sent}=makeCommands(store); - commands.getVipRooms(['123456']);commands.joinVipRoom('123456',null);commands.joinVipRoom(900001,{city:'测试'}); - assert.deepEqual(sent[0],{app:'youle',route:'agent',rpc:'getInfoByShortCode',data:{agentid:'agent-from-config',gameid:'game-from-config',shortcodeList:['123456']}}); - assert.deepEqual(sent[1],{app:'youle',route:'agent',rpc:'self_join_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:'123456',joinmode:0,location:null,ip:store.getState().players.entities[430511].ip}}); - assert.equal((sent[2].data as {roomcode:number}).roomcode,900001); - assert.throws(()=>makeCommands(new PlatformStore()).commands.getVipRooms([]),/logged-in/); -}); - -test('task requests preserve legacy identity fields and reject unclaimable entries',()=>{ - const store=makeStore('outside');const {commands,sent}=makeCommands(store); - commands.getPlayerTasks(); - store.applyTaskList({tasks:[{taskid:7,title:'任务',award:1,finish:1,total:1,state:1}]}); - commands.claimTask(7); + commands.getPlayerInvitecode();commands.bindInvitecode('00123'); assert.deepEqual(sent,[ - {app:'youle',route:'agent',rpc:'get_player_task',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config'}}, - {app:'youle',route:'agent',rpc:'get_task_award',data:{agentid:'agent-from-config',playerid:430511,taskid:7}}, + {app:'youle',route:'agent',rpc:'get_player_invitecode',data:{agentid:'agent-from-config',playerid:430511,unionid:'test_unionid',openid:'o1'}}, + {app:'youle',route:'agent',rpc:'binding_invitecode',data:{agentid:'agent-from-config',playerid:430511,invitecode:'00123'}}, ]); - store.applyTaskAward({taskid:7,taskstate:0}); - assert.equal(store.getState().tasks?.[0].state,2); - assert.equal(store.getState().players.entities[430511].taskstate,0); - assert.throws(()=>commands.claimTask(7),/claimable/); - const before=store.getState(); - assert.throws(()=>store.applyTaskList({tasks:[{taskid:7}]})); - assert.equal(store.getState(),before); - assert.throws(()=>makeCommands(new PlatformStore()).commands.getPlayerTasks(),/logged-in/); + assert.throws(()=>makeCommands(new PlatformStore()).commands.getPlayerInvitecode(),/logged-in/); + assert.throws(()=>makeCommands(makeStore('inside')).commands.bindInvitecode('1'),/logged-in/); }); -test('task numeric strings normalize at ingestion and invalid values never commit',()=>{ - const store=makeStore('outside'); - const task={taskid:'7',title:'任务',award:'3',finish:'2',total:'5',state:'0'}; - store.applyTaskList({tasks:[task]}); - assert.deepEqual(store.getState().tasks,[{...task,award:3,finish:2,total:5,state:0}]); - assert.equal(task.award,'3'); +test('invite responses publish even unchanged codes, update only on success, and reject malformed data atomically',()=>{ + const store=makeStore('outside');let updates=0;store.subscribe(()=>updates++); + store.applyInvitecode({invitecode:''});store.applyInvitecode({invitecode:''}); + assert.equal(updates,2); + store.applyInviteBinding({state:0,invitecode:'00123',error:'绑定成功'}); + assert.equal(store.getState().players.entities[430511].invitecode,'00123'); + store.applyInviteBinding({state:1,error:'邀请码无效'}); + assert.equal(store.getState().players.entities[430511].invitecode,'00123'); const before=store.getState(); - for(const field of ['award','finish','total','state']){ - for(const value of ['', ' ', 'abc', 'Infinity', null, true, [], {}, undefined]){ - assert.throws(()=>store.applyTaskList({tasks:[{...task,[field]:value}]}),new RegExp(field)); - assert.equal(store.getState(),before); - } + assert.throws(()=>store.applyInvitecode({}),/invitecode/); + assert.throws(()=>store.applyInviteBinding({state:0,invitecode:123,error:'bad'}),/invitecode/); + assert.throws(()=>store.applyInviteBinding({state:0,invitecode:'wrong'}),/error/); + for(const state of ['',null,true,'invalid','0.5']){ + assert.throws(()=>store.applyInviteBinding({state,invitecode:'wrong',error:'bad'}),/state/); } - assert.throws(()=>store.applyTaskList({tasks:[{...task,state:'1.5'}]}),/state/); + assert.equal(store.getState(),before); +}); + +test('VIP query and both entry forms use the exact agent protocol',()=>{ + const store=makeStore('outside'),{commands,sent}=makeCommands(store); + commands.getVipRooms(['123456']);commands.joinVipRoom('123456',null);commands.joinVipRoom(900001,{city:'测试'}); + assert.deepEqual(sent[0],{app:'youle',route:'agent',rpc:'getInfoByShortCode',data:{agentid:'agent-from-config',gameid:'game-from-config',shortcodeList:['123456']}}); + assert.deepEqual(sent[1],{app:'youle',route:'agent',rpc:'self_join_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:'123456',joinmode:0,location:null,ip:store.getState().players.entities[430511].ip}}); + assert.equal((sent[2].data as {roomcode:number}).roomcode,900001); + assert.throws(()=>makeCommands(new PlatformStore()).commands.getVipRooms([]),/logged-in/); }); -test('prepare and exitRoom use the first-slice builders with authoritative config and room state', () => { - const { commands, sent } = makeCommands(makeStore('inside', 0)); - - commands.prepare(); - commands.exitRoom(); - - assert.deepEqual(sent, [ - { - app: 'youle', - route: 'room', - rpc: 'player_prepare', - data: { - agentid: 'agent-from-config', - playerid: 430511, - gameid: 'game-from-config', - roomcode: '100001', - }, - }, - { - app: 'youle', - route: 'room', - rpc: 'self_exit_room', - data: { - agentid: 'agent-from-config', - playerid: 430511, - gameid: 'game-from-config', - roomcode: '100001', - }, - }, +test('ranking and redemption use exact legacy fields, preserving card text',()=>{ + const {commands,sent}=makeCommands(makeStore('outside')); + commands.getTreasureList();commands.redeemCard('00Ab-12'); + assert.deepEqual(sent,[ + {app:'youle',route:'agent',rpc:'get_treasurelist',data:{agentid:'agent-from-config',gameid:'game-from-config'}}, + {app:'youle',route:'agent',rpc:'topup_card',data:{agentid:'agent-from-config',playerid:430511,cardno:'00Ab-12'}}, ]); - assert.equal(commands.prepare.length, 0); - assert.equal(commands.exitRoom.length, 0); + assert.throws(()=>commands.redeemCard(' '),/cardno/); + assert.throws(()=>makeCommands(new PlatformStore()).commands.getTreasureList(),/logged-in/); +}); + +test('task requests preserve legacy identity fields and reject unclaimable entries',()=>{ + const store=makeStore('outside');const {commands,sent}=makeCommands(store); + commands.getPlayerTasks(); + store.applyTaskList({tasks:[{taskid:7,title:'任务',award:1,finish:1,total:1,state:1}]}); + commands.claimTask(7); + assert.deepEqual(sent,[ + {app:'youle',route:'agent',rpc:'get_player_task',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config'}}, + {app:'youle',route:'agent',rpc:'get_task_award',data:{agentid:'agent-from-config',playerid:430511,taskid:7}}, + ]); + store.applyTaskAward({taskid:7,taskstate:0}); + assert.equal(store.getState().tasks?.[0].state,2); + assert.equal(store.getState().players.entities[430511].taskstate,0); + assert.throws(()=>commands.claimTask(7),/claimable/); + const before=store.getState(); + assert.throws(()=>store.applyTaskList({tasks:[{taskid:7}]})); + assert.equal(store.getState(),before); + assert.throws(()=>makeCommands(new PlatformStore()).commands.getPlayerTasks(),/logged-in/); +}); + +test('task numeric strings normalize at ingestion and invalid values never commit',()=>{ + const store=makeStore('outside'); + const task={taskid:'7',title:'任务',award:'3',finish:'2',total:'5',state:'0'}; + store.applyTaskList({tasks:[task]}); + assert.deepEqual(store.getState().tasks,[{...task,award:3,finish:2,total:5,state:0}]); + assert.equal(task.award,'3'); + const before=store.getState(); + for(const field of ['award','finish','total','state']){ + for(const value of ['', ' ', 'abc', 'Infinity', null, true, [], {}, undefined]){ + assert.throws(()=>store.applyTaskList({tasks:[{...task,[field]:value}]}),new RegExp(field)); + assert.equal(store.getState(),before); + } + } + assert.throws(()=>store.applyTaskList({tasks:[{...task,state:'1.5'}]}),/state/); +}); + +test('prepare and exitRoom use the first-slice builders with authoritative config and room state', () => { + const { commands, sent } = makeCommands(makeStore('inside', 0)); + + commands.prepare(); + commands.exitRoom(); + + assert.deepEqual(sent, [ + { + app: 'youle', + route: 'room', + rpc: 'player_prepare', + data: { + agentid: 'agent-from-config', + playerid: 430511, + gameid: 'game-from-config', + roomcode: '100001', + }, + }, + { + app: 'youle', + route: 'room', + rpc: 'self_exit_room', + data: { + agentid: 'agent-from-config', + playerid: 430511, + gameid: 'game-from-config', + roomcode: '100001', + }, + }, + ]); + assert.equal(commands.prepare.length, 0); + assert.equal(commands.exitRoom.length, 0); +}); + +test('joinRoom uses authenticated Store identity and requires every non-optional environment field', () => { + const { commands, sent } = makeCommands(makeStore('outside')); + const location = { latitude: 28.7, longitude: 115.8 }; + + commands.joinRoom({ + roomcode: '654321', + location, + ip: '203.0.113.7', + vipMatch: 1, + match_id: 'match-9', + }); + + assert.deepEqual(sent, [{ + app: 'youle', + route: 'agent', + rpc: 'self_join_room', + data: { + agentid: 'agent-from-config', + playerid: 430511, + gameid: 'game-from-config', + roomcode: '654321', + location, + ip: '203.0.113.7', + vipMatch: 1, + match_id: 'match-9', + }, + }]); + assert.equal((sent[0]!.data as { location: unknown }).location, location); + assert.equal(commands.joinRoom.length, 1); + + assert.throws( + () => commands.joinRoom({ roomcode: '654321', ip: '203.0.113.7' } as never), + /location/i, + ); + assert.throws( + () => commands.joinRoom({ roomcode: '654321', location } as never), + /ip/i, + ); + assert.equal(sent.length, 1); +}); + +test('exitRoom sends self_exit_room while an infinite room is in battle', () => { + const { commands, sent } = makeCommands(makeStore('inside', 1, 1)); + + commands.exitRoom(); + + assert.deepEqual(sent, [{ + app: 'youle', + route: 'room', + rpc: 'self_exit_room', + data: { + agentid: 'agent-from-config', + playerid: 430511, + gameid: 'game-from-config', + roomcode: '100001', + }, + }]); +}); + +test('command preconditions reject missing authentication, wrong room location, and battle exit without sending', () => { + const unauthenticated = makeCommands(new PlatformStore()); + assert.throws( + () => unauthenticated.commands.joinRoom({ roomcode: '654321', location: null, ip: '' }), + /authenticated|player/i, + ); + + const outside = makeCommands(makeStore('outside')); + assert.throws(() => outside.commands.prepare(), /inside.*room|room.*inside/i); + assert.throws(() => outside.commands.exitRoom(), /inside.*room|room.*inside/i); + + const inside = makeCommands(makeStore('inside', 0)); + assert.throws( + () => inside.commands.joinRoom({ roomcode: '654321', location: null, ip: '' }), + /outside.*room|already.*room/i, + ); + + const battle = makeCommands(makeStore('inside', 1)); + assert.throws(() => battle.commands.exitRoom(), /room\.exit.*stage=0/i); + + assert.equal(unauthenticated.sent.length, 0); + assert.equal(outside.sent.length, 0); + assert.equal(inside.sent.length, 0); + assert.equal(battle.sent.length, 0); +}); + +test('VIP rank and owned room requests preserve legacy exact fields',()=>{ + const {commands,sent}=makeCommands(makeStore('outside')); + commands.getVipRankList(10);commands.joinOwnedRoom(123456,{city:'x'}); + assert.deepEqual(sent,[ + {app:'youle',route:'agent',rpc:'getVipRankList',data:{agentid:'agent-from-config',limit:10}}, + {app:'youle',route:'agent',rpc:'self_join_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:123456,joinmode:1,location:{city:'x'},ip:'127.0.0.1'}}]); + assert.throws(()=>commands.getVipRankList(-1)); }); -test('joinRoom uses authenticated Store identity and requires every non-optional environment field', () => { - const { commands, sent } = makeCommands(makeStore('outside')); - const location = { latitude: 28.7, longitude: 115.8 }; - - commands.joinRoom({ - roomcode: '654321', - location, - ip: '203.0.113.7', - vipMatch: 1, - match_id: 'match-9', - }); - - assert.deepEqual(sent, [{ - app: 'youle', - route: 'agent', - rpc: 'self_join_room', - data: { - agentid: 'agent-from-config', - playerid: 430511, - gameid: 'game-from-config', - roomcode: '654321', - location, - ip: '203.0.113.7', - vipMatch: 1, - match_id: 'match-9', - }, - }]); - assert.equal((sent[0]!.data as { location: unknown }).location, location); - assert.equal(commands.joinRoom.length, 1); - - assert.throws( - () => commands.joinRoom({ roomcode: '654321', ip: '203.0.113.7' } as never), - /location/i, - ); - assert.throws( - () => commands.joinRoom({ roomcode: '654321', location } as never), - /ip/i, - ); - assert.equal(sent.length, 1); -}); - -test('exitRoom sends self_exit_room while an infinite room is in battle', () => { - const { commands, sent } = makeCommands(makeStore('inside', 1, 1)); - - commands.exitRoom(); - - assert.deepEqual(sent, [{ - app: 'youle', - route: 'room', - rpc: 'self_exit_room', - data: { - agentid: 'agent-from-config', - playerid: 430511, - gameid: 'game-from-config', - roomcode: '100001', - }, - }]); -}); - -test('command preconditions reject missing authentication, wrong room location, and battle exit without sending', () => { - const unauthenticated = makeCommands(new PlatformStore()); - assert.throws( - () => unauthenticated.commands.joinRoom({ roomcode: '654321', location: null, ip: '' }), - /authenticated|player/i, - ); - - const outside = makeCommands(makeStore('outside')); - assert.throws(() => outside.commands.prepare(), /inside.*room|room.*inside/i); - assert.throws(() => outside.commands.exitRoom(), /inside.*room|room.*inside/i); - - const inside = makeCommands(makeStore('inside', 0)); - assert.throws( - () => inside.commands.joinRoom({ roomcode: '654321', location: null, ip: '' }), - /outside.*room|already.*room/i, - ); - - const battle = makeCommands(makeStore('inside', 1)); - assert.throws(() => battle.commands.exitRoom(), /room\.exit.*stage=0/i); - - assert.equal(unauthenticated.sent.length, 0); - assert.equal(outside.sent.length, 0); - assert.equal(inside.sent.length, 0); - assert.equal(battle.sent.length, 0); -}); +test('create command emits exact legacy packet and numeric keypad join stays numeric', () => { + const {commands,sent}=makeCommands(makeStore('outside')); + const roomtype=[1,4,[7,8],{option:true}]; + commands.createRoom({roomtype,ip:'127.0.0.8',location:null}); + commands.joinRoom({roomcode:12345,ip:'127.0.0.8',location:null}); + assert.deepEqual(sent[0],{app:'youle',route:'agent',rpc:'create_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomtype,ip:'127.0.0.8',location:null}}); + assert.deepEqual(sent[1].data,{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:12345,ip:'127.0.0.8',location:null}); + assert.throws(()=>commands.createRoom({roomtype:[undefined],ip:'x',location:null}),/JSON/); + assert.throws(()=>makeCommands(makeStore('inside')).commands.createRoom({roomtype,ip:'x',location:null}),/outside|lobby/); +}); diff --git a/cocoscreator_projects/framework-tests/platform/platform-store.test.ts b/cocoscreator_projects/framework-tests/platform/platform-store.test.ts index af95495..c8874ae 100644 --- a/cocoscreator_projects/framework-tests/platform/platform-store.test.ts +++ b/cocoscreator_projects/framework-tests/platform/platform-store.test.ts @@ -1,994 +1,995 @@ -import { readFileSync } from 'node:fs'; -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; -import type { PlatformState } from '../../YouleNexus/assets/framework/platform/stores/platform-types.ts'; -import { - parseLoginResponse, - parseOtherExitRoomPayload, - parseOtherOfflinePayload, - parseOtherJoinRoomPayload, - parseOtherOnlinePayload, - parsePlayerPreparePayload, - parseSelfJoinRoomResponse, -} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts'; - -function fixture(name: string): { data: Record } { - return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')); -} - -test('PlatformStore starts unauthenticated and outside a room', () => { - const state = new PlatformStore().getState(); - assert.equal(state.players.selfPlayerId, null); - assert.deepEqual(state.room, { kind: 'outside' }); -}); - -test('applyLoginSuccess commits the complete login once and notifies once', () => { - const store = new PlatformStore(); - const parsedLogin = parseLoginResponse(fixture('player-login-success.json').data); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - store.applyLoginSuccess(parsedLogin); - - const state = store.getState(); - assert.equal(notifications, 1); - assert.equal(state.app.phase, 'logged-in'); - assert.equal(state.players.selfPlayerId, 430511); - assert.deepEqual(state.players.entities[430511], { - playerid: 430511, +import { readFileSync } from 'node:fs'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; +import type { PlatformState } from '../../YouleNexus/assets/framework/platform/stores/platform-types.ts'; +import { + parseLoginResponse, + parseOtherExitRoomPayload, + parseOtherOfflinePayload, + parseOtherJoinRoomPayload, + parseOtherOnlinePayload, + parsePlayerPreparePayload, + parseSelfJoinRoomResponse, +} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts'; + +function fixture(name: string): { data: Record } { + return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')); +} + +test('PlatformStore starts unauthenticated and outside a room', () => { + const state = new PlatformStore().getState(); + assert.equal(state.players.selfPlayerId, null); + assert.deepEqual(state.room, { kind: 'outside' }); +}); + +test('applyLoginSuccess commits the complete login once and notifies once', () => { + const store = new PlatformStore(); + const parsedLogin = parseLoginResponse(fixture('player-login-success.json').data); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + store.applyLoginSuccess(parsedLogin); + + const state = store.getState(); + assert.equal(notifications, 1); + assert.equal(state.app.phase, 'logged-in'); + assert.equal(state.players.selfPlayerId, 430511); + assert.deepEqual(state.players.entities[430511], { + playerid: 430511, nickname: '测试号', - avatar: 'http://a', - sex: 0, - bean: 0, - ip: '127.0.0.1', - roomcard: 3, taskstate: 1, advanced: 0, score: 0, sign: null, - }); - assert.deepEqual(state.room, { kind: 'outside' }); -}); - -test('room login stores one canonical player entity and recursively freezes roomtype', () => { - const data = fixture('player-login-room.json').data; - const sourceRoomtype = data.roomtype as unknown[]; - const expectedRoomtype = structuredClone(sourceRoomtype); - const parsedLogin = parseLoginResponse(data); - const store = new PlatformStore(); - - store.applyLoginSuccess(parsedLogin); - - const state = store.getState(); - assert.equal(state.players.selfPlayerId, 430511); - assert.deepEqual(Object.keys(state.players.entities), ['430511']); - assert.equal(state.players.entities[430511]?.nickname, '测试号'); - assert.equal(state.room.kind, 'inside'); - if (state.room.kind !== 'inside') return; - const room = state.room; - assert.deepEqual(room.seatPlayerIds, [null, 430511, null, null]); - assert.equal(state.players.entities[430511]?.isprepare, 1); - assert.equal(state.players.entities[430511]?.onstate, 1); - assert.equal(typeof state.players.entities[430511]?.isprepare, 'number'); - assert.equal(typeof state.players.entities[430511]?.onstate, 'number'); - assert.notEqual(room.roomtype, sourceRoomtype); - assert.deepEqual(room.roomtype, expectedRoomtype); - assert.equal(Object.isFrozen(room.roomtype), true); - assert.equal(Object.isFrozen(room.roomtype[5]), true); - assert.equal(Object.isFrozen((room.roomtype[5] as unknown[])[2]), true); - assert.equal(Object.isFrozen(sourceRoomtype), false); - assert.equal(Object.isFrozen(sourceRoomtype[5]), false); - assert.equal(Object.isFrozen((sourceRoomtype[5] as unknown[])[2]), false); - assert.deepEqual(sourceRoomtype, expectedRoomtype); - assert.equal('deskinfo' in state, false); - assert.equal('deskinfo' in state.room, false); - assert.throws(() => { - (room.roomtype[5] as unknown[])[0] = 99; - }, TypeError); -}); - -test('playerJoin atomically adds one canonical entity and its seat id', () => { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - const joined = parseOtherJoinRoomPayload({ - seat: 2, - playerid: 77, - nickname: '对家', - avatar: 'http://b', - sex: 1, - ip: '127.0.0.2', - onstate: 0, - bean: 90, - isprepare: 0, - }); - - store.playerJoin(joined); - - const state = store.getState(); - assert.equal(notifications, 1); - assert.equal(state.room.kind, 'inside'); - if (state.room.kind !== 'inside') return; - assert.deepEqual(state.room.seatPlayerIds, [null, 430511, 77, null]); - assert.deepEqual(state.players.entities[77], { - playerid: 77, - nickname: '对家', - avatar: 'http://b', - sex: 1, - bean: 90, - ip: '127.0.0.2', - isprepare: 0, - onstate: 0, - }); - assert.equal(Object.isFrozen(state.players.entities[77]), true); -}); - -test('playerJoin applies deskwar battle start in the same root commit', () => { - const store = new PlatformStore(); - const loginData = fixture('player-login-room.json').data; - loginData.isbattle = 0; - store.applyLoginSuccess(parseLoginResponse(loginData)); - const before = store.getState(); - assert.equal(before.room.kind, 'inside'); - if (before.room.kind !== 'inside') return; - assert.equal(before.room.stage, 0); - const transitions: PlatformState[] = []; - store.subscribe((state) => { transitions.push(state); }); - - store.playerJoin(parseOtherJoinRoomPayload({ - seat: 2, - playerid: 77, - nickname: '对家', - avatar: 'http://b', - sex: 1, - ip: '127.0.0.2', - onstate: 0, - bean: 90, - isprepare: 0, - deskwar: 1, - })); - - const after = store.getState(); - assert.equal(transitions.length, 1); - assert.equal(transitions[0], after); - assert.equal(after.room.kind, 'inside'); - if (after.room.kind !== 'inside') return; - assert.equal(after.room.stage, 1); - assert.equal(after.room.seatPlayerIds[2], 77); - assert.equal(after.players.entities[77]?.playerid, 77); -}); - -test('playerJoin preserves the current stage when deskwar is absent or zero', () => { - for (const initialStage of [0, 1] as const) { - for (const deskwar of [undefined, 0] as const) { - const store = new PlatformStore(); - const loginData = fixture('player-login-room.json').data; - loginData.isbattle = initialStage; - store.applyLoginSuccess(parseLoginResponse(loginData)); - const joinData: Record = { - seat: 2, - playerid: 77, - nickname: '对家', - avatar: 'http://b', - sex: 1, - ip: '127.0.0.2', - onstate: 0, - bean: 90, - isprepare: 0, - }; - if (deskwar !== undefined) joinData.deskwar = deskwar; - - store.playerJoin(parseOtherJoinRoomPayload(joinData)); - - const state = store.getState(); - assert.equal(state.room.kind, 'inside'); - if (state.room.kind !== 'inside') continue; - assert.equal( - state.room.stage, - initialStage, - `initial stage ${initialStage}, deskwar ${String(deskwar)}`, - ); - } - } -}); - -test('playerJoin rejects an invalid own deskwar atomically without changing input', () => { - for (const deskwar of [2, -1, true, '1', null, undefined]) { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - const joinData: Record = { - seat: 2, - playerid: 77, - nickname: '对家', - avatar: 'http://b', - sex: 1, - ip: '127.0.0.2', - onstate: 0, - bean: 90, - isprepare: 0, - deskwar, - }; - const originalInput = { ...joinData }; - const parsed = parseOtherJoinRoomPayload(joinData); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.playerJoin(parsed), /deskwar/i); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); - assert.deepEqual(joinData, originalInput); - assert.equal(Object.prototype.hasOwnProperty.call(joinData, 'deskwar'), true); - assert.equal(Object.isFrozen(joinData), false); - } -}); - -test('playerReady replaces the affected player and room subtrees without prior mutation', () => { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - store.playerJoin(parseOtherJoinRoomPayload({ - seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', - sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, - })); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - store.playerReady(parsePlayerPreparePayload({ seat: 2 })); - - const after = store.getState(); - assert.equal(notifications, 1); - assert.equal(after.app, before.app); - assert.notEqual(after.players, before.players); - assert.equal(after.players.entities[77]?.isprepare, 1); - assert.notEqual(after.room, before.room); - assert.equal(after.room.kind, 'inside'); - assert.equal(before.room.kind, 'inside'); - if (after.room.kind !== 'inside' || before.room.kind !== 'inside') return; - assert.deepEqual(before.room.readySeats, [1]); - assert.deepEqual(after.room.readySeats, [1, 2]); - assert.equal(Object.isFrozen(after.room.readySeats), true); -}); - -test('playerReady applies deskwar battle start in the same root commit', () => { - const store = new PlatformStore(); - const loginData = fixture('player-login-room.json').data; - loginData.isbattle = 0; - store.applyLoginSuccess(parseLoginResponse(loginData)); - store.playerJoin(parseOtherJoinRoomPayload({ - seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', - sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, - })); - const before = store.getState(); - assert.equal(before.room.kind, 'inside'); - if (before.room.kind !== 'inside') return; - assert.equal(before.room.stage, 0); - const transitions: PlatformState[] = []; - store.subscribe((state) => { transitions.push(state); }); - - store.playerReady(parsePlayerPreparePayload({ seat: 2, deskwar: 1 })); - - const after = store.getState(); - assert.equal(transitions.length, 1); - assert.equal(transitions[0], after); - assert.equal(after.room.kind, 'inside'); - if (after.room.kind !== 'inside') return; - assert.equal(after.room.stage, 1); - assert.deepEqual(after.room.readySeats, [1, 2]); - assert.equal(after.players.entities[77]?.isprepare, 1); -}); - -test('playerReady preserves the current stage when deskwar is absent or zero', () => { - for (const initialStage of [0, 1] as const) { - for (const deskwar of [undefined, 0] as const) { - const store = new PlatformStore(); - const loginData = fixture('player-login-room.json').data; - loginData.isbattle = initialStage; - store.applyLoginSuccess(parseLoginResponse(loginData)); - store.playerJoin(parseOtherJoinRoomPayload({ - seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', - sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, - })); - const prepareData: Record = { seat: 2 }; - if (deskwar !== undefined) prepareData.deskwar = deskwar; - - store.playerReady(parsePlayerPreparePayload(prepareData)); - - const state = store.getState(); - assert.equal(state.room.kind, 'inside'); - if (state.room.kind !== 'inside') continue; - assert.equal( - state.room.stage, - initialStage, - `initial stage ${initialStage}, deskwar ${String(deskwar)}`, - ); - } - } -}); - -test('playerReady rejects an invalid own deskwar atomically without changing input', () => { - for (const deskwar of [2, -1, true, '1', null, undefined]) { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - store.playerJoin(parseOtherJoinRoomPayload({ - seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', - sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, - })); - const prepareData: Record = { seat: 2, deskwar }; - const originalInput = { ...prepareData }; - const parsed = parsePlayerPreparePayload(prepareData); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.playerReady(parsed), /deskwar/i); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); - assert.deepEqual(prepareData, originalInput); - assert.equal(Object.prototype.hasOwnProperty.call(prepareData, 'deskwar'), true); - assert.equal(Object.isFrozen(prepareData), false); - } -}); - -test('playerOffline records protocol and room status with one atomic commit', () => { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - store.playerJoin(parseOtherJoinRoomPayload({ - seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', - sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, - })); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - store.playerOffline(parseOtherOfflinePayload({ seat: 2 })); - - const after = store.getState(); - assert.equal(notifications, 1); - assert.equal(after.app, before.app); - assert.notEqual(after.players, before.players); - assert.equal(after.players.entities[77]?.onstate, 1); - assert.equal(after.room.kind, 'inside'); - assert.equal(before.room.kind, 'inside'); - if (after.room.kind !== 'inside' || before.room.kind !== 'inside') return; - assert.deepEqual(before.room.offlineSeats, [1]); - assert.deepEqual(after.room.offlineSeats, [1, 2]); -}); - -test('playerOnline updates the canonical player ip and removes the offline seat atomically', () => { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - store.playerOnline(parseOtherOnlinePayload({ seat: 1, ip: '127.0.0.9' })); - - const after = store.getState(); - assert.equal(notifications, 1); - assert.equal(after.app, before.app); - assert.notEqual(after.players, before.players); - assert.notEqual(after.room, before.room); - assert.equal(after.players.entities[430511]?.ip, '127.0.0.9'); - assert.equal(before.players.entities[430511]?.ip, '127.0.0.1'); - assert.equal(after.room.kind, 'inside'); - if (after.room.kind !== 'inside') return; - assert.deepEqual(after.room.offlineSeats, []); -}); - -test('playerExit removes the seat entity and every seat-owned status in one commit', () => { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - store.playerJoin(parseOtherJoinRoomPayload({ - seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', - sex: 1, bean: 90, ip: '127.0.0.2', onstate: 1, isprepare: 1, - })); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - store.playerExit(parseOtherExitRoomPayload({ seat: 2 })); - - const after = store.getState(); - assert.equal(notifications, 1); - assert.equal(after.app, before.app); - assert.equal(after.players.selfPlayerId, 430511); - assert.equal(after.players.entities[430511], before.players.entities[430511]); - assert.equal(after.players.entities[77], undefined); - assert.equal(after.room.kind, 'inside'); - if (after.room.kind !== 'inside') return; - assert.deepEqual(after.room.seatPlayerIds, [null, 430511, null, null]); - assert.deepEqual(after.room.readySeats, [1]); - assert.deepEqual(after.room.offlineSeats, [1]); -}); - -test('replaceRoom atomically replaces the complete room from a parsed DTO', () => { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - const parsedRoom = parseSelfJoinRoomResponse(fixture('self-join-room.json').data); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - store.replaceRoom(parsedRoom); - - const after = store.getState(); - assert.equal(notifications, 1); - assert.equal(after.app, before.app); - assert.equal(after.room.kind, 'inside'); - if (after.room.kind !== 'inside') return; - assert.equal(after.room.roomcode, '100001'); - assert.equal(after.room.selfSeat, 1); - assert.equal(after.room.stage, 1); - assert.equal(after.room.needprepare, 1); - assert.equal(after.room.infinite, 0); - assert.deepEqual(after.room.seatPlayerIds, [null, 430511, null, null]); - assert.notEqual(after.room.roomtype, parsedRoom.room?.roomtype); - assert.deepEqual(after.room.roomtype, parsedRoom.room?.roomtype); - assert.equal(after.players.entities[430511]?.ip, '127.0.0.1'); -}); - -test('replaceRoom consumes the parser-owned deskwar default and reconnect stage matrix', () => { - const cases = [ - { deskwar: 'missing', deskinfo: false, expectedStage: 0 }, - { deskwar: 0, deskinfo: false, expectedStage: 0 }, - { deskwar: 1, deskinfo: false, expectedStage: 1 }, - { deskwar: 'missing', deskinfo: true, expectedStage: 1 }, - { deskwar: 0, deskinfo: true, expectedStage: 1 }, - { deskwar: 1, deskinfo: true, expectedStage: 1 }, - ] as const; - for (const row of cases) { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - const data = fixture('self-join-room.json').data; - if (row.deskwar === 'missing') delete data.deskwar; - else data.deskwar = row.deskwar; - if (row.deskinfo) data.deskinfo = { round: 7 }; - else delete data.deskinfo; - data.isbattle = 1; - const parsed = parseSelfJoinRoomResponse(data); - - store.replaceRoom(parsed); - - const state = store.getState(); - assert.equal(state.room.kind, 'inside'); - if (state.room.kind !== 'inside') continue; - assert.equal(state.room.stage, row.expectedStage, JSON.stringify(row)); - } -}); - -test('applyLoginSuccess requires isbattle even when raw deskwar is fabricated', () => { - const store = new PlatformStore(); - const data = fixture('player-login-room.json').data; - delete data.isbattle; - data.deskwar = 1; - const parsed = parseLoginResponse(data); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.applyLoginSuccess(parsed), /isbattle/i); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); -}); - -test('replaceRoom never substitutes self-join isbattle for missing deskwar or deskinfo', () => { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - const data = fixture('self-join-room.json').data; - delete data.deskinfo; - delete data.deskwar; - data.isbattle = 1; - const parsed = parseSelfJoinRoomResponse(data); - - store.replaceRoom(parsed); - - const state = store.getState(); - assert.equal(state.room.kind, 'inside'); - if (state.room.kind === 'inside') assert.equal(state.room.stage, 0); -}); - -test('clearRoom leaves no fabricated room fields and retains only the self entity', () => { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - store.playerJoin(parseOtherJoinRoomPayload({ - seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', - sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, - })); - const before = store.getState(); - const self = before.players.entities[430511]; - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - store.clearRoom(); - - const after = store.getState(); - assert.equal(notifications, 1); - assert.equal(after.app, before.app); - assert.deepEqual(after.room, { kind: 'outside' }); - assert.deepEqual(Object.keys(after.room), ['kind']); - assert.deepEqual(Object.keys(after.players.entities), ['430511']); - assert.equal(after.players.entities[430511], self); -}); - -test('failed actions keep the exact root, notify nobody, and do not mutate inputs', () => { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - const tampered = parseSelfJoinRoomResponse(fixture('self-join-room.json').data); - (tampered.room as { roomcode: string }).roomcode = 'tampered'; - assert.throws(() => store.replaceRoom(tampered), /roomcode/); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); - - assert.throws( - () => store.playerReady(parsePlayerPreparePayload({ seat: 99 })), - /seat/, - ); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); - - assert.throws( - () => store.playerJoin(parseOtherJoinRoomPayload({ - seat: 2, playerid: 430511, nickname: '重复', avatar: 'http://duplicate', - sex: 0, bean: 0, ip: '127.0.0.3', onstate: 0, isprepare: 0, - })), - /duplicate player seat/, - ); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); - - const duplicateData = fixture('self-join-room.json').data; - const duplicatePlayers = duplicateData.players as Array | null>; - duplicatePlayers[2] = { ...duplicatePlayers[1] as Record }; - const mutableRoomtype = duplicateData.roomtype as unknown[]; - const duplicateRoom = parseSelfJoinRoomResponse(duplicateData); - assert.throws(() => store.replaceRoom(duplicateRoom), /duplicate player seat/); - assert.equal(Object.isFrozen(mutableRoomtype), false); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); - - assert.throws(() => { - (before.players.entities[430511] as { nickname: string }).nickname = '篡改'; - }, TypeError); - assert.throws(() => { - (before.room as { kind: string }).kind = 'outside'; - }, TypeError); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); -}); - -test('applyLoginSuccess rejects a room login whose parsed room was changed to null', () => { - const store = new PlatformStore(); - const parsed = parseLoginResponse(fixture('player-login-room.json').data); - (parsed as { room: null }).room = null; - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.applyLoginSuccess(parsed), /room.*raw/i); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); -}); - -test('applyLoginSuccess rejects a failed login whose parsed state was changed to success', () => { - const store = new PlatformStore(); - const parsed = parseLoginResponse({ state: 1 }); - (parsed as { state: number }).state = 0; - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.applyLoginSuccess(parsed), /state.*raw/i); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); -}); - -test('replaceRoom rejects a successful parsed join whose raw state was changed to failure', () => { - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - const parsed = parseSelfJoinRoomResponse(fixture('self-join-room.json').data); - (parsed.raw as Record).state = 9; - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.replaceRoom(parsed), /state.*raw/i); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); -}); - -test('room self profile conflicts are rejected instead of overwriting login authority', () => { - const conflicts: ReadonlyArray = [ - ['playerid', 77], - ['nickname', '冲突昵称'], - ['avatar', 'http://conflict'], - ['sex', 2], - ['bean', 999], - ['ip', '127.0.0.99'], - ]; - - for (const [field, conflict] of conflicts) { - const store = new PlatformStore(); - const parsed = parseLoginResponse(fixture('player-login-room.json').data); - const selfRoomPlayer = parsed.room?.players[1] as Record; - selfRoomPlayer[field] = conflict; - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws( - () => store.applyLoginSuccess(parsed), - new RegExp(field, 'i'), - `expected conflicting ${field} to fail`, - ); - assert.equal(store.getState(), before, `${field} changed the root`); - assert.equal(notifications, 0, `${field} notified subscribers`); - } -}); - -test('roomtype accessors are rejected without invocation or input mutation', () => { - const child = { value: 1 }; - const throwing = Object.create(null) as Record; - let getterCalls = 0; - Object.defineProperty(throwing, 'boom', { - enumerable: true, - get(): never { - getterCalls += 1; - throw new Error('roomtype getter boom'); - }, - }); - const roomtype = [child, throwing]; - const data = fixture('self-join-room.json').data; - data.roomtype = roomtype; - const parsed = parseSelfJoinRoomResponse(data); - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.replaceRoom(parsed), /roomtype.*boom.*accessor/i); - assert.equal(getterCalls, 0); - assert.equal(Object.isFrozen(child), false); - assert.equal(Object.isFrozen(throwing), false); - assert.equal(Object.isFrozen(roomtype), false); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); -}); - -test('roomtype copying never runs preventExtensions on caller-owned proxies', () => { - const sibling = { value: 1 }; - const nested = { value: 2 }; - let preventExtensionsCalls = 0; - const proxy = new Proxy({ nested }, { - preventExtensions(): boolean { - preventExtensionsCalls += 1; - return false; - }, - }); - const roomtype = [sibling, proxy]; - const data = fixture('self-join-room.json').data; - data.roomtype = roomtype; - const parsed = parseSelfJoinRoomResponse(data); - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - - store.replaceRoom(parsed); - - const state = store.getState(); - assert.equal(state.room.kind, 'inside'); - if (state.room.kind !== 'inside') return; - assert.notEqual(state.room.roomtype, roomtype); - assert.deepEqual(state.room.roomtype, roomtype); - assert.equal(Object.isFrozen(state.room.roomtype), true); - assert.equal(Object.isFrozen(state.room.roomtype[0]), true); - assert.equal(Object.isFrozen(state.room.roomtype[1]), true); - assert.equal( - Object.isFrozen( - (state.room.roomtype[1] as { readonly nested: object }).nested, - ), - true, - ); - assert.equal(preventExtensionsCalls, 0); - assert.equal(Object.isFrozen(roomtype), false); - assert.equal(Object.isFrozen(sibling), false); - assert.equal(Object.isFrozen(proxy), false); - assert.equal(Object.isFrozen(nested), false); -}); - -test('dynamic roomtype accessors cannot leak mutable children into canonical state', () => { - const first = { value: 1 }; - const second = { value: 2 }; - let getterCalls = 0; - const dynamic = {} as Record; - Object.defineProperty(dynamic, 'child', { - enumerable: true, - get(): object { - getterCalls += 1; - return getterCalls === 1 ? first : second; - }, - }); - const roomtype = [dynamic]; - const data = fixture('self-join-room.json').data; - data.roomtype = roomtype; - const parsed = parseSelfJoinRoomResponse(data); - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.replaceRoom(parsed), /roomtype.*child.*accessor/i); - assert.equal(getterCalls, 0); - assert.equal(Object.isFrozen(roomtype), false); - assert.equal(Object.isFrozen(dynamic), false); - assert.equal(Object.isFrozen(first), false); - assert.equal(Object.isFrozen(second), false); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); -}); - -test('cyclic roomtype input is rejected atomically without mutating caller data', () => { - const cyclic: unknown[] = []; - cyclic.push(cyclic); - const roomtype = [{ value: 1 }, cyclic]; - const data = fixture('self-join-room.json').data; - data.roomtype = roomtype; - const parsed = parseSelfJoinRoomResponse(data); - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.replaceRoom(parsed), /roomtype\[1\]\[0\].*cyclic/i); - assert.equal(Object.isFrozen(roomtype), false); - assert.equal(Object.isFrozen(roomtype[0]), false); - assert.equal(Object.isFrozen(cyclic), false); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); -}); - -test('roomtype descriptor changes between copy and verification fail atomically', () => { - const sibling = { stable: true }; - const target = { value: 0 }; - let descriptorReads = 0; - const unstable = new Proxy(target, { - getOwnPropertyDescriptor(object, key): PropertyDescriptor | undefined { - const descriptor = Reflect.getOwnPropertyDescriptor(object, key); - if (key !== 'value' || descriptor === undefined) return descriptor; - descriptorReads += 1; - return { ...descriptor, value: descriptorReads }; - }, - }); - const roomtype = [sibling, unstable]; - const data = fixture('self-join-room.json').data; - data.roomtype = roomtype; - const parsed = parseSelfJoinRoomResponse(data); - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.replaceRoom(parsed), /roomtype\[1\]\.value.*unstable/i); - assert.equal(descriptorReads >= 2, true); - assert.equal(target.value, 0); - assert.equal(Object.isFrozen(roomtype), false); - assert.equal(Object.isFrozen(sibling), false); - assert.equal(Object.isFrozen(target), false); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); -}); - -test('roomtype own-key changes between copy and verification fail atomically', () => { - const target = { first: 1, second: 2 }; - let ownKeyReads = 0; - const unstable = new Proxy(target, { - ownKeys(): ArrayLike { - ownKeyReads += 1; - return ownKeyReads === 1 ? ['first'] : ['second']; - }, - }); - const roomtype = [unstable]; - const data = fixture('self-join-room.json').data; - data.roomtype = roomtype; - const parsed = parseSelfJoinRoomResponse(data); - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.replaceRoom(parsed), /roomtype\[0\].*unstable.*own keys/i); - assert.equal(ownKeyReads >= 2, true); - assert.deepEqual(target, { first: 1, second: 2 }); - assert.equal(Object.isFrozen(roomtype), false); - assert.equal(Object.isFrozen(target), false); - assert.equal(store.getState(), before); - assert.equal(notifications, 0); -}); - -test('canonical roomtype preserves sparse length, holes, and negative zero', () => { - const sparse = new Array(5); - sparse[1] = -0; - sparse[4] = { tail: true }; - const data = fixture('player-login-room.json').data; - data.roomtype = sparse; - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(data)); - const state = store.getState(); - - assert.equal(state.room.kind, 'inside'); - if (state.room.kind !== 'inside') return; - assert.equal(state.room.roomtype.length, 5); - assert.equal(0 in state.room.roomtype, false); - assert.equal(1 in state.room.roomtype, true); - assert.equal(Object.is(state.room.roomtype[1], -0), true); - assert.equal(2 in state.room.roomtype, false); - assert.equal(3 in state.room.roomtype, false); - assert.deepEqual(state.room.roomtype[4], { tail: true }); - assert.equal(Object.isFrozen(state.room.roomtype[4]), true); - assert.equal(Object.isFrozen(sparse), false); - assert.equal(Object.isFrozen(sparse[4]), false); -}); - -test('canonical roomtype keeps own __proto__ data without prototype pollution', () => { - const source = {} as Record; - Object.defineProperty(source, '__proto__', { - configurable: true, - enumerable: true, - writable: true, - value: { polluted: true }, - }); - const data = fixture('player-login-room.json').data; - data.roomtype = [source]; - const store = new PlatformStore(); - store.applyLoginSuccess(parseLoginResponse(data)); - const state = store.getState(); - - assert.equal(state.room.kind, 'inside'); - if (state.room.kind !== 'inside') return; - const canonical = state.room.roomtype[0] as Record; - assert.equal(Object.getPrototypeOf(canonical), Object.prototype); - assert.equal(Object.prototype.hasOwnProperty.call(canonical, '__proto__'), true); - assert.deepEqual(canonical.__proto__, { polluted: true }); - assert.equal(({} as { polluted?: boolean }).polluted, undefined); - assert.equal(Object.isFrozen(canonical.__proto__), true); -}); - -test('non-JSON roomtype own data is rejected without state or caller mutation', () => { - const cases: ReadonlyArray unknown[]]> = [ - ['symbol', () => { - const source = { value: 1 } as Record; - source[Symbol('hidden')] = 2; - return [source]; - }], - ['enumerable', () => { - const source = { value: 1 }; - Object.defineProperty(source, 'hidden', { enumerable: false, value: 2 }); - return [source]; - }], - ['finite', () => [Number.NaN]], - ]; - - for (const [error, makeRoomtype] of cases) { - const roomtype = makeRoomtype(); - const data = fixture('player-login-room.json').data; - data.roomtype = roomtype; - const parsed = parseLoginResponse(data); - const store = new PlatformStore(); - const before = store.getState(); - let notifications = 0; - store.subscribe(() => { notifications += 1; }); - - assert.throws(() => store.applyLoginSuccess(parsed), new RegExp(error, 'i')); - assert.equal(Object.isFrozen(roomtype), false); - if (typeof roomtype[0] === 'object' && roomtype[0] !== null) { - assert.equal(Object.isFrozen(roomtype[0]), false); - } - assert.equal(store.getState(), before); - assert.equal(notifications, 0); - } -}); - -test('reentrant store commits publish each stable transition before the next', () => { - const store = new PlatformStore(); - const events: string[] = []; - let nested = false; - const describe = (label: string, next: PlatformState, previous: PlatformState): void => { - events.push(`${label}:${next.room.kind}<-${previous.room.kind}`); - }; - store.subscribe((next, previous) => { - describe('A', next, previous); - if (!nested && next.room.kind === 'inside' && previous.room.kind === 'outside') { - nested = true; - store.clearRoom(); - } - }); - store.subscribe((next, previous) => { describe('B', next, previous); }); - - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - - assert.deepEqual(events, [ - 'A:inside<-outside', - 'B:inside<-outside', - 'A:outside<-inside', - 'B:outside<-inside', - ]); - assert.equal(store.getState().room.kind, 'outside'); -}); - -test('multiple reentrant commits retain their real previous and next states', () => { - const store = new PlatformStore(); - const events: string[] = []; - let nested = false; - store.subscribe((next, previous) => { - events.push(`A:${next.room.kind}<-${previous.room.kind}`); - if (!nested && next.room.kind === 'inside' && previous.room.kind === 'outside') { - nested = true; - store.clearRoom(); - assert.equal(store.getState().room.kind, 'outside'); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - assert.equal(store.getState().room.kind, 'inside'); - } - }); - store.subscribe((next, previous) => { - events.push(`B:${next.room.kind}<-${previous.room.kind}`); - }); - - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - - assert.deepEqual(events, [ - 'A:inside<-outside', 'B:inside<-outside', - 'A:outside<-inside', 'B:outside<-inside', - 'A:inside<-outside', 'B:inside<-outside', - ]); -}); - -test('subscription changes during publish apply only to later transition snapshots', () => { - const store = new PlatformStore(); - const events: string[] = []; - let nested = false; - let unsubscribeB = (): void => undefined; - store.subscribe((next, previous) => { - events.push(`A:${next.room.kind}<-${previous.room.kind}`); - if (!nested && next.room.kind === 'inside') { - nested = true; - unsubscribeB(); - store.subscribe((laterNext, laterPrevious) => { - events.push(`C:${laterNext.room.kind}<-${laterPrevious.room.kind}`); - }); - store.clearRoom(); - } - }); - unsubscribeB = store.subscribe((next, previous) => { - events.push(`B:${next.room.kind}<-${previous.room.kind}`); - }); - - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); - - assert.deepEqual(events, [ - 'A:inside<-outside', - 'B:inside<-outside', - 'A:outside<-inside', - 'C:outside<-inside', - ]); -}); - -test('subscriber exceptions remain isolated from later store listeners', () => { - const store = new PlatformStore(); - let reached = false; - const originalConsoleError = console.error; - console.error = (): void => undefined; - try { - store.subscribe(() => { throw new Error('subscriber boom'); }); - store.subscribe(() => { reached = true; }); - store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); - } finally { - console.error = originalConsoleError; - } - assert.equal(reached, true); -}); + openid:'o1',unionid:'test_unionid',invitecode:null, + avatar: 'http://a', + sex: 0, + bean: 0, + ip: '127.0.0.1', + roomcard: 3, taskstate: 1, advanced: 0, score: 0, sign: null, + }); + assert.deepEqual(state.room, { kind: 'outside' }); +}); + +test('room login stores one canonical player entity and recursively freezes roomtype', () => { + const data = fixture('player-login-room.json').data; + const sourceRoomtype = data.roomtype as unknown[]; + const expectedRoomtype = structuredClone(sourceRoomtype); + const parsedLogin = parseLoginResponse(data); + const store = new PlatformStore(); + + store.applyLoginSuccess(parsedLogin); + + const state = store.getState(); + assert.equal(state.players.selfPlayerId, 430511); + assert.deepEqual(Object.keys(state.players.entities), ['430511']); + assert.equal(state.players.entities[430511]?.nickname, '测试号'); + assert.equal(state.room.kind, 'inside'); + if (state.room.kind !== 'inside') return; + const room = state.room; + assert.deepEqual(room.seatPlayerIds, [null, 430511, null, null]); + assert.equal(state.players.entities[430511]?.isprepare, 1); + assert.equal(state.players.entities[430511]?.onstate, 1); + assert.equal(typeof state.players.entities[430511]?.isprepare, 'number'); + assert.equal(typeof state.players.entities[430511]?.onstate, 'number'); + assert.notEqual(room.roomtype, sourceRoomtype); + assert.deepEqual(room.roomtype, expectedRoomtype); + assert.equal(Object.isFrozen(room.roomtype), true); + assert.equal(Object.isFrozen(room.roomtype[5]), true); + assert.equal(Object.isFrozen((room.roomtype[5] as unknown[])[2]), true); + assert.equal(Object.isFrozen(sourceRoomtype), false); + assert.equal(Object.isFrozen(sourceRoomtype[5]), false); + assert.equal(Object.isFrozen((sourceRoomtype[5] as unknown[])[2]), false); + assert.deepEqual(sourceRoomtype, expectedRoomtype); + assert.equal('deskinfo' in state, false); + assert.equal('deskinfo' in state.room, false); + assert.throws(() => { + (room.roomtype[5] as unknown[])[0] = 99; + }, TypeError); +}); + +test('playerJoin atomically adds one canonical entity and its seat id', () => { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + const joined = parseOtherJoinRoomPayload({ + seat: 2, + playerid: 77, + nickname: '对家', + avatar: 'http://b', + sex: 1, + ip: '127.0.0.2', + onstate: 0, + bean: 90, + isprepare: 0, + }); + + store.playerJoin(joined); + + const state = store.getState(); + assert.equal(notifications, 1); + assert.equal(state.room.kind, 'inside'); + if (state.room.kind !== 'inside') return; + assert.deepEqual(state.room.seatPlayerIds, [null, 430511, 77, null]); + assert.deepEqual(state.players.entities[77], { + playerid: 77, + nickname: '对家', + avatar: 'http://b', + sex: 1, + bean: 90, + ip: '127.0.0.2', + isprepare: 0, + onstate: 0, + }); + assert.equal(Object.isFrozen(state.players.entities[77]), true); +}); + +test('playerJoin applies deskwar battle start in the same root commit', () => { + const store = new PlatformStore(); + const loginData = fixture('player-login-room.json').data; + loginData.isbattle = 0; + store.applyLoginSuccess(parseLoginResponse(loginData)); + const before = store.getState(); + assert.equal(before.room.kind, 'inside'); + if (before.room.kind !== 'inside') return; + assert.equal(before.room.stage, 0); + const transitions: PlatformState[] = []; + store.subscribe((state) => { transitions.push(state); }); + + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, + playerid: 77, + nickname: '对家', + avatar: 'http://b', + sex: 1, + ip: '127.0.0.2', + onstate: 0, + bean: 90, + isprepare: 0, + deskwar: 1, + })); + + const after = store.getState(); + assert.equal(transitions.length, 1); + assert.equal(transitions[0], after); + assert.equal(after.room.kind, 'inside'); + if (after.room.kind !== 'inside') return; + assert.equal(after.room.stage, 1); + assert.equal(after.room.seatPlayerIds[2], 77); + assert.equal(after.players.entities[77]?.playerid, 77); +}); + +test('playerJoin preserves the current stage when deskwar is absent or zero', () => { + for (const initialStage of [0, 1] as const) { + for (const deskwar of [undefined, 0] as const) { + const store = new PlatformStore(); + const loginData = fixture('player-login-room.json').data; + loginData.isbattle = initialStage; + store.applyLoginSuccess(parseLoginResponse(loginData)); + const joinData: Record = { + seat: 2, + playerid: 77, + nickname: '对家', + avatar: 'http://b', + sex: 1, + ip: '127.0.0.2', + onstate: 0, + bean: 90, + isprepare: 0, + }; + if (deskwar !== undefined) joinData.deskwar = deskwar; + + store.playerJoin(parseOtherJoinRoomPayload(joinData)); + + const state = store.getState(); + assert.equal(state.room.kind, 'inside'); + if (state.room.kind !== 'inside') continue; + assert.equal( + state.room.stage, + initialStage, + `initial stage ${initialStage}, deskwar ${String(deskwar)}`, + ); + } + } +}); + +test('playerJoin rejects an invalid own deskwar atomically without changing input', () => { + for (const deskwar of [2, -1, true, '1', null, undefined]) { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + const joinData: Record = { + seat: 2, + playerid: 77, + nickname: '对家', + avatar: 'http://b', + sex: 1, + ip: '127.0.0.2', + onstate: 0, + bean: 90, + isprepare: 0, + deskwar, + }; + const originalInput = { ...joinData }; + const parsed = parseOtherJoinRoomPayload(joinData); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.playerJoin(parsed), /deskwar/i); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); + assert.deepEqual(joinData, originalInput); + assert.equal(Object.prototype.hasOwnProperty.call(joinData, 'deskwar'), true); + assert.equal(Object.isFrozen(joinData), false); + } +}); + +test('playerReady replaces the affected player and room subtrees without prior mutation', () => { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', + sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, + })); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + store.playerReady(parsePlayerPreparePayload({ seat: 2 })); + + const after = store.getState(); + assert.equal(notifications, 1); + assert.equal(after.app, before.app); + assert.notEqual(after.players, before.players); + assert.equal(after.players.entities[77]?.isprepare, 1); + assert.notEqual(after.room, before.room); + assert.equal(after.room.kind, 'inside'); + assert.equal(before.room.kind, 'inside'); + if (after.room.kind !== 'inside' || before.room.kind !== 'inside') return; + assert.deepEqual(before.room.readySeats, [1]); + assert.deepEqual(after.room.readySeats, [1, 2]); + assert.equal(Object.isFrozen(after.room.readySeats), true); +}); + +test('playerReady applies deskwar battle start in the same root commit', () => { + const store = new PlatformStore(); + const loginData = fixture('player-login-room.json').data; + loginData.isbattle = 0; + store.applyLoginSuccess(parseLoginResponse(loginData)); + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', + sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, + })); + const before = store.getState(); + assert.equal(before.room.kind, 'inside'); + if (before.room.kind !== 'inside') return; + assert.equal(before.room.stage, 0); + const transitions: PlatformState[] = []; + store.subscribe((state) => { transitions.push(state); }); + + store.playerReady(parsePlayerPreparePayload({ seat: 2, deskwar: 1 })); + + const after = store.getState(); + assert.equal(transitions.length, 1); + assert.equal(transitions[0], after); + assert.equal(after.room.kind, 'inside'); + if (after.room.kind !== 'inside') return; + assert.equal(after.room.stage, 1); + assert.deepEqual(after.room.readySeats, [1, 2]); + assert.equal(after.players.entities[77]?.isprepare, 1); +}); + +test('playerReady preserves the current stage when deskwar is absent or zero', () => { + for (const initialStage of [0, 1] as const) { + for (const deskwar of [undefined, 0] as const) { + const store = new PlatformStore(); + const loginData = fixture('player-login-room.json').data; + loginData.isbattle = initialStage; + store.applyLoginSuccess(parseLoginResponse(loginData)); + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', + sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, + })); + const prepareData: Record = { seat: 2 }; + if (deskwar !== undefined) prepareData.deskwar = deskwar; + + store.playerReady(parsePlayerPreparePayload(prepareData)); + + const state = store.getState(); + assert.equal(state.room.kind, 'inside'); + if (state.room.kind !== 'inside') continue; + assert.equal( + state.room.stage, + initialStage, + `initial stage ${initialStage}, deskwar ${String(deskwar)}`, + ); + } + } +}); + +test('playerReady rejects an invalid own deskwar atomically without changing input', () => { + for (const deskwar of [2, -1, true, '1', null, undefined]) { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', + sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, + })); + const prepareData: Record = { seat: 2, deskwar }; + const originalInput = { ...prepareData }; + const parsed = parsePlayerPreparePayload(prepareData); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.playerReady(parsed), /deskwar/i); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); + assert.deepEqual(prepareData, originalInput); + assert.equal(Object.prototype.hasOwnProperty.call(prepareData, 'deskwar'), true); + assert.equal(Object.isFrozen(prepareData), false); + } +}); + +test('playerOffline records protocol and room status with one atomic commit', () => { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', + sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, + })); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + store.playerOffline(parseOtherOfflinePayload({ seat: 2 })); + + const after = store.getState(); + assert.equal(notifications, 1); + assert.equal(after.app, before.app); + assert.notEqual(after.players, before.players); + assert.equal(after.players.entities[77]?.onstate, 1); + assert.equal(after.room.kind, 'inside'); + assert.equal(before.room.kind, 'inside'); + if (after.room.kind !== 'inside' || before.room.kind !== 'inside') return; + assert.deepEqual(before.room.offlineSeats, [1]); + assert.deepEqual(after.room.offlineSeats, [1, 2]); +}); + +test('playerOnline updates the canonical player ip and removes the offline seat atomically', () => { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + store.playerOnline(parseOtherOnlinePayload({ seat: 1, ip: '127.0.0.9' })); + + const after = store.getState(); + assert.equal(notifications, 1); + assert.equal(after.app, before.app); + assert.notEqual(after.players, before.players); + assert.notEqual(after.room, before.room); + assert.equal(after.players.entities[430511]?.ip, '127.0.0.9'); + assert.equal(before.players.entities[430511]?.ip, '127.0.0.1'); + assert.equal(after.room.kind, 'inside'); + if (after.room.kind !== 'inside') return; + assert.deepEqual(after.room.offlineSeats, []); +}); + +test('playerExit removes the seat entity and every seat-owned status in one commit', () => { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', + sex: 1, bean: 90, ip: '127.0.0.2', onstate: 1, isprepare: 1, + })); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + store.playerExit(parseOtherExitRoomPayload({ seat: 2 })); + + const after = store.getState(); + assert.equal(notifications, 1); + assert.equal(after.app, before.app); + assert.equal(after.players.selfPlayerId, 430511); + assert.equal(after.players.entities[430511], before.players.entities[430511]); + assert.equal(after.players.entities[77], undefined); + assert.equal(after.room.kind, 'inside'); + if (after.room.kind !== 'inside') return; + assert.deepEqual(after.room.seatPlayerIds, [null, 430511, null, null]); + assert.deepEqual(after.room.readySeats, [1]); + assert.deepEqual(after.room.offlineSeats, [1]); +}); + +test('replaceRoom atomically replaces the complete room from a parsed DTO', () => { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + const parsedRoom = parseSelfJoinRoomResponse(fixture('self-join-room.json').data); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + store.replaceRoom(parsedRoom); + + const after = store.getState(); + assert.equal(notifications, 1); + assert.equal(after.app, before.app); + assert.equal(after.room.kind, 'inside'); + if (after.room.kind !== 'inside') return; + assert.equal(after.room.roomcode, '100001'); + assert.equal(after.room.selfSeat, 1); + assert.equal(after.room.stage, 1); + assert.equal(after.room.needprepare, 1); + assert.equal(after.room.infinite, 0); + assert.deepEqual(after.room.seatPlayerIds, [null, 430511, null, null]); + assert.notEqual(after.room.roomtype, parsedRoom.room?.roomtype); + assert.deepEqual(after.room.roomtype, parsedRoom.room?.roomtype); + assert.equal(after.players.entities[430511]?.ip, '127.0.0.1'); +}); + +test('replaceRoom consumes the parser-owned deskwar default and reconnect stage matrix', () => { + const cases = [ + { deskwar: 'missing', deskinfo: false, expectedStage: 0 }, + { deskwar: 0, deskinfo: false, expectedStage: 0 }, + { deskwar: 1, deskinfo: false, expectedStage: 1 }, + { deskwar: 'missing', deskinfo: true, expectedStage: 1 }, + { deskwar: 0, deskinfo: true, expectedStage: 1 }, + { deskwar: 1, deskinfo: true, expectedStage: 1 }, + ] as const; + for (const row of cases) { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + const data = fixture('self-join-room.json').data; + if (row.deskwar === 'missing') delete data.deskwar; + else data.deskwar = row.deskwar; + if (row.deskinfo) data.deskinfo = { round: 7 }; + else delete data.deskinfo; + data.isbattle = 1; + const parsed = parseSelfJoinRoomResponse(data); + + store.replaceRoom(parsed); + + const state = store.getState(); + assert.equal(state.room.kind, 'inside'); + if (state.room.kind !== 'inside') continue; + assert.equal(state.room.stage, row.expectedStage, JSON.stringify(row)); + } +}); + +test('applyLoginSuccess requires isbattle even when raw deskwar is fabricated', () => { + const store = new PlatformStore(); + const data = fixture('player-login-room.json').data; + delete data.isbattle; + data.deskwar = 1; + const parsed = parseLoginResponse(data); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.applyLoginSuccess(parsed), /isbattle/i); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); +}); + +test('replaceRoom never substitutes self-join isbattle for missing deskwar or deskinfo', () => { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + const data = fixture('self-join-room.json').data; + delete data.deskinfo; + delete data.deskwar; + data.isbattle = 1; + const parsed = parseSelfJoinRoomResponse(data); + + store.replaceRoom(parsed); + + const state = store.getState(); + assert.equal(state.room.kind, 'inside'); + if (state.room.kind === 'inside') assert.equal(state.room.stage, 0); +}); + +test('clearRoom leaves no fabricated room fields and retains only the self entity', () => { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', + sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, + })); + const before = store.getState(); + const self = before.players.entities[430511]; + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + store.clearRoom(); + + const after = store.getState(); + assert.equal(notifications, 1); + assert.equal(after.app, before.app); + assert.deepEqual(after.room, { kind: 'outside' }); + assert.deepEqual(Object.keys(after.room), ['kind']); + assert.deepEqual(Object.keys(after.players.entities), ['430511']); + assert.equal(after.players.entities[430511], self); +}); + +test('failed actions keep the exact root, notify nobody, and do not mutate inputs', () => { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + const tampered = parseSelfJoinRoomResponse(fixture('self-join-room.json').data); + (tampered.room as { roomcode: string }).roomcode = 'tampered'; + assert.throws(() => store.replaceRoom(tampered), /roomcode/); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); + + assert.throws( + () => store.playerReady(parsePlayerPreparePayload({ seat: 99 })), + /seat/, + ); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); + + assert.throws( + () => store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 430511, nickname: '重复', avatar: 'http://duplicate', + sex: 0, bean: 0, ip: '127.0.0.3', onstate: 0, isprepare: 0, + })), + /duplicate player seat/, + ); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); + + const duplicateData = fixture('self-join-room.json').data; + const duplicatePlayers = duplicateData.players as Array | null>; + duplicatePlayers[2] = { ...duplicatePlayers[1] as Record }; + const mutableRoomtype = duplicateData.roomtype as unknown[]; + const duplicateRoom = parseSelfJoinRoomResponse(duplicateData); + assert.throws(() => store.replaceRoom(duplicateRoom), /duplicate player seat/); + assert.equal(Object.isFrozen(mutableRoomtype), false); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); + + assert.throws(() => { + (before.players.entities[430511] as { nickname: string }).nickname = '篡改'; + }, TypeError); + assert.throws(() => { + (before.room as { kind: string }).kind = 'outside'; + }, TypeError); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); +}); + +test('applyLoginSuccess rejects a room login whose parsed room was changed to null', () => { + const store = new PlatformStore(); + const parsed = parseLoginResponse(fixture('player-login-room.json').data); + (parsed as { room: null }).room = null; + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.applyLoginSuccess(parsed), /room.*raw/i); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); +}); + +test('applyLoginSuccess rejects a failed login whose parsed state was changed to success', () => { + const store = new PlatformStore(); + const parsed = parseLoginResponse({ state: 1 }); + (parsed as { state: number }).state = 0; + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.applyLoginSuccess(parsed), /state.*raw/i); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); +}); + +test('replaceRoom rejects a successful parsed join whose raw state was changed to failure', () => { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + const parsed = parseSelfJoinRoomResponse(fixture('self-join-room.json').data); + (parsed.raw as Record).state = 9; + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.replaceRoom(parsed), /state.*raw/i); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); +}); + +test('room self profile conflicts are rejected instead of overwriting login authority', () => { + const conflicts: ReadonlyArray = [ + ['playerid', 77], + ['nickname', '冲突昵称'], + ['avatar', 'http://conflict'], + ['sex', 2], + ['bean', 999], + ['ip', '127.0.0.99'], + ]; + + for (const [field, conflict] of conflicts) { + const store = new PlatformStore(); + const parsed = parseLoginResponse(fixture('player-login-room.json').data); + const selfRoomPlayer = parsed.room?.players[1] as Record; + selfRoomPlayer[field] = conflict; + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws( + () => store.applyLoginSuccess(parsed), + new RegExp(field, 'i'), + `expected conflicting ${field} to fail`, + ); + assert.equal(store.getState(), before, `${field} changed the root`); + assert.equal(notifications, 0, `${field} notified subscribers`); + } +}); + +test('roomtype accessors are rejected without invocation or input mutation', () => { + const child = { value: 1 }; + const throwing = Object.create(null) as Record; + let getterCalls = 0; + Object.defineProperty(throwing, 'boom', { + enumerable: true, + get(): never { + getterCalls += 1; + throw new Error('roomtype getter boom'); + }, + }); + const roomtype = [child, throwing]; + const data = fixture('self-join-room.json').data; + data.roomtype = roomtype; + const parsed = parseSelfJoinRoomResponse(data); + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.replaceRoom(parsed), /roomtype.*boom.*accessor/i); + assert.equal(getterCalls, 0); + assert.equal(Object.isFrozen(child), false); + assert.equal(Object.isFrozen(throwing), false); + assert.equal(Object.isFrozen(roomtype), false); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); +}); + +test('roomtype copying never runs preventExtensions on caller-owned proxies', () => { + const sibling = { value: 1 }; + const nested = { value: 2 }; + let preventExtensionsCalls = 0; + const proxy = new Proxy({ nested }, { + preventExtensions(): boolean { + preventExtensionsCalls += 1; + return false; + }, + }); + const roomtype = [sibling, proxy]; + const data = fixture('self-join-room.json').data; + data.roomtype = roomtype; + const parsed = parseSelfJoinRoomResponse(data); + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + + store.replaceRoom(parsed); + + const state = store.getState(); + assert.equal(state.room.kind, 'inside'); + if (state.room.kind !== 'inside') return; + assert.notEqual(state.room.roomtype, roomtype); + assert.deepEqual(state.room.roomtype, roomtype); + assert.equal(Object.isFrozen(state.room.roomtype), true); + assert.equal(Object.isFrozen(state.room.roomtype[0]), true); + assert.equal(Object.isFrozen(state.room.roomtype[1]), true); + assert.equal( + Object.isFrozen( + (state.room.roomtype[1] as { readonly nested: object }).nested, + ), + true, + ); + assert.equal(preventExtensionsCalls, 0); + assert.equal(Object.isFrozen(roomtype), false); + assert.equal(Object.isFrozen(sibling), false); + assert.equal(Object.isFrozen(proxy), false); + assert.equal(Object.isFrozen(nested), false); +}); + +test('dynamic roomtype accessors cannot leak mutable children into canonical state', () => { + const first = { value: 1 }; + const second = { value: 2 }; + let getterCalls = 0; + const dynamic = {} as Record; + Object.defineProperty(dynamic, 'child', { + enumerable: true, + get(): object { + getterCalls += 1; + return getterCalls === 1 ? first : second; + }, + }); + const roomtype = [dynamic]; + const data = fixture('self-join-room.json').data; + data.roomtype = roomtype; + const parsed = parseSelfJoinRoomResponse(data); + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.replaceRoom(parsed), /roomtype.*child.*accessor/i); + assert.equal(getterCalls, 0); + assert.equal(Object.isFrozen(roomtype), false); + assert.equal(Object.isFrozen(dynamic), false); + assert.equal(Object.isFrozen(first), false); + assert.equal(Object.isFrozen(second), false); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); +}); + +test('cyclic roomtype input is rejected atomically without mutating caller data', () => { + const cyclic: unknown[] = []; + cyclic.push(cyclic); + const roomtype = [{ value: 1 }, cyclic]; + const data = fixture('self-join-room.json').data; + data.roomtype = roomtype; + const parsed = parseSelfJoinRoomResponse(data); + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.replaceRoom(parsed), /roomtype\[1\]\[0\].*cyclic/i); + assert.equal(Object.isFrozen(roomtype), false); + assert.equal(Object.isFrozen(roomtype[0]), false); + assert.equal(Object.isFrozen(cyclic), false); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); +}); + +test('roomtype descriptor changes between copy and verification fail atomically', () => { + const sibling = { stable: true }; + const target = { value: 0 }; + let descriptorReads = 0; + const unstable = new Proxy(target, { + getOwnPropertyDescriptor(object, key): PropertyDescriptor | undefined { + const descriptor = Reflect.getOwnPropertyDescriptor(object, key); + if (key !== 'value' || descriptor === undefined) return descriptor; + descriptorReads += 1; + return { ...descriptor, value: descriptorReads }; + }, + }); + const roomtype = [sibling, unstable]; + const data = fixture('self-join-room.json').data; + data.roomtype = roomtype; + const parsed = parseSelfJoinRoomResponse(data); + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.replaceRoom(parsed), /roomtype\[1\]\.value.*unstable/i); + assert.equal(descriptorReads >= 2, true); + assert.equal(target.value, 0); + assert.equal(Object.isFrozen(roomtype), false); + assert.equal(Object.isFrozen(sibling), false); + assert.equal(Object.isFrozen(target), false); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); +}); + +test('roomtype own-key changes between copy and verification fail atomically', () => { + const target = { first: 1, second: 2 }; + let ownKeyReads = 0; + const unstable = new Proxy(target, { + ownKeys(): ArrayLike { + ownKeyReads += 1; + return ownKeyReads === 1 ? ['first'] : ['second']; + }, + }); + const roomtype = [unstable]; + const data = fixture('self-join-room.json').data; + data.roomtype = roomtype; + const parsed = parseSelfJoinRoomResponse(data); + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.replaceRoom(parsed), /roomtype\[0\].*unstable.*own keys/i); + assert.equal(ownKeyReads >= 2, true); + assert.deepEqual(target, { first: 1, second: 2 }); + assert.equal(Object.isFrozen(roomtype), false); + assert.equal(Object.isFrozen(target), false); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); +}); + +test('canonical roomtype preserves sparse length, holes, and negative zero', () => { + const sparse = new Array(5); + sparse[1] = -0; + sparse[4] = { tail: true }; + const data = fixture('player-login-room.json').data; + data.roomtype = sparse; + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(data)); + const state = store.getState(); + + assert.equal(state.room.kind, 'inside'); + if (state.room.kind !== 'inside') return; + assert.equal(state.room.roomtype.length, 5); + assert.equal(0 in state.room.roomtype, false); + assert.equal(1 in state.room.roomtype, true); + assert.equal(Object.is(state.room.roomtype[1], -0), true); + assert.equal(2 in state.room.roomtype, false); + assert.equal(3 in state.room.roomtype, false); + assert.deepEqual(state.room.roomtype[4], { tail: true }); + assert.equal(Object.isFrozen(state.room.roomtype[4]), true); + assert.equal(Object.isFrozen(sparse), false); + assert.equal(Object.isFrozen(sparse[4]), false); +}); + +test('canonical roomtype keeps own __proto__ data without prototype pollution', () => { + const source = {} as Record; + Object.defineProperty(source, '__proto__', { + configurable: true, + enumerable: true, + writable: true, + value: { polluted: true }, + }); + const data = fixture('player-login-room.json').data; + data.roomtype = [source]; + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(data)); + const state = store.getState(); + + assert.equal(state.room.kind, 'inside'); + if (state.room.kind !== 'inside') return; + const canonical = state.room.roomtype[0] as Record; + assert.equal(Object.getPrototypeOf(canonical), Object.prototype); + assert.equal(Object.prototype.hasOwnProperty.call(canonical, '__proto__'), true); + assert.deepEqual(canonical.__proto__, { polluted: true }); + assert.equal(({} as { polluted?: boolean }).polluted, undefined); + assert.equal(Object.isFrozen(canonical.__proto__), true); +}); + +test('non-JSON roomtype own data is rejected without state or caller mutation', () => { + const cases: ReadonlyArray unknown[]]> = [ + ['symbol', () => { + const source = { value: 1 } as Record; + source[Symbol('hidden')] = 2; + return [source]; + }], + ['enumerable', () => { + const source = { value: 1 }; + Object.defineProperty(source, 'hidden', { enumerable: false, value: 2 }); + return [source]; + }], + ['finite', () => [Number.NaN]], + ]; + + for (const [error, makeRoomtype] of cases) { + const roomtype = makeRoomtype(); + const data = fixture('player-login-room.json').data; + data.roomtype = roomtype; + const parsed = parseLoginResponse(data); + const store = new PlatformStore(); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.applyLoginSuccess(parsed), new RegExp(error, 'i')); + assert.equal(Object.isFrozen(roomtype), false); + if (typeof roomtype[0] === 'object' && roomtype[0] !== null) { + assert.equal(Object.isFrozen(roomtype[0]), false); + } + assert.equal(store.getState(), before); + assert.equal(notifications, 0); + } +}); + +test('reentrant store commits publish each stable transition before the next', () => { + const store = new PlatformStore(); + const events: string[] = []; + let nested = false; + const describe = (label: string, next: PlatformState, previous: PlatformState): void => { + events.push(`${label}:${next.room.kind}<-${previous.room.kind}`); + }; + store.subscribe((next, previous) => { + describe('A', next, previous); + if (!nested && next.room.kind === 'inside' && previous.room.kind === 'outside') { + nested = true; + store.clearRoom(); + } + }); + store.subscribe((next, previous) => { describe('B', next, previous); }); + + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + + assert.deepEqual(events, [ + 'A:inside<-outside', + 'B:inside<-outside', + 'A:outside<-inside', + 'B:outside<-inside', + ]); + assert.equal(store.getState().room.kind, 'outside'); +}); + +test('multiple reentrant commits retain their real previous and next states', () => { + const store = new PlatformStore(); + const events: string[] = []; + let nested = false; + store.subscribe((next, previous) => { + events.push(`A:${next.room.kind}<-${previous.room.kind}`); + if (!nested && next.room.kind === 'inside' && previous.room.kind === 'outside') { + nested = true; + store.clearRoom(); + assert.equal(store.getState().room.kind, 'outside'); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + assert.equal(store.getState().room.kind, 'inside'); + } + }); + store.subscribe((next, previous) => { + events.push(`B:${next.room.kind}<-${previous.room.kind}`); + }); + + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + + assert.deepEqual(events, [ + 'A:inside<-outside', 'B:inside<-outside', + 'A:outside<-inside', 'B:outside<-inside', + 'A:inside<-outside', 'B:inside<-outside', + ]); +}); + +test('subscription changes during publish apply only to later transition snapshots', () => { + const store = new PlatformStore(); + const events: string[] = []; + let nested = false; + let unsubscribeB = (): void => undefined; + store.subscribe((next, previous) => { + events.push(`A:${next.room.kind}<-${previous.room.kind}`); + if (!nested && next.room.kind === 'inside') { + nested = true; + unsubscribeB(); + store.subscribe((laterNext, laterPrevious) => { + events.push(`C:${laterNext.room.kind}<-${laterPrevious.room.kind}`); + }); + store.clearRoom(); + } + }); + unsubscribeB = store.subscribe((next, previous) => { + events.push(`B:${next.room.kind}<-${previous.room.kind}`); + }); + + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + + assert.deepEqual(events, [ + 'A:inside<-outside', + 'B:inside<-outside', + 'A:outside<-inside', + 'C:outside<-inside', + ]); +}); + +test('subscriber exceptions remain isolated from later store listeners', () => { + const store = new PlatformStore(); + let reached = false; + const originalConsoleError = console.error; + console.error = (): void => undefined; + try { + store.subscribe(() => { throw new Error('subscriber boom'); }); + store.subscribe(() => { reached = true; }); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-success.json').data)); + } finally { + console.error = originalConsoleError; + } + assert.equal(reached, true); +}); diff --git a/cocoscreator_projects/framework-tests/platform/runtime-session.test.ts b/cocoscreator_projects/framework-tests/platform/runtime-session.test.ts index 965dcd5..ec859d8 100644 --- a/cocoscreator_projects/framework-tests/platform/runtime-session.test.ts +++ b/cocoscreator_projects/framework-tests/platform/runtime-session.test.ts @@ -1,544 +1,615 @@ -import { readFileSync } from 'node:fs'; -import assert from 'node:assert/strict'; -import { test } from 'node:test'; -import { runInNewContext } from 'node:vm'; -import type { - GameEntry, - GameHost, - GameModule, - PlatformGameSnapshot, - PlatformToGameEvent, -} from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; -import { - GameSessionHost, - GameSessionOpenFault, -} from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts'; -import { RuntimeSession } from '../../YouleNexus/assets/framework/platform/runtime-session.ts'; -import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; -import { selectGameSnapshot } from '../../YouleNexus/assets/framework/platform/stores/selectors.ts'; - -function fixture(name: string): Record { - return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')).data; -} - -function makeSession() { - const store = new PlatformStore(); - const log: string[] = []; - const restored: unknown[] = []; - const denials: unknown[] = []; - const events: Array<{ event: PlatformToGameEvent; snapshot: PlatformGameSnapshot }> = []; - let host: GameHost; - const entry: GameEntry = { - key: 'fixture', - gameId: 41, - route: 'fixture-game', - resolveSeatCount: () => 4, - createModule: (): GameModule => ({ - attach(attachedHost) { - host = attachedHost; - attachedHost.getSnapshot(); - log.push('attach'); - }, - handlePlatformEvent(event) { - events.push({ event, snapshot: host.getSnapshot() }); - log.push(`event:${event.type}`); - }, - handleGameMessage() {}, - restore(deskinfo) { - restored.push(deskinfo); - log.push('restore'); - }, - dispose() { log.push('dispose'); }, - }), - }; - const gameSession = new GameSessionHost(entry); - log.length = 0; +import { readFileSync } from 'node:fs'; +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { runInNewContext } from 'node:vm'; +import type { + GameEntry, + GameHost, + GameModule, + PlatformGameSnapshot, + PlatformToGameEvent, +} from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; +import { + GameSessionHost, + GameSessionOpenFault, +} from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts'; +import {OwnedRoomHistory} from '../../YouleNexus/assets/framework/adapters/native/owned-room-history.ts'; +import { RuntimeSession } from '../../YouleNexus/assets/framework/platform/runtime-session.ts'; +import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; +import { selectGameSnapshot } from '../../YouleNexus/assets/framework/platform/stores/selectors.ts'; + +function fixture(name: string): Record { + return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')).data; +} + +function makeSession(roomHistory?:OwnedRoomHistory) { + const store = new PlatformStore(); + const log: string[] = []; + const restored: unknown[] = []; + const denials: unknown[] = []; + const events: Array<{ event: PlatformToGameEvent; snapshot: PlatformGameSnapshot }> = []; + let host: GameHost; + const entry: GameEntry = { + key: 'fixture', + gameId: 41, + route: 'fixture-game', + resolveSeatCount: () => 4, + createModule: (): GameModule => ({ + attach(attachedHost) { + host = attachedHost; + attachedHost.getSnapshot(); + log.push('attach'); + }, + handlePlatformEvent(event) { + events.push({ event, snapshot: host.getSnapshot() }); + log.push(`event:${event.type}`); + }, + handleGameMessage() {}, + restore(deskinfo) { + restored.push(deskinfo); + log.push('restore'); + }, + dispose() { log.push('dispose'); }, + }), + }; + const gameSession = new GameSessionHost(entry); + log.length = 0; const session = new RuntimeSession({ - store, - gameSession, - onServerDenial: outcome => { denials.push(outcome); }, - createGameHostLease: () => ({ - host: { - seat: { toView: (seat) => seat, toServer: (seat) => seat }, - getSnapshot: () => selectGameSnapshot(store.getState()), - subscribe: () => () => {}, - sendGameMessage() {}, - execute() {}, - }, - invalidate() {}, - }), - }); - return { store, gameSession, session, log, restored, events, denials }; -} + roomHistory, + store, + gameSession, + onServerDenial: outcome => { denials.push(outcome); }, + createGameHostLease: () => ({ + host: { + seat: { toView: (seat) => seat, toServer: (seat) => seat }, + getSnapshot: () => selectGameSnapshot(store.getState()), + subscribe: () => () => {}, + sendGameMessage() {}, + execute() {}, + }, + invalidate() {}, + }), + }); + return { store, gameSession, session, log, restored, events, denials }; +} + +test('server denials preserve an active room and module without committing a success action', () => { + const setup = makeSession(); + setup.session.handleLogin(fixture('player-login-room.json')); + const before = setup.store.getState(); + setup.log.length = 0; + const joinDenial = { state: 99, roomcode: '999999', showerror: 1, error: 'Room does not exist' }; + const loginDenial = { state: 1, error: 'Login denied' }; + setup.session.handleSelfJoin(joinDenial); + setup.session.markLoginPending(); + setup.session.handleLogin(loginDenial); + assert.equal(setup.store.getState(), before); + assert.equal(setup.session.loginPending, false); + assert.equal(setup.session.state, 'operational'); + assert.equal(setup.gameSession.state, 'active'); + assert.deepEqual(setup.log, []); + assert.deepEqual(setup.denials, [ + { rpc: 'self_join_room', state: 99, data: joinDenial }, + { rpc: 'player_login', state: 1, data: loginDenial }, + ]); +}); + +test('RuntimeSession commits a room login before opening, publishing, and restoring deskinfo', () => { + const setup = makeSession(); + const data = fixture('player-login-room.json'); + setup.store.subscribe(() => { setup.log.push('store'); }); + + setup.session.markLoginPending(); + setup.session.handleLogin(data); + + assert.equal(setup.session.loginPending, false); + assert.equal(setup.store.getState().room.kind, 'inside'); + assert.equal('deskinfo' in setup.store.getState().room, false); + assert.equal(setup.restored.length, 1); + assert.equal(setup.restored[0], data.deskinfo); + assert.deepEqual(setup.log, ['store', 'attach', 'event:room.entered', 'restore']); + assert.equal(setup.events[0]?.snapshot.room.roomcode, '100001'); + assert.equal(setup.events[0]?.event.type, 'room.entered'); + if (setup.events[0]?.event.type === 'room.entered') { + assert.equal(setup.events[0].event.roomtype, setup.events[0].snapshot.room.roomtype); + } +}); + +test('RuntimeSession never treats isbattle as a restore signal without truthy deskinfo', () => { + for (const deskinfo of [undefined, null, false, 0, '']) { + const setup = makeSession(); + const data = fixture('player-login-room.json'); + if (deskinfo === undefined) delete data.deskinfo; + else data.deskinfo = deskinfo; + data.isbattle = 1; + + setup.session.handleLogin(data); + + assert.deepEqual(setup.restored, [], `deskinfo=${String(deskinfo)}`); + assert.equal(setup.store.getState().room.kind, 'inside'); + } +}); + +test('RuntimeSession self join commits, opens a fresh session, and restores the same snapshot once', () => { + const setup = makeSession(); + setup.session.handleLogin(fixture('player-login-success.json')); + setup.log.length = 0; + const join = fixture('self-join-room.json'); + setup.store.subscribe(() => { setup.log.push('store'); }); + + setup.session.handleSelfJoin(join); + + assert.equal(setup.store.getState().room.kind, 'inside'); + assert.equal(setup.restored.length, 1); + assert.equal(setup.restored[0], join.deskinfo); + assert.deepEqual(setup.log, ['store', 'attach', 'event:room.entered', 'restore']); +}); + +test('RuntimeSession self join honors the parser-owned deskwar and deskinfo matrix', () => { + const cases = [ + { deskwar: 'missing', deskinfo: false, expectedStage: 0 }, + { deskwar: 0, deskinfo: false, expectedStage: 0 }, + { deskwar: 1, deskinfo: false, expectedStage: 1 }, + { deskwar: 'missing', deskinfo: true, expectedStage: 1 }, + { deskwar: 0, deskinfo: true, expectedStage: 1 }, + { deskwar: 1, deskinfo: true, expectedStage: 1 }, + ] as const; + + for (const row of cases) { + const setup = makeSession(); + setup.session.handleLogin(fixture('player-login-success.json')); + const data = fixture('self-join-room.json'); + if (row.deskwar === 'missing') delete data.deskwar; + else data.deskwar = row.deskwar; + const deskinfo = { round: 7 }; + if (row.deskinfo) data.deskinfo = deskinfo; + else delete data.deskinfo; + data.isbattle = 1; + + setup.session.handleSelfJoin(data); + + const state = setup.store.getState(); + assert.equal(state.room.kind, 'inside'); + if (state.room.kind === 'inside') { + assert.equal(state.room.stage, row.expectedStage, JSON.stringify(row)); + } + assert.equal(setup.restored.length, row.deskinfo ? 1 : 0, JSON.stringify(row)); + if (row.deskinfo) assert.equal(setup.restored[0], deskinfo); + } +}); + +type LeaseFailurePoint = + | 'lease-factory' + | 'module-create' + | 'attach' + | 'event' + | 'restore' + | 'dispose' + | 'invalidate'; + +interface LeaseRecord { + readonly host: GameHost; + invalidations: number; +} + +function makeLeaseSession() { + const store = new PlatformStore(); + const leases: LeaseRecord[] = []; + const attachedHosts: GameHost[] = []; + const failures = new Set(); + const errors = Object.fromEntries([ + 'lease-factory', 'module-create', 'attach', 'event', 'restore', 'dispose', 'invalidate', + ].map((point) => [point, new Error(`failure:${point}`)])) as Record; + let validatingEntry = true; + let moduleDisposals = 0; + + function host(): GameHost { + return { + seat: { toView: (seat) => seat, toServer: (seat) => seat }, + getSnapshot: () => selectGameSnapshot(store.getState()), + subscribe: () => () => {}, + sendGameMessage() {}, + execute() {}, + }; + } + + const entry: GameEntry = { + key: 'lease-fixture', + gameId: 41, + route: 'fixture-game', + resolveSeatCount: () => 4, + createModule: (): GameModule => { + if (!validatingEntry && failures.has('module-create')) throw errors['module-create']; + return { + attach(attachedHost) { + if (!validatingEntry) attachedHosts.push(attachedHost); + if (!validatingEntry && failures.has('attach')) throw errors.attach; + }, + handlePlatformEvent() { + if (!validatingEntry && failures.has('event')) throw errors.event; + }, + handleGameMessage() {}, + restore() { + if (!validatingEntry && failures.has('restore')) throw errors.restore; + }, + dispose() { + if (validatingEntry) return; + moduleDisposals += 1; + if (failures.has('dispose')) throw errors.dispose; + }, + }; + }, + }; + const gameSession = new GameSessionHost(entry); + validatingEntry = false; + const sessionOptions = { + store, + gameSession, + onServerDenial() { assert.fail('Unexpected server denial'); }, + createGameHostLease() { + if (failures.has('lease-factory')) throw errors['lease-factory']; + const record: LeaseRecord = { host: host(), invalidations: 0 }; + leases.push(record); + return { + host: record.host, + invalidate() { + record.invalidations += 1; + if (failures.has('invalidate')) throw errors.invalidate; + }, + }; + }, + }; + const session = new RuntimeSession(sessionOptions); + return { + store, + gameSession, + session, + leases, + attachedHosts, + errors, + setFailures(...points: LeaseFailurePoint[]) { + failures.clear(); + for (const point of points) failures.add(point); + }, + getModuleDisposals: () => moduleDisposals, + }; +} + +function caughtError(operation: () => void): Error { + try { + operation(); + } catch (error) { + assert.ok(error instanceof Error); + return error; + } + assert.fail('expected operation to throw'); +} + +test('RuntimeSession creates a fresh host lease per login/self-join replacement and invalidates on self exit', () => { + const setup = makeLeaseSession(); + + setup.session.handleLogin(fixture('player-login-room.json')); + setup.session.handleSelfJoin(fixture('self-join-room.json')); + setup.session.handleLogin(fixture('player-login-room.json')); + + assert.equal(setup.leases.length, 3); + assert.equal(new Set(setup.attachedHosts).size, 3); + assert.deepEqual(setup.leases.map((lease) => lease.invalidations), [1, 1, 0]); + assert.equal(setup.attachedHosts[0], setup.leases[0]?.host); + assert.equal(setup.attachedHosts[1], setup.leases[1]?.host); + assert.equal(setup.attachedHosts[2], setup.leases[2]?.host); + + setup.session.handleSelfExit({ raw: {} }); + + assert.deepEqual(setup.leases.map((lease) => lease.invalidations), [1, 1, 1]); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.session.state, 'operational'); +}); + +test('RuntimeSession invalidates the active lease on a successful server switch', () => { + const setup = makeLeaseSession(); + setup.session.handleLogin(fixture('player-login-room.json')); + + setup.session.handleServerSwitch(() => {}); + + assert.equal(setup.leases[0]?.invalidations, 1); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.session.state, 'operational'); +}); + +for (const failure of [ + 'lease-factory', 'module-create', 'attach', 'event', 'restore', +] as const) { + test(`RuntimeSession cleans a failed room open and enters terminal fault at ${failure}`, () => { + const setup = makeLeaseSession(); + setup.setFailures(failure); + + const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); + + assert.equal(setup.session.state, 'faulted'); + assert.equal(setup.session.fault, error); + assert.equal(setup.gameSession.state, 'idle'); + if (failure === 'lease-factory') { + assert.equal(setup.leases.length, 0); + } else { + assert.equal(setup.leases.length, 1); + assert.equal(setup.leases[0]?.invalidations, 1); + } + assert.throws( + () => setup.session.handleLogin(fixture('player-login-success.json')), + /fault/i, + ); + }); +} + +for (const replacement of ['login', 'self-join'] as const) { + test(`RuntimeSession invalidates the old lease and faults when ${replacement} replacement disposal fails`, () => { + const setup = makeLeaseSession(); + setup.session.handleLogin(fixture('player-login-room.json')); + setup.setFailures('dispose'); + + const error = caughtError(() => { + if (replacement === 'login') setup.session.handleLogin(fixture('player-login-room.json')); + else setup.session.handleSelfJoin(fixture('self-join-room.json')); + }); + + assert.equal(setup.session.state, 'faulted'); + assert.equal(setup.session.fault, error); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.leases.length, 1, 'a failed old-session cleanup must not create a new lease'); + assert.equal(setup.leases[0]?.invalidations, 1); + assert.equal(setup.getModuleDisposals(), 1); + }); +} + +for (const operation of ['self-exit', 'switch'] as const) { + test(`RuntimeSession invalidates once and faults when ${operation} disposal fails`, () => { + const setup = makeLeaseSession(); + setup.session.handleLogin(fixture('player-login-room.json')); + setup.setFailures('dispose'); + + const error = caughtError(() => { + if (operation === 'self-exit') setup.session.handleSelfExit({ raw: {} }); + else setup.session.handleServerSwitch(() => {}); + }); + + assert.equal(setup.session.state, 'faulted'); + assert.equal(setup.session.fault, error); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.leases[0]?.invalidations, 1); + assert.equal(setup.getModuleDisposals(), 1); + }); +} + +test('RuntimeSession aggregates event, dispose, and invalidation failures after completing cleanup', () => { + const setup = makeLeaseSession(); + setup.setFailures('event', 'dispose', 'invalidate'); + + const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); + + assert.equal(setup.session.state, 'faulted'); + assert.equal(setup.session.fault, error); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.leases[0]?.invalidations, 1); + assert.deepEqual( + (error as Error & { errors?: readonly unknown[] }).errors, + [setup.errors.event, setup.errors.dispose, setup.errors.invalidate], + ); +}); + +test('RuntimeSession flattens attach cleanup failures before appending lease invalidation', () => { + const setup = makeLeaseSession(); + setup.setFailures('attach', 'dispose', 'invalidate'); + + const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); + + assert.equal(setup.session.state, 'faulted'); + assert.equal(setup.session.fault, error); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.leases[0]?.invalidations, 1); + assert.equal(setup.getModuleDisposals(), 1); + assert.deepEqual( + (error as Error & { errors?: readonly unknown[] }).errors, + [setup.errors.attach, setup.errors.dispose, setup.errors.invalidate], + ); +}); + +test('RuntimeSession flattens a foreign-realm branded open fault before lease invalidation', () => { + const setup = makeLeaseSession(); + const primary = new Error('foreign primary'); + const cleanup = new Error('foreign cleanup'); + const foreignFault = runInNewContext(`(() => { + const fault = new Error('foreign open fault'); + Object.defineProperties(fault, { + [Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true }, + errors: { value: [primary, cleanup] }, + }); + return fault; + })()`, { primary, cleanup }); + assert.equal(foreignFault instanceof GameSessionOpenFault, false); + setup.setFailures('invalidate'); + setup.gameSession.open = () => { throw foreignFault; }; + + const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); + + assert.equal(setup.session.state, 'faulted'); + assert.equal(setup.session.fault, error); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.leases[0]?.invalidations, 1); + assert.deepEqual( + (error as Error & { errors?: readonly unknown[] }).errors, + [primary, cleanup, setup.errors.invalidate], + ); +}); + +test('RuntimeSession snapshots branded open errors without invoking their hostile iterator', () => { + const setup = makeLeaseSession(); + const iteratorReplacement = new Error('iterator replacement'); + let iteratorReads = 0; + const sourceErrors = [setup.errors.attach, setup.errors.dispose]; + Object.defineProperty(sourceErrors, Symbol.iterator, { + configurable: true, + get() { + iteratorReads += 1; + sourceErrors[0] = iteratorReplacement; + throw new Error('source iterator must not run'); + }, + }); + const brandedFault = new Error('hostile branded open fault'); + Object.defineProperties(brandedFault, { + [Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true }, + errors: { value: sourceErrors }, + }); + setup.setFailures('invalidate'); + setup.gameSession.open = () => { throw brandedFault; }; + + const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); + + assert.equal(setup.session.state, 'faulted'); + assert.equal(setup.session.fault, error); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.leases[0]?.invalidations, 1); + assert.equal(iteratorReads, 0); + assert.equal(sourceErrors[0], setup.errors.attach); + assert.deepEqual( + (error as Error & { errors?: readonly unknown[] }).errors, + [setup.errors.attach, setup.errors.dispose, setup.errors.invalidate], + ); +}); + +test('RuntimeSession keeps ordinary aggregates and unbranded or accessor spoofs as single errors', () => { + const primary = new Error('user primary'); + const cleanup = new Error('user cleanup'); + const ordinaryAggregate = runInNewContext( + 'new AggregateError([primary, cleanup], "user aggregate")', + { primary, cleanup }, + ); + const unbranded = { errors: [primary, cleanup] }; + let accessorReads = 0; + const accessorSpoof = new Error('accessor spoof'); + Object.defineProperties(accessorSpoof, { + [Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true }, + errors: { + get() { + accessorReads += 1; + throw new Error('errors getter must not run'); + }, + }, + }); + const brandAccessorSpoof = new Error('brand accessor spoof'); + Object.defineProperties(brandAccessorSpoof, { + [Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { + get() { + accessorReads += 1; + throw new Error('brand getter must not run'); + }, + }, + errors: { value: [primary, cleanup] }, + }); + const descriptorTarget = new Error('descriptor failure'); + Object.defineProperties(descriptorTarget, { + [Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true }, + errors: { value: [primary, cleanup] }, + }); + let descriptorReads = 0; + const descriptorFailure = new Proxy(descriptorTarget, { + getOwnPropertyDescriptor(target, key) { + descriptorReads += 1; + if (key === 'errors') throw new Error('descriptor trap failed'); + return Reflect.getOwnPropertyDescriptor(target, key); + }, + }); + + for (const candidate of [ + ordinaryAggregate, + unbranded, + accessorSpoof, + brandAccessorSpoof, + descriptorFailure, + ]) { + const setup = makeLeaseSession(); + setup.setFailures('invalidate'); + setup.gameSession.open = () => { throw candidate; }; + + const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); + const errors = (error as Error & { errors?: readonly unknown[] }).errors; + + assert.equal(errors?.length, 2); + assert.equal(errors?.[0], candidate); + assert.equal(errors?.[1], setup.errors.invalidate); + assert.equal(setup.session.state, 'faulted'); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.leases[0]?.invalidations, 1); + } + assert.equal(accessorReads, 0); + assert.equal(descriptorReads, 2); +}); -test('server denials preserve an active room and module without committing a success action', () => { - const setup = makeSession(); - setup.session.handleLogin(fixture('player-login-room.json')); - const before = setup.store.getState(); - setup.log.length = 0; - const joinDenial = { state: 99, roomcode: '999999', showerror: 1, error: 'Room does not exist' }; - const loginDenial = { state: 1, error: 'Login denied' }; - setup.session.handleSelfJoin(joinDenial); - setup.session.markLoginPending(); - setup.session.handleLogin(loginDenial); - assert.equal(setup.store.getState(), before); - assert.equal(setup.session.loginPending, false); - assert.equal(setup.session.state, 'operational'); - assert.equal(setup.gameSession.state, 'active'); - assert.deepEqual(setup.log, []); - assert.deepEqual(setup.denials, [ - { rpc: 'self_join_room', state: 99, data: joinDenial }, - { rpc: 'player_login', state: 1, data: loginDenial }, - ]); +test('server room outcomes update owned room cache at the session boundary',()=>{ + const values=new Map();const history=new OwnedRoomHistory({getItem:k=>values.get(k)??null,setItem:(k,v)=>{values.set(k,v);}},'g','a'); + const setup=makeSession(history);setup.session.handleLogin({...fixture('player-login-room.json'),infinite:0}); + const room=setup.store.getState().room;if(room.kind!=='inside')throw Error('fixture room'); + setup.session.handleSelfExit({seat:room.selfSeat,raw:{isowner:1,roomcode:room.roomcode}}); + assert.deepEqual(history.read(),[Number(room.roomcode)]); + setup.session.handleSelfJoin({state:99,roomcode:room.roomcode});assert.deepEqual(history.read(),[]); }); -test('RuntimeSession commits a room login before opening, publishing, and restoring deskinfo', () => { - const setup = makeSession(); - const data = fixture('player-login-room.json'); - setup.store.subscribe(() => { setup.log.push('store'); }); - - setup.session.markLoginPending(); - setup.session.handleLogin(data); - - assert.equal(setup.session.loginPending, false); - assert.equal(setup.store.getState().room.kind, 'inside'); - assert.equal('deskinfo' in setup.store.getState().room, false); - assert.equal(setup.restored.length, 1); - assert.equal(setup.restored[0], data.deskinfo); - assert.deepEqual(setup.log, ['store', 'attach', 'event:room.entered', 'restore']); - assert.equal(setup.events[0]?.snapshot.room.roomcode, '100001'); - assert.equal(setup.events[0]?.event.type, 'room.entered'); - if (setup.events[0]?.event.type === 'room.entered') { - assert.equal(setup.events[0].event.roomtype, setup.events[0].snapshot.room.roomtype); - } +test('create success uses login profile and entry seats without wire players, opens once and exits', () => { + const setup=makeSession();setup.session.handleLogin(fixture('player-login-success.json')); + const data={state:0,roomcode:'789123',seat:2,roomtype:[1,4],makewar:0,asetcount:8}; + setup.store.subscribe(()=>setup.log.push('store')); + setup.session.handleCreateRoom(data); + const state=setup.store.getState(); + assert.equal(state.room.kind,'inside'); + if(state.room.kind!=='inside')throw Error('missing room'); + assert.deepEqual(state.room.seatPlayerIds,[null,null,430511,null]); + assert.equal(state.room.selfSeat,2);assert.equal(state.room.stage,0); + assert.equal(state.room.needprepare,0);assert.equal(state.room.infinite,0); + assert.deepEqual(state.room.readySeats,[]);assert.deepEqual(state.room.offlineSeats,[]); + assert.equal(state.players.entities[430511].isprepare,0); + assert.equal('players' in data,false); + assert.deepEqual(setup.log,['store','attach','event:room.entered']); + setup.session.handleCreateRoom({...data}); + assert.equal(setup.store.getState(),state); + assert.deepEqual(setup.log,['store','attach','event:room.entered']); + setup.session.handleSelfExit({raw:{}}); + assert.equal(setup.store.getState().room.kind,'outside'); + assert.equal(setup.gameSession.state,'idle');assert.ok(setup.log.includes('dispose')); }); -test('RuntimeSession never treats isbattle as a restore signal without truthy deskinfo', () => { - for (const deskinfo of [undefined, null, false, 0, '']) { - const setup = makeSession(); - const data = fixture('player-login-room.json'); - if (deskinfo === undefined) delete data.deskinfo; - else data.deskinfo = deskinfo; - data.isbattle = 1; +test('create denial never commits or instantiates a game; invalid seat fails before state mutation',()=>{ + const setup=makeSession();setup.session.handleLogin(fixture('player-login-success.json')); + const before=setup.store.getState(), denial={state:2,showerror:1,error:'denied'}; + setup.session.handleCreateRoom(denial); + assert.equal(setup.store.getState(),before);assert.deepEqual(setup.log,[]); + assert.deepEqual(setup.denials,[{rpc:'create_room',state:2,data:denial}]); + assert.throws(()=>setup.session.handleCreateRoom({state:0,roomcode:'123',seat:4,roomtype:[1]}),/seat/); + assert.equal(setup.store.getState(),before); +}); - setup.session.handleLogin(data); +test('duplicate successful self join responses do not replace module or restore twice',()=>{ + const setup=makeSession();setup.session.handleLogin(fixture('player-login-success.json')); + const data=fixture('self-join-room.json');setup.session.handleSelfJoin(data); + const before=setup.store.getState(),log=[...setup.log]; + setup.session.handleSelfJoin({...data}); + assert.equal(setup.store.getState(),before);assert.deepEqual(setup.log,log); + assert.equal(setup.restored.length,1); +}); - assert.deepEqual(setup.restored, [], `deskinfo=${String(deskinfo)}`); - assert.equal(setup.store.getState().room.kind, 'inside'); - } -}); - -test('RuntimeSession self join commits, opens a fresh session, and restores the same snapshot once', () => { +for (const mutate of [ + (roomtype: readonly unknown[]) => { (roomtype as unknown[])[0] = 999; }, + (roomtype: readonly unknown[]) => { (roomtype[1] as {count: number}).count = 999; }, +]) test('create seat resolver cannot mutate raw roomtype or partially commit a room', () => { const setup = makeSession(); setup.session.handleLogin(fixture('player-login-success.json')); - setup.log.length = 0; - const join = fixture('self-join-room.json'); - setup.store.subscribe(() => { setup.log.push('store'); }); - - setup.session.handleSelfJoin(join); - - assert.equal(setup.store.getState().room.kind, 'inside'); - assert.equal(setup.restored.length, 1); - assert.equal(setup.restored[0], join.deskinfo); - assert.deepEqual(setup.log, ['store', 'attach', 'event:room.entered', 'restore']); -}); - -test('RuntimeSession self join honors the parser-owned deskwar and deskinfo matrix', () => { - const cases = [ - { deskwar: 'missing', deskinfo: false, expectedStage: 0 }, - { deskwar: 0, deskinfo: false, expectedStage: 0 }, - { deskwar: 1, deskinfo: false, expectedStage: 1 }, - { deskwar: 'missing', deskinfo: true, expectedStage: 1 }, - { deskwar: 0, deskinfo: true, expectedStage: 1 }, - { deskwar: 1, deskinfo: true, expectedStage: 1 }, - ] as const; - - for (const row of cases) { - const setup = makeSession(); - setup.session.handleLogin(fixture('player-login-success.json')); - const data = fixture('self-join-room.json'); - if (row.deskwar === 'missing') delete data.deskwar; - else data.deskwar = row.deskwar; - const deskinfo = { round: 7 }; - if (row.deskinfo) data.deskinfo = deskinfo; - else delete data.deskinfo; - data.isbattle = 1; - - setup.session.handleSelfJoin(data); - - const state = setup.store.getState(); - assert.equal(state.room.kind, 'inside'); - if (state.room.kind === 'inside') { - assert.equal(state.room.stage, row.expectedStage, JSON.stringify(row)); - } - assert.equal(setup.restored.length, row.deskinfo ? 1 : 0, JSON.stringify(row)); - if (row.deskinfo) assert.equal(setup.restored[0], deskinfo); - } -}); - -type LeaseFailurePoint = - | 'lease-factory' - | 'module-create' - | 'attach' - | 'event' - | 'restore' - | 'dispose' - | 'invalidate'; - -interface LeaseRecord { - readonly host: GameHost; - invalidations: number; -} - -function makeLeaseSession() { - const store = new PlatformStore(); - const leases: LeaseRecord[] = []; - const attachedHosts: GameHost[] = []; - const failures = new Set(); - const errors = Object.fromEntries([ - 'lease-factory', 'module-create', 'attach', 'event', 'restore', 'dispose', 'invalidate', - ].map((point) => [point, new Error(`failure:${point}`)])) as Record; - let validatingEntry = true; - let moduleDisposals = 0; - - function host(): GameHost { - return { - seat: { toView: (seat) => seat, toServer: (seat) => seat }, - getSnapshot: () => selectGameSnapshot(store.getState()), - subscribe: () => () => {}, - sendGameMessage() {}, - execute() {}, - }; - } - - const entry: GameEntry = { - key: 'lease-fixture', - gameId: 41, - route: 'fixture-game', - resolveSeatCount: () => 4, - createModule: (): GameModule => { - if (!validatingEntry && failures.has('module-create')) throw errors['module-create']; - return { - attach(attachedHost) { - if (!validatingEntry) attachedHosts.push(attachedHost); - if (!validatingEntry && failures.has('attach')) throw errors.attach; - }, - handlePlatformEvent() { - if (!validatingEntry && failures.has('event')) throw errors.event; - }, - handleGameMessage() {}, - restore() { - if (!validatingEntry && failures.has('restore')) throw errors.restore; - }, - dispose() { - if (validatingEntry) return; - moduleDisposals += 1; - if (failures.has('dispose')) throw errors.dispose; - }, - }; - }, - }; - const gameSession = new GameSessionHost(entry); - validatingEntry = false; - const sessionOptions = { - store, - gameSession, - onServerDenial() { assert.fail('Unexpected server denial'); }, - createGameHostLease() { - if (failures.has('lease-factory')) throw errors['lease-factory']; - const record: LeaseRecord = { host: host(), invalidations: 0 }; - leases.push(record); - return { - host: record.host, - invalidate() { - record.invalidations += 1; - if (failures.has('invalidate')) throw errors.invalidate; - }, - }; - }, - }; - const session = new RuntimeSession(sessionOptions); - return { - store, - gameSession, - session, - leases, - attachedHosts, - errors, - setFailures(...points: LeaseFailurePoint[]) { - failures.clear(); - for (const point of points) failures.add(point); - }, - getModuleDisposals: () => moduleDisposals, - }; -} - -function caughtError(operation: () => void): Error { - try { - operation(); - } catch (error) { - assert.ok(error instanceof Error); - return error; - } - assert.fail('expected operation to throw'); -} - -test('RuntimeSession creates a fresh host lease per login/self-join replacement and invalidates on self exit', () => { - const setup = makeLeaseSession(); - - setup.session.handleLogin(fixture('player-login-room.json')); - setup.session.handleSelfJoin(fixture('self-join-room.json')); - setup.session.handleLogin(fixture('player-login-room.json')); - - assert.equal(setup.leases.length, 3); - assert.equal(new Set(setup.attachedHosts).size, 3); - assert.deepEqual(setup.leases.map((lease) => lease.invalidations), [1, 1, 0]); - assert.equal(setup.attachedHosts[0], setup.leases[0]?.host); - assert.equal(setup.attachedHosts[1], setup.leases[1]?.host); - assert.equal(setup.attachedHosts[2], setup.leases[2]?.host); - - setup.session.handleSelfExit({ raw: {} }); - - assert.deepEqual(setup.leases.map((lease) => lease.invalidations), [1, 1, 1]); + const before = setup.store.getState(); + const data = {state: 0, roomcode: '123456', seat: 0, roomtype: [1, {count: 4}]}; + setup.gameSession.entry.resolveSeatCount = roomtype => { mutate(roomtype); return 4; }; + assert.throws(() => setup.session.handleCreateRoom(data), TypeError); + assert.deepEqual(data.roomtype, [1, {count: 4}]); + assert.equal(Object.isFrozen(data.roomtype), false); + assert.equal(Object.isFrozen(data.roomtype[1]), false); + assert.equal(setup.store.getState(), before); assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.session.state, 'operational'); -}); - -test('RuntimeSession invalidates the active lease on a successful server switch', () => { - const setup = makeLeaseSession(); - setup.session.handleLogin(fixture('player-login-room.json')); - - setup.session.handleServerSwitch(() => {}); - - assert.equal(setup.leases[0]?.invalidations, 1); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.session.state, 'operational'); -}); - -for (const failure of [ - 'lease-factory', 'module-create', 'attach', 'event', 'restore', -] as const) { - test(`RuntimeSession cleans a failed room open and enters terminal fault at ${failure}`, () => { - const setup = makeLeaseSession(); - setup.setFailures(failure); - - const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); - - assert.equal(setup.session.state, 'faulted'); - assert.equal(setup.session.fault, error); - assert.equal(setup.gameSession.state, 'idle'); - if (failure === 'lease-factory') { - assert.equal(setup.leases.length, 0); - } else { - assert.equal(setup.leases.length, 1); - assert.equal(setup.leases[0]?.invalidations, 1); - } - assert.throws( - () => setup.session.handleLogin(fixture('player-login-success.json')), - /fault/i, - ); - }); -} - -for (const replacement of ['login', 'self-join'] as const) { - test(`RuntimeSession invalidates the old lease and faults when ${replacement} replacement disposal fails`, () => { - const setup = makeLeaseSession(); - setup.session.handleLogin(fixture('player-login-room.json')); - setup.setFailures('dispose'); - - const error = caughtError(() => { - if (replacement === 'login') setup.session.handleLogin(fixture('player-login-room.json')); - else setup.session.handleSelfJoin(fixture('self-join-room.json')); - }); - - assert.equal(setup.session.state, 'faulted'); - assert.equal(setup.session.fault, error); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.leases.length, 1, 'a failed old-session cleanup must not create a new lease'); - assert.equal(setup.leases[0]?.invalidations, 1); - assert.equal(setup.getModuleDisposals(), 1); - }); -} - -for (const operation of ['self-exit', 'switch'] as const) { - test(`RuntimeSession invalidates once and faults when ${operation} disposal fails`, () => { - const setup = makeLeaseSession(); - setup.session.handleLogin(fixture('player-login-room.json')); - setup.setFailures('dispose'); - - const error = caughtError(() => { - if (operation === 'self-exit') setup.session.handleSelfExit({ raw: {} }); - else setup.session.handleServerSwitch(() => {}); - }); - - assert.equal(setup.session.state, 'faulted'); - assert.equal(setup.session.fault, error); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.leases[0]?.invalidations, 1); - assert.equal(setup.getModuleDisposals(), 1); - }); -} - -test('RuntimeSession aggregates event, dispose, and invalidation failures after completing cleanup', () => { - const setup = makeLeaseSession(); - setup.setFailures('event', 'dispose', 'invalidate'); - - const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); - - assert.equal(setup.session.state, 'faulted'); - assert.equal(setup.session.fault, error); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.leases[0]?.invalidations, 1); - assert.deepEqual( - (error as Error & { errors?: readonly unknown[] }).errors, - [setup.errors.event, setup.errors.dispose, setup.errors.invalidate], - ); -}); - -test('RuntimeSession flattens attach cleanup failures before appending lease invalidation', () => { - const setup = makeLeaseSession(); - setup.setFailures('attach', 'dispose', 'invalidate'); - - const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); - - assert.equal(setup.session.state, 'faulted'); - assert.equal(setup.session.fault, error); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.leases[0]?.invalidations, 1); - assert.equal(setup.getModuleDisposals(), 1); - assert.deepEqual( - (error as Error & { errors?: readonly unknown[] }).errors, - [setup.errors.attach, setup.errors.dispose, setup.errors.invalidate], - ); -}); - -test('RuntimeSession flattens a foreign-realm branded open fault before lease invalidation', () => { - const setup = makeLeaseSession(); - const primary = new Error('foreign primary'); - const cleanup = new Error('foreign cleanup'); - const foreignFault = runInNewContext(`(() => { - const fault = new Error('foreign open fault'); - Object.defineProperties(fault, { - [Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true }, - errors: { value: [primary, cleanup] }, - }); - return fault; - })()`, { primary, cleanup }); - assert.equal(foreignFault instanceof GameSessionOpenFault, false); - setup.setFailures('invalidate'); - setup.gameSession.open = () => { throw foreignFault; }; - - const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); - - assert.equal(setup.session.state, 'faulted'); - assert.equal(setup.session.fault, error); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.leases[0]?.invalidations, 1); - assert.deepEqual( - (error as Error & { errors?: readonly unknown[] }).errors, - [primary, cleanup, setup.errors.invalidate], - ); -}); - -test('RuntimeSession snapshots branded open errors without invoking their hostile iterator', () => { - const setup = makeLeaseSession(); - const iteratorReplacement = new Error('iterator replacement'); - let iteratorReads = 0; - const sourceErrors = [setup.errors.attach, setup.errors.dispose]; - Object.defineProperty(sourceErrors, Symbol.iterator, { - configurable: true, - get() { - iteratorReads += 1; - sourceErrors[0] = iteratorReplacement; - throw new Error('source iterator must not run'); - }, - }); - const brandedFault = new Error('hostile branded open fault'); - Object.defineProperties(brandedFault, { - [Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true }, - errors: { value: sourceErrors }, - }); - setup.setFailures('invalidate'); - setup.gameSession.open = () => { throw brandedFault; }; - - const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); - - assert.equal(setup.session.state, 'faulted'); - assert.equal(setup.session.fault, error); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.leases[0]?.invalidations, 1); - assert.equal(iteratorReads, 0); - assert.equal(sourceErrors[0], setup.errors.attach); - assert.deepEqual( - (error as Error & { errors?: readonly unknown[] }).errors, - [setup.errors.attach, setup.errors.dispose, setup.errors.invalidate], - ); -}); - -test('RuntimeSession keeps ordinary aggregates and unbranded or accessor spoofs as single errors', () => { - const primary = new Error('user primary'); - const cleanup = new Error('user cleanup'); - const ordinaryAggregate = runInNewContext( - 'new AggregateError([primary, cleanup], "user aggregate")', - { primary, cleanup }, - ); - const unbranded = { errors: [primary, cleanup] }; - let accessorReads = 0; - const accessorSpoof = new Error('accessor spoof'); - Object.defineProperties(accessorSpoof, { - [Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true }, - errors: { - get() { - accessorReads += 1; - throw new Error('errors getter must not run'); - }, - }, - }); - const brandAccessorSpoof = new Error('brand accessor spoof'); - Object.defineProperties(brandAccessorSpoof, { - [Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { - get() { - accessorReads += 1; - throw new Error('brand getter must not run'); - }, - }, - errors: { value: [primary, cleanup] }, - }); - const descriptorTarget = new Error('descriptor failure'); - Object.defineProperties(descriptorTarget, { - [Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true }, - errors: { value: [primary, cleanup] }, - }); - let descriptorReads = 0; - const descriptorFailure = new Proxy(descriptorTarget, { - getOwnPropertyDescriptor(target, key) { - descriptorReads += 1; - if (key === 'errors') throw new Error('descriptor trap failed'); - return Reflect.getOwnPropertyDescriptor(target, key); - }, - }); - - for (const candidate of [ - ordinaryAggregate, - unbranded, - accessorSpoof, - brandAccessorSpoof, - descriptorFailure, - ]) { - const setup = makeLeaseSession(); - setup.setFailures('invalidate'); - setup.gameSession.open = () => { throw candidate; }; - - const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json'))); - const errors = (error as Error & { errors?: readonly unknown[] }).errors; - - assert.equal(errors?.length, 2); - assert.equal(errors?.[0], candidate); - assert.equal(errors?.[1], setup.errors.invalidate); - assert.equal(setup.session.state, 'faulted'); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.leases[0]?.invalidations, 1); - } - assert.equal(accessorReads, 0); - assert.equal(descriptorReads, 2); -}); + assert.deepEqual(setup.log, []); +}); diff --git a/cocoscreator_projects/framework-tests/platform/runtime.test.ts b/cocoscreator_projects/framework-tests/platform/runtime.test.ts index 6ace6de..249eef4 100644 --- a/cocoscreator_projects/framework-tests/platform/runtime.test.ts +++ b/cocoscreator_projects/framework-tests/platform/runtime.test.ts @@ -1,2314 +1,2396 @@ -import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; -// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 -const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); -import { remoteFixture } from '../fixtures/remote-config.ts'; -import { readFileSync } from 'node:fs'; -import assert from 'node:assert/strict'; -import { test } from 'node:test'; -import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; -import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts'; -import type { Clock } from '../../YouleNexus/assets/framework/net/heartbeat.ts'; -import type { WireEvent } from '../../YouleNexus/assets/framework/net/wire-client.ts'; -import { Router } from '../../YouleNexus/assets/framework/protocol/router.ts'; -import type { - GameEntry, - GameHost, - GameModule, - PlatformToGameEvent, -} from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; -import { - PlatformRuntime, - type PlatformWireClient, -} from '../../YouleNexus/assets/framework/platform/runtime.ts'; -import type { ScenePort } from '../../YouleNexus/assets/framework/platform/scene-port.ts'; -import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); +import { remoteFixture } from '../fixtures/remote-config.ts'; +import { readFileSync } from 'node:fs'; +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; +import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts'; +import type { Clock } from '../../YouleNexus/assets/framework/net/heartbeat.ts'; +import type { WireEvent } from '../../YouleNexus/assets/framework/net/wire-client.ts'; +import { Router } from '../../YouleNexus/assets/framework/protocol/router.ts'; +import type { + GameEntry, + GameHost, + GameModule, + PlatformToGameEvent, +} from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; +import { + PlatformRuntime, + type PlatformWireClient, +} from '../../YouleNexus/assets/framework/platform/runtime.ts'; +import type { ScenePort } from '../../YouleNexus/assets/framework/platform/scene-port.ts'; +import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; import { PlatformUiController } from '../../YouleNexus/assets/framework/presentation/platform-ui-controller.ts'; - -function fixture(name: string): Record { - return JSON.parse( - readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8'), - ).data; -} - -const ACCOUNT = Object.freeze({ - openid: 'o1', - nickname: '测试号', - avatar: 'http://a', - sex: 0, - province: '江西', - city: '南昌', - unionid: 'test_unionid', +import type {LoginDeviceSnapshot} from '../../YouleNexus/assets/framework/protocol/contracts/login-contract.ts'; + +function fixture(name: string): Record { + return JSON.parse( + readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8'), + ).data; +} + +const ACCOUNT = Object.freeze({ + openid: 'o1', + nickname: '测试号', + avatar: 'http://a', + sex: 0, + province: '江西', + city: '南昌', + unionid: 'test_unionid', +}); + +const DEVICE = Object.freeze({ + ip: '127.0.0.1', + location: Object.freeze({ latitude: 1, longitude: 2 }), + machineid: 'machine-1', + machineroom: 'room-install-1', + deviceLogin: Object.freeze({ enabled: false as const }), + loginPlayerId: Object.freeze({ enabled: false as const }), +}); + +function runtimeConfig(gameid: string | number = '41'): RuntimeConfig { + return Object.freeze({ + startupConfig: STARTUP_CONFIG, mode: 'debug', + isDebugger: true, + identity: Object.freeze({ + agentid: 'veRa0qrBf', + channelid: 'FtJf073aa', + gameid, + marketid: 4, + version: 10000, + }), + servers: Object.freeze(['ws://agent']), + source: 'remote', + gameserver: 'https://fixture/config', + ...remoteFixture(), + }) as RuntimeConfig; +} + +interface Deferred { + readonly promise: Promise; + resolve(value: T): void; + reject(error: unknown): void; +} + +function deferred(): Deferred { + let resolvePromise!: (value: T) => void; + let rejectPromise!: (error: unknown) => void; + const promise = new Promise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + return { promise, resolve: resolvePromise, reject: rejectPromise }; +} + +function rejectedValue(promise: Promise): Promise { + return promise.then( + () => { throw new Error('Expected promise to reject'); }, + (error: unknown) => error, + ); +} + +function thrownValue(operation: () => void): unknown { + let didThrow = false; + let thrown: unknown; + try { + operation(); + } catch (error) { + didThrow = true; + thrown = error; + } + assert.equal(didThrow, true, 'Expected operation to throw'); + return thrown; +} + +class ManualClock implements Clock { + private nextHandle = 1; + readonly timers = new Map void; readonly ms: number }>(); + + setTimeout(fn: () => void, ms: number): unknown { + const handle = this.nextHandle; + this.nextHandle += 1; + this.timers.set(handle, { fn, ms }); + return handle; + } + + clearTimeout(handle: unknown): void { + this.timers.delete(handle as number); + } + + runOnlyTimer(): void { + assert.equal(this.timers.size, 1); + const [handle, timer] = [...this.timers.entries()][0]!; + this.timers.delete(handle); + timer.fn(); + } +} + +class ControlledClearClock extends ManualClock { + private failureEnabled = false; + private failure: unknown; + onFailingClear: () => void = () => {}; + + enableClearFailure(error: unknown): void { + this.failure = error; + this.failureEnabled = true; + } + + clearTimeout(handle: unknown): void { + super.clearTimeout(handle); + if (!this.failureEnabled) return; + this.onFailingClear(); + throw this.failure; + } +} + +class RecordingWireClient implements PlatformWireClient { + readonly sent: OutboundEnvelope[] = []; + readonly switchCalls: string[] = []; + readonly lifecycle: string[] = []; + reconnectCalls = 0; + stopCalls = 0; + onAction: (name: string) => void = () => {}; + private readonly listeners = new Set<(event: WireEvent) => void>(); + + subscribe(listener: (event: WireEvent) => void): () => void { + this.lifecycle.push('subscribe'); + this.onAction('subscribe'); + this.listeners.add(listener); + return () => { + this.lifecycle.push('unsubscribe'); + this.onAction('unsubscribe'); + this.listeners.delete(listener); + }; + } + + start(): void { + this.lifecycle.push('start'); + this.onAction('start'); + } + + send(envelope: OutboundEnvelope): void { + this.sent.push(envelope); + } + + switchServer(target: string): void { + this.switchCalls.push(target); + } + + reconnectCurrent(): void { + this.reconnectCalls += 1; + } + + stop(): void { + this.stopCalls += 1; + this.lifecycle.push('stop'); + this.onAction('stop'); + } + + emit(event: WireEvent): void { + for (const listener of [...this.listeners]) listener(event); + } +} + +class SynchronousSubscribeWireClient implements PlatformWireClient { + readonly sent: OutboundEnvelope[] = []; + readonly switchCalls: string[] = []; + startCalls = 0; + stopCalls = 0; + unsubscribeCalls = 0; + private listener: ((event: WireEvent) => void) | null = null; + + constructor( + private readonly subscribeEvent: WireEvent, + private readonly unsubscribeFailure?: { readonly value: unknown }, + ) {} + + subscribe(listener: (event: WireEvent) => void): () => void { + this.listener = listener; + listener(this.subscribeEvent); + return () => { + this.unsubscribeCalls += 1; + this.listener = null; + if (this.unsubscribeFailure !== undefined) throw this.unsubscribeFailure.value; + }; + } + + start(): void { + this.startCalls += 1; + } + + send(envelope: OutboundEnvelope): void { + this.sent.push(envelope); + } + + switchServer(target: string): void { + this.switchCalls.push(target); + } + + reconnectCurrent(): void {} + + stop(): void { + this.stopCalls += 1; + } + + emit(event: WireEvent): void { + this.listener?.(event); + } + + get listenerAttached(): boolean { + return this.listener !== null; + } +} + +class InvalidSubscribeWireClient extends RecordingWireClient { + constructor(private readonly invalidUnsubscribe: unknown) { + super(); + } + + override subscribe(_listener: (event: WireEvent) => void): () => void { + this.lifecycle.push('subscribe'); + this.onAction('subscribe'); + return this.invalidUnsubscribe as () => void; + } +} + +class RecordingScene implements ScenePort { + readonly calls: Array<{ readonly name: string; readonly value?: unknown }> = []; + onCall: (name: string, value?: unknown) => void = () => {}; + private record(name: string, value?: unknown): void { + this.calls.push(value === undefined ? { name } : { name, value }); + this.onCall(name, value); + } + showLoading(): void { this.record('loading'); } + showLogin(): void { this.record('login'); } + showLobby(): void { this.record('lobby'); } + showRoom(): void { this.record('room'); } + showReconnect(): void { this.record('reconnect'); } + showKicked(data: unknown): void { this.record('kicked', data); } + showTips(message:string,time:number):void { this.record('tips',{message,time}); } + showServerDenial(outcome: unknown): void { this.record('denied', outcome); } + showFatal(error: Error): void { this.record('fatal', error); } +} + +function makeGameEntry(overrides: Partial = {}): GameEntry { + return { + key: 'fixture', + gameId: '41', + route: 'fixture-game', + resolveSeatCount: () => 4, + createModule: (): GameModule => ({ + attach() {}, + handlePlatformEvent() {}, + handleGameMessage() {}, + restore() {}, + dispose() {}, + }), + ...overrides, + }; +} + +function makeRuntime(config: RuntimeConfig = runtimeConfig(), entry = makeGameEntry(), device:LoginDeviceSnapshot=DEVICE) { + const wire = new RecordingWireClient(); + const scene = new RecordingScene(); + const clock = new ManualClock(); + const factoryConfigs: RuntimeConfig[] = []; + const runtime = new PlatformRuntime({ + gameEntry: entry, + resolveRuntimeConfig: async () => config, + createWireClient(resolved) { + factoryConfigs.push(resolved); + return wire; + }, + scene, + loadResources: async () => {}, + waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => device, + clock, + }); + return { runtime, wire, scene, clock, factoryConfigs, config }; +} + +test('invite identity follows authorization for non-device login and server identity for device login',async()=>{ + for(const deviceLogin of [{enabled:false},{enabled:true,telphone:'13800138000'}] as const){ + const {runtime,wire}=makeRuntime(runtimeConfig(),makeGameEntry(),{...DEVICE,deviceLogin}); + await runtime.start();wire.emit({type:'open',server:'ws://agent'}); + runtime.login({...ACCOUNT,openid:'authorized-openid',unionid:12345}); + const data=fixture('player-login-success.json'); + if(!deviceLogin.enabled){delete data.openid;delete data.unionid;} + wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data}}); + runtime.getPlayerInvitecode(); + assert.deepEqual(wire.sent[1].data,{agentid:'veRa0qrBf',playerid:430511, + openid:deviceLogin.enabled?'o1':'authorized-openid',unionid:deviceLogin.enabled?'test_unionid':12345}); + wire.emit({type:'slow'});wire.emit({type:'open',server:'ws://agent'}); + wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data}}); + runtime.getPlayerInvitecode();assert.deepEqual(wire.sent[wire.sent.length-1].data,wire.sent[1].data); + runtime.stop(); + } }); -const DEVICE = Object.freeze({ - ip: '127.0.0.1', - location: Object.freeze({ latitude: 1, longitude: 2 }), - machineid: 'machine-1', - machineroom: 'room-install-1', - deviceLogin: Object.freeze({ enabled: false as const }), - loginPlayerId: Object.freeze({ enabled: false as const }), +test('binding reply numeric and string states show server feedback without closing the wire',async()=>{ + for(const state of [0,'0',1,'1',-1,'-1']){ + const {runtime,wire,scene}=makeRuntime();await runtime.start(); + wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT); + wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data:fixture('player-login-success.json')}}); + runtime.bindInvitecode('00123'); + const success=state===0||state==='0',error=success?'绑定成功':'邀请码无效'; + wire.emit({type:'message',message:{route:'agent',rpc:'binding_invitecode',data:{state,error,...(success?{invitecode:'00123'}:{})}}}); + assert.equal(wire.stopCalls,0,`state=${JSON.stringify(state)} must not disconnect`); + assert.equal(runtime.state.app.phase,'logged-in'); + assert.equal(runtime.state.players.entities[430511].invitecode,success?'00123':null); + assert.ok(scene.calls.some(event=>event.name==='tips'&&JSON.stringify(event.value)===JSON.stringify({message:error,time:3000}))); + assert.equal(scene.calls.some(event=>event.name==='fatal'),false); + runtime.getPlayerInvitecode();assert.equal(wire.sent[wire.sent.length-1].rpc,'get_player_invitecode'); + runtime.stop(); + } }); -function runtimeConfig(gameid: string | number = '41'): RuntimeConfig { - return Object.freeze({ - startupConfig: STARTUP_CONFIG, mode: 'debug', - isDebugger: true, - identity: Object.freeze({ - agentid: 'veRa0qrBf', - channelid: 'FtJf073aa', - gameid, - marketid: 4, - version: 10000, - }), - servers: Object.freeze(['ws://agent']), - source: 'remote', - gameserver: 'https://fixture/config', - ...remoteFixture(), - }) as RuntimeConfig; -} - -interface Deferred { - readonly promise: Promise; - resolve(value: T): void; - reject(error: unknown): void; -} - -function deferred(): Deferred { - let resolvePromise!: (value: T) => void; - let rejectPromise!: (error: unknown) => void; - const promise = new Promise((resolve, reject) => { - resolvePromise = resolve; - rejectPromise = reject; - }); - return { promise, resolve: resolvePromise, reject: rejectPromise }; -} - -function rejectedValue(promise: Promise): Promise { - return promise.then( - () => { throw new Error('Expected promise to reject'); }, - (error: unknown) => error, - ); -} - -function thrownValue(operation: () => void): unknown { - let didThrow = false; - let thrown: unknown; - try { - operation(); - } catch (error) { - didThrow = true; - thrown = error; - } - assert.equal(didThrow, true, 'Expected operation to throw'); - return thrown; -} - -class ManualClock implements Clock { - private nextHandle = 1; - readonly timers = new Map void; readonly ms: number }>(); - - setTimeout(fn: () => void, ms: number): unknown { - const handle = this.nextHandle; - this.nextHandle += 1; - this.timers.set(handle, { fn, ms }); - return handle; - } - - clearTimeout(handle: unknown): void { - this.timers.delete(handle as number); - } - - runOnlyTimer(): void { - assert.equal(this.timers.size, 1); - const [handle, timer] = [...this.timers.entries()][0]!; - this.timers.delete(handle); - timer.fn(); - } -} - -class ControlledClearClock extends ManualClock { - private failureEnabled = false; - private failure: unknown; - onFailingClear: () => void = () => {}; - - enableClearFailure(error: unknown): void { - this.failure = error; - this.failureEnabled = true; - } - - clearTimeout(handle: unknown): void { - super.clearTimeout(handle); - if (!this.failureEnabled) return; - this.onFailingClear(); - throw this.failure; - } -} - -class RecordingWireClient implements PlatformWireClient { - readonly sent: OutboundEnvelope[] = []; - readonly switchCalls: string[] = []; - readonly lifecycle: string[] = []; - reconnectCalls = 0; - stopCalls = 0; - onAction: (name: string) => void = () => {}; - private readonly listeners = new Set<(event: WireEvent) => void>(); - - subscribe(listener: (event: WireEvent) => void): () => void { - this.lifecycle.push('subscribe'); - this.onAction('subscribe'); - this.listeners.add(listener); - return () => { - this.lifecycle.push('unsubscribe'); - this.onAction('unsubscribe'); - this.listeners.delete(listener); - }; - } - - start(): void { - this.lifecycle.push('start'); - this.onAction('start'); - } - - send(envelope: OutboundEnvelope): void { - this.sent.push(envelope); - } - - switchServer(target: string): void { - this.switchCalls.push(target); - } - - reconnectCurrent(): void { - this.reconnectCalls += 1; - } - - stop(): void { - this.stopCalls += 1; - this.lifecycle.push('stop'); - this.onAction('stop'); - } - - emit(event: WireEvent): void { - for (const listener of [...this.listeners]) listener(event); - } -} - -class SynchronousSubscribeWireClient implements PlatformWireClient { - readonly sent: OutboundEnvelope[] = []; - readonly switchCalls: string[] = []; - startCalls = 0; - stopCalls = 0; - unsubscribeCalls = 0; - private listener: ((event: WireEvent) => void) | null = null; - - constructor( - private readonly subscribeEvent: WireEvent, - private readonly unsubscribeFailure?: { readonly value: unknown }, - ) {} - - subscribe(listener: (event: WireEvent) => void): () => void { - this.listener = listener; - listener(this.subscribeEvent); - return () => { - this.unsubscribeCalls += 1; - this.listener = null; - if (this.unsubscribeFailure !== undefined) throw this.unsubscribeFailure.value; - }; - } - - start(): void { - this.startCalls += 1; - } - - send(envelope: OutboundEnvelope): void { - this.sent.push(envelope); - } - - switchServer(target: string): void { - this.switchCalls.push(target); - } - - reconnectCurrent(): void {} - - stop(): void { - this.stopCalls += 1; - } - - emit(event: WireEvent): void { - this.listener?.(event); - } - - get listenerAttached(): boolean { - return this.listener !== null; - } -} - -class InvalidSubscribeWireClient extends RecordingWireClient { - constructor(private readonly invalidUnsubscribe: unknown) { - super(); - } - - override subscribe(_listener: (event: WireEvent) => void): () => void { - this.lifecycle.push('subscribe'); - this.onAction('subscribe'); - return this.invalidUnsubscribe as () => void; - } -} - -class RecordingScene implements ScenePort { - readonly calls: Array<{ readonly name: string; readonly value?: unknown }> = []; - onCall: (name: string, value?: unknown) => void = () => {}; - private record(name: string, value?: unknown): void { - this.calls.push(value === undefined ? { name } : { name, value }); - this.onCall(name, value); - } - showLoading(): void { this.record('loading'); } - showLogin(): void { this.record('login'); } - showLobby(): void { this.record('lobby'); } - showRoom(): void { this.record('room'); } - showReconnect(): void { this.record('reconnect'); } - showKicked(data: unknown): void { this.record('kicked', data); } - showTips(message:string,time:number):void { this.record('tips',{message,time}); } - showServerDenial(outcome: unknown): void { this.record('denied', outcome); } - showFatal(error: Error): void { this.record('fatal', error); } -} - -function makeGameEntry(overrides: Partial = {}): GameEntry { - return { - key: 'fixture', - gameId: '41', - route: 'fixture-game', - resolveSeatCount: () => 4, - createModule: (): GameModule => ({ - attach() {}, - handlePlatformEvent() {}, - handleGameMessage() {}, - restore() {}, - dispose() {}, - }), - ...overrides, - }; -} - -function makeRuntime(config: RuntimeConfig = runtimeConfig(), entry = makeGameEntry()) { - const wire = new RecordingWireClient(); - const scene = new RecordingScene(); - const clock = new ManualClock(); - const factoryConfigs: RuntimeConfig[] = []; - const runtime = new PlatformRuntime({ - gameEntry: entry, - resolveRuntimeConfig: async () => config, - createWireClient(resolved) { - factoryConfigs.push(resolved); - return wire; - }, - scene, - loadResources: async () => {}, - waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, - clock, - }); - return { runtime, wire, scene, clock, factoryConfigs, config }; -} - test('VIP wire query response reaches canonical state and clears on login replacement',async()=>{ - const {runtime,wire}=makeRuntime();await runtime.start(); - wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT); - const login=()=>wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data:fixture('player-login-success.json')}}); - login();runtime.getVipRooms(['123456']); - wire.emit({type:'message',message:{route:'agent',rpc:'getInfoByShortCode',data:{roomInfo:[{shortCode:123456,owner:'房主',playerList:[]}]}}}); - assert.equal(runtime.state.vipRooms?.[0].shortCode,123456); - wire.emit({type:'message',message:{route:'agent',rpc:'getInfoByShortCode',data:{roomInfo:[]}}}); - assert.deepEqual(runtime.state.vipRooms,[]); - login();assert.equal(runtime.state.vipRooms,undefined);runtime.stop(); -}); + const {runtime,wire}=makeRuntime();await runtime.start(); + wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT); + const login=()=>wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data:fixture('player-login-success.json')}}); + login();runtime.getVipRooms(['123456']); + wire.emit({type:'message',message:{route:'agent',rpc:'getInfoByShortCode',data:{roomInfo:[{shortCode:123456,owner:'房主',playerList:[]}]}}}); + assert.equal(runtime.state.vipRooms?.[0].shortCode,123456); + wire.emit({type:'message',message:{route:'agent',rpc:'getInfoByShortCode',data:{roomInfo:[]}}}); + assert.deepEqual(runtime.state.vipRooms,[]); + login();assert.equal(runtime.state.vipRooms,undefined);runtime.stop(); +}); + +test('task wire responses reach the store and claim does not invent reward balances',async()=>{ + const {runtime,wire}=makeRuntime();await runtime.start(); + wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT); + wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data:fixture('player-login-success.json')}}); + runtime.getPlayerTasks(); + wire.emit({type:'message',message:{route:'agent',rpc:'get_player_task',data:{tasks:[{taskid:7,title:'任务',award:'3',finish:'1',total:'1',state:'1'}]}}}); + assert.equal(runtime.state.tasks?.[0].award,3); + const balance=runtime.state.players.entities[430511].roomcard; + runtime.claimTask(7); + wire.emit({type:'message',message:{route:'agent',rpc:'get_task_award',data:{taskid:'7',taskstate:0}}}); + assert.equal(runtime.state.tasks?.[0].state,2); + assert.equal(runtime.state.players.entities[430511].taskstate,0); + assert.equal(runtime.state.players.entities[430511].roomcard,balance); + wire.emit({type:'message',message:{route:'agent',rpc:'update_roomcard',data:{roomcard:25}}}); + assert.equal(runtime.state.players.entities[430511].roomcard,25); + wire.emit({type:'message',message:{route:'agent',rpc:'player_finish_task',data:{state:1}}}); + assert.equal(runtime.state.players.entities[430511].taskstate,1); + wire.emit({type:'slow'});assert.throws(()=>runtime.getPlayerTasks(),/logged-in/); + runtime.stop(); +}); + +test('UI observes canonical commits and unsubscribe is idempotent', async () => { + const { runtime, wire } = makeRuntime(); + const seen: unknown[] = []; + const off = runtime.subscribeState( + state => { seen.push(state); }, + error => { throw error; }, + ); + assert.equal(seen.length, 0); + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + assert.equal(seen[seen.length - 1], runtime.state); + off(); + off(); + const count = seen.length; + runtime.login(ACCOUNT); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), + } }); + assert.equal(seen.length, count); + runtime.stop(); + assert.throws(() => runtime.subscribeState(() => {}, () => {}), /stopped/); +}); + +test('a failing UI observer reports its original error once while other observers continue', async () => { + const { runtime, wire } = makeRuntime(); + const failure = { source: 'render' }; + const reported: unknown[] = []; + let failingCalls = 0; + const healthyStates: unknown[] = []; + runtime.subscribeState( + () => { + failingCalls += 1; + throw failure; + }, + error => { reported.push(error); }, + ); + runtime.subscribeState( + state => { healthyStates.push(state); }, + error => { throw error; }, + ); + + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + + assert.equal(failingCalls, 1); + assert.deepEqual(reported, [failure]); + assert.equal(reported[0], failure); + assert.equal(healthyStates[0], runtime.state); + + runtime.login(ACCOUNT); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), + } }); + assert.equal(failingCalls, 1); + assert.equal(healthyStates[healthyStates.length - 1], runtime.state); + runtime.stop(); +}); + +test('an onError failure enters fatal with both UI error identities and cancels observers', async () => { + const { runtime, wire, scene } = makeRuntime(); + const listenerFailure = { source: 'listener' }; + const onErrorFailure = { source: 'onError' }; + let laterObserverCalls = 0; + runtime.subscribeState( + () => { throw listenerFailure; }, + () => { throw onErrorFailure; }, + ); + runtime.subscribeState( + () => { laterObserverCalls += 1; }, + error => { throw error; }, + ); + await runtime.start(); + + wire.emit({ type: 'open', server: 'ws://agent' }); + + assert.equal(laterObserverCalls, 0); + assert.equal(wire.stopCalls, 1); + const fatal = scene.calls.find(call => call.name === 'fatal')?.value; + assert.ok(fatal instanceof Error); + const errors = (fatal as Error & { readonly errors: readonly unknown[] }).errors; + assert.equal(errors.length, 2); + assert.equal(errors[0], listenerFailure); + assert.equal(errors[1], onErrorFailure); + assert.throws(() => runtime.subscribeState(() => {}, () => {}), /fatal/); +}); + +for (const terminal of ['stop', 'fatal', 'kick'] as const) { + test(`${terminal} invalidates a late Store delivery and rejects new UI observers`, async () => { + const originalSubscribe = PlatformStore.prototype.subscribe; + let retainedListener: Parameters[0] | null = null; + PlatformStore.prototype.subscribe = function retainFirstListener(listener) { + retainedListener ??= listener; + return originalSubscribe.call(this, listener); + }; + + try { + const setup = makeRuntime(); + let calls = 0; + setup.runtime.subscribeState( + () => { calls += 1; }, + error => { throw error; }, + ); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + const callsBeforeTermination = calls; + setup.wire.onAction = (name) => { + if (name !== 'stop') return; + const listener = retainedListener; + assert.ok(listener); + listener(setup.runtime.state, setup.runtime.state); + }; + + if (terminal === 'stop') { + setup.runtime.stop(); + } else if (terminal === 'fatal') { + setup.scene.onCall = (name) => { + if (name === 'reconnect') throw new Error('reconnect render failed'); + }; + setup.wire.emit({ type: 'slow' }); + } else { + setup.runtime.login(ACCOUNT); + setup.wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'kick_server', data: { code: 1, msg: 'terminal' }, + } }); + } + + assert.equal( + calls, + callsBeforeTermination + (terminal === 'fatal' ? 1 : 0), + 'observers must be inactive before terminal cleanup callbacks', + ); + const callsAtTermination = calls; + const lateListener = retainedListener as Parameters[0] | null; + assert.ok(lateListener); + lateListener(setup.runtime.state, setup.runtime.state); + assert.equal(calls, callsAtTermination); + assert.throws( + () => setup.runtime.subscribeState(() => {}, () => {}), + new RegExp(terminal === 'kick' ? 'kicked' : terminal), + ); + } finally { + PlatformStore.prototype.subscribe = originalSubscribe; + } + }); +} + +test('stop during a Store commit cancels later observers and prevents scene revival', async () => { + const setup = makeRuntime(); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + let laterObserverCalls = 0; + let resubscribeFailure: unknown; + setup.runtime.subscribeState( + () => { + setup.runtime.stop(); + resubscribeFailure = thrownValue( + () => setup.runtime.subscribeState(() => {}, () => {}), + ); + }, + error => { throw error; }, + ); + setup.runtime.subscribeState( + () => { laterObserverCalls += 1; }, + error => { throw error; }, + ); + const sceneCallsBefore = setup.scene.calls.length; + + setup.wire.emit({ type: 'slow' }); + + assert.equal(laterObserverCalls, 0); + assert.match(String(resubscribeFailure), /stopped/); + assert.equal(setup.scene.calls.length, sceneCallsBefore); + assert.equal(setup.wire.stopCalls, 1); +}); + +test('listener and onError failures after stopping escape the Store boundary without reviving fatal', async () => { + const setup = makeRuntime(); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + const listenerFailure = { source: 'listener-after-stop' }; + const onErrorFailure = { source: 'onError-after-stop' }; + const reported: unknown[] = []; + setup.runtime.subscribeState( + () => { + setup.runtime.stop(); + throw listenerFailure; + }, + error => { + reported.push(error); + setup.runtime.stop(); + throw onErrorFailure; + }, + ); + const sceneCallsBefore = setup.scene.calls.length; + + const thrown = thrownValue(() => { setup.wire.emit({ type: 'slow' }); }); + + assert.deepEqual(reported, [listenerFailure]); + assert.ok(thrown instanceof Error); + const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; + assert.equal(errors.length, 2); + assert.equal(errors[0], listenerFailure); + assert.equal(errors[1], onErrorFailure); + assert.equal(setup.scene.calls.length, sceneCallsBefore); + assert.equal(setup.scene.calls.filter(call => call.name === 'fatal').length, 0); + assert.equal(setup.wire.stopCalls, 1); + assert.throws(() => setup.runtime.subscribeState(() => {}, () => {}), /stopped/); +}); + +test('stopped observer errors survive a later active-room publish failure', async () => { + const setup = makeRuntime(); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + const roomLogin = fixture('player-login-room.json'); + (roomLogin.players as unknown[])[2] = { + playerid: 430512, + nickname: '同桌玩家', + avatar: 'http://b', + sex: 0, + bean: 10, + isprepare: 0, + onstate: 0, + }; + setup.wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: roomLogin, + } }); + const listenerFailure = { source: 'room-listener-after-stop' }; + const onErrorFailure = { source: 'room-onError-after-stop' }; + setup.runtime.subscribeState( + () => { + setup.runtime.stop(); + throw listenerFailure; + }, + () => { throw onErrorFailure; }, + ); + + const thrown = thrownValue(() => { + setup.wire.emit({ type: 'message', message: { + route: 'room', rpc: 'player_prepare', data: { seat: 2 }, + } }); + }); + + assert.ok(thrown instanceof Error); + const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; + assert.equal(errors.length, 2); + const observerFailure = errors[0]; + assert.ok(observerFailure instanceof Error); + const observerErrors = ( + observerFailure as Error & { readonly errors: readonly unknown[] } + ).errors; + assert.equal(observerErrors[0], listenerFailure); + assert.equal(observerErrors[1], onErrorFailure); + assert.match(String(errors[1]), /disposed game session/i); + assert.equal(setup.scene.calls.filter(call => call.name === 'fatal').length, 0); + assert.throws(() => setup.runtime.subscribeState(() => {}, () => {}), /stopped/); +}); + +test('UI observers receive each committed snapshot even when a listener commits reentrantly', async () => { + const setup = makeRuntime(); + const delivered: unknown[] = []; + let firstCommit: unknown; + let latestDuringFirstCommit: unknown; + let reentered = false; + setup.runtime.subscribeState( + state => { + if (reentered) return; + reentered = true; + firstCommit = state; + setup.wire.emit({ type: 'slow' }); + latestDuringFirstCommit = setup.runtime.state; + }, + error => { throw error; }, + ); + setup.runtime.subscribeState( + state => { delivered.push(state); }, + error => { throw error; }, + ); + await setup.runtime.start(); + + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + + assert.notEqual(firstCommit, latestDuringFirstCommit); + assert.equal(delivered[0], firstCommit); + assert.equal(delivered[1], latestDuringFirstCommit); + assert.deepEqual( + delivered.map(state => (state as { app: { phase: string } }).app.phase), + ['connected', 'slow'], + ); + setup.runtime.stop(); +}); + +test('cancelling and subscribing observers during a commit cannot alter that delivery snapshot', async () => { + const setup = makeRuntime(); + let cancelledCalls = 0; + const replacementPhases: string[] = []; + let changedObservers = false; + let offCancelled = (): void => {}; + setup.runtime.subscribeState( + () => { + if (changedObservers) return; + changedObservers = true; + offCancelled(); + setup.runtime.subscribeState( + state => { replacementPhases.push(state.app.phase); }, + error => { throw error; }, + ); + }, + error => { throw error; }, + ); + offCancelled = setup.runtime.subscribeState( + () => { cancelledCalls += 1; }, + error => { throw error; }, + ); + await setup.runtime.start(); + + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + assert.equal(cancelledCalls, 0); + assert.deepEqual(replacementPhases, []); + + setup.wire.emit({ type: 'slow' }); + assert.equal(cancelledCalls, 0); + assert.deepEqual(replacementPhases, ['slow']); + setup.runtime.stop(); +}); + +test('a denied join retains the lobby root, reports the raw outcome and permits a valid retry', async () => { + const { runtime, wire, scene } = makeRuntime(); + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + runtime.login(ACCOUNT); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), + } }); + const lobby = runtime.state; + const denial = { state: 99, roomcode: '999999', showerror: 1, error: 'Room does not exist' }; + runtime.joinRoom({ roomcode: '999999', location: null, ip: '127.0.0.1' }); + wire.emit({ type: 'message', message: { route: 'agent', rpc: 'self_join_room', data: denial } }); + + assert.equal(runtime.state, lobby); + assert.equal(wire.stopCalls, 0); + assert.deepEqual(scene.calls.filter(call => call.name === 'fatal'), []); + const outcome = scene.calls.find(call => call.name === 'denied')?.value; + assert.deepEqual(outcome, { rpc: 'self_join_room', state: 99, data: denial }); + assert.equal((outcome as { data: unknown }).data, denial); + + runtime.joinRoom({ roomcode: '100001', location: null, ip: '127.0.0.1' }); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'self_join_room', data: fixture('self-join-room.json'), + } }); + assert.equal(runtime.state.room.kind, 'inside'); + assert.equal(scene.calls[scene.calls.length - 1]?.name, 'room'); + runtime.stop(); +}); + +test('runtime resolves seats only from the actual game-owned server roomtype at room entry', async () => { + const roomtypes: Array = []; + const attached: GameHost[] = []; + const setup = makeRuntime(runtimeConfig(), makeGameEntry({ + resolveSeatCount(roomtype) { + roomtypes.push(roomtype); + if (roomtype.length !== 7 || roomtype[1] !== 4 || !Array.isArray(roomtype[5])) { + throw new Error('Game roomtype requires the full nested server configuration'); + } + return 4; + }, + createModule: () => ({ + attach(host) { attached.push(host); }, + handlePlatformEvent() {}, handleGameMessage() {}, restore() {}, dispose() {}, + }), + })); + await setup.runtime.start(); + assert.deepEqual(roomtypes, []); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + } }); + assert.equal(roomtypes.length, 1); + assert.equal(attached.length, 1); + assert.equal(roomtypes[0], attached[0]!.getSnapshot().room.roomtype); + assert.equal(attached[0]!.seat.toView(1), 0); + assert.deepEqual(setup.scene.calls.filter(call => call.name === 'fatal'), []); + setup.runtime.stop(); +}); + +for (const count of [0, 2.5, 2]) { + test(`runtime rejects invalid game count ${count} at the real room boundary`, async () => { + let attaches = 0; + const setup = makeRuntime(runtimeConfig(), makeGameEntry({ + resolveSeatCount: () => count, + createModule: () => ({ + attach() { attaches += 1; }, + handlePlatformEvent() {}, handleGameMessage() {}, restore() {}, dispose() {}, + }), + })); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + } }); + assert.equal(attaches, 0); + assert.equal(setup.wire.stopCalls, 1); + const failure = setup.scene.calls.find(call => call.name === 'fatal')?.value; + assert.match(String((failure as { errors: unknown[] }).errors[0]), /seat count/); + setup.runtime.stop(); + }); +} + +for(const loginRoute of ['agent','room','platform'])test(`${loginRoute} login denial clears its guard, retains state and permits account retry`, async () => { + const { runtime, wire, scene, clock } = makeRuntime(); + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + const before = runtime.state; + runtime.login(ACCOUNT); + const denial = { state: -1, msg: '短信验证码不正确', time: 0 }; + assert.equal(wire.sent.at(-1)?.route,'agent','outbound login remains agent'); + wire.emit({ type: 'message', message: { route: loginRoute, rpc: 'player_login', data: denial } }); + + assert.equal(runtime.state, before); + assert.equal(wire.stopCalls, 0); + assert.equal(clock.timers.size, 0); + const outcome = scene.calls.find(call => call.name === 'denied')?.value; + assert.deepEqual(outcome, { rpc: 'player_login', state: -1, data: denial }); + assert.equal((outcome as { data: unknown }).data, denial); + assert.deepEqual(scene.calls.filter(call => call.name === 'fatal'), []); + const sentBeforeReconnect=wire.sent.length; + wire.emit({type:'reconnecting',server:'ws://agent'}); + wire.emit({type:'open',server:'ws://agent'}); + assert.equal(wire.sent.length,sentBeforeReconnect,'a denied first login must not be replayed'); + runtime.login({ ...ACCOUNT, openid: 'retry-account' }); + assert.equal((wire.sent[wire.sent.length - 1]!.data as Record).openid, 'retry-account'); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), + } }); + assert.equal(runtime.state.app.phase, 'logged-in'); + runtime.stop(); +}); + +for (const interruption of ['attach', 'restore', 'room-scene', 'room-event'] as const) { + test(`stop from ${interruption} releases the actual module and host without reviving runtime`, async () => { + let runtime: PlatformRuntime; + let attachedHost: GameHost | undefined; + let moduleNumber = 0; + const disposed: number[] = []; + const events: string[] = []; + const entry = makeGameEntry({ + createModule() { + const id = moduleNumber++; + return { + attach(host) { + attachedHost = host; + host.subscribe(() => { events.push('snapshot'); }); + if (interruption === 'attach') runtime.stop(); + }, + handlePlatformEvent() { + events.push('room-event'); + if (interruption === 'room-event') runtime.stop(); + }, + handleGameMessage() { events.push('game-message'); }, + restore() { + events.push('restore'); + if (interruption === 'restore') runtime.stop(); + }, + dispose() { disposed.push(id); }, + }; + }, + }); + const setup = makeRuntime(runtimeConfig(), entry); + runtime = setup.runtime; + setup.scene.onCall = name => { + if (name === 'room' && interruption === 'room-scene') runtime.stop(); + }; + await runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + runtime.login(ACCOUNT); + setup.wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + } }); + + assert.deepEqual(disposed, [0, 1], 'validation and actual room modules each dispose once'); + assert.ok(attachedHost); + const stoppedHost = attachedHost; + assert.throws(() => stoppedHost.getSnapshot(), /disposed/); + assert.throws(() => stoppedHost.subscribe(() => {}), /disposed/); + assert.equal(setup.clock.timers.size, 0); + assert.deepEqual(setup.scene.calls.filter(call => call.name === 'fatal'), []); + assert.equal(setup.wire.stopCalls, 1); + assert.deepEqual(setup.wire.lifecycle, ['subscribe', 'start', 'unsubscribe', 'stop']); + if (interruption === 'attach') assert.deepEqual(events, ['snapshot']); + if (interruption === 'room-event' || interruption === 'room-scene') { + assert.deepEqual(events, ['snapshot', 'room-event']); + } + const stoppedState = runtime.state; + const stoppedEvents = [...events]; + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.wire.emit({ type: 'message', message: { + route: 'fixture-game', rpc: 'late', data: {}, + } }); + assert.equal(runtime.state, stoppedState); + assert.deepEqual(events, stoppedEvents); + assert.throws(() => runtime.login(ACCOUNT), /stopped/); + await assert.rejects(runtime.start(), /stopped/); + runtime.stop(); + assert.deepEqual(disposed, [0, 1]); + assert.equal(setup.wire.stopCalls, 1); + }); +} + +test('stop reentered by an old module disposal prevents replacement room activation', async () => { + let runtime: PlatformRuntime; + const attached: GameHost[] = []; + let disposals = 0; + const setup = makeRuntime(runtimeConfig(), makeGameEntry({ + createModule() { + let host: GameHost | undefined; + return { + attach(value) { host = value; attached.push(value); }, + handlePlatformEvent() {}, handleGameMessage() {}, restore() {}, + dispose() { + if (host === undefined) return; + disposals += 1; + runtime.stop(); + }, + }; + }, + })); + runtime = setup.runtime; + await runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + } }); + setup.wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + } }); + assert.equal(disposals, 1); + assert.equal(attached.length, 1); + assert.throws(() => attached[0]!.getSnapshot(), /disposed/); + assert.deepEqual(setup.scene.calls.filter(call => call.name === 'fatal'), []); + assert.equal(setup.wire.stopCalls, 1); + assert.throws(() => runtime.login(ACCOUNT), /stopped/); + runtime.stop(); +}); + +test('an actual callback error after stop remains explicit without changing the stopped lifecycle', async () => { + let runtime: PlatformRuntime; + const failure = new Error('restore callback failed after stop'); + const setup = makeRuntime(runtimeConfig(), makeGameEntry({ + createModule: () => ({ + attach() {}, handlePlatformEvent() {}, handleGameMessage() {}, dispose() {}, + restore() { runtime.stop(); throw failure; }, + }), + })); + runtime = setup.runtime; + await runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + const thrown = thrownValue(() => setup.wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + } })); + assert.deepEqual((thrown as { errors: unknown[] }).errors, [failure]); + assert.deepEqual(setup.scene.calls.filter(call => call.name === 'fatal'), []); + assert.throws(() => runtime.login(ACCOUNT), /stopped/); + runtime.stop(); + assert.equal(setup.wire.stopCalls, 1); +}); + +for (const rpc of ['player_login', 'self_join_room']) { + test(`malformed successful ${rpc} still terminates explicitly`, async () => { + const { runtime, wire, scene } = makeRuntime(); + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + wire.emit({ type: 'message', message: { route: 'agent', rpc, data: { state: 0 } } }); + assert.equal(wire.stopCalls, 1); + assert.equal(scene.calls.filter(call => call.name === 'fatal').length, 1); + assert.deepEqual(scene.calls.filter(call => call.name === 'denied'), []); + runtime.stop(); + }); +} + +function makeLoggingEntry(log: string[], restored: unknown[], attached: GameHost[]): GameEntry { + let recording = false; + const entry = makeGameEntry({ + createModule: (): GameModule => { + let host: GameHost | null = null; + return { + attach(value) { + host = value; + if (!recording) return; + attached.push(value); + log.push(`attach:${value.getSnapshot().room.roomcode}`); + }, + handlePlatformEvent(event: PlatformToGameEvent) { + if (recording) log.push(`event:${event.type}`); + }, + handleGameMessage() {}, + restore(deskinfo) { + if (!recording) return; + assert.ok(host); + restored.push(deskinfo); + log.push('restore'); + }, + dispose() { + if (!recording) return; + assert.ok(host); + host.getSnapshot(); + log.push('dispose'); + }, + }; + }, + }); + Object.defineProperty(entry, 'beginRecording', { + value: () => { recording = true; }, + }); + return entry; +} + +function beginEntryRecording(entry: GameEntry): void { + (entry as GameEntry & { beginRecording(): void }).beginRecording(); +} + +interface CapturedGameHost { + host: GameHost | null; +} + +function makeCleanupFailureEntry( + log: string[], + disposeFailure: unknown, + captured: CapturedGameHost, +): GameEntry { + let moduleNumber = 0; + return makeGameEntry({ + createModule(): GameModule { + moduleNumber += 1; + const validationModule = moduleNumber === 1; + return { + attach(host) { + if (validationModule) return; + captured.host = host; + host.subscribe(() => {}); + }, + handlePlatformEvent() {}, + handleGameMessage() {}, + restore() {}, + dispose() { + if (validationModule) return; + log.push('game:dispose'); + throw disposeFailure; + }, + }; + }, + }); +} + +test('start resolves config, subscribes before connect, and never logs in without an account', async () => { + const setup = makeRuntime(); + + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + + assert.equal(setup.factoryConfigs.length, 1); + assert.equal(setup.factoryConfigs[0], setup.config); + assert.deepEqual(setup.wire.lifecycle, ['subscribe', 'start']); + assert.deepEqual(setup.wire.sent, []); + assert.equal(setup.runtime.ready, true); + assert.deepEqual(setup.scene.calls.map((call) => call.name), ['loading', 'login']); +}); + +test('showLoading failure enters terminal fatal before any startup dependency runs', async () => { + const primary = new Error('loading scene failed'); + const scene = new RecordingScene(); + const wire = new RecordingWireClient(); + let resourcesStarted = 0; + let minimumDisplayStarted = 0; + let configsResolved = 0; + let wireFactories = 0; + scene.onCall = (name) => { + if (name === 'loading') throw primary; + }; + const runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), + resolveRuntimeConfig: async () => { + configsResolved += 1; + return runtimeConfig(); + }, + createWireClient: () => { + wireFactories += 1; + return wire; + }, + scene, + loadResources: async () => { resourcesStarted += 1; }, + waitForMinimumDisplay: async () => { minimumDisplayStarted += 1; }, + getLoginDeviceSnapshot: () => DEVICE, + }); + + const thrown = await rejectedValue(runtime.start()); + + assert.equal(thrown, primary); + assert.deepEqual(scene.calls.map((call) => call.name), ['loading', 'fatal']); + assert.equal(scene.calls[1]?.value, primary); + assert.equal(resourcesStarted, 0); + assert.equal(minimumDisplayStarted, 0); + assert.equal(configsResolved, 0); + assert.equal(wireFactories, 0); + assert.deepEqual(wire.lifecycle, []); + assert.equal(runtime.ready, false); + + await assert.rejects(() => runtime.start(), /fatal/i); + assert.equal(scene.calls.filter((call) => call.name === 'fatal').length, 1); +}); + +test('showLoading and showFatal failures preserve both error identities without starting dependencies', async () => { + const primary = new Error('loading scene failed'); + const fatalSceneFailure = { step: 'scene.showFatal' }; + const scene = new RecordingScene(); + let dependencyCalls = 0; + scene.onCall = (name) => { + if (name === 'loading') throw primary; + if (name === 'fatal') throw fatalSceneFailure; + }; + const runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), + resolveRuntimeConfig: async () => { + dependencyCalls += 1; + return runtimeConfig(); + }, + createWireClient: () => { + dependencyCalls += 1; + return new RecordingWireClient(); + }, + scene, + loadResources: async () => { dependencyCalls += 1; }, + waitForMinimumDisplay: async () => { dependencyCalls += 1; }, + getLoginDeviceSnapshot: () => DEVICE, + }); + + const thrown = await rejectedValue(runtime.start()); + assert.ok(thrown instanceof Error); + const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; + + assert.equal(errors[0], primary); + assert.equal(errors[1], fatalSceneFailure); + assert.equal(dependencyCalls, 0); + assert.deepEqual(scene.calls.map((call) => call.name), ['loading', 'fatal']); + await assert.rejects(() => runtime.start(), /fatal/i); + assert.equal(scene.calls.filter((call) => call.name === 'fatal').length, 1); +}); + +test('stop while config is pending prevents every late startup continuation from reviving runtime', async () => { + const configReady = deferred(); + const resourcesReady = deferred(); + const minimumDisplayReady = deferred(); + const wire = new RecordingWireClient(); + const scene = new RecordingScene(); + let resourcesStarted = 0; + let minimumDisplayStarted = 0; + let wireFactories = 0; + const runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), + resolveRuntimeConfig: () => configReady.promise, + createWireClient() { + wireFactories += 1; + return wire; + }, + scene, + loadResources() { + resourcesStarted += 1; + return resourcesReady.promise; + }, + waitForMinimumDisplay() { + minimumDisplayStarted += 1; + return minimumDisplayReady.promise; + }, + getLoginDeviceSnapshot: () => DEVICE, + }); + + const starting = runtime.start(); + await Promise.resolve(); + assert.equal(resourcesStarted, 1); + assert.equal(minimumDisplayStarted, 1); + + runtime.stop(); + configReady.resolve(runtimeConfig()); + resourcesReady.resolve(undefined); + minimumDisplayReady.resolve(undefined); + + await assert.rejects(starting, /stopped/i); + await assert.rejects(() => runtime.start(), /stopped/i); + assert.equal(wireFactories, 0); + assert.deepEqual(wire.lifecycle, []); + assert.equal(runtime.ready, false); + assert.deepEqual(scene.calls.map((call) => call.name), ['loading']); +}); + +test('a synchronous subscribe callback that stops runtime hands the returned unsubscribe to teardown exactly once', async () => { + const unsubscribeFailure = { step: 'late-unsubscribe' }; + const wire = new SynchronousSubscribeWireClient( + { type: 'slow' }, + { value: unsubscribeFailure }, + ); + const scene = new RecordingScene(); + let runtime!: PlatformRuntime; + runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), + resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, + scene, + loadResources: async () => {}, + waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, + }); + scene.onCall = (name) => { + if (name === 'reconnect') runtime.stop(); + }; + + const thrown = await rejectedValue(runtime.start()); + assert.ok(thrown instanceof Error); + const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; + + assert.match(String(errors[0]), /stopped/i); + assert.equal(errors[1], unsubscribeFailure); + assert.equal(wire.unsubscribeCalls, 1); + assert.equal(wire.listenerAttached, false); + assert.equal(wire.stopCalls, 1); + assert.equal(wire.startCalls, 0); + + const sceneCallCount = scene.calls.length; + wire.emit({ type: 'open', server: 'ws://agent' }); + runtime.stop(); + assert.equal(scene.calls.length, sceneCallCount); + assert.equal(wire.unsubscribeCalls, 1); + assert.equal(wire.stopCalls, 1); +}); + +test('a fatal synchronous subscribe callback still adopts unsubscribe and ignores late events', async () => { + const primary = new Error('synchronous reconnect scene failed'); + const wire = new SynchronousSubscribeWireClient({ type: 'slow' }); + const scene = new RecordingScene(); + scene.onCall = (name) => { + if (name === 'reconnect') throw primary; + }; + const runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), + resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, + scene, + loadResources: async () => {}, + waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, + }); + + const thrown = await rejectedValue(runtime.start()); + + assert.equal(thrown, primary); + assert.equal(wire.unsubscribeCalls, 1); + assert.equal(wire.listenerAttached, false); + assert.equal(wire.stopCalls, 1); + assert.equal(wire.startCalls, 0); + assert.deepEqual( + scene.calls.filter((call) => call.name === 'fatal').map((call) => call.value), + [primary], + ); + + const sceneCallCount = scene.calls.length; + wire.emit({ type: 'reconnecting', server: 'ws://agent' }); + assert.equal(scene.calls.length, sceneCallCount); +}); + +for (const invalidUnsubscribe of [7, null, undefined] as const) { + test(`subscribe returning ${String(invalidUnsubscribe)} is fatal before Wire start`, async () => { + const wire = new InvalidSubscribeWireClient(invalidUnsubscribe); + const scene = new RecordingScene(); + const runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), + resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, + scene, + loadResources: async () => {}, + waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, + }); + + const thrown = await rejectedValue(runtime.start()); + + assert.ok(thrown instanceof TypeError); + assert.match(thrown.message, /subscribe.*unsubscribe.*function/i); + assert.deepEqual(wire.lifecycle, ['subscribe', 'stop']); + assert.equal(wire.stopCalls, 1); + assert.deepEqual( + scene.calls.filter((call) => call.name === 'fatal').map((call) => call.value), + [thrown], + ); + await assert.rejects(() => runtime.start(), /fatal/i); + assert.equal(scene.calls.filter((call) => call.name === 'fatal').length, 1); + }); +} + +test('subscribe startup failure enters fatal once and stops the created wire without starting it', async () => { + const setup = makeRuntime(); + const primary = new Error('subscribe failed'); + setup.wire.onAction = (name) => { + if (name === 'subscribe') throw primary; + }; + + const thrown = await rejectedValue(setup.runtime.start()); + + assert.equal(thrown, primary); + assert.deepEqual(setup.wire.lifecycle, ['subscribe', 'stop']); + assert.equal(setup.wire.stopCalls, 1); + assert.deepEqual( + setup.scene.calls.filter((call) => call.name === 'fatal').map((call) => call.value), + [primary], + ); +}); + +test('wire start failure cleans every established layer and preserves cleanup error order', async () => { + const primary = new Error('wire start failed'); + const unsubscribeFailure = { step: 'unsubscribe' }; + const stopFailure = { step: 'wire.stop' }; + const cleanupLog: string[] = []; + let moduleNumber = 0; + let activeHost: GameHost | null = null; + const entry = makeGameEntry({ + createModule(): GameModule { + moduleNumber += 1; + const currentModule = moduleNumber; + return { + attach(host) { + if (currentModule === 1) return; + activeHost = host; + host.subscribe(() => {}); + }, + handlePlatformEvent() {}, + handleGameMessage() {}, + restore() {}, + dispose() { + if (currentModule === 1) return; + cleanupLog.push('game:dispose'); + throw undefined; + }, + }; + }, + }); + const setup = makeRuntime(runtimeConfig(), entry); + setup.wire.onAction = (name) => { + if (name === 'start') { + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + setup.wire.emit({ + type: 'message', + message: { + route: 'agent', + rpc: 'player_login', + data: fixture('player-login-room.json'), + }, + }); + throw primary; + } + if (name === 'unsubscribe') { + cleanupLog.push('wire:unsubscribe'); + throw unsubscribeFailure; + } + if (name === 'stop') { + cleanupLog.push('wire:stop'); + const host = activeHost; + assert.ok(host); + assert.throws(() => host.getSnapshot(), /disposed/i); + throw stopFailure; + } + }; + + const thrown = await rejectedValue(setup.runtime.start()); + assert.ok(thrown instanceof Error); + const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; + + assert.deepEqual(cleanupLog, ['wire:unsubscribe', 'game:dispose', 'wire:stop']); + assert.equal(errors.length, 4); + assert.equal(errors[0], primary); + assert.equal(errors[1], unsubscribeFailure); + assert.equal(errors[2], undefined); + assert.equal(errors[3], stopFailure); + assert.equal(setup.scene.calls.filter((call) => call.name === 'fatal').length, 1); + assert.equal( + setup.scene.calls.find((call) => call.name === 'fatal')?.value, + thrown, + ); + + const sceneCalls = setup.scene.calls.length; + setup.wire.emit({ type: 'reconnecting', server: 'ws://agent' }); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + assert.equal(setup.scene.calls.length, sceneCalls); + assert.equal(setup.wire.stopCalls, 1); +}); + +for (const failedGate of ['resources', 'minimum-display'] as const) { + test(`${failedGate} failure after Wire start tears down once and stays fatal`, async () => { + const resourcesReady = deferred(); + const minimumDisplayReady = deferred(); + const wireStarted = deferred(); + const primary = new Error(`${failedGate} failed`); + const setup = makeRuntime(); + const runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), + resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => setup.wire, + scene: setup.scene, + loadResources: () => resourcesReady.promise, + waitForMinimumDisplay: () => minimumDisplayReady.promise, + getLoginDeviceSnapshot: () => DEVICE, + clock: setup.clock, + }); + setup.wire.onAction = (name) => { + if (name === 'start') wireStarted.resolve(undefined); + }; + + const starting = runtime.start(); + await wireStarted.promise; + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + if (failedGate === 'resources') { + resourcesReady.reject(primary); + minimumDisplayReady.resolve(undefined); + } else { + resourcesReady.resolve(undefined); + minimumDisplayReady.reject(primary); + } + + const thrown = await rejectedValue(starting); + assert.equal(thrown, primary); + assert.deepEqual(setup.wire.lifecycle, ['subscribe', 'start', 'unsubscribe', 'stop']); + assert.equal(setup.wire.stopCalls, 1); + assert.deepEqual( + setup.scene.calls.filter((call) => call.name === 'fatal').map((call) => call.value), + [primary], + ); + + const sceneCalls = setup.scene.calls.length; + setup.wire.emit({ type: 'reconnecting', server: 'ws://agent' }); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + assert.equal(setup.scene.calls.length, sceneCalls); + }); +} + +test('login sends one exact request and an ordinary reopen resends the same data with a four-second guard', async () => { + const setup = makeRuntime(); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + + setup.runtime.login(ACCOUNT); + + assert.deepEqual(setup.wire.sent[0], { + app: 'youle', + route: 'agent', + rpc: 'player_login', + data: { + agentid: 'veRa0qrBf', gameid: '41', openid: 'o1', nickname: '测试号', + avatar: 'http://a', sex: 0, province: '江西', city: '南昌', + unionid: 'test_unionid', version: 10000, channelid: 'FtJf073aa', + marketid: 4, ip: '127.0.0.1', location: DEVICE.location, + machineid: 'machine-1', machineroom: 'room-install-1', + }, + }); + assert.deepEqual([...setup.clock.timers.values()].map((timer) => timer.ms), [4000]); + + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, + }); + assert.equal(setup.clock.timers.size, 0); + + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + + assert.equal(setup.wire.sent.length, 2); + assert.equal(setup.wire.sent[1]?.rpc, 'player_login'); + assert.equal(setup.wire.sent[1]?.data, setup.wire.sent[0]?.data); + assert.deepEqual([...setup.clock.timers.values()].map((timer) => timer.ms), [4000]); + setup.clock.runOnlyTimer(); + assert.equal(setup.wire.reconnectCalls, 1); +}); + +for (const mismatch of [ + { label: 'different value', configGameId: '42', entryGameId: '41' }, + { label: 'different type', configGameId: '41', entryGameId: 41 }, +] as const) { + test(`gameid ${mismatch.label} fails fatally before WireClient creation`, async () => { + const setup = makeRuntime( + runtimeConfig(mismatch.configGameId), + makeGameEntry({ gameId: mismatch.entryGameId }), + ); + + await assert.rejects(() => setup.runtime.start(), /gameid/i); + await assert.rejects(() => setup.runtime.start(), /fatal/i); + + assert.equal(setup.factoryConfigs.length, 0); + assert.deepEqual(setup.wire.lifecycle, []); + const fatalCalls = setup.scene.calls.filter((call) => call.name === 'fatal'); + assert.equal(fatalCalls.length, 1); + assert.ok(fatalCalls[0]?.value instanceof Error); + }); +} + +for (const switchCase of [ + { + label: 'room', + route: 'room', + rpc: 'connect_roomserver', + target: 'ws://room.example:3088', + data: { roomserver: 'ws://room.example:3088', epoch: 7, nested: { trace: 'r' } }, + }, + { + label: 'agent', + route: 'agent', + rpc: 'connect_agentserver', + target: 'wss://agent.example/socket', + data: { + agentserver: 'wss://agent.example/socket', + opt: 'free_room', + nested: { trace: 'a' }, + }, + }, +] as const) { + test(`${switchCase.label} switch resends the exact switch envelope before returning to steady login`, async () => { + const setup = makeRuntime(); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + const loginEnvelope = setup.wire.sent[0]!; + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, + }); + setup.wire.sent.length = 0; + + setup.wire.emit({ + type: 'message', + message: { route: switchCase.route, rpc: switchCase.rpc, data: switchCase.data }, + }); + + assert.deepEqual(setup.wire.switchCalls, [switchCase.target]); + assert.equal(setup.wire.sent.length, 0); + + setup.wire.emit({ type: 'open', server: switchCase.target }); + + assert.equal(setup.wire.sent.length, 1); + assert.equal(setup.wire.sent[0]?.rpc, switchCase.rpc); + assert.deepEqual(setup.wire.sent[0], { + app: 'youle', + route: switchCase.route, + rpc: switchCase.rpc, + data: switchCase.data, + }); + assert.equal(setup.wire.sent[0]?.data, switchCase.data); + assert.equal(setup.clock.timers.size, 0, 'switch resend must not start the login guard'); + + setup.wire.emit({ type: 'open', server: switchCase.target }); + + assert.equal(setup.wire.sent.length, 2); + assert.equal(setup.wire.sent[1]?.rpc, 'player_login'); + assert.equal(setup.wire.sent[1]?.data, loginEnvelope.data); + assert.deepEqual([...setup.clock.timers.values()].map((timer) => timer.ms), [4000]); + }); +} + +test('successful login without a room shows the lobby', async () => { + const setup = makeRuntime(); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, + }); + + assert.equal(setup.runtime.state.room.kind, 'outside'); + assert.equal(setup.scene.calls[setup.scene.calls.length - 1]?.name, 'lobby'); +}); + +test('room login commits and opens before showRoom, then restores the exact deskinfo', async () => { + const log: string[] = []; + const restored: unknown[] = []; + const attached: GameHost[] = []; + const entry = makeLoggingEntry(log, restored, attached); + const setup = makeRuntime(runtimeConfig(), entry); + setup.scene.onCall = (name) => { if (name === 'room') log.push('scene:room'); }; + await setup.runtime.start(); + beginEntryRecording(entry); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + const roomLogin = fixture('player-login-room.json'); + + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: roomLogin }, + }); + + assert.equal(setup.runtime.state.room.kind, 'inside'); + assert.deepEqual(log, [ + 'attach:100001', + 'event:room.entered', + 'scene:room', + 'restore', + ]); + assert.deepEqual(restored, [roomLogin.deskinfo]); + assert.equal(restored[0], roomLogin.deskinfo); + assert.equal(attached.length, 1); +}); + +for (const withDeskinfo of [false, true]) { + test(`self join opens and shows the room ${withDeskinfo ? 'before restoring deskinfo' : 'without restoring absent deskinfo'}`, async () => { + const log: string[] = []; + const restored: unknown[] = []; + const attached: GameHost[] = []; + const entry = makeLoggingEntry(log, restored, attached); + const setup = makeRuntime(runtimeConfig(), entry); + setup.scene.onCall = (name) => { if (name === 'room') log.push('scene:room'); }; + await setup.runtime.start(); + beginEntryRecording(entry); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, + }); + log.length = 0; + const join = fixture('self-join-room.json'); + const deskinfo = join.deskinfo; + if (!withDeskinfo) delete join.deskinfo; + + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'self_join_room', data: join }, + }); + + assert.deepEqual(log, withDeskinfo + ? ['attach:100001', 'event:room.entered', 'scene:room', 'restore'] + : ['attach:100001', 'event:room.entered', 'scene:room']); + assert.deepEqual(restored, withDeskinfo ? [deskinfo] : []); + if (withDeskinfo) assert.equal(restored[0], deskinfo); + }); +} + +test('slow and reconnecting show reconnect without clearing or replacing room state', async () => { + const setup = makeRuntime(); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, + }); + const roomState = setup.runtime.state; + const callsBefore = setup.scene.calls.length; + + setup.wire.emit({ type: 'slow' }); + assert.equal(setup.runtime.state.app.phase, 'slow'); + setup.wire.emit({ type: 'reconnecting', server: 'ws://agent' }); + + assert.equal(setup.runtime.state.app.phase, 'reconnecting'); + assert.equal(setup.runtime.state.room, roomState.room); + assert.equal(setup.runtime.state.players, roomState.players); + assert.equal(setup.runtime.state.room.kind, 'inside'); + assert.deepEqual( + setup.scene.calls.slice(callsBefore).map((call) => call.name), + ['reconnect', 'reconnect'], + ); +}); + +test('kick_offline bypasses pending login and stops late navigation', async () => { + const {runtime,wire,scene}=makeRuntime(); + await runtime.start();wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT); + const data={fromOther:true,gameid:'another-game'}; + wire.emit({type:'message',message:{route:'agent',rpc:'kick_offline',data}}); + assert.equal(scene.calls.at(-1)?.name,'kicked');assert.equal(scene.calls.at(-1)?.value,data); + assert.equal(wire.stopCalls,1); + const count=scene.calls.length;wire.emit({type:'open',server:'ws://agent'}); + assert.equal(scene.calls.length,count); +}); + +test('show_message preserves text and legacy numeric timeout without navigating', async () => { + const {runtime,wire,scene}=makeRuntime(); + await runtime.start();wire.emit({type:'open',server:'ws://agent'}); + const before=runtime.state; + for(const time of ['3000','0','-1']){ + wire.emit({type:'message',message:{route:'agent',rpc:'show_message',data:{msg:'服务器提示#第二行',time}}}); + assert.deepEqual(scene.calls.at(-1),{name:'tips',value:{message:'服务器提示#第二行',time:Number(time)}}); + assert.equal(runtime.state,before); + } + runtime.stop(); +}); + +test('kick closes the game before showing kicked and stopping the wire', async () => { + const log: string[] = []; + const restored: unknown[] = []; + const attached: GameHost[] = []; + const entry = makeLoggingEntry(log, restored, attached); + const setup = makeRuntime(runtimeConfig(), entry); + setup.scene.onCall = (name) => { if (name === 'kicked') log.push('scene:kicked'); }; + setup.wire.onAction = (name) => { if (name === 'stop') log.push('wire:stop'); }; + await setup.runtime.start(); + beginEntryRecording(entry); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, + }); + log.length = 0; + const kick = { msg: '检查到新版本', code: 1 }; + + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'kick_server', data: kick }, + }); + + assert.deepEqual(log, ['dispose', 'scene:kicked', 'wire:stop']); + assert.equal(setup.wire.stopCalls, 1); + assert.equal(setup.scene.calls[setup.scene.calls.length - 1]?.value, kick); + + const sceneCalls = setup.scene.calls.length; + setup.wire.emit({ type: 'reconnecting', server: 'ws://agent' }); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + assert.equal(setup.scene.calls.length, sceneCalls, 'late wire events stay silent after kick'); + assert.equal(setup.wire.sent.length, 1, 'late open never revives login after kick'); +}); + +test('a game rpc named like a platform rpc cannot drive platform scenes', async () => { + const setup = makeRuntime(); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, + }); + const sceneCalls = setup.scene.calls.length; + + setup.wire.emit({ + type: 'message', + message: { route: 'fixture-game', rpc: 'self_exit_room', data: { reason: 'game-owned' } }, + }); + + assert.equal(setup.runtime.state.room.kind, 'inside'); + assert.equal(setup.scene.calls.length, sceneCalls); +}); + +test('login gate routes each ignored room message once with zero runtime side effects', async () => { + const originalDispatch = Router.prototype.dispatch; + let routerDispatches = 0; + Router.prototype.dispatch = function dispatchWithCount(message) { + routerDispatches += 1; + return originalDispatch.call(this, message); + }; + + try { + const log: string[] = []; + const entry = makeLoggingEntry(log, [], []); + const setup = makeRuntime(runtimeConfig(), entry); + await setup.runtime.start(); + beginEntryRecording(entry); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + const stateBefore = setup.runtime.state; + const sceneCallsBefore = setup.scene.calls.length; + const lifecycleBefore = [...setup.wire.lifecycle]; + + setup.wire.emit({ + type: 'message', + message: { route: 'room', rpc: 'self_exit_room', data: { seat: 1 } }, + }); + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'self_join_room', data: fixture('self-join-room.json') }, + }); + + assert.equal(routerDispatches, 2); + assert.equal(setup.runtime.state, stateBefore); + assert.deepEqual(log, []); + assert.equal(setup.scene.calls.length, sceneCallsBefore); + assert.equal(setup.scene.calls.filter((call) => call.name === 'fatal').length, 0); + assert.deepEqual(setup.wire.lifecycle, lifecycleBefore); + assert.equal(setup.wire.stopCalls, 0); + } finally { + Router.prototype.dispatch = originalDispatch; + } +}); + +test('a handled no-op room message cannot drive a scene without a state transition', async () => { + const setup = makeRuntime(); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, + }); + const stateBefore = setup.runtime.state; + const sceneCallsBefore = setup.scene.calls.length; + + setup.wire.emit({ + type: 'message', + message: { route: 'room', rpc: 'self_exit_room', data: {} }, + }); + + assert.equal(setup.runtime.state, stateBefore); + assert.equal(setup.scene.calls.length, sceneCallsBefore); + assert.equal(setup.scene.calls.filter((call) => call.name === 'fatal').length, 0); +}); + +test('joinRoom, prepare, and exitRoom delegate through the authoritative platform commands', async () => { + const setup = makeRuntime(); + await setup.runtime.start(); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, + }); + setup.wire.sent.length = 0; + const location = { latitude: 28.7, longitude: 115.8 }; + + setup.runtime.joinRoom({ + roomcode: '100001', + location, + ip: '203.0.113.7', + vipMatch: 1, + match_id: 'match-9', + }); + + assert.deepEqual(setup.wire.sent, [{ + app: 'youle', + route: 'agent', + rpc: 'self_join_room', + data: { + agentid: 'veRa0qrBf', + playerid: 430511, + gameid: '41', + roomcode: '100001', + location, + ip: '203.0.113.7', + vipMatch: 1, + match_id: 'match-9', + }, + }]); + assert.equal((setup.wire.sent[0]!.data as { location: unknown }).location, location); + + const joinedRoom = fixture('self-join-room.json'); + delete joinedRoom.deskinfo; + joinedRoom.deskwar = 0; + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'self_join_room', data: joinedRoom }, + }); + setup.wire.sent.length = 0; + setup.runtime.prepare(); + setup.runtime.exitRoom(); + + assert.deepEqual(setup.wire.sent, [ + { + app: 'youle', + route: 'room', + rpc: 'player_prepare', + data: { + agentid: 'veRa0qrBf', playerid: 430511, gameid: '41', roomcode: '100001', + }, + }, + { + app: 'youle', + route: 'room', + rpc: 'self_exit_room', + data: { + agentid: 'veRa0qrBf', playerid: 430511, gameid: '41', roomcode: '100001', + }, + }, + ]); +}); + +test('stop unsubscribes, disposes while the host is live, invalidates it, then stops the wire', async () => { + const log: string[] = []; + const restored: unknown[] = []; + const attached: GameHost[] = []; + const entry = makeLoggingEntry(log, restored, attached); + const setup = makeRuntime(runtimeConfig(), entry); + setup.wire.onAction = (name) => { + if (name === 'unsubscribe' || name === 'stop') log.push(`wire:${name}`); + }; + await setup.runtime.start(); + beginEntryRecording(entry); + setup.wire.emit({ type: 'open', server: 'ws://agent' }); + setup.runtime.login(ACCOUNT); + setup.wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, + }); + const activeHost = attached[0]!; + log.length = 0; + + setup.runtime.stop(); + + assert.deepEqual(log, ['wire:unsubscribe', 'dispose', 'wire:stop']); + assert.throws(() => activeHost.getSnapshot(), /disposed/i); + setup.runtime.stop(); + assert.deepEqual(log, ['wire:unsubscribe', 'dispose', 'wire:stop']); + assert.equal(setup.wire.stopCalls, 1); +}); + +test('stop continues after clearTimeout and every later cleanup failure in exact order', async () => { + const clearFailure = { step: 'clock.clearTimeout' }; + const disposeFailure = { step: 'game.dispose' }; + const leaseFailure = { step: 'lease.invalidate' }; + const stopFailure = { step: 'wire.stop' }; + const log: string[] = []; + const captured: CapturedGameHost = { host: null }; + const clock = new ControlledClearClock(); + const wire = new RecordingWireClient(); + const scene = new RecordingScene(); + const originalStoreSubscribe = PlatformStore.prototype.subscribe; + PlatformStore.prototype.subscribe = function subscribeWithFailingRelease(listener) { + const release = originalStoreSubscribe.call(this, listener); + return () => { + release(); + log.push('lease:invalidate'); + throw leaseFailure; + }; + }; + + try { + const runtime = new PlatformRuntime({ + gameEntry: makeCleanupFailureEntry(log, disposeFailure, captured), + resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, + scene, + loadResources: async () => {}, + waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, + clock, + }); + clock.onFailingClear = () => { log.push('clock:clear'); }; + wire.onAction = (name) => { + if (name === 'unsubscribe') { + log.push('wire:unsubscribe'); + throw undefined; + } + if (name === 'stop') { + log.push('wire:stop'); + const host = captured.host; + assert.ok(host); + assert.throws(() => host.getSnapshot(), /disposed/i); + throw stopFailure; + } + }; + + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + runtime.login(ACCOUNT); + wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, + }); + wire.emit({ type: 'open', server: 'ws://agent' }); + assert.equal(clock.timers.size, 1); + clock.enableClearFailure(clearFailure); + log.length = 0; + + const thrown = thrownValue(() => { runtime.stop(); }); + assert.ok(thrown instanceof Error); + const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; + assert.deepEqual(log, [ + 'clock:clear', + 'wire:unsubscribe', + 'game:dispose', + 'lease:invalidate', + 'wire:stop', + ]); + assert.equal(errors.length, 5); + assert.equal(errors[0], clearFailure); + assert.equal(errors[1], undefined); + assert.equal(errors[2], disposeFailure); + assert.equal(errors[3], leaseFailure); + assert.equal(errors[4], stopFailure); + assert.equal(scene.calls.filter((call) => call.name === 'fatal').length, 0); + + const completedLog = [...log]; + assert.doesNotThrow(() => { runtime.stop(); }); + assert.deepEqual(log, completedLog); + assert.equal(wire.stopCalls, 1); + } finally { + PlatformStore.prototype.subscribe = originalStoreSubscribe; + } +}); + +test('kick completes close, invalidate, clear, kicked scene, and wire stop despite all failures', async () => { + const disposeFailure = { step: 'game.dispose' }; + const leaseFailure = { step: 'lease.invalidate' }; + const clearFailure = { step: 'clock.clearTimeout' }; + const stopFailure = { step: 'wire.stop' }; + const log: string[] = []; + const captured: CapturedGameHost = { host: null }; + const clock = new ControlledClearClock(); + const wire = new RecordingWireClient(); + const scene = new RecordingScene(); + const originalStoreSubscribe = PlatformStore.prototype.subscribe; + PlatformStore.prototype.subscribe = function subscribeWithFailingRelease(listener) { + const release = originalStoreSubscribe.call(this, listener); + return () => { + release(); + log.push('lease:invalidate'); + throw leaseFailure; + }; + }; + + try { + const runtime = new PlatformRuntime({ + gameEntry: makeCleanupFailureEntry(log, disposeFailure, captured), + resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, + scene, + loadResources: async () => {}, + waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, + clock, + }); + clock.onFailingClear = () => { log.push('clock:clear'); }; + scene.onCall = (name) => { + if (name === 'kicked') { + log.push('scene:kicked'); + throw undefined; + } + if (name === 'fatal') log.push('scene:fatal'); + }; + wire.onAction = (name) => { + if (name === 'unsubscribe') log.push('wire:unsubscribe'); + if (name === 'stop') { + log.push('wire:stop'); + const host = captured.host; + assert.ok(host); + assert.throws(() => host.getSnapshot(), /disposed/i); + throw stopFailure; + } + }; + + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + runtime.login(ACCOUNT); + wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, + }); + wire.emit({ type: 'open', server: 'ws://agent' }); + assert.equal(clock.timers.size, 1); + clock.enableClearFailure(clearFailure); + log.length = 0; + const kick = { msg: 'terminal cleanup test', code: 1 }; + + wire.emit({ + type: 'message', + message: { route: 'agent', rpc: 'kick_server', data: kick }, + }); + + assert.deepEqual(log, [ + 'game:dispose', + 'lease:invalidate', + 'clock:clear', + 'scene:kicked', + 'wire:stop', + 'wire:unsubscribe', + 'scene:fatal', + ]); + assert.equal(wire.stopCalls, 1); + assert.equal(scene.calls.filter((call) => call.name === 'kicked').length, 1); + assert.equal(scene.calls.find((call) => call.name === 'kicked')?.value, kick); + const fatal = scene.calls.find((call) => call.name === 'fatal')?.value; + assert.ok(fatal instanceof Error); + const sessionErrors = (fatal as Error & { readonly errors: readonly unknown[] }).errors; + assert.equal(sessionErrors.length, 3); + assert.equal(sessionErrors[0], disposeFailure); + assert.equal(sessionErrors[1], leaseFailure); + const kickCleanup = sessionErrors[2]; + assert.ok(kickCleanup instanceof Error); + const kickErrors = ( + kickCleanup as Error & { readonly errors: readonly unknown[] } + ).errors; + assert.equal(kickErrors.length, 3); + assert.equal(kickErrors[0], clearFailure); + assert.equal(kickErrors[1], undefined); + assert.equal(kickErrors[2], stopFailure); + + const completedLog = [...log]; + assert.doesNotThrow(() => { runtime.stop(); }); + assert.doesNotThrow(() => { runtime.stop(); }); + assert.deepEqual(log, completedLog); + assert.equal(wire.stopCalls, 1); + } finally { + PlatformStore.prototype.subscribe = originalStoreSubscribe; + } +}); + +test('real runtime renders the room before restoring canonical deskinfo', async () => { + const events: string[] = []; + const wire = new RecordingWireClient(); + const controller = new PlatformUiController({ + render: model => { events.push(`render:${model.page}`); }, + activate: page => { events.push(`activate:${page}`); }, + showOverlay: model => { events.push(`overlay:${model.kind}`); }, + }, { request: () => () => {} }, error => { throw error; }); + const deskinfo = { fixture: 'opaque' }; + const entry = makeGameEntry({ createModule: () => ({ + attach() { events.push('attach'); }, + handlePlatformEvent() {}, handleGameMessage() {}, dispose() {}, + restore(value) { assert.equal(value, deskinfo); events.push('restore'); }, + }) }); + const runtime = new PlatformRuntime({ + gameEntry: entry, resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, scene: controller, + loadResources: async () => {}, waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), + }); + controller.connect(runtime); + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + controller.login(ACCOUNT); + wire.emit({ type: 'message', message: { route: 'agent', rpc: 'player_login', + data: fixture('player-login-success.json') } }); + controller.joinRoom({ roomcode: '100001', location: null, ip: '127.0.0.1' }); + wire.emit({ type: 'message', message: { route: 'agent', rpc: 'self_join_room', + data: { ...fixture('self-join-room.json'), deskinfo } } }); + assert.ok(events.includes('render:lobby')); + assert.ok(events.indexOf('attach') < events.indexOf('render:room')); + assert.ok(events.indexOf('render:room') < events.indexOf('activate:room')); + assert.ok(events.indexOf('activate:room') < events.indexOf('restore')); + controller.dispose(); runtime.stop(); + assert.equal(wire.stopCalls, 1); +}); + +test('real runtime keeps canonical roots and raw denials while login and join retries succeed', async () => { + const wire = new RecordingWireClient(); + const overlays: unknown[] = []; + const controller = new PlatformUiController({ + render: () => {}, activate: () => {}, + showOverlay: model => { overlays.push(model); }, + }, { request: () => () => {} }, error => { throw error; }); + const runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, scene: controller, + loadResources: async () => {}, waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), + }); + controller.connect(runtime); + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + + const preLoginRoot = runtime.state; + controller.login(ACCOUNT); + const loginDenial = { state: 7, showerror: 1, error: 'Login denied' }; + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: loginDenial, + } }); + assert.equal(runtime.state, preLoginRoot); + const loginOverlay = overlays[overlays.length - 1] as { kind: string; outcome: { data: unknown } }; + assert.equal(loginOverlay.kind, 'denial'); + assert.equal(loginOverlay.outcome.data, loginDenial); + + const retryAccount = { ...ACCOUNT, openid: 'retry-account' }; + controller.login(retryAccount); + assert.equal((wire.sent[wire.sent.length - 1]!.data as Record).openid, 'retry-account'); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), + } }); + const lobbyRoot = runtime.state; + + const firstJoin = { roomcode: '999999', location: null, ip: '127.0.0.1' }; + controller.joinRoom(firstJoin); + const joinDenial = { state: 99, roomcode: '999999', showerror: 1, error: 'No room' }; + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'self_join_room', data: joinDenial, + } }); + assert.equal(runtime.state, lobbyRoot); + const joinOverlay = overlays[overlays.length - 1] as { kind: string; outcome: { data: unknown } }; + assert.equal(joinOverlay.kind, 'denial'); + assert.equal(joinOverlay.outcome.data, joinDenial); + + controller.joinRoom({ roomcode: '100001', location: null, ip: '127.0.0.1' }); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'self_join_room', data: fixture('self-join-room.json'), + } }); + assert.equal(runtime.state.room.kind, 'inside'); + assert.equal((overlays[overlays.length - 1] as { kind: string }).kind, 'none'); + controller.dispose(); runtime.stop(); +}); + +test('real runtime restores reconnect overlay and silences UI after kick or fatal', async () => { + for (const terminal of ['kick', 'fatal'] as const) { + const wire = new RecordingWireClient(); + const events: string[] = []; + const controller = new PlatformUiController({ + render: model => { events.push(`render:${model.page}`); }, + activate: page => { events.push(`activate:${page}`); }, + showOverlay: model => { events.push(`overlay:${model.kind}`); }, + }, { request: () => () => {} }, error => { throw error; }); + const runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, scene: controller, + loadResources: async () => {}, waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), + }); + controller.connect(runtime); + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + controller.login(ACCOUNT); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), + } }); + + wire.emit({ type: 'slow' }); + assert.equal(events[events.length - 1], 'overlay:reconnect'); + const activations = events.filter(event => event.startsWith('activate:')).length; + wire.emit({ type: 'open', server: 'ws://agent' }); + assert.equal(events[events.length - 1], 'overlay:none'); + assert.equal(events.filter(event => event.startsWith('activate:')).length, activations); + + if (terminal === 'kick') { + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'kick_server', data: { code: 1, msg: 'terminal' }, + } }); + assert.equal(events[events.length - 1], 'overlay:kicked'); + } else { + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: { state: 0 }, + } }); + assert.equal(events[events.length - 1], 'overlay:fatal'); + } + const terminalEvents = [...events]; + assert.throws(() => controller.login(ACCOUNT), /terminal/); + wire.emit({ type: 'slow' }); + wire.emit({ type: 'open', server: 'ws://agent' }); + assert.deepEqual(events, terminalEvents); + assert.equal(wire.stopCalls, 1); + controller.dispose(); runtime.stop(); + } +}); + +test('real runtime alone governs login readiness while resources are pending', async () => { + const resourcesReady = deferred(); + const wire = new RecordingWireClient(); + const controller = new PlatformUiController({ + render: () => {}, activate: () => {}, showOverlay: () => {}, + }, { request: () => () => {} }, error => { throw error; }); + const runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, scene: controller, + loadResources: () => resourcesReady.promise, waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), + }); + controller.connect(runtime); + const starting = runtime.start(); + await Promise.resolve(); + await Promise.resolve(); + assert.deepEqual(wire.lifecycle, ['subscribe', 'start']); + wire.emit({ type: 'open', server: 'ws://agent' }); + assert.equal(runtime.ready, false); + assert.throws(() => controller.login(ACCOUNT), /login page/); + assert.equal(wire.sent.length, 0); + + resourcesReady.resolve(undefined); + await starting; + assert.equal(runtime.ready, true); + controller.login(ACCOUNT); + assert.equal(wire.sent.length, 1); + controller.dispose(); runtime.stop(); +}); + +for (const phase of ['render', 'activate'] as const) { + test(`real room ${phase} disposal interrupts restore and terminates the runtime`, async () => { + const wire = new RecordingWireClient(); + let armed = false; + let restores = 0; + let controller!: PlatformUiController; + controller = new PlatformUiController({ + render: model => { + if (armed && phase === 'render' && model.page === 'room') controller.dispose(); + }, + activate: page => { + if (armed && phase === 'activate' && page === 'room') controller.dispose(); + }, + showOverlay: () => {}, + }, { request: () => () => {} }, error => { throw error; }); + const runtime = new PlatformRuntime({ + gameEntry: makeGameEntry({ createModule: () => ({ + attach() {}, handlePlatformEvent() {}, handleGameMessage() {}, dispose() {}, + restore() { restores += 1; }, + }) }), + resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, scene: controller, + loadResources: async () => {}, waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), + }); + controller.connect(runtime); + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + controller.login(ACCOUNT); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), + } }); + armed = true; + controller.joinRoom({ roomcode: '100001', location: null, ip: '127.0.0.1' }); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'self_join_room', data: fixture('self-join-room.json'), + } }); + + assert.equal(restores, 0); + assert.equal(wire.stopCalls, 1); + assert.throws(() => runtime.login(ACCOUNT), error => { + assert.ok(error instanceof Error); + const errors = (error as Error & { readonly errors: readonly unknown[] }).errors; + assert.match(String(errors[0]), /lifecycle interrupted/); + return true; + }); + runtime.stop(); + }); +} + +test('application overlay teardown disposes Presenter, stops Runtime, and invalidates a late frame', async () => { + const wire = new RecordingWireClient(); + const frames: Array<{ callback: () => void; cancelled: boolean }> = []; + const renders: string[] = []; + let armed = false; + let runtime!: PlatformRuntime; + let controller!: PlatformUiController; + controller = new PlatformUiController({ + render: model => { renders.push(model.page); }, + activate: () => {}, + showOverlay: model => { + if (armed && model.kind === 'reconnect') { + controller.dispose(); + runtime.stop(); + } + }, + }, { request: callback => { + const frame = { callback, cancelled: false }; + frames.push(frame); + return () => { frame.cancelled = true; }; + } }, error => { throw error; }); + runtime = new PlatformRuntime({ + gameEntry: makeGameEntry(), resolveRuntimeConfig: async () => runtimeConfig(), + createWireClient: () => wire, scene: controller, + loadResources: async () => {}, waitForMinimumDisplay: async () => {}, + getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), + }); + controller.connect(runtime); + await runtime.start(); + wire.emit({ type: 'open', server: 'ws://agent' }); + controller.login(ACCOUNT); + wire.emit({ type: 'message', message: { + route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), + } }); + armed = true; + const framesBeforeStop = frames.length; + + wire.emit({ type: 'slow' }); + + assert.equal(frames.length, framesBeforeStop + 1); + const lateFrame = frames[frames.length - 1]!; + assert.equal(lateFrame.cancelled, true); + assert.equal(wire.stopCalls, 1); + const rendered = [...renders]; + lateFrame.callback(); + wire.emit({ type: 'open', server: 'ws://agent' }); + assert.deepEqual(renders, rendered); + assert.throws(() => runtime.login(ACCOUNT), /stopped/); + runtime.stop(); +}); -test('task wire responses reach the store and claim does not invent reward balances',async()=>{ +test('rank and card wire flow uses server balances and accepts repeated rank responses',async()=>{ const {runtime,wire}=makeRuntime();await runtime.start(); wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT); wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data:fixture('player-login-success.json')}}); - runtime.getPlayerTasks(); - wire.emit({type:'message',message:{route:'agent',rpc:'get_player_task',data:{tasks:[{taskid:7,title:'任务',award:'3',finish:'1',total:'1',state:'1'}]}}}); - assert.equal(runtime.state.tasks?.[0].award,3); - const balance=runtime.state.players.entities[430511].roomcard; - runtime.claimTask(7); - wire.emit({type:'message',message:{route:'agent',rpc:'get_task_award',data:{taskid:'7',taskstate:0}}}); - assert.equal(runtime.state.tasks?.[0].state,2); - assert.equal(runtime.state.players.entities[430511].taskstate,0); - assert.equal(runtime.state.players.entities[430511].roomcard,balance); - wire.emit({type:'message',message:{route:'agent',rpc:'update_roomcard',data:{roomcard:25}}}); - assert.equal(runtime.state.players.entities[430511].roomcard,25); - wire.emit({type:'message',message:{route:'agent',rpc:'player_finish_task',data:{state:1}}}); - assert.equal(runtime.state.players.entities[430511].taskstate,1); - wire.emit({type:'slow'});assert.throws(()=>runtime.getPlayerTasks(),/logged-in/); - runtime.stop(); + runtime.getTreasureList();const row={name:'甲',headimg:'',playerid:12,star:100,signature:'a#b'}; + wire.emit({type:'message',message:{route:'agent',rpc:'get_treasurelist',data:{list:[row]}}}); + assert.deepEqual(runtime.state.treasureList,[row]);const old=runtime.state.treasureList; + wire.emit({type:'message',message:{route:'agent',rpc:'get_treasurelist',data:{list:[row]}}}); + assert.notEqual(runtime.state.treasureList,old); + runtime.redeemCard('00aB-12');const bean=runtime.state.players.entities[430511].bean; + wire.emit({type:'message',message:{route:'agent',rpc:'topup_card',data:{}}}); + assert.equal(runtime.state.players.entities[430511].bean,bean); + for(const route of ['agent','room']){ + wire.emit({type:'message',message:{route,rpc:'update_bean',data:{bean:123}}}); + assert.equal(runtime.state.players.entities[430511].bean,123); + } + wire.emit({type:'message',message:{route:'agent',rpc:'update_bean',data:{change:20}}}); + assert.equal(runtime.state.players.entities[430511].bean,123);runtime.stop(); }); -test('UI observes canonical commits and unsubscribe is idempotent', async () => { - const { runtime, wire } = makeRuntime(); - const seen: unknown[] = []; - const off = runtime.subscribeState( - state => { seen.push(state); }, - error => { throw error; }, - ); - assert.equal(seen.length, 0); - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - assert.equal(seen[seen.length - 1], runtime.state); - off(); - off(); - const count = seen.length; - runtime.login(ACCOUNT); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), - } }); - assert.equal(seen.length, count); - runtime.stop(); - assert.throws(() => runtime.subscribeState(() => {}, () => {}), /stopped/); -}); - -test('a failing UI observer reports its original error once while other observers continue', async () => { - const { runtime, wire } = makeRuntime(); - const failure = { source: 'render' }; - const reported: unknown[] = []; - let failingCalls = 0; - const healthyStates: unknown[] = []; - runtime.subscribeState( - () => { - failingCalls += 1; - throw failure; - }, - error => { reported.push(error); }, - ); - runtime.subscribeState( - state => { healthyStates.push(state); }, - error => { throw error; }, - ); - - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - - assert.equal(failingCalls, 1); - assert.deepEqual(reported, [failure]); - assert.equal(reported[0], failure); - assert.equal(healthyStates[0], runtime.state); - - runtime.login(ACCOUNT); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), - } }); - assert.equal(failingCalls, 1); - assert.equal(healthyStates[healthyStates.length - 1], runtime.state); - runtime.stop(); -}); - -test('an onError failure enters fatal with both UI error identities and cancels observers', async () => { - const { runtime, wire, scene } = makeRuntime(); - const listenerFailure = { source: 'listener' }; - const onErrorFailure = { source: 'onError' }; - let laterObserverCalls = 0; - runtime.subscribeState( - () => { throw listenerFailure; }, - () => { throw onErrorFailure; }, - ); - runtime.subscribeState( - () => { laterObserverCalls += 1; }, - error => { throw error; }, - ); - await runtime.start(); - - wire.emit({ type: 'open', server: 'ws://agent' }); - - assert.equal(laterObserverCalls, 0); - assert.equal(wire.stopCalls, 1); - const fatal = scene.calls.find(call => call.name === 'fatal')?.value; - assert.ok(fatal instanceof Error); - const errors = (fatal as Error & { readonly errors: readonly unknown[] }).errors; - assert.equal(errors.length, 2); - assert.equal(errors[0], listenerFailure); - assert.equal(errors[1], onErrorFailure); - assert.throws(() => runtime.subscribeState(() => {}, () => {}), /fatal/); -}); - -for (const terminal of ['stop', 'fatal', 'kick'] as const) { - test(`${terminal} invalidates a late Store delivery and rejects new UI observers`, async () => { - const originalSubscribe = PlatformStore.prototype.subscribe; - let retainedListener: Parameters[0] | null = null; - PlatformStore.prototype.subscribe = function retainFirstListener(listener) { - retainedListener ??= listener; - return originalSubscribe.call(this, listener); - }; - - try { - const setup = makeRuntime(); - let calls = 0; - setup.runtime.subscribeState( - () => { calls += 1; }, - error => { throw error; }, - ); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - const callsBeforeTermination = calls; - setup.wire.onAction = (name) => { - if (name !== 'stop') return; - const listener = retainedListener; - assert.ok(listener); - listener(setup.runtime.state, setup.runtime.state); - }; - - if (terminal === 'stop') { - setup.runtime.stop(); - } else if (terminal === 'fatal') { - setup.scene.onCall = (name) => { - if (name === 'reconnect') throw new Error('reconnect render failed'); - }; - setup.wire.emit({ type: 'slow' }); - } else { - setup.runtime.login(ACCOUNT); - setup.wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'kick_server', data: { code: 1, msg: 'terminal' }, - } }); - } - - assert.equal( - calls, - callsBeforeTermination + (terminal === 'fatal' ? 1 : 0), - 'observers must be inactive before terminal cleanup callbacks', - ); - const callsAtTermination = calls; - const lateListener = retainedListener as Parameters[0] | null; - assert.ok(lateListener); - lateListener(setup.runtime.state, setup.runtime.state); - assert.equal(calls, callsAtTermination); - assert.throws( - () => setup.runtime.subscribeState(() => {}, () => {}), - new RegExp(terminal === 'kick' ? 'kicked' : terminal), - ); - } finally { - PlatformStore.prototype.subscribe = originalSubscribe; - } - }); -} - -test('stop during a Store commit cancels later observers and prevents scene revival', async () => { - const setup = makeRuntime(); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - let laterObserverCalls = 0; - let resubscribeFailure: unknown; - setup.runtime.subscribeState( - () => { - setup.runtime.stop(); - resubscribeFailure = thrownValue( - () => setup.runtime.subscribeState(() => {}, () => {}), - ); - }, - error => { throw error; }, - ); - setup.runtime.subscribeState( - () => { laterObserverCalls += 1; }, - error => { throw error; }, - ); - const sceneCallsBefore = setup.scene.calls.length; - - setup.wire.emit({ type: 'slow' }); - - assert.equal(laterObserverCalls, 0); - assert.match(String(resubscribeFailure), /stopped/); - assert.equal(setup.scene.calls.length, sceneCallsBefore); - assert.equal(setup.wire.stopCalls, 1); -}); - -test('listener and onError failures after stopping escape the Store boundary without reviving fatal', async () => { - const setup = makeRuntime(); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - const listenerFailure = { source: 'listener-after-stop' }; - const onErrorFailure = { source: 'onError-after-stop' }; - const reported: unknown[] = []; - setup.runtime.subscribeState( - () => { - setup.runtime.stop(); - throw listenerFailure; - }, - error => { - reported.push(error); - setup.runtime.stop(); - throw onErrorFailure; - }, - ); - const sceneCallsBefore = setup.scene.calls.length; - - const thrown = thrownValue(() => { setup.wire.emit({ type: 'slow' }); }); - - assert.deepEqual(reported, [listenerFailure]); - assert.ok(thrown instanceof Error); - const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; - assert.equal(errors.length, 2); - assert.equal(errors[0], listenerFailure); - assert.equal(errors[1], onErrorFailure); - assert.equal(setup.scene.calls.length, sceneCallsBefore); - assert.equal(setup.scene.calls.filter(call => call.name === 'fatal').length, 0); - assert.equal(setup.wire.stopCalls, 1); - assert.throws(() => setup.runtime.subscribeState(() => {}, () => {}), /stopped/); -}); - -test('stopped observer errors survive a later active-room publish failure', async () => { - const setup = makeRuntime(); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - const roomLogin = fixture('player-login-room.json'); - (roomLogin.players as unknown[])[2] = { - playerid: 430512, - nickname: '同桌玩家', - avatar: 'http://b', - sex: 0, - bean: 10, - isprepare: 0, - onstate: 0, - }; - setup.wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: roomLogin, - } }); - const listenerFailure = { source: 'room-listener-after-stop' }; - const onErrorFailure = { source: 'room-onError-after-stop' }; - setup.runtime.subscribeState( - () => { - setup.runtime.stop(); - throw listenerFailure; - }, - () => { throw onErrorFailure; }, - ); - - const thrown = thrownValue(() => { - setup.wire.emit({ type: 'message', message: { - route: 'room', rpc: 'player_prepare', data: { seat: 2 }, - } }); - }); - - assert.ok(thrown instanceof Error); - const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; - assert.equal(errors.length, 2); - const observerFailure = errors[0]; - assert.ok(observerFailure instanceof Error); - const observerErrors = ( - observerFailure as Error & { readonly errors: readonly unknown[] } - ).errors; - assert.equal(observerErrors[0], listenerFailure); - assert.equal(observerErrors[1], onErrorFailure); - assert.match(String(errors[1]), /disposed game session/i); - assert.equal(setup.scene.calls.filter(call => call.name === 'fatal').length, 0); - assert.throws(() => setup.runtime.subscribeState(() => {}, () => {}), /stopped/); -}); - -test('UI observers receive each committed snapshot even when a listener commits reentrantly', async () => { - const setup = makeRuntime(); - const delivered: unknown[] = []; - let firstCommit: unknown; - let latestDuringFirstCommit: unknown; - let reentered = false; - setup.runtime.subscribeState( - state => { - if (reentered) return; - reentered = true; - firstCommit = state; - setup.wire.emit({ type: 'slow' }); - latestDuringFirstCommit = setup.runtime.state; - }, - error => { throw error; }, - ); - setup.runtime.subscribeState( - state => { delivered.push(state); }, - error => { throw error; }, - ); - await setup.runtime.start(); - - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - - assert.notEqual(firstCommit, latestDuringFirstCommit); - assert.equal(delivered[0], firstCommit); - assert.equal(delivered[1], latestDuringFirstCommit); - assert.deepEqual( - delivered.map(state => (state as { app: { phase: string } }).app.phase), - ['connected', 'slow'], - ); - setup.runtime.stop(); -}); - -test('cancelling and subscribing observers during a commit cannot alter that delivery snapshot', async () => { - const setup = makeRuntime(); - let cancelledCalls = 0; - const replacementPhases: string[] = []; - let changedObservers = false; - let offCancelled = (): void => {}; - setup.runtime.subscribeState( - () => { - if (changedObservers) return; - changedObservers = true; - offCancelled(); - setup.runtime.subscribeState( - state => { replacementPhases.push(state.app.phase); }, - error => { throw error; }, - ); - }, - error => { throw error; }, - ); - offCancelled = setup.runtime.subscribeState( - () => { cancelledCalls += 1; }, - error => { throw error; }, - ); - await setup.runtime.start(); - - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - assert.equal(cancelledCalls, 0); - assert.deepEqual(replacementPhases, []); - - setup.wire.emit({ type: 'slow' }); - assert.equal(cancelledCalls, 0); - assert.deepEqual(replacementPhases, ['slow']); - setup.runtime.stop(); -}); - -test('a denied join retains the lobby root, reports the raw outcome and permits a valid retry', async () => { - const { runtime, wire, scene } = makeRuntime(); - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - runtime.login(ACCOUNT); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), - } }); - const lobby = runtime.state; - const denial = { state: 99, roomcode: '999999', showerror: 1, error: 'Room does not exist' }; - runtime.joinRoom({ roomcode: '999999', location: null, ip: '127.0.0.1' }); - wire.emit({ type: 'message', message: { route: 'agent', rpc: 'self_join_room', data: denial } }); - - assert.equal(runtime.state, lobby); - assert.equal(wire.stopCalls, 0); - assert.deepEqual(scene.calls.filter(call => call.name === 'fatal'), []); - const outcome = scene.calls.find(call => call.name === 'denied')?.value; - assert.deepEqual(outcome, { rpc: 'self_join_room', state: 99, data: denial }); - assert.equal((outcome as { data: unknown }).data, denial); - - runtime.joinRoom({ roomcode: '100001', location: null, ip: '127.0.0.1' }); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'self_join_room', data: fixture('self-join-room.json'), - } }); - assert.equal(runtime.state.room.kind, 'inside'); - assert.equal(scene.calls[scene.calls.length - 1]?.name, 'room'); - runtime.stop(); -}); - -test('runtime resolves seats only from the actual game-owned server roomtype at room entry', async () => { - const roomtypes: Array = []; - const attached: GameHost[] = []; - const setup = makeRuntime(runtimeConfig(), makeGameEntry({ - resolveSeatCount(roomtype) { - roomtypes.push(roomtype); - if (roomtype.length !== 7 || roomtype[1] !== 4 || !Array.isArray(roomtype[5])) { - throw new Error('Game roomtype requires the full nested server configuration'); - } - return 4; - }, - createModule: () => ({ - attach(host) { attached.push(host); }, - handlePlatformEvent() {}, handleGameMessage() {}, restore() {}, dispose() {}, - }), - })); - await setup.runtime.start(); - assert.deepEqual(roomtypes, []); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - } }); - assert.equal(roomtypes.length, 1); - assert.equal(attached.length, 1); - assert.equal(roomtypes[0], attached[0]!.getSnapshot().room.roomtype); - assert.equal(attached[0]!.seat.toView(1), 0); - assert.deepEqual(setup.scene.calls.filter(call => call.name === 'fatal'), []); - setup.runtime.stop(); -}); - -for (const count of [0, 2.5, 2]) { - test(`runtime rejects invalid game count ${count} at the real room boundary`, async () => { - let attaches = 0; - const setup = makeRuntime(runtimeConfig(), makeGameEntry({ - resolveSeatCount: () => count, - createModule: () => ({ - attach() { attaches += 1; }, - handlePlatformEvent() {}, handleGameMessage() {}, restore() {}, dispose() {}, - }), - })); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - } }); - assert.equal(attaches, 0); - assert.equal(setup.wire.stopCalls, 1); - const failure = setup.scene.calls.find(call => call.name === 'fatal')?.value; - assert.match(String((failure as { errors: unknown[] }).errors[0]), /seat count/); - setup.runtime.stop(); - }); -} - -for(const loginRoute of ['agent','room','platform'])test(`${loginRoute} login denial clears its guard, retains state and permits account retry`, async () => { - const { runtime, wire, scene, clock } = makeRuntime(); - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - const before = runtime.state; - runtime.login(ACCOUNT); - const denial = { state: -1, msg: '短信验证码不正确', time: 0 }; - assert.equal(wire.sent.at(-1)?.route,'agent','outbound login remains agent'); - wire.emit({ type: 'message', message: { route: loginRoute, rpc: 'player_login', data: denial } }); - - assert.equal(runtime.state, before); - assert.equal(wire.stopCalls, 0); - assert.equal(clock.timers.size, 0); - const outcome = scene.calls.find(call => call.name === 'denied')?.value; - assert.deepEqual(outcome, { rpc: 'player_login', state: -1, data: denial }); - assert.equal((outcome as { data: unknown }).data, denial); - assert.deepEqual(scene.calls.filter(call => call.name === 'fatal'), []); - const sentBeforeReconnect=wire.sent.length; - wire.emit({type:'reconnecting',server:'ws://agent'}); - wire.emit({type:'open',server:'ws://agent'}); - assert.equal(wire.sent.length,sentBeforeReconnect,'a denied first login must not be replayed'); - runtime.login({ ...ACCOUNT, openid: 'retry-account' }); - assert.equal((wire.sent[wire.sent.length - 1]!.data as Record).openid, 'retry-account'); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), - } }); - assert.equal(runtime.state.app.phase, 'logged-in'); - runtime.stop(); -}); - -for (const interruption of ['attach', 'restore', 'room-scene', 'room-event'] as const) { - test(`stop from ${interruption} releases the actual module and host without reviving runtime`, async () => { - let runtime: PlatformRuntime; - let attachedHost: GameHost | undefined; - let moduleNumber = 0; - const disposed: number[] = []; - const events: string[] = []; - const entry = makeGameEntry({ - createModule() { - const id = moduleNumber++; - return { - attach(host) { - attachedHost = host; - host.subscribe(() => { events.push('snapshot'); }); - if (interruption === 'attach') runtime.stop(); - }, - handlePlatformEvent() { - events.push('room-event'); - if (interruption === 'room-event') runtime.stop(); - }, - handleGameMessage() { events.push('game-message'); }, - restore() { - events.push('restore'); - if (interruption === 'restore') runtime.stop(); - }, - dispose() { disposed.push(id); }, - }; - }, - }); - const setup = makeRuntime(runtimeConfig(), entry); - runtime = setup.runtime; - setup.scene.onCall = name => { - if (name === 'room' && interruption === 'room-scene') runtime.stop(); - }; - await runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - runtime.login(ACCOUNT); - setup.wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - } }); - - assert.deepEqual(disposed, [0, 1], 'validation and actual room modules each dispose once'); - assert.ok(attachedHost); - const stoppedHost = attachedHost; - assert.throws(() => stoppedHost.getSnapshot(), /disposed/); - assert.throws(() => stoppedHost.subscribe(() => {}), /disposed/); - assert.equal(setup.clock.timers.size, 0); - assert.deepEqual(setup.scene.calls.filter(call => call.name === 'fatal'), []); - assert.equal(setup.wire.stopCalls, 1); - assert.deepEqual(setup.wire.lifecycle, ['subscribe', 'start', 'unsubscribe', 'stop']); - if (interruption === 'attach') assert.deepEqual(events, ['snapshot']); - if (interruption === 'room-event' || interruption === 'room-scene') { - assert.deepEqual(events, ['snapshot', 'room-event']); - } - const stoppedState = runtime.state; - const stoppedEvents = [...events]; - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.wire.emit({ type: 'message', message: { - route: 'fixture-game', rpc: 'late', data: {}, - } }); - assert.equal(runtime.state, stoppedState); - assert.deepEqual(events, stoppedEvents); - assert.throws(() => runtime.login(ACCOUNT), /stopped/); - await assert.rejects(runtime.start(), /stopped/); - runtime.stop(); - assert.deepEqual(disposed, [0, 1]); - assert.equal(setup.wire.stopCalls, 1); - }); -} - -test('stop reentered by an old module disposal prevents replacement room activation', async () => { - let runtime: PlatformRuntime; - const attached: GameHost[] = []; - let disposals = 0; - const setup = makeRuntime(runtimeConfig(), makeGameEntry({ - createModule() { - let host: GameHost | undefined; - return { - attach(value) { host = value; attached.push(value); }, - handlePlatformEvent() {}, handleGameMessage() {}, restore() {}, - dispose() { - if (host === undefined) return; - disposals += 1; - runtime.stop(); - }, - }; - }, - })); - runtime = setup.runtime; - await runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - } }); - setup.wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - } }); - assert.equal(disposals, 1); - assert.equal(attached.length, 1); - assert.throws(() => attached[0]!.getSnapshot(), /disposed/); - assert.deepEqual(setup.scene.calls.filter(call => call.name === 'fatal'), []); - assert.equal(setup.wire.stopCalls, 1); - assert.throws(() => runtime.login(ACCOUNT), /stopped/); - runtime.stop(); -}); - -test('an actual callback error after stop remains explicit without changing the stopped lifecycle', async () => { - let runtime: PlatformRuntime; - const failure = new Error('restore callback failed after stop'); - const setup = makeRuntime(runtimeConfig(), makeGameEntry({ - createModule: () => ({ - attach() {}, handlePlatformEvent() {}, handleGameMessage() {}, dispose() {}, - restore() { runtime.stop(); throw failure; }, - }), - })); - runtime = setup.runtime; - await runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - const thrown = thrownValue(() => setup.wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - } })); - assert.deepEqual((thrown as { errors: unknown[] }).errors, [failure]); - assert.deepEqual(setup.scene.calls.filter(call => call.name === 'fatal'), []); - assert.throws(() => runtime.login(ACCOUNT), /stopped/); - runtime.stop(); - assert.equal(setup.wire.stopCalls, 1); -}); - -for (const rpc of ['player_login', 'self_join_room']) { - test(`malformed successful ${rpc} still terminates explicitly`, async () => { - const { runtime, wire, scene } = makeRuntime(); - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - wire.emit({ type: 'message', message: { route: 'agent', rpc, data: { state: 0 } } }); - assert.equal(wire.stopCalls, 1); - assert.equal(scene.calls.filter(call => call.name === 'fatal').length, 1); - assert.deepEqual(scene.calls.filter(call => call.name === 'denied'), []); - runtime.stop(); - }); -} - -function makeLoggingEntry(log: string[], restored: unknown[], attached: GameHost[]): GameEntry { - let recording = false; - const entry = makeGameEntry({ - createModule: (): GameModule => { - let host: GameHost | null = null; - return { - attach(value) { - host = value; - if (!recording) return; - attached.push(value); - log.push(`attach:${value.getSnapshot().room.roomcode}`); - }, - handlePlatformEvent(event: PlatformToGameEvent) { - if (recording) log.push(`event:${event.type}`); - }, - handleGameMessage() {}, - restore(deskinfo) { - if (!recording) return; - assert.ok(host); - restored.push(deskinfo); - log.push('restore'); - }, - dispose() { - if (!recording) return; - assert.ok(host); - host.getSnapshot(); - log.push('dispose'); - }, - }; - }, - }); - Object.defineProperty(entry, 'beginRecording', { - value: () => { recording = true; }, - }); - return entry; -} - -function beginEntryRecording(entry: GameEntry): void { - (entry as GameEntry & { beginRecording(): void }).beginRecording(); -} - -interface CapturedGameHost { - host: GameHost | null; -} - -function makeCleanupFailureEntry( - log: string[], - disposeFailure: unknown, - captured: CapturedGameHost, -): GameEntry { - let moduleNumber = 0; - return makeGameEntry({ - createModule(): GameModule { - moduleNumber += 1; - const validationModule = moduleNumber === 1; - return { - attach(host) { - if (validationModule) return; - captured.host = host; - host.subscribe(() => {}); - }, - handlePlatformEvent() {}, - handleGameMessage() {}, - restore() {}, - dispose() { - if (validationModule) return; - log.push('game:dispose'); - throw disposeFailure; - }, - }; - }, - }); -} - -test('start resolves config, subscribes before connect, and never logs in without an account', async () => { - const setup = makeRuntime(); - - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - - assert.equal(setup.factoryConfigs.length, 1); - assert.equal(setup.factoryConfigs[0], setup.config); - assert.deepEqual(setup.wire.lifecycle, ['subscribe', 'start']); - assert.deepEqual(setup.wire.sent, []); - assert.equal(setup.runtime.ready, true); - assert.deepEqual(setup.scene.calls.map((call) => call.name), ['loading', 'login']); -}); - -test('showLoading failure enters terminal fatal before any startup dependency runs', async () => { - const primary = new Error('loading scene failed'); - const scene = new RecordingScene(); - const wire = new RecordingWireClient(); - let resourcesStarted = 0; - let minimumDisplayStarted = 0; - let configsResolved = 0; - let wireFactories = 0; - scene.onCall = (name) => { - if (name === 'loading') throw primary; - }; - const runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), - resolveRuntimeConfig: async () => { - configsResolved += 1; - return runtimeConfig(); - }, - createWireClient: () => { - wireFactories += 1; - return wire; - }, - scene, - loadResources: async () => { resourcesStarted += 1; }, - waitForMinimumDisplay: async () => { minimumDisplayStarted += 1; }, - getLoginDeviceSnapshot: () => DEVICE, - }); - - const thrown = await rejectedValue(runtime.start()); - - assert.equal(thrown, primary); - assert.deepEqual(scene.calls.map((call) => call.name), ['loading', 'fatal']); - assert.equal(scene.calls[1]?.value, primary); - assert.equal(resourcesStarted, 0); - assert.equal(minimumDisplayStarted, 0); - assert.equal(configsResolved, 0); - assert.equal(wireFactories, 0); - assert.deepEqual(wire.lifecycle, []); - assert.equal(runtime.ready, false); - - await assert.rejects(() => runtime.start(), /fatal/i); - assert.equal(scene.calls.filter((call) => call.name === 'fatal').length, 1); -}); - -test('showLoading and showFatal failures preserve both error identities without starting dependencies', async () => { - const primary = new Error('loading scene failed'); - const fatalSceneFailure = { step: 'scene.showFatal' }; - const scene = new RecordingScene(); - let dependencyCalls = 0; - scene.onCall = (name) => { - if (name === 'loading') throw primary; - if (name === 'fatal') throw fatalSceneFailure; - }; - const runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), - resolveRuntimeConfig: async () => { - dependencyCalls += 1; - return runtimeConfig(); - }, - createWireClient: () => { - dependencyCalls += 1; - return new RecordingWireClient(); - }, - scene, - loadResources: async () => { dependencyCalls += 1; }, - waitForMinimumDisplay: async () => { dependencyCalls += 1; }, - getLoginDeviceSnapshot: () => DEVICE, - }); - - const thrown = await rejectedValue(runtime.start()); - assert.ok(thrown instanceof Error); - const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; - - assert.equal(errors[0], primary); - assert.equal(errors[1], fatalSceneFailure); - assert.equal(dependencyCalls, 0); - assert.deepEqual(scene.calls.map((call) => call.name), ['loading', 'fatal']); - await assert.rejects(() => runtime.start(), /fatal/i); - assert.equal(scene.calls.filter((call) => call.name === 'fatal').length, 1); -}); - -test('stop while config is pending prevents every late startup continuation from reviving runtime', async () => { - const configReady = deferred(); - const resourcesReady = deferred(); - const minimumDisplayReady = deferred(); - const wire = new RecordingWireClient(); - const scene = new RecordingScene(); - let resourcesStarted = 0; - let minimumDisplayStarted = 0; - let wireFactories = 0; - const runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), - resolveRuntimeConfig: () => configReady.promise, - createWireClient() { - wireFactories += 1; - return wire; - }, - scene, - loadResources() { - resourcesStarted += 1; - return resourcesReady.promise; - }, - waitForMinimumDisplay() { - minimumDisplayStarted += 1; - return minimumDisplayReady.promise; - }, - getLoginDeviceSnapshot: () => DEVICE, - }); - - const starting = runtime.start(); - await Promise.resolve(); - assert.equal(resourcesStarted, 1); - assert.equal(minimumDisplayStarted, 1); - - runtime.stop(); - configReady.resolve(runtimeConfig()); - resourcesReady.resolve(undefined); - minimumDisplayReady.resolve(undefined); - - await assert.rejects(starting, /stopped/i); - await assert.rejects(() => runtime.start(), /stopped/i); - assert.equal(wireFactories, 0); - assert.deepEqual(wire.lifecycle, []); - assert.equal(runtime.ready, false); - assert.deepEqual(scene.calls.map((call) => call.name), ['loading']); -}); - -test('a synchronous subscribe callback that stops runtime hands the returned unsubscribe to teardown exactly once', async () => { - const unsubscribeFailure = { step: 'late-unsubscribe' }; - const wire = new SynchronousSubscribeWireClient( - { type: 'slow' }, - { value: unsubscribeFailure }, - ); - const scene = new RecordingScene(); - let runtime!: PlatformRuntime; - runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), - resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, - scene, - loadResources: async () => {}, - waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, - }); - scene.onCall = (name) => { - if (name === 'reconnect') runtime.stop(); - }; - - const thrown = await rejectedValue(runtime.start()); - assert.ok(thrown instanceof Error); - const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; - - assert.match(String(errors[0]), /stopped/i); - assert.equal(errors[1], unsubscribeFailure); - assert.equal(wire.unsubscribeCalls, 1); - assert.equal(wire.listenerAttached, false); - assert.equal(wire.stopCalls, 1); - assert.equal(wire.startCalls, 0); - - const sceneCallCount = scene.calls.length; - wire.emit({ type: 'open', server: 'ws://agent' }); - runtime.stop(); - assert.equal(scene.calls.length, sceneCallCount); - assert.equal(wire.unsubscribeCalls, 1); - assert.equal(wire.stopCalls, 1); -}); - -test('a fatal synchronous subscribe callback still adopts unsubscribe and ignores late events', async () => { - const primary = new Error('synchronous reconnect scene failed'); - const wire = new SynchronousSubscribeWireClient({ type: 'slow' }); - const scene = new RecordingScene(); - scene.onCall = (name) => { - if (name === 'reconnect') throw primary; - }; - const runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), - resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, - scene, - loadResources: async () => {}, - waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, - }); - - const thrown = await rejectedValue(runtime.start()); - - assert.equal(thrown, primary); - assert.equal(wire.unsubscribeCalls, 1); - assert.equal(wire.listenerAttached, false); - assert.equal(wire.stopCalls, 1); - assert.equal(wire.startCalls, 0); - assert.deepEqual( - scene.calls.filter((call) => call.name === 'fatal').map((call) => call.value), - [primary], - ); - - const sceneCallCount = scene.calls.length; - wire.emit({ type: 'reconnecting', server: 'ws://agent' }); - assert.equal(scene.calls.length, sceneCallCount); -}); - -for (const invalidUnsubscribe of [7, null, undefined] as const) { - test(`subscribe returning ${String(invalidUnsubscribe)} is fatal before Wire start`, async () => { - const wire = new InvalidSubscribeWireClient(invalidUnsubscribe); - const scene = new RecordingScene(); - const runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), - resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, - scene, - loadResources: async () => {}, - waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, - }); - - const thrown = await rejectedValue(runtime.start()); - - assert.ok(thrown instanceof TypeError); - assert.match(thrown.message, /subscribe.*unsubscribe.*function/i); - assert.deepEqual(wire.lifecycle, ['subscribe', 'stop']); - assert.equal(wire.stopCalls, 1); - assert.deepEqual( - scene.calls.filter((call) => call.name === 'fatal').map((call) => call.value), - [thrown], - ); - await assert.rejects(() => runtime.start(), /fatal/i); - assert.equal(scene.calls.filter((call) => call.name === 'fatal').length, 1); - }); -} - -test('subscribe startup failure enters fatal once and stops the created wire without starting it', async () => { - const setup = makeRuntime(); - const primary = new Error('subscribe failed'); - setup.wire.onAction = (name) => { - if (name === 'subscribe') throw primary; - }; - - const thrown = await rejectedValue(setup.runtime.start()); - - assert.equal(thrown, primary); - assert.deepEqual(setup.wire.lifecycle, ['subscribe', 'stop']); - assert.equal(setup.wire.stopCalls, 1); - assert.deepEqual( - setup.scene.calls.filter((call) => call.name === 'fatal').map((call) => call.value), - [primary], - ); -}); - -test('wire start failure cleans every established layer and preserves cleanup error order', async () => { - const primary = new Error('wire start failed'); - const unsubscribeFailure = { step: 'unsubscribe' }; - const stopFailure = { step: 'wire.stop' }; - const cleanupLog: string[] = []; - let moduleNumber = 0; - let activeHost: GameHost | null = null; - const entry = makeGameEntry({ - createModule(): GameModule { - moduleNumber += 1; - const currentModule = moduleNumber; - return { - attach(host) { - if (currentModule === 1) return; - activeHost = host; - host.subscribe(() => {}); - }, - handlePlatformEvent() {}, - handleGameMessage() {}, - restore() {}, - dispose() { - if (currentModule === 1) return; - cleanupLog.push('game:dispose'); - throw undefined; - }, - }; - }, - }); - const setup = makeRuntime(runtimeConfig(), entry); - setup.wire.onAction = (name) => { - if (name === 'start') { - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - setup.wire.emit({ - type: 'message', - message: { - route: 'agent', - rpc: 'player_login', - data: fixture('player-login-room.json'), - }, - }); - throw primary; - } - if (name === 'unsubscribe') { - cleanupLog.push('wire:unsubscribe'); - throw unsubscribeFailure; - } - if (name === 'stop') { - cleanupLog.push('wire:stop'); - const host = activeHost; - assert.ok(host); - assert.throws(() => host.getSnapshot(), /disposed/i); - throw stopFailure; - } - }; - - const thrown = await rejectedValue(setup.runtime.start()); - assert.ok(thrown instanceof Error); - const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; - - assert.deepEqual(cleanupLog, ['wire:unsubscribe', 'game:dispose', 'wire:stop']); - assert.equal(errors.length, 4); - assert.equal(errors[0], primary); - assert.equal(errors[1], unsubscribeFailure); - assert.equal(errors[2], undefined); - assert.equal(errors[3], stopFailure); - assert.equal(setup.scene.calls.filter((call) => call.name === 'fatal').length, 1); - assert.equal( - setup.scene.calls.find((call) => call.name === 'fatal')?.value, - thrown, - ); - - const sceneCalls = setup.scene.calls.length; - setup.wire.emit({ type: 'reconnecting', server: 'ws://agent' }); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - assert.equal(setup.scene.calls.length, sceneCalls); - assert.equal(setup.wire.stopCalls, 1); -}); - -for (const failedGate of ['resources', 'minimum-display'] as const) { - test(`${failedGate} failure after Wire start tears down once and stays fatal`, async () => { - const resourcesReady = deferred(); - const minimumDisplayReady = deferred(); - const wireStarted = deferred(); - const primary = new Error(`${failedGate} failed`); - const setup = makeRuntime(); - const runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), - resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => setup.wire, - scene: setup.scene, - loadResources: () => resourcesReady.promise, - waitForMinimumDisplay: () => minimumDisplayReady.promise, - getLoginDeviceSnapshot: () => DEVICE, - clock: setup.clock, - }); - setup.wire.onAction = (name) => { - if (name === 'start') wireStarted.resolve(undefined); - }; - - const starting = runtime.start(); - await wireStarted.promise; - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - if (failedGate === 'resources') { - resourcesReady.reject(primary); - minimumDisplayReady.resolve(undefined); - } else { - resourcesReady.resolve(undefined); - minimumDisplayReady.reject(primary); - } - - const thrown = await rejectedValue(starting); - assert.equal(thrown, primary); - assert.deepEqual(setup.wire.lifecycle, ['subscribe', 'start', 'unsubscribe', 'stop']); - assert.equal(setup.wire.stopCalls, 1); - assert.deepEqual( - setup.scene.calls.filter((call) => call.name === 'fatal').map((call) => call.value), - [primary], - ); - - const sceneCalls = setup.scene.calls.length; - setup.wire.emit({ type: 'reconnecting', server: 'ws://agent' }); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - assert.equal(setup.scene.calls.length, sceneCalls); - }); -} - -test('login sends one exact request and an ordinary reopen resends the same data with a four-second guard', async () => { - const setup = makeRuntime(); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - - setup.runtime.login(ACCOUNT); - - assert.deepEqual(setup.wire.sent[0], { - app: 'youle', - route: 'agent', - rpc: 'player_login', - data: { - agentid: 'veRa0qrBf', gameid: '41', openid: 'o1', nickname: '测试号', - avatar: 'http://a', sex: 0, province: '江西', city: '南昌', - unionid: 'test_unionid', version: 10000, channelid: 'FtJf073aa', - marketid: 4, ip: '127.0.0.1', location: DEVICE.location, - machineid: 'machine-1', machineroom: 'room-install-1', - }, - }); - assert.deepEqual([...setup.clock.timers.values()].map((timer) => timer.ms), [4000]); - - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, - }); - assert.equal(setup.clock.timers.size, 0); - - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - - assert.equal(setup.wire.sent.length, 2); - assert.equal(setup.wire.sent[1]?.rpc, 'player_login'); - assert.equal(setup.wire.sent[1]?.data, setup.wire.sent[0]?.data); - assert.deepEqual([...setup.clock.timers.values()].map((timer) => timer.ms), [4000]); - setup.clock.runOnlyTimer(); - assert.equal(setup.wire.reconnectCalls, 1); -}); - -for (const mismatch of [ - { label: 'different value', configGameId: '42', entryGameId: '41' }, - { label: 'different type', configGameId: '41', entryGameId: 41 }, -] as const) { - test(`gameid ${mismatch.label} fails fatally before WireClient creation`, async () => { - const setup = makeRuntime( - runtimeConfig(mismatch.configGameId), - makeGameEntry({ gameId: mismatch.entryGameId }), - ); - - await assert.rejects(() => setup.runtime.start(), /gameid/i); - await assert.rejects(() => setup.runtime.start(), /fatal/i); - - assert.equal(setup.factoryConfigs.length, 0); - assert.deepEqual(setup.wire.lifecycle, []); - const fatalCalls = setup.scene.calls.filter((call) => call.name === 'fatal'); - assert.equal(fatalCalls.length, 1); - assert.ok(fatalCalls[0]?.value instanceof Error); - }); -} - -for (const switchCase of [ - { - label: 'room', - route: 'room', - rpc: 'connect_roomserver', - target: 'ws://room.example:3088', - data: { roomserver: 'ws://room.example:3088', epoch: 7, nested: { trace: 'r' } }, - }, - { - label: 'agent', - route: 'agent', - rpc: 'connect_agentserver', - target: 'wss://agent.example/socket', - data: { - agentserver: 'wss://agent.example/socket', - opt: 'free_room', - nested: { trace: 'a' }, - }, - }, -] as const) { - test(`${switchCase.label} switch resends the exact switch envelope before returning to steady login`, async () => { - const setup = makeRuntime(); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - const loginEnvelope = setup.wire.sent[0]!; - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, - }); - setup.wire.sent.length = 0; - - setup.wire.emit({ - type: 'message', - message: { route: switchCase.route, rpc: switchCase.rpc, data: switchCase.data }, - }); - - assert.deepEqual(setup.wire.switchCalls, [switchCase.target]); - assert.equal(setup.wire.sent.length, 0); - - setup.wire.emit({ type: 'open', server: switchCase.target }); - - assert.equal(setup.wire.sent.length, 1); - assert.equal(setup.wire.sent[0]?.rpc, switchCase.rpc); - assert.deepEqual(setup.wire.sent[0], { - app: 'youle', - route: switchCase.route, - rpc: switchCase.rpc, - data: switchCase.data, - }); - assert.equal(setup.wire.sent[0]?.data, switchCase.data); - assert.equal(setup.clock.timers.size, 0, 'switch resend must not start the login guard'); - - setup.wire.emit({ type: 'open', server: switchCase.target }); - - assert.equal(setup.wire.sent.length, 2); - assert.equal(setup.wire.sent[1]?.rpc, 'player_login'); - assert.equal(setup.wire.sent[1]?.data, loginEnvelope.data); - assert.deepEqual([...setup.clock.timers.values()].map((timer) => timer.ms), [4000]); - }); -} - -test('successful login without a room shows the lobby', async () => { - const setup = makeRuntime(); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, - }); - - assert.equal(setup.runtime.state.room.kind, 'outside'); - assert.equal(setup.scene.calls[setup.scene.calls.length - 1]?.name, 'lobby'); -}); - -test('room login commits and opens before showRoom, then restores the exact deskinfo', async () => { - const log: string[] = []; - const restored: unknown[] = []; - const attached: GameHost[] = []; - const entry = makeLoggingEntry(log, restored, attached); - const setup = makeRuntime(runtimeConfig(), entry); - setup.scene.onCall = (name) => { if (name === 'room') log.push('scene:room'); }; - await setup.runtime.start(); - beginEntryRecording(entry); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - const roomLogin = fixture('player-login-room.json'); - - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: roomLogin }, - }); - - assert.equal(setup.runtime.state.room.kind, 'inside'); - assert.deepEqual(log, [ - 'attach:100001', - 'event:room.entered', - 'scene:room', - 'restore', - ]); - assert.deepEqual(restored, [roomLogin.deskinfo]); - assert.equal(restored[0], roomLogin.deskinfo); - assert.equal(attached.length, 1); -}); - -for (const withDeskinfo of [false, true]) { - test(`self join opens and shows the room ${withDeskinfo ? 'before restoring deskinfo' : 'without restoring absent deskinfo'}`, async () => { - const log: string[] = []; - const restored: unknown[] = []; - const attached: GameHost[] = []; - const entry = makeLoggingEntry(log, restored, attached); - const setup = makeRuntime(runtimeConfig(), entry); - setup.scene.onCall = (name) => { if (name === 'room') log.push('scene:room'); }; - await setup.runtime.start(); - beginEntryRecording(entry); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, - }); - log.length = 0; - const join = fixture('self-join-room.json'); - const deskinfo = join.deskinfo; - if (!withDeskinfo) delete join.deskinfo; - - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'self_join_room', data: join }, - }); - - assert.deepEqual(log, withDeskinfo - ? ['attach:100001', 'event:room.entered', 'scene:room', 'restore'] - : ['attach:100001', 'event:room.entered', 'scene:room']); - assert.deepEqual(restored, withDeskinfo ? [deskinfo] : []); - if (withDeskinfo) assert.equal(restored[0], deskinfo); - }); -} - -test('slow and reconnecting show reconnect without clearing or replacing room state', async () => { - const setup = makeRuntime(); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, - }); - const roomState = setup.runtime.state; - const callsBefore = setup.scene.calls.length; - - setup.wire.emit({ type: 'slow' }); - assert.equal(setup.runtime.state.app.phase, 'slow'); - setup.wire.emit({ type: 'reconnecting', server: 'ws://agent' }); - - assert.equal(setup.runtime.state.app.phase, 'reconnecting'); - assert.equal(setup.runtime.state.room, roomState.room); - assert.equal(setup.runtime.state.players, roomState.players); - assert.equal(setup.runtime.state.room.kind, 'inside'); - assert.deepEqual( - setup.scene.calls.slice(callsBefore).map((call) => call.name), - ['reconnect', 'reconnect'], - ); -}); - -test('kick_offline bypasses pending login and stops late navigation', async () => { - const {runtime,wire,scene}=makeRuntime(); - await runtime.start();wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT); - const data={fromOther:true,gameid:'another-game'}; - wire.emit({type:'message',message:{route:'agent',rpc:'kick_offline',data}}); - assert.equal(scene.calls.at(-1)?.name,'kicked');assert.equal(scene.calls.at(-1)?.value,data); - assert.equal(wire.stopCalls,1); - const count=scene.calls.length;wire.emit({type:'open',server:'ws://agent'}); - assert.equal(scene.calls.length,count); -}); - -test('show_message preserves text and legacy numeric timeout without navigating', async () => { - const {runtime,wire,scene}=makeRuntime(); - await runtime.start();wire.emit({type:'open',server:'ws://agent'}); +test('create wire outcome enters only on success and a repeated success leaves the module mounted',async()=>{ + let attachments=0,disposals=0; + const entry=makeGameEntry({resolveSeatCount:()=>10,createModule:()=>({ + attach(){attachments++;},handlePlatformEvent(){},handleGameMessage(){},restore(){},dispose(){disposals++;}, + })}); + const {runtime,wire,scene}=makeRuntime(runtimeConfig(),entry); + await runtime.start();attachments=0;disposals=0;wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT); + wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data:fixture('player-login-success.json')}}); + runtime.createRoom({roomtype:[1,4],location:null,ip:'127.0.0.1'}); + assert.equal(wire.sent[wire.sent.length-1]?.rpc,'create_room'); const before=runtime.state; - for(const time of ['3000','0','-1']){ - wire.emit({type:'message',message:{route:'agent',rpc:'show_message',data:{msg:'服务器提示#第二行',time}}}); - assert.deepEqual(scene.calls.at(-1),{name:'tips',value:{message:'服务器提示#第二行',time:Number(time)}}); - assert.equal(runtime.state,before); - } - runtime.stop(); -}); - -test('kick closes the game before showing kicked and stopping the wire', async () => { - const log: string[] = []; - const restored: unknown[] = []; - const attached: GameHost[] = []; - const entry = makeLoggingEntry(log, restored, attached); - const setup = makeRuntime(runtimeConfig(), entry); - setup.scene.onCall = (name) => { if (name === 'kicked') log.push('scene:kicked'); }; - setup.wire.onAction = (name) => { if (name === 'stop') log.push('wire:stop'); }; - await setup.runtime.start(); - beginEntryRecording(entry); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, - }); - log.length = 0; - const kick = { msg: '检查到新版本', code: 1 }; - - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'kick_server', data: kick }, - }); - - assert.deepEqual(log, ['dispose', 'scene:kicked', 'wire:stop']); - assert.equal(setup.wire.stopCalls, 1); - assert.equal(setup.scene.calls[setup.scene.calls.length - 1]?.value, kick); - - const sceneCalls = setup.scene.calls.length; - setup.wire.emit({ type: 'reconnecting', server: 'ws://agent' }); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - assert.equal(setup.scene.calls.length, sceneCalls, 'late wire events stay silent after kick'); - assert.equal(setup.wire.sent.length, 1, 'late open never revives login after kick'); -}); - -test('a game rpc named like a platform rpc cannot drive platform scenes', async () => { - const setup = makeRuntime(); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, - }); - const sceneCalls = setup.scene.calls.length; - - setup.wire.emit({ - type: 'message', - message: { route: 'fixture-game', rpc: 'self_exit_room', data: { reason: 'game-owned' } }, - }); - - assert.equal(setup.runtime.state.room.kind, 'inside'); - assert.equal(setup.scene.calls.length, sceneCalls); -}); - -test('login gate routes each ignored room message once with zero runtime side effects', async () => { - const originalDispatch = Router.prototype.dispatch; - let routerDispatches = 0; - Router.prototype.dispatch = function dispatchWithCount(message) { - routerDispatches += 1; - return originalDispatch.call(this, message); - }; - - try { - const log: string[] = []; - const entry = makeLoggingEntry(log, [], []); - const setup = makeRuntime(runtimeConfig(), entry); - await setup.runtime.start(); - beginEntryRecording(entry); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - const stateBefore = setup.runtime.state; - const sceneCallsBefore = setup.scene.calls.length; - const lifecycleBefore = [...setup.wire.lifecycle]; - - setup.wire.emit({ - type: 'message', - message: { route: 'room', rpc: 'self_exit_room', data: { seat: 1 } }, - }); - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'self_join_room', data: fixture('self-join-room.json') }, - }); - - assert.equal(routerDispatches, 2); - assert.equal(setup.runtime.state, stateBefore); - assert.deepEqual(log, []); - assert.equal(setup.scene.calls.length, sceneCallsBefore); - assert.equal(setup.scene.calls.filter((call) => call.name === 'fatal').length, 0); - assert.deepEqual(setup.wire.lifecycle, lifecycleBefore); - assert.equal(setup.wire.stopCalls, 0); - } finally { - Router.prototype.dispatch = originalDispatch; - } -}); - -test('a handled no-op room message cannot drive a scene without a state transition', async () => { - const setup = makeRuntime(); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, - }); - const stateBefore = setup.runtime.state; - const sceneCallsBefore = setup.scene.calls.length; - - setup.wire.emit({ - type: 'message', - message: { route: 'room', rpc: 'self_exit_room', data: {} }, - }); - - assert.equal(setup.runtime.state, stateBefore); - assert.equal(setup.scene.calls.length, sceneCallsBefore); - assert.equal(setup.scene.calls.filter((call) => call.name === 'fatal').length, 0); -}); - -test('joinRoom, prepare, and exitRoom delegate through the authoritative platform commands', async () => { - const setup = makeRuntime(); - await setup.runtime.start(); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') }, - }); - setup.wire.sent.length = 0; - const location = { latitude: 28.7, longitude: 115.8 }; - - setup.runtime.joinRoom({ - roomcode: '100001', - location, - ip: '203.0.113.7', - vipMatch: 1, - match_id: 'match-9', - }); - - assert.deepEqual(setup.wire.sent, [{ - app: 'youle', - route: 'agent', - rpc: 'self_join_room', - data: { - agentid: 'veRa0qrBf', - playerid: 430511, - gameid: '41', - roomcode: '100001', - location, - ip: '203.0.113.7', - vipMatch: 1, - match_id: 'match-9', - }, - }]); - assert.equal((setup.wire.sent[0]!.data as { location: unknown }).location, location); - - const joinedRoom = fixture('self-join-room.json'); - delete joinedRoom.deskinfo; - joinedRoom.deskwar = 0; - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'self_join_room', data: joinedRoom }, - }); - setup.wire.sent.length = 0; - setup.runtime.prepare(); - setup.runtime.exitRoom(); - - assert.deepEqual(setup.wire.sent, [ - { - app: 'youle', - route: 'room', - rpc: 'player_prepare', - data: { - agentid: 'veRa0qrBf', playerid: 430511, gameid: '41', roomcode: '100001', - }, - }, - { - app: 'youle', - route: 'room', - rpc: 'self_exit_room', - data: { - agentid: 'veRa0qrBf', playerid: 430511, gameid: '41', roomcode: '100001', - }, - }, - ]); -}); - -test('stop unsubscribes, disposes while the host is live, invalidates it, then stops the wire', async () => { - const log: string[] = []; - const restored: unknown[] = []; - const attached: GameHost[] = []; - const entry = makeLoggingEntry(log, restored, attached); - const setup = makeRuntime(runtimeConfig(), entry); - setup.wire.onAction = (name) => { - if (name === 'unsubscribe' || name === 'stop') log.push(`wire:${name}`); - }; - await setup.runtime.start(); - beginEntryRecording(entry); - setup.wire.emit({ type: 'open', server: 'ws://agent' }); - setup.runtime.login(ACCOUNT); - setup.wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, - }); - const activeHost = attached[0]!; - log.length = 0; - - setup.runtime.stop(); - - assert.deepEqual(log, ['wire:unsubscribe', 'dispose', 'wire:stop']); - assert.throws(() => activeHost.getSnapshot(), /disposed/i); - setup.runtime.stop(); - assert.deepEqual(log, ['wire:unsubscribe', 'dispose', 'wire:stop']); - assert.equal(setup.wire.stopCalls, 1); -}); - -test('stop continues after clearTimeout and every later cleanup failure in exact order', async () => { - const clearFailure = { step: 'clock.clearTimeout' }; - const disposeFailure = { step: 'game.dispose' }; - const leaseFailure = { step: 'lease.invalidate' }; - const stopFailure = { step: 'wire.stop' }; - const log: string[] = []; - const captured: CapturedGameHost = { host: null }; - const clock = new ControlledClearClock(); - const wire = new RecordingWireClient(); - const scene = new RecordingScene(); - const originalStoreSubscribe = PlatformStore.prototype.subscribe; - PlatformStore.prototype.subscribe = function subscribeWithFailingRelease(listener) { - const release = originalStoreSubscribe.call(this, listener); - return () => { - release(); - log.push('lease:invalidate'); - throw leaseFailure; - }; - }; - - try { - const runtime = new PlatformRuntime({ - gameEntry: makeCleanupFailureEntry(log, disposeFailure, captured), - resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, - scene, - loadResources: async () => {}, - waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, - clock, - }); - clock.onFailingClear = () => { log.push('clock:clear'); }; - wire.onAction = (name) => { - if (name === 'unsubscribe') { - log.push('wire:unsubscribe'); - throw undefined; - } - if (name === 'stop') { - log.push('wire:stop'); - const host = captured.host; - assert.ok(host); - assert.throws(() => host.getSnapshot(), /disposed/i); - throw stopFailure; - } - }; - - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - runtime.login(ACCOUNT); - wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, - }); - wire.emit({ type: 'open', server: 'ws://agent' }); - assert.equal(clock.timers.size, 1); - clock.enableClearFailure(clearFailure); - log.length = 0; - - const thrown = thrownValue(() => { runtime.stop(); }); - assert.ok(thrown instanceof Error); - const errors = (thrown as Error & { readonly errors: readonly unknown[] }).errors; - assert.deepEqual(log, [ - 'clock:clear', - 'wire:unsubscribe', - 'game:dispose', - 'lease:invalidate', - 'wire:stop', - ]); - assert.equal(errors.length, 5); - assert.equal(errors[0], clearFailure); - assert.equal(errors[1], undefined); - assert.equal(errors[2], disposeFailure); - assert.equal(errors[3], leaseFailure); - assert.equal(errors[4], stopFailure); - assert.equal(scene.calls.filter((call) => call.name === 'fatal').length, 0); - - const completedLog = [...log]; - assert.doesNotThrow(() => { runtime.stop(); }); - assert.deepEqual(log, completedLog); - assert.equal(wire.stopCalls, 1); - } finally { - PlatformStore.prototype.subscribe = originalStoreSubscribe; - } -}); - -test('kick completes close, invalidate, clear, kicked scene, and wire stop despite all failures', async () => { - const disposeFailure = { step: 'game.dispose' }; - const leaseFailure = { step: 'lease.invalidate' }; - const clearFailure = { step: 'clock.clearTimeout' }; - const stopFailure = { step: 'wire.stop' }; - const log: string[] = []; - const captured: CapturedGameHost = { host: null }; - const clock = new ControlledClearClock(); - const wire = new RecordingWireClient(); - const scene = new RecordingScene(); - const originalStoreSubscribe = PlatformStore.prototype.subscribe; - PlatformStore.prototype.subscribe = function subscribeWithFailingRelease(listener) { - const release = originalStoreSubscribe.call(this, listener); - return () => { - release(); - log.push('lease:invalidate'); - throw leaseFailure; - }; - }; - - try { - const runtime = new PlatformRuntime({ - gameEntry: makeCleanupFailureEntry(log, disposeFailure, captured), - resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, - scene, - loadResources: async () => {}, - waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, - clock, - }); - clock.onFailingClear = () => { log.push('clock:clear'); }; - scene.onCall = (name) => { - if (name === 'kicked') { - log.push('scene:kicked'); - throw undefined; - } - if (name === 'fatal') log.push('scene:fatal'); - }; - wire.onAction = (name) => { - if (name === 'unsubscribe') log.push('wire:unsubscribe'); - if (name === 'stop') { - log.push('wire:stop'); - const host = captured.host; - assert.ok(host); - assert.throws(() => host.getSnapshot(), /disposed/i); - throw stopFailure; - } - }; - - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - runtime.login(ACCOUNT); - wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }, - }); - wire.emit({ type: 'open', server: 'ws://agent' }); - assert.equal(clock.timers.size, 1); - clock.enableClearFailure(clearFailure); - log.length = 0; - const kick = { msg: 'terminal cleanup test', code: 1 }; - - wire.emit({ - type: 'message', - message: { route: 'agent', rpc: 'kick_server', data: kick }, - }); - - assert.deepEqual(log, [ - 'game:dispose', - 'lease:invalidate', - 'clock:clear', - 'scene:kicked', - 'wire:stop', - 'wire:unsubscribe', - 'scene:fatal', - ]); - assert.equal(wire.stopCalls, 1); - assert.equal(scene.calls.filter((call) => call.name === 'kicked').length, 1); - assert.equal(scene.calls.find((call) => call.name === 'kicked')?.value, kick); - const fatal = scene.calls.find((call) => call.name === 'fatal')?.value; - assert.ok(fatal instanceof Error); - const sessionErrors = (fatal as Error & { readonly errors: readonly unknown[] }).errors; - assert.equal(sessionErrors.length, 3); - assert.equal(sessionErrors[0], disposeFailure); - assert.equal(sessionErrors[1], leaseFailure); - const kickCleanup = sessionErrors[2]; - assert.ok(kickCleanup instanceof Error); - const kickErrors = ( - kickCleanup as Error & { readonly errors: readonly unknown[] } - ).errors; - assert.equal(kickErrors.length, 3); - assert.equal(kickErrors[0], clearFailure); - assert.equal(kickErrors[1], undefined); - assert.equal(kickErrors[2], stopFailure); - - const completedLog = [...log]; - assert.doesNotThrow(() => { runtime.stop(); }); - assert.doesNotThrow(() => { runtime.stop(); }); - assert.deepEqual(log, completedLog); - assert.equal(wire.stopCalls, 1); - } finally { - PlatformStore.prototype.subscribe = originalStoreSubscribe; - } -}); - -test('real runtime renders the room before restoring canonical deskinfo', async () => { - const events: string[] = []; - const wire = new RecordingWireClient(); - const controller = new PlatformUiController({ - render: model => { events.push(`render:${model.page}`); }, - activate: page => { events.push(`activate:${page}`); }, - showOverlay: model => { events.push(`overlay:${model.kind}`); }, - }, { request: () => () => {} }, error => { throw error; }); - const deskinfo = { fixture: 'opaque' }; - const entry = makeGameEntry({ createModule: () => ({ - attach() { events.push('attach'); }, - handlePlatformEvent() {}, handleGameMessage() {}, dispose() {}, - restore(value) { assert.equal(value, deskinfo); events.push('restore'); }, - }) }); - const runtime = new PlatformRuntime({ - gameEntry: entry, resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, scene: controller, - loadResources: async () => {}, waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), - }); - controller.connect(runtime); - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - controller.login(ACCOUNT); - wire.emit({ type: 'message', message: { route: 'agent', rpc: 'player_login', - data: fixture('player-login-success.json') } }); - controller.joinRoom({ roomcode: '100001', location: null, ip: '127.0.0.1' }); - wire.emit({ type: 'message', message: { route: 'agent', rpc: 'self_join_room', - data: { ...fixture('self-join-room.json'), deskinfo } } }); - assert.ok(events.includes('render:lobby')); - assert.ok(events.indexOf('attach') < events.indexOf('render:room')); - assert.ok(events.indexOf('render:room') < events.indexOf('activate:room')); - assert.ok(events.indexOf('activate:room') < events.indexOf('restore')); - controller.dispose(); runtime.stop(); - assert.equal(wire.stopCalls, 1); -}); - -test('real runtime keeps canonical roots and raw denials while login and join retries succeed', async () => { - const wire = new RecordingWireClient(); - const overlays: unknown[] = []; - const controller = new PlatformUiController({ - render: () => {}, activate: () => {}, - showOverlay: model => { overlays.push(model); }, - }, { request: () => () => {} }, error => { throw error; }); - const runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, scene: controller, - loadResources: async () => {}, waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), - }); - controller.connect(runtime); - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - - const preLoginRoot = runtime.state; - controller.login(ACCOUNT); - const loginDenial = { state: 7, showerror: 1, error: 'Login denied' }; - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: loginDenial, - } }); - assert.equal(runtime.state, preLoginRoot); - const loginOverlay = overlays[overlays.length - 1] as { kind: string; outcome: { data: unknown } }; - assert.equal(loginOverlay.kind, 'denial'); - assert.equal(loginOverlay.outcome.data, loginDenial); - - const retryAccount = { ...ACCOUNT, openid: 'retry-account' }; - controller.login(retryAccount); - assert.equal((wire.sent[wire.sent.length - 1]!.data as Record).openid, 'retry-account'); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), - } }); - const lobbyRoot = runtime.state; - - const firstJoin = { roomcode: '999999', location: null, ip: '127.0.0.1' }; - controller.joinRoom(firstJoin); - const joinDenial = { state: 99, roomcode: '999999', showerror: 1, error: 'No room' }; - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'self_join_room', data: joinDenial, - } }); - assert.equal(runtime.state, lobbyRoot); - const joinOverlay = overlays[overlays.length - 1] as { kind: string; outcome: { data: unknown } }; - assert.equal(joinOverlay.kind, 'denial'); - assert.equal(joinOverlay.outcome.data, joinDenial); - - controller.joinRoom({ roomcode: '100001', location: null, ip: '127.0.0.1' }); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'self_join_room', data: fixture('self-join-room.json'), - } }); - assert.equal(runtime.state.room.kind, 'inside'); - assert.equal((overlays[overlays.length - 1] as { kind: string }).kind, 'none'); - controller.dispose(); runtime.stop(); -}); - -test('real runtime restores reconnect overlay and silences UI after kick or fatal', async () => { - for (const terminal of ['kick', 'fatal'] as const) { - const wire = new RecordingWireClient(); - const events: string[] = []; - const controller = new PlatformUiController({ - render: model => { events.push(`render:${model.page}`); }, - activate: page => { events.push(`activate:${page}`); }, - showOverlay: model => { events.push(`overlay:${model.kind}`); }, - }, { request: () => () => {} }, error => { throw error; }); - const runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, scene: controller, - loadResources: async () => {}, waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), - }); - controller.connect(runtime); - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - controller.login(ACCOUNT); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), - } }); - - wire.emit({ type: 'slow' }); - assert.equal(events[events.length - 1], 'overlay:reconnect'); - const activations = events.filter(event => event.startsWith('activate:')).length; - wire.emit({ type: 'open', server: 'ws://agent' }); - assert.equal(events[events.length - 1], 'overlay:none'); - assert.equal(events.filter(event => event.startsWith('activate:')).length, activations); - - if (terminal === 'kick') { - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'kick_server', data: { code: 1, msg: 'terminal' }, - } }); - assert.equal(events[events.length - 1], 'overlay:kicked'); - } else { - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: { state: 0 }, - } }); - assert.equal(events[events.length - 1], 'overlay:fatal'); - } - const terminalEvents = [...events]; - assert.throws(() => controller.login(ACCOUNT), /terminal/); - wire.emit({ type: 'slow' }); - wire.emit({ type: 'open', server: 'ws://agent' }); - assert.deepEqual(events, terminalEvents); - assert.equal(wire.stopCalls, 1); - controller.dispose(); runtime.stop(); - } -}); - -test('real runtime alone governs login readiness while resources are pending', async () => { - const resourcesReady = deferred(); - const wire = new RecordingWireClient(); - const controller = new PlatformUiController({ - render: () => {}, activate: () => {}, showOverlay: () => {}, - }, { request: () => () => {} }, error => { throw error; }); - const runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, scene: controller, - loadResources: () => resourcesReady.promise, waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), - }); - controller.connect(runtime); - const starting = runtime.start(); - await Promise.resolve(); - await Promise.resolve(); - assert.deepEqual(wire.lifecycle, ['subscribe', 'start']); - wire.emit({ type: 'open', server: 'ws://agent' }); - assert.equal(runtime.ready, false); - assert.throws(() => controller.login(ACCOUNT), /login page/); - assert.equal(wire.sent.length, 0); - - resourcesReady.resolve(undefined); - await starting; - assert.equal(runtime.ready, true); - controller.login(ACCOUNT); - assert.equal(wire.sent.length, 1); - controller.dispose(); runtime.stop(); -}); - -for (const phase of ['render', 'activate'] as const) { - test(`real room ${phase} disposal interrupts restore and terminates the runtime`, async () => { - const wire = new RecordingWireClient(); - let armed = false; - let restores = 0; - let controller!: PlatformUiController; - controller = new PlatformUiController({ - render: model => { - if (armed && phase === 'render' && model.page === 'room') controller.dispose(); - }, - activate: page => { - if (armed && phase === 'activate' && page === 'room') controller.dispose(); - }, - showOverlay: () => {}, - }, { request: () => () => {} }, error => { throw error; }); - const runtime = new PlatformRuntime({ - gameEntry: makeGameEntry({ createModule: () => ({ - attach() {}, handlePlatformEvent() {}, handleGameMessage() {}, dispose() {}, - restore() { restores += 1; }, - }) }), - resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, scene: controller, - loadResources: async () => {}, waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), - }); - controller.connect(runtime); - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - controller.login(ACCOUNT); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), - } }); - armed = true; - controller.joinRoom({ roomcode: '100001', location: null, ip: '127.0.0.1' }); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'self_join_room', data: fixture('self-join-room.json'), - } }); - - assert.equal(restores, 0); - assert.equal(wire.stopCalls, 1); - assert.throws(() => runtime.login(ACCOUNT), error => { - assert.ok(error instanceof Error); - const errors = (error as Error & { readonly errors: readonly unknown[] }).errors; - assert.match(String(errors[0]), /lifecycle interrupted/); - return true; - }); - runtime.stop(); - }); -} - -test('application overlay teardown disposes Presenter, stops Runtime, and invalidates a late frame', async () => { - const wire = new RecordingWireClient(); - const frames: Array<{ callback: () => void; cancelled: boolean }> = []; - const renders: string[] = []; - let armed = false; - let runtime!: PlatformRuntime; - let controller!: PlatformUiController; - controller = new PlatformUiController({ - render: model => { renders.push(model.page); }, - activate: () => {}, - showOverlay: model => { - if (armed && model.kind === 'reconnect') { - controller.dispose(); - runtime.stop(); - } - }, - }, { request: callback => { - const frame = { callback, cancelled: false }; - frames.push(frame); - return () => { frame.cancelled = true; }; - } }, error => { throw error; }); - runtime = new PlatformRuntime({ - gameEntry: makeGameEntry(), resolveRuntimeConfig: async () => runtimeConfig(), - createWireClient: () => wire, scene: controller, - loadResources: async () => {}, waitForMinimumDisplay: async () => {}, - getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), - }); - controller.connect(runtime); - await runtime.start(); - wire.emit({ type: 'open', server: 'ws://agent' }); - controller.login(ACCOUNT); - wire.emit({ type: 'message', message: { - route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), - } }); - armed = true; - const framesBeforeStop = frames.length; - - wire.emit({ type: 'slow' }); - - assert.equal(frames.length, framesBeforeStop + 1); - const lateFrame = frames[frames.length - 1]!; - assert.equal(lateFrame.cancelled, true); - assert.equal(wire.stopCalls, 1); - const rendered = [...renders]; - lateFrame.callback(); - wire.emit({ type: 'open', server: 'ws://agent' }); - assert.deepEqual(renders, rendered); - assert.throws(() => runtime.login(ACCOUNT), /stopped/); - runtime.stop(); -}); + wire.emit({type:'message',message:{route:'agent',rpc:'create_room',data:{state:1,showerror:1,error:'denied'}}}); + assert.equal(runtime.state,before);assert.equal(attachments,0); + assert.equal(scene.calls.some(call=>call.name==='room'||call.name==='fatal'),false); + runtime.createRoom({roomtype:[1,4],location:null,ip:'127.0.0.1'}); + const data={state:0,roomcode:'123456',seat:0,roomtype:[1,4],needprepare:1,infinite:0}; + wire.emit({type:'message',message:{route:'agent',rpc:'create_room',data}}); + wire.emit({type:'message',message:{route:'agent',rpc:'create_room',data:{...data}}}); + assert.equal(attachments,1);assert.equal(disposals,0); + assert.equal(scene.calls.filter(call=>call.name==='room').length,1); + assert.equal(runtime.state.room.kind,'inside'); + if(runtime.state.room.kind==='inside')assert.equal(runtime.state.room.seatPlayerIds.length,10); + runtime.stop();assert.equal(disposals,1); +}); diff --git a/cocoscreator_projects/framework-tests/presentation/announcement.test.ts b/cocoscreator_projects/framework-tests/presentation/announcement.test.ts new file mode 100644 index 0000000..b61b5de --- /dev/null +++ b/cocoscreator_projects/framework-tests/presentation/announcement.test.ts @@ -0,0 +1,17 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { announcementMotion } from '../../YouleNexus/assets/framework/presentation/lobby-model.ts'; + +test('announcement follows source byte width, speed, right entry and looping left exit',()=>{ + const initial=announcementMotion('公告ABC',645,0); + assert.equal(initial.textWidth,105); // gblen=7, Game_Config.Notice.width=15 + assert.equal(initial.duration,7.5); // source speed=0.1 px/ms + assert.equal(initial.left,645); + assert.equal(announcementMotion('公告ABC',645,1).left,545); + assert.ok(announcementMotion('公告ABC',645,7.49).left < -103); + assert.equal(announcementMotion('公告ABC',645,7.5).left,645); + assert.equal(announcementMotion('公告ABC',645,15).left,645); + assert.equal(announcementMotion('',645,0).visible,false); + assert.equal(announcementMotion(' ',645,0).visible,true); + assert.throws(()=>announcementMotion('公告',0,0),/width/); +}); diff --git a/cocoscreator_projects/framework-tests/presentation/lobby-menu.test.ts b/cocoscreator_projects/framework-tests/presentation/lobby-menu.test.ts index 546a7ef..53e5b25 100644 --- a/cocoscreator_projects/framework-tests/presentation/lobby-menu.test.ts +++ b/cocoscreator_projects/framework-tests/presentation/lobby-menu.test.ts @@ -1,84 +1,98 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; -import vm from 'node:vm'; -import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts'; -import { resolveLobbyMenuConfig, lobbyMenuState, lobbyTrayState, LOBBY_MENU_ACTIONS, LOBBY_UNAVAILABLE } from '../../YouleNexus/assets/framework/presentation/lobby-menu.ts'; - -const identity={agentid:'a',gameid:'g',channelid:'c',marketid:'m',version:1}; -const remote=(data:Record)=>createRemoteConfig(data,identity); -const source=readFileSync(new URL('../../../projects/Game_Surface_3/js/00_Surface/11_GameUI.js',import.meta.url),'utf8'); +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import vm from 'node:vm'; +import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts'; +import { resolveLobbyMenuConfig, lobbyMenuState, lobbyTrayState, LOBBY_MENU_ACTIONS, LOBBY_UNAVAILABLE, lobbyLockedTip } from '../../YouleNexus/assets/framework/presentation/lobby-menu.ts'; + +const identity={agentid:'a',gameid:'g',channelid:'c',marketid:'m',version:1}; +const remote=(data:Record)=>createRemoteConfig(data,identity); +const source=readFileSync(new URL('../../../projects/Game_Surface_3/js/00_Surface/11_GameUI.js',import.meta.url),'utf8'); const ids=[11,12,13,14,17,7,234,483,568,286,287,10,508,496,509,3233,3232,3256,3264,76,3139,679,99,3120,3161,3162,3016,3018]; -function original(config:ReturnType,versionState:number,launchMode:number) { - const visible:Record=Object.fromEntries(ids.map(id=>[id,true])); - const left:Record={}; - const noop=()=>{}; - const ui=new Proxy({onstate:[],isprepare:[]} as Record,{get:(o,k)=>k in o?o[k as string]:noop}); - const context=vm.createContext({GameUI:ui,GameData:{...config,Scene:1,versionState,LaunchMode:launchMode, - hallConfig:config.hall,sysConfig:config.sys,isLocation:true,submitPhoneInfo:true,submitAddressBook:true}, - Game_Config:{sysConfig:config.template,Max:{},Share:{}},C_Player:{},ConstVal:{isGameHall:config.template.isGameHall, - Launch_mode:{from_self:0,from_hall:1},sysNotice:{}},Logic:new Proxy({},{get:()=>noop}),Func:new Proxy({},{get:()=>noop}), - Utl:new Proxy({},{get:()=>noop}),Game_Modify:{},gameHallImport:{jumpMenuScene:noop}, - set_self:(id:number,key:number,value:number)=>{if(key===37)visible[id]=Boolean(value);if(key===18)left[id]=value;}, - get_self:(id:number,key:number)=>key===37?Number(visible[id]):0,set_group:noop,shareLink:noop,isArray:Array.isArray}); - for(const name of ['JumpMenuScene','adaptSmartBar','adaptSmartBar2','updateActBtn','showStoreBtn']){ - const start=source.indexOf(`GameUI.${name} = function`); - assert.ok(start>=0,name); - const end=source.indexOf('\nGameUI.',start+1); - vm.runInContext(source.slice(start,end),context); - } - ui.JumpMenuScene(); - return {visible,left}; -} - -test('menu visibility and visible-only spacing match original JumpMenuScene across configuration combinations',()=>{ - for(const versionState of [0,1] as const)for(const isGameHall of [false,true])for(const agentmode of [1,2,3]) - for(const androidPay of [0,1])for(const flags of [0,1,2,3,4,7]){ - const config=resolveLobbyMenuConfig(remote({agentmode,androidPay,rankList:flags&1,hideTaskBtn:flags&1, - hideWxShare:flags&2,miniPro:flags&4,hideRoomCard:flags&2?2:1,cdKey:flags&4?2:1,hall_config:{actData:flags&1?['',0,'','']:0}}), - {isGameHall,mainMenuButton:isGameHall,hideNotice:isGameHall,vipInfinite:false}); - for(const launchMode of [0,1] as const){ - const expected=original(config,versionState,launchMode); - const actual=lobbyMenuState(config,{versionState,launchMode}); - for(const id of ids){ - assert.equal(actual.visible[id]??true,expected.visible[id],JSON.stringify({id,versionState,isGameHall,agentmode,androidPay,flags})); - assert.equal(actual.left[id],expected.left[id],`spacing ${id}`); - } - } +test('three locked entries preserve source flags and exact click tips',()=>{ + const closed=resolveLobbyMenuConfig(remote({})); + assert.deepEqual(closed.locked,{568:true,483:true,3139:true}); + assert.equal(lobbyLockedTip(closed,'world-rooms'),'暂未开放,敬请期待'); + assert.equal(lobbyLockedTip(closed,'match'),'暂未开放,敬请期待'); + assert.equal(lobbyLockedTip(closed,'official-rooms'),'暂未开放,敬请期待'); + for(const worldRoomList of [0,1,'0'])for(const vipConfig of [null,[['001234']]])for(const ofcShortCode of [null,'001234']){ + const config=resolveLobbyMenuConfig(remote({hall_config:{worldRoomList},vipConfig,ofcShortCode})); + assert.deepEqual(config.locked,{568:!worldRoomList,483:!vipConfig,3139:!ofcShortCode}); + assert.equal(lobbyLockedTip(config,'world-rooms'),worldRoomList?null:'暂未开放,敬请期待'); + assert.equal(lobbyLockedTip(config,'match'),vipConfig?null:'暂未开放,敬请期待'); + assert.equal(lobbyLockedTip(config,'official-rooms'),ofcShortCode?null:'暂未开放,敬请期待'); } }); -test('source hall merge preserves scoped top-level override and rejects invalid data',()=>{ - const config=resolveLobbyMenuConfig(remote({hall_config:{androidPay:1,rankList:2},rankList:1})); - assert.equal(config.hall.androidPay,1);assert.equal(config.hall.rankList,1); - assert.throws(()=>resolveLobbyMenuConfig(remote({hall_config:'bad'})),/hall_config/); - assert.throws(()=>resolveLobbyMenuConfig(remote({agentmode:'bad'})),/agentmode/); -}); -test('legacy hallConfigName actData colon typo leaves activity sourced only from hall_config',()=>{ - const config=resolveLobbyMenuConfig(remote({actData:['top'],hall_config:{actData:['hall']}})); - assert.deepEqual(config.hall.actData,['hall']); - assert.equal(resolveLobbyMenuConfig(remote({actData:['top']})).hall.actData,0); -}); -test('source truthiness is preserved for string zero flags rather than coerced to numeric zero',()=>{ - const config=resolveLobbyMenuConfig(remote({hideTaskBtn:'0',hideWxShare:'0',rankList:'0'})); - const actual=lobbyMenuState(config,{versionState:0,launchMode:0}); - const expected=original(config,0,0); - for(const id of [17,14,3233])assert.equal(actual.visible[id],expected.visible[id]); - assert.equal(actual.visible[17],false);assert.equal(actual.visible[3233],true); -}); -test('main menu override hides all four entries and cdKey runs after review restriction',()=>{ - const config=resolveLobbyMenuConfig(remote({cdKey:2}),{isGameHall:false,mainMenuButton:true,hideNotice:false,vipInfinite:true}); - const normal=lobbyMenuState(config,{versionState:0,launchMode:0}); - for(const id of [11,12,483,568,3139,679])assert.equal(normal.visible[id],false); - assert.equal(lobbyMenuState(config,{versionState:1,launchMode:0}).visible[3264],true); -}); -test('tray excludes hidden entries from placement and keeps original feedback hidden',()=>{ - const state=lobbyTrayState(resolveLobbyMenuConfig(remote({agentmode:2,rankList:1,bagUrl:'bag',wareHouse:1,miniPro:1})),{versionState:0,launchMode:0}); - assert.equal(state.visible[235],false);assert.equal(state.visible[15],false); - assert.deepEqual([state.left[604],state.left[3017],state.left[3057],state.left[5]],[216,371,526,681]); -}); -test('every menu entry has an existing local action or an explicit unavailable business description',()=>{ - const local=['settings','profile','join-room','notice','tray','tasks','share','vip-rooms']; - for(const action of Object.values(LOBBY_MENU_ACTIONS))assert.ok(local.includes(action)||LOBBY_UNAVAILABLE[action],action); - for(const id of [10,286,287,3161] as const)assert.equal(LOBBY_MENU_ACTIONS[id],'buy-roomcard'); - for(const id of [508,509,496,3162] as const)assert.equal(LOBBY_MENU_ACTIONS[id],'buy-bean'); -}); +function original(config:ReturnType,versionState:number,launchMode:number) { + const visible:Record=Object.fromEntries(ids.map(id=>[id,true])); + const left:Record={}; + const noop=()=>{}; + const ui=new Proxy({onstate:[],isprepare:[]} as Record,{get:(o,k)=>k in o?o[k as string]:noop}); + const context=vm.createContext({GameUI:ui,GameData:{...config,Scene:1,versionState,LaunchMode:launchMode, + hallConfig:config.hall,sysConfig:config.sys,isLocation:true,submitPhoneInfo:true,submitAddressBook:true}, + Game_Config:{sysConfig:config.template,Max:{},Share:{}},C_Player:{},ConstVal:{isGameHall:config.template.isGameHall, + Launch_mode:{from_self:0,from_hall:1},sysNotice:{}},Logic:new Proxy({},{get:()=>noop}),Func:new Proxy({},{get:()=>noop}), + Utl:new Proxy({},{get:()=>noop}),Game_Modify:{},gameHallImport:{jumpMenuScene:noop}, + set_self:(id:number,key:number,value:number)=>{if(key===37)visible[id]=Boolean(value);if(key===18)left[id]=value;}, + get_self:(id:number,key:number)=>key===37?Number(visible[id]):0,set_group:noop,shareLink:noop,isArray:Array.isArray}); + for(const name of ['JumpMenuScene','adaptSmartBar','adaptSmartBar2','updateActBtn','showStoreBtn']){ + const start=source.indexOf(`GameUI.${name} = function`); + assert.ok(start>=0,name); + const end=source.indexOf('\nGameUI.',start+1); + vm.runInContext(source.slice(start,end),context); + } + ui.JumpMenuScene(); + return {visible,left}; +} + +test('menu visibility and visible-only spacing match original JumpMenuScene across configuration combinations',()=>{ + for(const versionState of [0,1] as const)for(const isGameHall of [false,true])for(const agentmode of [1,2,3]) + for(const androidPay of [0,1])for(const flags of [0,1,2,3,4,7]){ + const config=resolveLobbyMenuConfig(remote({agentmode,androidPay,rankList:flags&1,hideTaskBtn:flags&1, + hideWxShare:flags&2,miniPro:flags&4,hideRoomCard:flags&2?2:1,cdKey:flags&4?2:1,hall_config:{actData:flags&1?['',0,'','']:0}}), + {isGameHall,mainMenuButton:isGameHall,hideNotice:isGameHall,vipInfinite:false}); + for(const launchMode of [0,1] as const){ + const expected=original(config,versionState,launchMode); + const actual=lobbyMenuState(config,{versionState,launchMode}); + for(const id of ids){ + assert.equal(actual.visible[id]??true,expected.visible[id],JSON.stringify({id,versionState,isGameHall,agentmode,androidPay,flags})); + assert.equal(actual.left[id],expected.left[id],`spacing ${id}`); + } + } + } +}); +test('source hall merge preserves scoped top-level override and rejects invalid data',()=>{ + const config=resolveLobbyMenuConfig(remote({hall_config:{androidPay:1,rankList:2},rankList:1})); + assert.equal(config.hall.androidPay,1);assert.equal(config.hall.rankList,1); + assert.throws(()=>resolveLobbyMenuConfig(remote({hall_config:'bad'})),/hall_config/); + assert.throws(()=>resolveLobbyMenuConfig(remote({agentmode:'bad'})),/agentmode/); +}); +test('legacy hallConfigName actData colon typo leaves activity sourced only from hall_config',()=>{ + const config=resolveLobbyMenuConfig(remote({actData:['top'],hall_config:{actData:['hall']}})); + assert.deepEqual(config.hall.actData,['hall']); + assert.equal(resolveLobbyMenuConfig(remote({actData:['top']})).hall.actData,0); +}); +test('source truthiness is preserved for string zero flags rather than coerced to numeric zero',()=>{ + const config=resolveLobbyMenuConfig(remote({hideTaskBtn:'0',hideWxShare:'0',rankList:'0'})); + const actual=lobbyMenuState(config,{versionState:0,launchMode:0}); + const expected=original(config,0,0); + for(const id of [17,14,3233])assert.equal(actual.visible[id],expected.visible[id]); + assert.equal(actual.visible[17],false);assert.equal(actual.visible[3233],true); +}); +test('main menu override hides all four entries and cdKey runs after review restriction',()=>{ + const config=resolveLobbyMenuConfig(remote({cdKey:2}),{isGameHall:false,mainMenuButton:true,hideNotice:false,vipInfinite:true}); + const normal=lobbyMenuState(config,{versionState:0,launchMode:0}); + for(const id of [11,12,483,568,3139,679])assert.equal(normal.visible[id],false); + assert.equal(lobbyMenuState(config,{versionState:1,launchMode:0}).visible[3264],true); +}); +test('tray excludes hidden entries from placement and keeps original feedback hidden',()=>{ + const state=lobbyTrayState(resolveLobbyMenuConfig(remote({agentmode:2,rankList:1,bagUrl:'bag',wareHouse:1,miniPro:1})),{versionState:0,launchMode:0}); + assert.equal(state.visible[235],false);assert.equal(state.visible[15],false); + assert.deepEqual([state.left[604],state.left[3017],state.left[3057],state.left[5]],[216,371,526,681]); +}); +test('every menu entry has an existing local action or an explicit unavailable business description',()=>{ + const local=['settings','profile','join-room','notice','tray','tasks','share','vip-rooms','qrcode','rank','cdkey','vip-rank','my-rooms','buy-roomcard','buy-bean']; + for(const action of Object.values(LOBBY_MENU_ACTIONS))assert.ok(local.includes(action)||LOBBY_UNAVAILABLE[action],action); + for(const id of [10,286,287,3161] as const)assert.equal(LOBBY_MENU_ACTIONS[id],'buy-roomcard'); + for(const id of [508,509,496,3162] as const)assert.equal(LOBBY_MENU_ACTIONS[id],'buy-bean'); +}); diff --git a/cocoscreator_projects/framework-tests/presentation/profile-auth.test.ts b/cocoscreator_projects/framework-tests/presentation/profile-auth.test.ts new file mode 100644 index 0000000..3e503f1 --- /dev/null +++ b/cocoscreator_projects/framework-tests/presentation/profile-auth.test.ts @@ -0,0 +1,30 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import {runInNewContext} from 'node:vm'; +import {validateProfileAuth} from '../../YouleNexus/assets/framework/presentation/profile-auth.ts'; + +const root=new URL('../../../projects/Game_Surface_3/js/00_Surface/',import.meta.url); +const source=readFileSync(new URL('05_Func.js',root),'utf8'); +const districts=readFileSync(new URL('03_Banwords.js',root),'utf8'); +const context:any={}; +runInNewContext(districts.slice(districts.indexOf('var GB2260 ='),districts.indexOf(';',districts.indexOf('var GB2260 ='))+1) + +source.slice(source.indexOf('var IDValidator ='),source.indexOf('Func.subString ='))+';validator=new IDValidator();',context); + +test('auth follows source name check and known checksum outcomes',()=>{ + assert.equal(validateProfileAuth('张','11010519491231002X'),'name'); + assert.equal(validateProfileAuth('测试','11010519491231002X'),null); + assert.equal(validateProfileAuth('测试','11010519491231002x'),null); + assert.equal(validateProfileAuth('测试','110105194912310020'),'id'); + assert.equal(validateProfileAuth('测试',''),'id'); + assert.equal(validateProfileAuth('测试','110105491231002'),null); +}); + +test('ID validation matches the original validator including legacy date and address semantics',()=>{ + const ids=['','11010519491231002X','110105194912310020','110105491231002','110105491332002','999999491231002', + '110199491231002','110105490231002','110105490000002','11010549aa31abc',' 11010519491231002X','11010519491231002X ']; + for(const base of ['11010519491231002X','110105491231002'])for(let i=0;i{ + const row={name:'一二三四五六七八九',headimg:'',playerid:'001',star:100,signature:'a#b'}; + assert.deepEqual(parseRankList([row]),[row]); + assert.deepEqual(parseRankList([row],true),[{...row,name:'一二三四五六七八...',signature:'a\nb'}]); + assert.equal(parseRankList([{...row,name:'12345678901234567',signature:null}],true)[0].name,'1234567890123456...'); + assert.deepEqual(parseRankList([]),[]);assert.throws(()=>parseRankList([{}])); +}); + +test('VIP ranks retain announcement fields and ignore absent optional text',()=>{ + const row={name:'VIP',headimg:'',playerid:1,star:8,signature:'签名',notice:'公告',destwo:'补充'}; + assert.deepEqual(parseVipRankList([row]),[row]); + assert.deepEqual(parseVipRankList([{...row,notice:null,destwo:null}]),[{...row,notice:'',destwo:''}]); +}); diff --git a/cocoscreator_projects/framework-tests/presentation/request-loading.test.ts b/cocoscreator_projects/framework-tests/presentation/request-loading.test.ts new file mode 100644 index 0000000..1ea25ad --- /dev/null +++ b/cocoscreator_projects/framework-tests/presentation/request-loading.test.ts @@ -0,0 +1,17 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {RequestLoading} from '../../YouleNexus/assets/framework/presentation/request-loading.ts'; +test('request loading closes on matching success or failure independent of payload',()=>{ + const shown:boolean[]=[];const loading=new RequestLoading(v=>shown.push(v)); + for(const rpc of ['get_player_task','get_task_award','getVipRankList'] as const){ + for(const data of [{state:0},{state:1,error:'失败'},null]){loading.begin(rpc);loading.received({route:'agent',rpc,data});assert.deepEqual(shown.splice(0),[true,false]);} + } + loading.begin('get_player_task');loading.received({route:'agent',rpc:'getVipRankList'});assert.deepEqual(shown,[true]); + loading.received({route:'agent',rpc:'show_message'});assert.deepEqual(shown,[true,false]); +}); +test('overlapping requests, cancellation, and duplicate replies do not flicker',()=>{ + const shown:boolean[]=[];const loading=new RequestLoading(v=>shown.push(v)); + loading.begin('get_player_task');loading.begin('getVipRankList');loading.received({route:'agent',rpc:'get_player_task'}); + assert.deepEqual(shown,[true]);loading.cancel('getVipRankList');loading.cancel('getVipRankList'); + assert.deepEqual(shown,[true,false]);loading.begin('get_task_award');loading.clear();assert.deepEqual(shown,[true,false,true,false]); +}); diff --git a/cocoscreator_projects/framework-tests/presentation/room-entry.test.ts b/cocoscreator_projects/framework-tests/presentation/room-entry.test.ts new file mode 100644 index 0000000..1ecf7c0 --- /dev/null +++ b/cocoscreator_projects/framework-tests/presentation/room-entry.test.ts @@ -0,0 +1,32 @@ +import {test} from 'node:test'; +import assert from 'node:assert/strict'; +import {JoinRoomInput} from '../../YouleNexus/assets/framework/presentation/join-room-input.ts'; +import {TemplateRoomRules} from '../../YouleNexus/assets/games/template/template-room-rules.ts'; + +test('keypad preserves display zeros and submits a number at six digits',()=>{ + const sent:number[]=[];const input=new JoinRoomInput(code=>sent.push(code)); + for(const digit of [0,1,2,3,4])input.digit(digit); + assert.deepEqual(sent,[]);input.digit(5); + assert.equal(input.code,'012345');assert.deepEqual(sent,[12345]); + input.setBusy(true);input.digit(6);input.remove();input.clear();input.confirm(); + assert.equal(input.code,'012345');assert.deepEqual(sent,[12345]); + input.setBusy(false);input.remove();assert.equal(input.code,'01234'); + input.clear();input.confirm();assert.deepEqual(sent,[12345]); + input.digit(8);input.confirm();assert.deepEqual(sent,[12345,8]); +}); +test('paste strips non-digits, truncates and submits; prefill does not submit',()=>{ + const sent:number[]=[];const input=new JoinRoomInput(code=>sent.push(code)); + input.prefill('123456');assert.deepEqual(sent,[]); + input.paste('房号:987-654321');assert.equal(input.code,'987654');assert.deepEqual(sent,[987654]); + input.paste('12a');assert.equal(input.code,'12');assert.equal(sent.length,1); + assert.throws(()=>input.digit(10));assert.throws(()=>input.prefill('corrupt')); +}); +test('template options do not rewrite source fixed sample; each submission is independent',()=>{ + const rules=new TemplateRoomRules();const expected=[1,4,1,2,2,[1,1,[1,2000,10],null,null,1],[1,0,5]]; + assert.deepEqual(rules.build(),expected);rules.select(0,1);rules.select(1,1); + assert.deepEqual(rules.build(),expected);const packet=rules.build();packet[0]=999; + assert.deepEqual(rules.build(),expected);assert.throws(()=>rules.select(0,2)); + // Original saved array's index 1 is 4: no corresponding Type_2 checkmark. + rules.open(expected);assert.deepEqual(rules.selection,[0,3]); + assert.equal(rules.seatCount(expected),10);assert.throws(()=>rules.open({})); +}); diff --git a/cocoscreator_projects/framework-tests/presentation/share-model.test.ts b/cocoscreator_projects/framework-tests/presentation/share-model.test.ts index 070f92e..9f92c1e 100644 --- a/cocoscreator_projects/framework-tests/presentation/share-model.test.ts +++ b/cocoscreator_projects/framework-tests/presentation/share-model.test.ts @@ -1,14 +1,23 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts'; -import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; -import { resolveSharePolicy,sharePayload } from '../../YouleNexus/assets/framework/presentation/share-model.ts'; -test('share reads url_download and preserves original friend and timeline payloads',()=>{ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts'; +import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; +import { resolveSharePolicy,sharePayload,resolveQrCodePolicy } from '../../YouleNexus/assets/framework/presentation/share-model.ts'; + +test('QR entry uses scoped WechatPublicNo_ewm and exact image share parameters',()=>{ const identity={agentid:'a',channelid:'c',gameid:'g',marketid:4,version:1}; - const remoteConfig=createRemoteConfig({url_download:'https://example.test/app',sharename:'标题',hall_config:{shareDesc:'描述',shareType:1,shareImgUrl:'https://example.test/share.png'}},identity); - const policy=resolveSharePolicy({identity,remoteConfig} as RuntimeConfig); - assert.equal(policy.url,'https://example.test/app?agentid=a&channelid=c&gameid=g&marketid=4'); - assert.deepEqual(sharePayload(policy,1),{sharefriend:1,type:1,webpageUrl:policy.url,title:'标题',description:'描述',sharetype:0}); - assert.deepEqual(sharePayload(policy,2),{sharefriend:2,type:3,webpageUrl:'https://example.test/share.png',title:'标题描述',description:undefined,sharetype:0}); - assert.equal(sharePayload({...policy,imageTimeline:false},2),null); + const config=(data:Record)=>({identity,remoteConfig:createRemoteConfig(data,identity)} as RuntimeConfig); + assert.equal(resolveQrCodePolicy(config({})),null); + const result=resolveQrCodePolicy(config({WechatPublicNo_ewm:'root.png',agentlist:[{agentid:'a',WechatPublicNo_ewm:'https://example.test/qr.png'}],sharename:'标题',shareDesc:'说明'})); + assert.deepEqual(result,{url:'https://example.test/qr.png',share:{sharefriend:1,type:3,webpageUrl:'https://example.test/qr.png',title:'标题说明',description:undefined,sharetype:0}}); + assert.throws(()=>resolveQrCodePolicy(config({WechatPublicNo_ewm:123})),/WechatPublicNo_ewm/); }); +test('share reads url_download and preserves original friend and timeline payloads',()=>{ + const identity={agentid:'a',channelid:'c',gameid:'g',marketid:4,version:1}; + const remoteConfig=createRemoteConfig({url_download:'https://example.test/app',sharename:'标题',hall_config:{shareDesc:'描述',shareType:1,shareImgUrl:'https://example.test/share.png'}},identity); + const policy=resolveSharePolicy({identity,remoteConfig} as RuntimeConfig); + assert.equal(policy.url,'https://example.test/app?agentid=a&channelid=c&gameid=g&marketid=4'); + assert.deepEqual(sharePayload(policy,1),{sharefriend:1,type:1,webpageUrl:policy.url,title:'标题',description:'描述',sharetype:0}); + assert.deepEqual(sharePayload(policy,2),{sharefriend:2,type:3,webpageUrl:'https://example.test/share.png',title:'标题描述',description:undefined,sharetype:0}); + assert.equal(sharePayload({...policy,imageTimeline:false},2),null); +}); diff --git a/cocoscreator_projects/framework-tests/presentation/template-game.test.ts b/cocoscreator_projects/framework-tests/presentation/template-game.test.ts new file mode 100644 index 0000000..03dc296 --- /dev/null +++ b/cocoscreator_projects/framework-tests/presentation/template-game.test.ts @@ -0,0 +1,15 @@ +import {test} from 'node:test'; +import assert from 'node:assert/strict'; +import {createTemplateGame} from '../../YouleNexus/assets/games/template/template-game.ts'; +test('template module attaches only when opened and releases its view and subscription',()=>{ + let unsubscribed=0;const commands:unknown[]=[];const views:unknown[]=[]; + const snapshot={connection:{phase:'logged-in'},self:{playerId:1,seat:0},room:{roomcode:'123456',roomtype:[],stage:0,needprepare:0,infinite:0},seats:[]}; + const game=createTemplateGame('g');const probe=game.entry.createModule();probe.dispose(); + assert.throws(()=>game.mount({render:s=>views.push(s),close(){}})); + const module=game.entry.createModule(); + module.attach({getSnapshot:()=>snapshot,subscribe:()=>()=>unsubscribed++,execute:(c:unknown)=>commands.push(c),seat:{toView:(n:number)=>n,toServer:(n:number)=>n},sendGameMessage(){}} as any); + let actions:any;game.mount({render:(s,a)=>{views.push(s);actions=a;},close:()=>views.push('closed')}); + actions.exit();actions.prepare();assert.deepEqual(commands,[{type:'room.exit'},{type:'room.prepare'}]); + const deskinfo={nested:[1,2]};module.restore(deskinfo);assert.equal(game.restoredSnapshot,deskinfo); + module.dispose();assert.equal(unsubscribed,1);assert.equal(views.at(-1),'closed');assert.throws(()=>actions.exit()); +}); diff --git a/cocoscreator_projects/framework-tests/protocol/first-slice-routes.test.ts b/cocoscreator_projects/framework-tests/protocol/first-slice-routes.test.ts index 729e172..3bf1aae 100644 --- a/cocoscreator_projects/framework-tests/protocol/first-slice-routes.test.ts +++ b/cocoscreator_projects/framework-tests/protocol/first-slice-routes.test.ts @@ -1,38 +1,45 @@ -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { - FIRST_SLICE_RPC_ROUTE, - routeForFirstSlice, -} from '../../YouleNexus/assets/framework/protocol/first-slice-routes.ts'; - -test('FIRST_SLICE_RPC_ROUTE 是第一纵向切片唯一且精确的 rpc route 表', () => { - assert.deepEqual(FIRST_SLICE_RPC_ROUTE, { - getInfoByShortCode: 'agent', - get_player_task: 'agent', - get_task_award: 'agent', - update_roomcard: 'agent', - player_finish_task: 'agent', - player_login: 'agent', - self_join_room: 'agent', - player_prepare: 'room', - self_exit_room: 'room', - other_join_room: 'room', - other_exit_room: 'room', - other_offline: 'room', - other_online: 'room', - connect_roomserver: 'room', - connect_agentserver: 'agent', - kick_server: 'agent', - kick_offline: 'agent', - show_message: 'agent', - }); -}); - -test('routeForFirstSlice 从单源映射取 route,未知 rpc 显式失败', () => { - assert.equal(routeForFirstSlice('self_join_room'), 'agent'); - assert.equal(routeForFirstSlice('connect_roomserver'), 'room'); - assert.throws( - () => routeForFirstSlice('unknown_rpc' as never), - /unknown_rpc/, - ); -}); +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { + FIRST_SLICE_RPC_ROUTE, + routeForFirstSlice, +} from '../../YouleNexus/assets/framework/protocol/first-slice-routes.ts'; + +test('FIRST_SLICE_RPC_ROUTE 是第一纵向切片唯一且精确的 rpc route 表', () => { + assert.deepEqual(FIRST_SLICE_RPC_ROUTE, { + getVipRankList: 'agent', + get_treasurelist: 'agent', + topup_card: 'agent', + update_bean: 'agent', + get_player_invitecode: 'agent', + binding_invitecode: 'agent', + getInfoByShortCode: 'agent', + get_player_task: 'agent', + get_task_award: 'agent', + update_roomcard: 'agent', + player_finish_task: 'agent', + player_login: 'agent', + create_room: 'agent', + self_join_room: 'agent', + player_prepare: 'room', + self_exit_room: 'room', + other_join_room: 'room', + other_exit_room: 'room', + other_offline: 'room', + other_online: 'room', + connect_roomserver: 'room', + connect_agentserver: 'agent', + kick_server: 'agent', + kick_offline: 'agent', + show_message: 'agent', + }); +}); + +test('routeForFirstSlice 从单源映射取 route,未知 rpc 显式失败', () => { + assert.equal(routeForFirstSlice('self_join_room'), 'agent'); + assert.equal(routeForFirstSlice('connect_roomserver'), 'room'); + assert.throws( + () => routeForFirstSlice('unknown_rpc' as never), + /unknown_rpc/, + ); +}); diff --git a/cocoscreator_projects/framework-tests/protocol/platform-handlers.test.ts b/cocoscreator_projects/framework-tests/protocol/platform-handlers.test.ts index 8ad38c1..f2ab626 100644 --- a/cocoscreator_projects/framework-tests/protocol/platform-handlers.test.ts +++ b/cocoscreator_projects/framework-tests/protocol/platform-handlers.test.ts @@ -1,677 +1,677 @@ -import { readFileSync } from 'node:fs'; -import assert from 'node:assert/strict'; -import { test } from 'node:test'; -import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts'; -import type { - GameEntry, - GameHost, - GameModule, - PlatformGameSnapshot, - PlatformToGameEvent, -} from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; -import { GameSessionHost } from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts'; -import type { ConnectionControlPort } from '../../YouleNexus/assets/framework/platform/connection-intent.ts'; -import { - RuntimeSession, - RuntimeSessionFault, -} from '../../YouleNexus/assets/framework/platform/runtime-session.ts'; -import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; -import { selectGameSnapshot } from '../../YouleNexus/assets/framework/platform/stores/selectors.ts'; -import { PlatformHandlers } from '../../YouleNexus/assets/framework/protocol/platform-handlers.ts'; -import { Router } from '../../YouleNexus/assets/framework/protocol/router.ts'; - -function fixture(name: string): Record { - return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')).data; -} - -function caughtError(operation: () => void): Error { - try { - operation(); - } catch (error) { - assert.ok(error instanceof Error); - return error; - } - assert.fail('expected operation to throw'); -} - -class ConnectionFixture implements ConnectionControlPort { - readonly switchCalls: Array<{ target: string; resend: OutboundEnvelope }> = []; - readonly stopCalls: unknown[] = []; - switchFailure: Error | undefined; - switchObserver: () => void = () => {}; - stopFailure: Error | undefined; - stopObserver: () => void = () => {}; - - switchServer(target: string, resend: OutboundEnvelope): void { - this.switchCalls.push({ target, resend }); - this.switchObserver(); - if (this.switchFailure) throw this.switchFailure; - } - - stopForKick(data: unknown): void { - this.stopCalls.push(data); - this.stopObserver(); - if (this.stopFailure) throw this.stopFailure; - } -} - -function makeHandlers() { - const store = new PlatformStore(); - let disposals = 0; - let eventObserver: ( - event: PlatformToGameEvent, - snapshot: PlatformGameSnapshot, - ) => void = () => {}; - let disposalObserver: () => void = () => {}; - let invalidationObserver: () => void = () => {}; - let invalidations = 0; - const entry: GameEntry = { - key: 'fixture', gameId: 41, route: 'fixture-game', resolveSeatCount: () => 4, - createModule: (): GameModule => ({ - attach() {}, - handlePlatformEvent(event) { - eventObserver(event, selectGameSnapshot(store.getState())); - }, - handleGameMessage() {}, - restore() {}, - dispose() { disposals += 1; disposalObserver(); }, - }), - }; - const gameSession = new GameSessionHost(entry); - disposals = 0; - const session = new RuntimeSession({ - store, - gameSession, - onServerDenial() { assert.fail('Unexpected server denial'); }, - createGameHostLease: () => ({ - host: { - seat: { toView: (seat) => seat, toServer: (seat) => seat }, - getSnapshot: () => selectGameSnapshot(store.getState()), - subscribe: () => () => {}, sendGameMessage() {}, execute() {}, - }, - invalidate() { - invalidations += 1; - invalidationObserver(); - }, - }), - }); - const connection = new ConnectionFixture(); - const handlers = new PlatformHandlers({ store, gameSession, session, connection }); - const router = new Router({ session, platform: handlers, gameSession }); - return { - store, - gameSession, - session, - connection, - handlers, - router, - getDisposals: () => disposals, - getInvalidations: () => invalidations, - observeEvent(observer: typeof eventObserver) { eventObserver = observer; }, - observeDisposal(observer: () => void) { disposalObserver = observer; }, - observeInvalidation(observer: () => void) { invalidationObserver = observer; }, - }; -} - -function makeReentrantRouterFixture(log: string[]) { - const store = new PlatformStore(); - let nextModuleId = 0; - const restores: Array<{ moduleId: number; deskinfo: unknown }> = []; - const entry: GameEntry = { - key: 'reentrant-fixture', - gameId: 41, - route: 'fixture-game', - resolveSeatCount: () => 4, - createModule: (): GameModule => { - const moduleId = ++nextModuleId; - let host: GameHost | undefined; - return { - attach(attachedHost) { - host = attachedHost; - log.push(`attach:${moduleId}:${attachedHost.getSnapshot().room.roomcode}`); - }, - handlePlatformEvent(event) { - if (!host) throw new Error('fixture module has no host'); - const snapshot = host.getSnapshot(); - if (event.type === 'room.entered') { - log.push(`event:${moduleId}:room.entered:${snapshot.room.roomcode}`); - return; - } - if (event.type === 'room.player-offline' || event.type === 'room.player-online') { - const online = snapshot.seats.find((seat) => seat.seat === event.seat)?.online; - log.push(`event:${moduleId}:${event.type}:${String(online)}`); - } - }, - handleGameMessage() {}, - restore(deskinfo) { - restores.push({ moduleId, deskinfo }); - log.push(`restore:${moduleId}`); - }, - dispose() { log.push(`dispose:${moduleId}`); }, - }; - }, - }; - const gameSession = new GameSessionHost(entry); - nextModuleId = 0; - log.length = 0; - const session = new RuntimeSession({ - store, - gameSession, - onServerDenial() { assert.fail('Unexpected server denial'); }, - createGameHostLease: () => ({ - host: { - seat: { toView: (seat) => seat, toServer: (seat) => seat }, - getSnapshot: () => selectGameSnapshot(store.getState()), - subscribe: () => () => {}, - sendGameMessage() {}, - execute() {}, - }, - invalidate() {}, - }), - }); - const connection = new ConnectionFixture(); - const handlers = new PlatformHandlers({ store, gameSession, session, connection }); - const router = new Router({ session, platform: handlers, gameSession }); - return { store, session, gameSession, router, restores }; -} - -test('Router drains reentrant room entries FIFO so each module receives its own committed room and deskinfo', () => { - const log: string[] = []; - const setup = makeReentrantRouterFixture(log); - setup.router.dispatch({ - route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), - }); - const roomA = fixture('self-join-room.json'); - const roomB = fixture('self-join-room.json'); - roomA.roomcode = 'room-A'; - roomB.roomcode = 'room-B'; - roomA.deskinfo = { round: 'A' }; - roomB.deskinfo = { round: 'B' }; - let queuedResult: ReturnType | undefined; - - setup.store.subscribe((state) => { - if (state.room.kind === 'inside' && state.room.roomcode === 'room-A') { - log.push('store:A'); - queuedResult = setup.router.dispatch({ - route: 'agent', rpc: 'self_join_room', data: roomB, - }); - } else if (state.room.kind === 'inside' && state.room.roomcode === 'room-B') { - log.push('store:B'); - } - }); - - const result = setup.router.dispatch({ - route: 'agent', rpc: 'self_join_room', data: roomA, - }); - - assert.equal(result, 'handled-platform'); - assert.equal(queuedResult, 'handled-platform'); - assert.deepEqual(log, [ - 'store:A', - 'attach:1:room-A', - 'event:1:room.entered:room-A', - 'restore:1', - 'store:B', - 'dispose:1', - 'attach:2:room-B', - 'event:2:room.entered:room-B', - 'restore:2', - ]); - assert.deepEqual(setup.restores, [ - { moduleId: 1, deskinfo: roomA.deskinfo }, - { moduleId: 2, deskinfo: roomB.deskinfo }, - ]); - assert.equal(setup.restores[0]?.deskinfo, roomA.deskinfo); - assert.equal(setup.restores[1]?.deskinfo, roomB.deskinfo); -}); - -test('Router finishes an offline state-first event before draining a reentrant online message', () => { - const log: string[] = []; - const setup = makeReentrantRouterFixture(log); - setup.router.dispatch({ - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - }); - setup.router.dispatch({ - route: 'room', - rpc: 'other_join_room', - data: { - seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', sex: 1, - ip: '127.0.0.2', onstate: 0, bean: 90, isprepare: 0, - }, - }); - log.length = 0; - let queuedResult: ReturnType | undefined; - - setup.store.subscribe((state, previous) => { - if (state.room.kind !== 'inside' || previous.room.kind !== 'inside') return; - const wasOffline = previous.room.offlineSeats.includes(2); - const isOffline = state.room.offlineSeats.includes(2); - if (!wasOffline && isOffline) { - log.push('store:offline'); - queuedResult = setup.router.dispatch({ - route: 'room', rpc: 'other_online', data: { seat: 2, ip: '127.0.0.3' }, - }); - } else if (wasOffline && !isOffline) { - log.push('store:online'); - } - }); - - const result = setup.router.dispatch({ - route: 'room', rpc: 'other_offline', data: { seat: 2 }, - }); - - assert.equal(result, 'handled-platform'); - assert.equal(queuedResult, 'handled-platform'); - assert.deepEqual(log, [ - 'store:offline', - 'event:1:room.player-offline:false', - 'store:online', - 'event:1:room.player-online:true', - ]); -}); - -test('PlatformHandlers commits every room event before one typed game event reads the snapshot', () => { - const setup = makeHandlers(); - setup.handlers.dispatch({ - route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), - }); - - function dispatchWithEvent( - message: { route: string; rpc: string; data: Record }, - expectedType: PlatformToGameEvent['type'], - assertSnapshot: (snapshot: PlatformGameSnapshot) => void, - ): void { - const order: string[] = []; - let received = 0; - const unsubscribe = setup.store.subscribe(() => { order.push('store'); }); - setup.observeEvent((event, snapshot) => { - received += 1; - order.push('game'); - assert.equal(event.type, expectedType); - assertSnapshot(snapshot); - }); - - assert.equal(setup.handlers.dispatch(message), 'handled-platform'); - - unsubscribe(); - assert.equal(received, 1); - assert.deepEqual(order, ['store', 'game']); - } - - const join = fixture('self-join-room.json'); - delete join.deskinfo; - join.deskwar = 0; - dispatchWithEvent( - { route: 'agent', rpc: 'self_join_room', data: join }, - 'room.entered', - (snapshot) => { assert.equal(snapshot.room.roomcode, '100001'); }, - ); - - dispatchWithEvent( - { - route: 'room', - rpc: 'other_join_room', - data: { - seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', sex: 1, - ip: '127.0.0.2', onstate: 0, bean: 90, isprepare: 0, - }, - }, - 'room.player-joined', - (snapshot) => { assert.equal(snapshot.seats.find((seat) => seat.seat === 2)?.playerId, 77); }, - ); - - dispatchWithEvent( - { route: 'room', rpc: 'player_prepare', data: { seat: 2, deskwar: 1 } }, - 'room.player-ready', - (snapshot) => { - assert.equal(snapshot.seats.find((seat) => seat.seat === 2)?.ready, true); - assert.equal(snapshot.room.stage, 1); - }, - ); - - dispatchWithEvent( - { route: 'room', rpc: 'other_offline', data: { seat: 2 } }, - 'room.player-offline', - (snapshot) => { assert.equal(snapshot.seats.find((seat) => seat.seat === 2)?.online, false); }, - ); - - dispatchWithEvent( - { route: 'room', rpc: 'other_online', data: { seat: 2, ip: '127.0.0.3' } }, - 'room.player-online', - (snapshot) => { assert.equal(snapshot.seats.find((seat) => seat.seat === 2)?.online, true); }, - ); - - dispatchWithEvent( - { route: 'room', rpc: 'other_exit_room', data: { seat: 2 } }, - 'room.player-left', - (snapshot) => { assert.equal(snapshot.seats.some((seat) => seat.seat === 2), false); }, - ); -}); - -test('self_exit_room commits the outside snapshot before disposing the game session', () => { - const setup = makeHandlers(); - setup.handlers.dispatch({ - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - }); - const order: string[] = []; - setup.store.subscribe(() => { order.push('store'); }); - setup.observeDisposal(() => { - order.push('dispose'); - assert.equal(setup.store.getState().room.kind, 'outside'); - }); - - setup.handlers.dispatch({ route: 'room', rpc: 'self_exit_room', data: { seat: 1 } }); - - assert.deepEqual(order, ['store', 'dispose']); - assert.equal(setup.gameSession.state, 'idle'); -}); - -test('self_exit_room accepts omitted seat, validates a present seat, and is idempotent outside', () => { - const setup = makeHandlers(); - setup.router.dispatch({ - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - }); - const inside = setup.store.getState(); - const disposalsBefore = setup.getDisposals(); - - assert.throws( - () => setup.router.dispatch({ route: 'room', rpc: 'self_exit_room', data: { seat: 0 } }), - /self_exit_room.*seat.*expected 1.*received 0/i, - ); - assert.equal(setup.store.getState(), inside); - assert.equal(setup.gameSession.state, 'active'); - assert.equal(setup.getDisposals(), disposalsBefore); - - let notifications = 0; - setup.store.subscribe(() => { notifications += 1; }); - assert.equal( - setup.router.dispatch({ route: 'room', rpc: 'self_exit_room', data: { reason: 'done' } }), - 'handled-platform', - ); - assert.equal(setup.store.getState().room.kind, 'outside'); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(notifications, 1); - assert.equal(setup.getDisposals(), disposalsBefore + 1); - - assert.equal( - setup.router.dispatch({ route: 'room', rpc: 'self_exit_room', data: { reason: 'duplicate' } }), - 'handled-platform', - ); - assert.equal(notifications, 1); - assert.equal(setup.getDisposals(), disposalsBefore + 1); -}); - -test('PlatformHandlers preserves complete switch payloads and builds the exact resend envelopes', () => { - const setup = makeHandlers(); - const roomData = { roomserver: '10.0.0.2:3088', epoch: 7 }; - const agentData = { - agentserver: 'secure.example:443/path', opt: 'free_room', trace: { id: 'x' }, - }; - - assert.equal(setup.router.dispatch({ - route: 'room', rpc: 'connect_roomserver', data: roomData, - }), 'handled-platform'); - assert.equal(setup.router.dispatch({ - route: 'agent', rpc: 'connect_agentserver', data: agentData, - }), 'handled-platform'); - - assert.deepEqual(setup.connection.switchCalls, [ - { - target: 'ws://10.0.0.2:3088', - resend: { app: 'youle', route: 'room', rpc: 'connect_roomserver', data: roomData }, - }, - { - target: 'ws://secure.example:443/path', - resend: { app: 'youle', route: 'agent', rpc: 'connect_agentserver', data: agentData }, - }, - ]); - assert.equal(setup.connection.switchCalls[0]?.resend.data, roomData); - assert.equal(setup.connection.switchCalls[1]?.resend.data, agentData); -}); - -test('PlatformHandlers rejects unknown, route-mismatched, missing, and invalid switch targets without effects', () => { - const setup = makeHandlers(); - const before = setup.store.getState(); - - assert.throws( - () => setup.handlers.dispatch({ route: 'agent', rpc: 'update_bean', data: { bean: 1 } }), - /update_bean/, - ); - assert.throws( - () => setup.handlers.dispatch({ route: 'agent', rpc: 'connect_roomserver', data: { roomserver: 'x:1' } }), - /route/i, - ); - assert.throws( - () => setup.handlers.dispatch({ route: 'room', rpc: 'connect_roomserver', data: {} }), - /roomserver/, - ); - assert.throws( - () => setup.handlers.dispatch({ - route: 'room', rpc: 'connect_roomserver', data: { roomserver: 'http://invalid.example' }, - }), - /roomserver|WebSocket/i, - ); - - assert.equal(setup.store.getState(), before); - assert.deepEqual(setup.connection.switchCalls, []); -}); - -test('unknown and every route-mismatched platform rpc leave all observable state unchanged', () => { - const setup = makeHandlers(); - setup.router.dispatch({ - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - }); - const before = setup.store.getState(); - let notifications = 0; - let gameEvents = 0; - setup.store.subscribe(() => { notifications += 1; }); - setup.observeEvent(() => { gameEvents += 1; }); - const rpcsByRoute = [ - ['player_login', 'agent'], - ['self_join_room', 'agent'], - ['player_prepare', 'room'], - ['self_exit_room', 'room'], - ['other_join_room', 'room'], - ['other_exit_room', 'room'], - ['other_offline', 'room'], - ['other_online', 'room'], - ['connect_roomserver', 'room'], - ['connect_agentserver', 'agent'], - ['kick_server', 'agent'], - ] as const; - - for (const route of ['platform', 'agent', 'room'] as const) { - assert.throws( - () => setup.router.dispatch({ route, rpc: 'unknown_rpc', data: {} }), - /unsupported.*unknown_rpc/i, - `${route}:unknown_rpc`, - ); - } - for (const [rpc, expectedRoute] of rpcsByRoute) { - for (const route of ['platform', 'agent', 'room'] as const) { - if (route === expectedRoute || rpc==='player_login') continue; - assert.throws( - () => setup.router.dispatch({ route, rpc, data: {} }), - /route mismatch/i, - `${route}:${rpc}`, - ); - } - } - - assert.equal(setup.store.getState(), before); - assert.equal(setup.gameSession.state, 'active'); - assert.equal(setup.session.state, 'operational'); - assert.equal(notifications, 0); - assert.equal(gameEvents, 0); - assert.equal(setup.getDisposals(), 0); - assert.equal(setup.getInvalidations(), 0); - assert.deepEqual(setup.connection.switchCalls, []); - assert.deepEqual(setup.connection.stopCalls, []); -}); - -test('PlatformHandlers validates every switch URL before closing the active game', () => { - const invalidTargets = [ - 'http://invalid.example', - 'ws://user@example.com', - 'ws://example.com/#fragment', - 'ws://example.com/#', - 'ws://example.com#', - 'ws://@example.com', - 'ws://:@example.com', - 'ws:///path', - 'ws://example.com:99999', - 'ws://example.com:abc', - 'ws://', - ]; - - for (const roomserver of invalidTargets) { - const setup = makeHandlers(); - setup.router.dispatch({ - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - }); - const before = setup.store.getState(); - - assert.throws( - () => setup.router.dispatch({ - route: 'room', rpc: 'connect_roomserver', data: { roomserver }, - }), - /roomserver|WebSocket/i, - roomserver, - ); - - assert.equal(setup.store.getState(), before, roomserver); - assert.equal(setup.gameSession.state, 'active', roomserver); - assert.equal(setup.session.state, 'operational', roomserver); - assert.equal(setup.getDisposals(), 0, roomserver); - assert.equal(setup.getInvalidations(), 0, roomserver); - assert.deepEqual(setup.connection.switchCalls, [], roomserver); - } -}); - -test('PlatformHandlers accepts case-insensitive WS/WSS schemes and preserves full payload identity', () => { - const setup = makeHandlers(); - const roomData = { - roomserver: 'WS://EXAMPLE.com:80/path/%23anchor?x=1', - extra: { trace: 1 }, - }; - const agentData = { - agentserver: 'WSS://secure.example:443/socket', opt: 'free_room', extra: { trace: 2 }, - }; - - setup.router.dispatch({ route: 'room', rpc: 'connect_roomserver', data: roomData }); - setup.router.dispatch({ route: 'agent', rpc: 'connect_agentserver', data: agentData }); - - assert.equal(setup.connection.switchCalls[0]?.target, roomData.roomserver); - assert.equal(setup.connection.switchCalls[0]?.resend.data, roomData); - assert.equal(setup.connection.switchCalls[1]?.target, agentData.agentserver); - assert.equal(setup.connection.switchCalls[1]?.resend.data, agentData); - assert.equal( - (setup.connection.switchCalls[1]?.resend.data as typeof agentData).extra, - agentData.extra, - ); -}); - -test('PlatformHandlers attempts a validated switch after cleanup errors and aggregates all failures', () => { - const setup = makeHandlers(); - setup.router.dispatch({ - route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), - }); - const data = { roomserver: 'wss://switch.example/socket', epoch: 9 }; - const disposeError = new Error('switch:dispose'); - const invalidateError = new Error('switch:invalidate'); - const switchError = new Error('switch:connection'); - setup.observeDisposal(() => { throw disposeError; }); - setup.observeInvalidation(() => { throw invalidateError; }); - setup.connection.switchFailure = switchError; - - const error = caughtError(() => setup.router.dispatch({ - route: 'room', rpc: 'connect_roomserver', data, - })); - - assert.ok(error instanceof RuntimeSessionFault); - assert.deepEqual(error.errors, [disposeError, invalidateError, switchError]); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.session.state, 'faulted'); - assert.equal(setup.getDisposals(), 1); - assert.equal(setup.getInvalidations(), 1); - assert.equal(setup.connection.switchCalls.length, 1); - assert.equal(setup.connection.switchCalls[0]?.target, data.roomserver); - assert.equal(setup.connection.switchCalls[0]?.resend.data, data); -}); - -test('kick_server closes the game, marks the runtime session, and stops the connection once', () => { - const setup = makeHandlers(); - setup.handlers.dispatch({ route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }); - const kickData = { msg: '检查到新版本', code: 1 }; - - setup.session.markLoginPending(); - setup.router.dispatch({ route: 'agent', rpc: 'kick_server', data: kickData }); - - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.session.kicked, true); - assert.equal(setup.session.loginPending, false); - assert.equal(setup.getDisposals(), 1); - assert.deepEqual(setup.connection.stopCalls, [kickData]); - assert.equal(setup.connection.stopCalls[0], kickData); -}); - -test('kick_server latches before cleanup and makes a reentrant duplicate a no-op', () => { - const setup = makeHandlers(); - setup.router.dispatch({ route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }); - const kickData = { msg: 'same-reference' }; - let reentrantResult: ReturnType | undefined; - - setup.session.markLoginPending(); - setup.observeDisposal(() => { - assert.equal(setup.session.kicked, true); - assert.equal(setup.session.loginPending, false); - reentrantResult = setup.router.dispatch({ route: 'agent', rpc: 'kick_server', data: kickData }); - }); - - assert.equal( - setup.router.dispatch({ route: 'agent', rpc: 'kick_server', data: kickData }), - 'handled-platform', - ); - assert.equal(reentrantResult, 'handled-platform'); - assert.equal(setup.getDisposals(), 1); - assert.equal(setup.getInvalidations(), 1); - assert.equal(setup.connection.stopCalls.length, 1); - assert.equal(setup.connection.stopCalls[0], kickData); - assert.equal(setup.session.state, 'kicked'); -}); - -test('kick_server attempts dispose, invalidate, mark, and stop once then aggregates failures', () => { - const setup = makeHandlers(); - setup.router.dispatch({ route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }); - const kickData = { msg: 'cleanup-failures' }; - const disposeError = new Error('kick:dispose'); - const invalidateError = new Error('kick:invalidate'); - const stopError = new Error('kick:stop'); - setup.observeDisposal(() => { throw disposeError; }); - setup.observeInvalidation(() => { throw invalidateError; }); - setup.connection.stopFailure = stopError; - setup.session.markLoginPending(); - - const error = caughtError( - () => setup.router.dispatch({ route: 'agent', rpc: 'kick_server', data: kickData }), - ); - - assert.ok(error instanceof RuntimeSessionFault); - assert.deepEqual(error.errors, [disposeError, invalidateError, stopError]); - assert.equal(setup.gameSession.state, 'idle'); - assert.equal(setup.session.kicked, true); - assert.equal(setup.session.state, 'kicked'); - assert.equal(setup.session.loginPending, false); - assert.equal(setup.getDisposals(), 1); - assert.equal(setup.getInvalidations(), 1); - assert.equal(setup.connection.stopCalls.length, 1); - assert.equal(setup.connection.stopCalls[0], kickData); - - assert.equal( - setup.router.dispatch({ route: 'agent', rpc: 'kick_server', data: kickData }), - 'handled-platform', - ); - assert.equal(setup.getDisposals(), 1); - assert.equal(setup.getInvalidations(), 1); - assert.equal(setup.connection.stopCalls.length, 1); -}); +import { readFileSync } from 'node:fs'; +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts'; +import type { + GameEntry, + GameHost, + GameModule, + PlatformGameSnapshot, + PlatformToGameEvent, +} from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; +import { GameSessionHost } from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts'; +import type { ConnectionControlPort } from '../../YouleNexus/assets/framework/platform/connection-intent.ts'; +import { + RuntimeSession, + RuntimeSessionFault, +} from '../../YouleNexus/assets/framework/platform/runtime-session.ts'; +import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; +import { selectGameSnapshot } from '../../YouleNexus/assets/framework/platform/stores/selectors.ts'; +import { PlatformHandlers } from '../../YouleNexus/assets/framework/protocol/platform-handlers.ts'; +import { Router } from '../../YouleNexus/assets/framework/protocol/router.ts'; + +function fixture(name: string): Record { + return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')).data; +} + +function caughtError(operation: () => void): Error { + try { + operation(); + } catch (error) { + assert.ok(error instanceof Error); + return error; + } + assert.fail('expected operation to throw'); +} + +class ConnectionFixture implements ConnectionControlPort { + readonly switchCalls: Array<{ target: string; resend: OutboundEnvelope }> = []; + readonly stopCalls: unknown[] = []; + switchFailure: Error | undefined; + switchObserver: () => void = () => {}; + stopFailure: Error | undefined; + stopObserver: () => void = () => {}; + + switchServer(target: string, resend: OutboundEnvelope): void { + this.switchCalls.push({ target, resend }); + this.switchObserver(); + if (this.switchFailure) throw this.switchFailure; + } + + stopForKick(data: unknown): void { + this.stopCalls.push(data); + this.stopObserver(); + if (this.stopFailure) throw this.stopFailure; + } +} + +function makeHandlers() { + const store = new PlatformStore(); + let disposals = 0; + let eventObserver: ( + event: PlatformToGameEvent, + snapshot: PlatformGameSnapshot, + ) => void = () => {}; + let disposalObserver: () => void = () => {}; + let invalidationObserver: () => void = () => {}; + let invalidations = 0; + const entry: GameEntry = { + key: 'fixture', gameId: 41, route: 'fixture-game', resolveSeatCount: () => 4, + createModule: (): GameModule => ({ + attach() {}, + handlePlatformEvent(event) { + eventObserver(event, selectGameSnapshot(store.getState())); + }, + handleGameMessage() {}, + restore() {}, + dispose() { disposals += 1; disposalObserver(); }, + }), + }; + const gameSession = new GameSessionHost(entry); + disposals = 0; + const session = new RuntimeSession({ + store, + gameSession, + onServerDenial() { assert.fail('Unexpected server denial'); }, + createGameHostLease: () => ({ + host: { + seat: { toView: (seat) => seat, toServer: (seat) => seat }, + getSnapshot: () => selectGameSnapshot(store.getState()), + subscribe: () => () => {}, sendGameMessage() {}, execute() {}, + }, + invalidate() { + invalidations += 1; + invalidationObserver(); + }, + }), + }); + const connection = new ConnectionFixture(); + const handlers = new PlatformHandlers({ store, gameSession, session, connection }); + const router = new Router({ session, platform: handlers, gameSession }); + return { + store, + gameSession, + session, + connection, + handlers, + router, + getDisposals: () => disposals, + getInvalidations: () => invalidations, + observeEvent(observer: typeof eventObserver) { eventObserver = observer; }, + observeDisposal(observer: () => void) { disposalObserver = observer; }, + observeInvalidation(observer: () => void) { invalidationObserver = observer; }, + }; +} + +function makeReentrantRouterFixture(log: string[]) { + const store = new PlatformStore(); + let nextModuleId = 0; + const restores: Array<{ moduleId: number; deskinfo: unknown }> = []; + const entry: GameEntry = { + key: 'reentrant-fixture', + gameId: 41, + route: 'fixture-game', + resolveSeatCount: () => 4, + createModule: (): GameModule => { + const moduleId = ++nextModuleId; + let host: GameHost | undefined; + return { + attach(attachedHost) { + host = attachedHost; + log.push(`attach:${moduleId}:${attachedHost.getSnapshot().room.roomcode}`); + }, + handlePlatformEvent(event) { + if (!host) throw new Error('fixture module has no host'); + const snapshot = host.getSnapshot(); + if (event.type === 'room.entered') { + log.push(`event:${moduleId}:room.entered:${snapshot.room.roomcode}`); + return; + } + if (event.type === 'room.player-offline' || event.type === 'room.player-online') { + const online = snapshot.seats.find((seat) => seat.seat === event.seat)?.online; + log.push(`event:${moduleId}:${event.type}:${String(online)}`); + } + }, + handleGameMessage() {}, + restore(deskinfo) { + restores.push({ moduleId, deskinfo }); + log.push(`restore:${moduleId}`); + }, + dispose() { log.push(`dispose:${moduleId}`); }, + }; + }, + }; + const gameSession = new GameSessionHost(entry); + nextModuleId = 0; + log.length = 0; + const session = new RuntimeSession({ + store, + gameSession, + onServerDenial() { assert.fail('Unexpected server denial'); }, + createGameHostLease: () => ({ + host: { + seat: { toView: (seat) => seat, toServer: (seat) => seat }, + getSnapshot: () => selectGameSnapshot(store.getState()), + subscribe: () => () => {}, + sendGameMessage() {}, + execute() {}, + }, + invalidate() {}, + }), + }); + const connection = new ConnectionFixture(); + const handlers = new PlatformHandlers({ store, gameSession, session, connection }); + const router = new Router({ session, platform: handlers, gameSession }); + return { store, session, gameSession, router, restores }; +} + +test('Router drains reentrant room entries FIFO so each module receives its own committed room and deskinfo', () => { + const log: string[] = []; + const setup = makeReentrantRouterFixture(log); + setup.router.dispatch({ + route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), + }); + const roomA = fixture('self-join-room.json'); + const roomB = fixture('self-join-room.json'); + roomA.roomcode = 'room-A'; + roomB.roomcode = 'room-B'; + roomA.deskinfo = { round: 'A' }; + roomB.deskinfo = { round: 'B' }; + let queuedResult: ReturnType | undefined; + + setup.store.subscribe((state) => { + if (state.room.kind === 'inside' && state.room.roomcode === 'room-A') { + log.push('store:A'); + queuedResult = setup.router.dispatch({ + route: 'agent', rpc: 'self_join_room', data: roomB, + }); + } else if (state.room.kind === 'inside' && state.room.roomcode === 'room-B') { + log.push('store:B'); + } + }); + + const result = setup.router.dispatch({ + route: 'agent', rpc: 'self_join_room', data: roomA, + }); + + assert.equal(result, 'handled-platform'); + assert.equal(queuedResult, 'handled-platform'); + assert.deepEqual(log, [ + 'store:A', + 'attach:1:room-A', + 'event:1:room.entered:room-A', + 'restore:1', + 'store:B', + 'dispose:1', + 'attach:2:room-B', + 'event:2:room.entered:room-B', + 'restore:2', + ]); + assert.deepEqual(setup.restores, [ + { moduleId: 1, deskinfo: roomA.deskinfo }, + { moduleId: 2, deskinfo: roomB.deskinfo }, + ]); + assert.equal(setup.restores[0]?.deskinfo, roomA.deskinfo); + assert.equal(setup.restores[1]?.deskinfo, roomB.deskinfo); +}); + +test('Router finishes an offline state-first event before draining a reentrant online message', () => { + const log: string[] = []; + const setup = makeReentrantRouterFixture(log); + setup.router.dispatch({ + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + }); + setup.router.dispatch({ + route: 'room', + rpc: 'other_join_room', + data: { + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', sex: 1, + ip: '127.0.0.2', onstate: 0, bean: 90, isprepare: 0, + }, + }); + log.length = 0; + let queuedResult: ReturnType | undefined; + + setup.store.subscribe((state, previous) => { + if (state.room.kind !== 'inside' || previous.room.kind !== 'inside') return; + const wasOffline = previous.room.offlineSeats.includes(2); + const isOffline = state.room.offlineSeats.includes(2); + if (!wasOffline && isOffline) { + log.push('store:offline'); + queuedResult = setup.router.dispatch({ + route: 'room', rpc: 'other_online', data: { seat: 2, ip: '127.0.0.3' }, + }); + } else if (wasOffline && !isOffline) { + log.push('store:online'); + } + }); + + const result = setup.router.dispatch({ + route: 'room', rpc: 'other_offline', data: { seat: 2 }, + }); + + assert.equal(result, 'handled-platform'); + assert.equal(queuedResult, 'handled-platform'); + assert.deepEqual(log, [ + 'store:offline', + 'event:1:room.player-offline:false', + 'store:online', + 'event:1:room.player-online:true', + ]); +}); + +test('PlatformHandlers commits every room event before one typed game event reads the snapshot', () => { + const setup = makeHandlers(); + setup.handlers.dispatch({ + route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), + }); + + function dispatchWithEvent( + message: { route: string; rpc: string; data: Record }, + expectedType: PlatformToGameEvent['type'], + assertSnapshot: (snapshot: PlatformGameSnapshot) => void, + ): void { + const order: string[] = []; + let received = 0; + const unsubscribe = setup.store.subscribe(() => { order.push('store'); }); + setup.observeEvent((event, snapshot) => { + received += 1; + order.push('game'); + assert.equal(event.type, expectedType); + assertSnapshot(snapshot); + }); + + assert.equal(setup.handlers.dispatch(message), 'handled-platform'); + + unsubscribe(); + assert.equal(received, 1); + assert.deepEqual(order, ['store', 'game']); + } + + const join = fixture('self-join-room.json'); + delete join.deskinfo; + join.deskwar = 0; + dispatchWithEvent( + { route: 'agent', rpc: 'self_join_room', data: join }, + 'room.entered', + (snapshot) => { assert.equal(snapshot.room.roomcode, '100001'); }, + ); + + dispatchWithEvent( + { + route: 'room', + rpc: 'other_join_room', + data: { + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', sex: 1, + ip: '127.0.0.2', onstate: 0, bean: 90, isprepare: 0, + }, + }, + 'room.player-joined', + (snapshot) => { assert.equal(snapshot.seats.find((seat) => seat.seat === 2)?.playerId, 77); }, + ); + + dispatchWithEvent( + { route: 'room', rpc: 'player_prepare', data: { seat: 2, deskwar: 1 } }, + 'room.player-ready', + (snapshot) => { + assert.equal(snapshot.seats.find((seat) => seat.seat === 2)?.ready, true); + assert.equal(snapshot.room.stage, 1); + }, + ); + + dispatchWithEvent( + { route: 'room', rpc: 'other_offline', data: { seat: 2 } }, + 'room.player-offline', + (snapshot) => { assert.equal(snapshot.seats.find((seat) => seat.seat === 2)?.online, false); }, + ); + + dispatchWithEvent( + { route: 'room', rpc: 'other_online', data: { seat: 2, ip: '127.0.0.3' } }, + 'room.player-online', + (snapshot) => { assert.equal(snapshot.seats.find((seat) => seat.seat === 2)?.online, true); }, + ); + + dispatchWithEvent( + { route: 'room', rpc: 'other_exit_room', data: { seat: 2 } }, + 'room.player-left', + (snapshot) => { assert.equal(snapshot.seats.some((seat) => seat.seat === 2), false); }, + ); +}); + +test('self_exit_room commits the outside snapshot before disposing the game session', () => { + const setup = makeHandlers(); + setup.handlers.dispatch({ + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + }); + const order: string[] = []; + setup.store.subscribe(() => { order.push('store'); }); + setup.observeDisposal(() => { + order.push('dispose'); + assert.equal(setup.store.getState().room.kind, 'outside'); + }); + + setup.handlers.dispatch({ route: 'room', rpc: 'self_exit_room', data: { seat: 1 } }); + + assert.deepEqual(order, ['store', 'dispose']); + assert.equal(setup.gameSession.state, 'idle'); +}); + +test('self_exit_room accepts omitted seat, validates a present seat, and is idempotent outside', () => { + const setup = makeHandlers(); + setup.router.dispatch({ + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + }); + const inside = setup.store.getState(); + const disposalsBefore = setup.getDisposals(); + + assert.throws( + () => setup.router.dispatch({ route: 'room', rpc: 'self_exit_room', data: { seat: 0 } }), + /self_exit_room.*seat.*expected 1.*received 0/i, + ); + assert.equal(setup.store.getState(), inside); + assert.equal(setup.gameSession.state, 'active'); + assert.equal(setup.getDisposals(), disposalsBefore); + + let notifications = 0; + setup.store.subscribe(() => { notifications += 1; }); + assert.equal( + setup.router.dispatch({ route: 'room', rpc: 'self_exit_room', data: { reason: 'done' } }), + 'handled-platform', + ); + assert.equal(setup.store.getState().room.kind, 'outside'); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(notifications, 1); + assert.equal(setup.getDisposals(), disposalsBefore + 1); + + assert.equal( + setup.router.dispatch({ route: 'room', rpc: 'self_exit_room', data: { reason: 'duplicate' } }), + 'handled-platform', + ); + assert.equal(notifications, 1); + assert.equal(setup.getDisposals(), disposalsBefore + 1); +}); + +test('PlatformHandlers preserves complete switch payloads and builds the exact resend envelopes', () => { + const setup = makeHandlers(); + const roomData = { roomserver: '10.0.0.2:3088', epoch: 7 }; + const agentData = { + agentserver: 'secure.example:443/path', opt: 'free_room', trace: { id: 'x' }, + }; + + assert.equal(setup.router.dispatch({ + route: 'room', rpc: 'connect_roomserver', data: roomData, + }), 'handled-platform'); + assert.equal(setup.router.dispatch({ + route: 'agent', rpc: 'connect_agentserver', data: agentData, + }), 'handled-platform'); + + assert.deepEqual(setup.connection.switchCalls, [ + { + target: 'ws://10.0.0.2:3088', + resend: { app: 'youle', route: 'room', rpc: 'connect_roomserver', data: roomData }, + }, + { + target: 'ws://secure.example:443/path', + resend: { app: 'youle', route: 'agent', rpc: 'connect_agentserver', data: agentData }, + }, + ]); + assert.equal(setup.connection.switchCalls[0]?.resend.data, roomData); + assert.equal(setup.connection.switchCalls[1]?.resend.data, agentData); +}); + +test('PlatformHandlers rejects unknown, route-mismatched, missing, and invalid switch targets without effects', () => { + const setup = makeHandlers(); + const before = setup.store.getState(); + + assert.throws( + () => setup.handlers.dispatch({ route: 'agent', rpc: 'unsupported_rpc', data: {} }), + /unsupported_rpc/, + ); + assert.throws( + () => setup.handlers.dispatch({ route: 'agent', rpc: 'connect_roomserver', data: { roomserver: 'x:1' } }), + /route/i, + ); + assert.throws( + () => setup.handlers.dispatch({ route: 'room', rpc: 'connect_roomserver', data: {} }), + /roomserver/, + ); + assert.throws( + () => setup.handlers.dispatch({ + route: 'room', rpc: 'connect_roomserver', data: { roomserver: 'http://invalid.example' }, + }), + /roomserver|WebSocket/i, + ); + + assert.equal(setup.store.getState(), before); + assert.deepEqual(setup.connection.switchCalls, []); +}); + +test('unknown and every route-mismatched platform rpc leave all observable state unchanged', () => { + const setup = makeHandlers(); + setup.router.dispatch({ + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + }); + const before = setup.store.getState(); + let notifications = 0; + let gameEvents = 0; + setup.store.subscribe(() => { notifications += 1; }); + setup.observeEvent(() => { gameEvents += 1; }); + const rpcsByRoute = [ + ['player_login', 'agent'], + ['self_join_room', 'agent'], + ['player_prepare', 'room'], + ['self_exit_room', 'room'], + ['other_join_room', 'room'], + ['other_exit_room', 'room'], + ['other_offline', 'room'], + ['other_online', 'room'], + ['connect_roomserver', 'room'], + ['connect_agentserver', 'agent'], + ['kick_server', 'agent'], + ] as const; + + for (const route of ['platform', 'agent', 'room'] as const) { + assert.throws( + () => setup.router.dispatch({ route, rpc: 'unknown_rpc', data: {} }), + /unsupported.*unknown_rpc/i, + `${route}:unknown_rpc`, + ); + } + for (const [rpc, expectedRoute] of rpcsByRoute) { + for (const route of ['platform', 'agent', 'room'] as const) { + if (route === expectedRoute || rpc==='player_login') continue; + assert.throws( + () => setup.router.dispatch({ route, rpc, data: {} }), + /route mismatch/i, + `${route}:${rpc}`, + ); + } + } + + assert.equal(setup.store.getState(), before); + assert.equal(setup.gameSession.state, 'active'); + assert.equal(setup.session.state, 'operational'); + assert.equal(notifications, 0); + assert.equal(gameEvents, 0); + assert.equal(setup.getDisposals(), 0); + assert.equal(setup.getInvalidations(), 0); + assert.deepEqual(setup.connection.switchCalls, []); + assert.deepEqual(setup.connection.stopCalls, []); +}); + +test('PlatformHandlers validates every switch URL before closing the active game', () => { + const invalidTargets = [ + 'http://invalid.example', + 'ws://user@example.com', + 'ws://example.com/#fragment', + 'ws://example.com/#', + 'ws://example.com#', + 'ws://@example.com', + 'ws://:@example.com', + 'ws:///path', + 'ws://example.com:99999', + 'ws://example.com:abc', + 'ws://', + ]; + + for (const roomserver of invalidTargets) { + const setup = makeHandlers(); + setup.router.dispatch({ + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + }); + const before = setup.store.getState(); + + assert.throws( + () => setup.router.dispatch({ + route: 'room', rpc: 'connect_roomserver', data: { roomserver }, + }), + /roomserver|WebSocket/i, + roomserver, + ); + + assert.equal(setup.store.getState(), before, roomserver); + assert.equal(setup.gameSession.state, 'active', roomserver); + assert.equal(setup.session.state, 'operational', roomserver); + assert.equal(setup.getDisposals(), 0, roomserver); + assert.equal(setup.getInvalidations(), 0, roomserver); + assert.deepEqual(setup.connection.switchCalls, [], roomserver); + } +}); + +test('PlatformHandlers accepts case-insensitive WS/WSS schemes and preserves full payload identity', () => { + const setup = makeHandlers(); + const roomData = { + roomserver: 'WS://EXAMPLE.com:80/path/%23anchor?x=1', + extra: { trace: 1 }, + }; + const agentData = { + agentserver: 'WSS://secure.example:443/socket', opt: 'free_room', extra: { trace: 2 }, + }; + + setup.router.dispatch({ route: 'room', rpc: 'connect_roomserver', data: roomData }); + setup.router.dispatch({ route: 'agent', rpc: 'connect_agentserver', data: agentData }); + + assert.equal(setup.connection.switchCalls[0]?.target, roomData.roomserver); + assert.equal(setup.connection.switchCalls[0]?.resend.data, roomData); + assert.equal(setup.connection.switchCalls[1]?.target, agentData.agentserver); + assert.equal(setup.connection.switchCalls[1]?.resend.data, agentData); + assert.equal( + (setup.connection.switchCalls[1]?.resend.data as typeof agentData).extra, + agentData.extra, + ); +}); + +test('PlatformHandlers attempts a validated switch after cleanup errors and aggregates all failures', () => { + const setup = makeHandlers(); + setup.router.dispatch({ + route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json'), + }); + const data = { roomserver: 'wss://switch.example/socket', epoch: 9 }; + const disposeError = new Error('switch:dispose'); + const invalidateError = new Error('switch:invalidate'); + const switchError = new Error('switch:connection'); + setup.observeDisposal(() => { throw disposeError; }); + setup.observeInvalidation(() => { throw invalidateError; }); + setup.connection.switchFailure = switchError; + + const error = caughtError(() => setup.router.dispatch({ + route: 'room', rpc: 'connect_roomserver', data, + })); + + assert.ok(error instanceof RuntimeSessionFault); + assert.deepEqual(error.errors, [disposeError, invalidateError, switchError]); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.session.state, 'faulted'); + assert.equal(setup.getDisposals(), 1); + assert.equal(setup.getInvalidations(), 1); + assert.equal(setup.connection.switchCalls.length, 1); + assert.equal(setup.connection.switchCalls[0]?.target, data.roomserver); + assert.equal(setup.connection.switchCalls[0]?.resend.data, data); +}); + +test('kick_server closes the game, marks the runtime session, and stops the connection once', () => { + const setup = makeHandlers(); + setup.handlers.dispatch({ route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }); + const kickData = { msg: '检查到新版本', code: 1 }; + + setup.session.markLoginPending(); + setup.router.dispatch({ route: 'agent', rpc: 'kick_server', data: kickData }); + + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.session.kicked, true); + assert.equal(setup.session.loginPending, false); + assert.equal(setup.getDisposals(), 1); + assert.deepEqual(setup.connection.stopCalls, [kickData]); + assert.equal(setup.connection.stopCalls[0], kickData); +}); + +test('kick_server latches before cleanup and makes a reentrant duplicate a no-op', () => { + const setup = makeHandlers(); + setup.router.dispatch({ route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }); + const kickData = { msg: 'same-reference' }; + let reentrantResult: ReturnType | undefined; + + setup.session.markLoginPending(); + setup.observeDisposal(() => { + assert.equal(setup.session.kicked, true); + assert.equal(setup.session.loginPending, false); + reentrantResult = setup.router.dispatch({ route: 'agent', rpc: 'kick_server', data: kickData }); + }); + + assert.equal( + setup.router.dispatch({ route: 'agent', rpc: 'kick_server', data: kickData }), + 'handled-platform', + ); + assert.equal(reentrantResult, 'handled-platform'); + assert.equal(setup.getDisposals(), 1); + assert.equal(setup.getInvalidations(), 1); + assert.equal(setup.connection.stopCalls.length, 1); + assert.equal(setup.connection.stopCalls[0], kickData); + assert.equal(setup.session.state, 'kicked'); +}); + +test('kick_server attempts dispose, invalidate, mark, and stop once then aggregates failures', () => { + const setup = makeHandlers(); + setup.router.dispatch({ route: 'agent', rpc: 'player_login', data: fixture('player-login-room.json') }); + const kickData = { msg: 'cleanup-failures' }; + const disposeError = new Error('kick:dispose'); + const invalidateError = new Error('kick:invalidate'); + const stopError = new Error('kick:stop'); + setup.observeDisposal(() => { throw disposeError; }); + setup.observeInvalidation(() => { throw invalidateError; }); + setup.connection.stopFailure = stopError; + setup.session.markLoginPending(); + + const error = caughtError( + () => setup.router.dispatch({ route: 'agent', rpc: 'kick_server', data: kickData }), + ); + + assert.ok(error instanceof RuntimeSessionFault); + assert.deepEqual(error.errors, [disposeError, invalidateError, stopError]); + assert.equal(setup.gameSession.state, 'idle'); + assert.equal(setup.session.kicked, true); + assert.equal(setup.session.state, 'kicked'); + assert.equal(setup.session.loginPending, false); + assert.equal(setup.getDisposals(), 1); + assert.equal(setup.getInvalidations(), 1); + assert.equal(setup.connection.stopCalls.length, 1); + assert.equal(setup.connection.stopCalls[0], kickData); + + assert.equal( + setup.router.dispatch({ route: 'agent', rpc: 'kick_server', data: kickData }), + 'handled-platform', + ); + assert.equal(setup.getDisposals(), 1); + assert.equal(setup.getInvalidations(), 1); + assert.equal(setup.connection.stopCalls.length, 1); +}); diff --git a/cocoscreator_projects/games/erqiwang/docs/design/design.md b/cocoscreator_projects/games/erqiwang/docs/design/design.md new file mode 100644 index 0000000..7bc8e98 --- /dev/null +++ b/cocoscreator_projects/games/erqiwang/docs/design/design.md @@ -0,0 +1,784 @@ +# 二七王 玩法设计文档 + +> 本文档是二七王玩法的唯一权威规则说明。内容随讨论持续与规则设计者逐项确认、修订;文中标注"待确认"的地方是当前仍未有定论的开放问题,其余内容均已确认生效。 + +## 目录 + +1. [术语约定](#1-术语约定) +2. [牌局构成](#2-牌局构成) +3. [主牌顺序](#3-主牌顺序) +4. [开局与坐庄流程](#4-开局与坐庄流程) +5. [出牌规则](#5-出牌规则) + - [5.1 跟牌基本原则](#51-跟牌基本原则) + - [5.2 按牌型跟牌](#52-按牌型跟牌) + - [5.3 拖拉机定义与相邻关系](#53-拖拉机定义与相邻关系) + - [5.4 甩牌详细规则](#54-甩牌详细规则) +6. [捡分与扣底](#6-捡分与扣底) + - [6.1 分牌](#61-分牌) + - [6.2 捡分规则](#62-捡分规则) + - [6.3 扣底](#63-扣底) +7. [结算:子数与升级](#7-结算子数与升级) + - [7.0 四层关系总览 / 查表索引](#70-四层关系总览) + - [7.1 常规算子:叫分 → 基础子数](#71-常规算子叫分-→-基础子数未勾选爬坡时的默认规则) + - [7.2 常规算子的判定表](#72-捡分-vs-叫分庄家的过庄-小光-大光闲家的升级常规算子) + - [7.3 爬坡](#73-爬坡叫分-→-基础子数与保小光分段可选规则开房时勾选爬坡生效) +8. [算奖规则](#8-算奖规则) + - [8.1 冲关](#81-冲关) + - [8.2 亮牌规则](#82-亮牌规则) + - [8.3 傍王(可选规则)](#83-傍王可选规则) + - [8.4 算奖如何影响最终结算](#84-算奖如何影响最终结算) +9. [牌局查看模式](#9-牌局查看模式) +10. [房间设置选项](#10-房间设置选项) +11. [牌局交互提示](#11-牌局交互提示) +12. [完整牌局游玩流程](#12-完整牌局游玩流程) + +--- + +## 1. 术语约定 + +| 术语 | 含义 | +| --- | --- | +| 主牌 / 副牌 | 庄家选定的花色为"主牌",其余三个花色为"副牌";主牌大于副牌 | +| 固定主牌 | 不论选择哪个花色为主,大王、小王、以及所有花色的"2""7"恒为主牌 | +| 正2 / 正7 | 选定花色的"2""7",大于其余花色的"2""7"(口语里也叫"主2""主7",本文档统一用"正2""正7")| +| 副2 / 副7 | 非选定花色的"2""7" | +| 底牌 | 发牌时**没有发给玩家**、扣在桌面的 8 张牌;庄家确认后可摸起查看,摸起后就不再扣在桌面,而是并入庄家手牌 | +| 埋牌底牌 | 庄家"埋牌"时从手牌中选出重新扣下的 8 张牌;"底牌"与"埋牌底牌"是两批不同的牌,不通用、不互换 | +| 埋牌 | 庄家看到底牌、摸起查看后,从手牌中选出 8 张重新扣下,作为"埋牌底牌" | +| 捡分 | 出牌中赢下的分牌(5、10、K)计入闲家总得分的过程 | +| 扣底 | 出完所有手牌后的最后一轮,若被闲家用主牌牌型压过庄家,则埋下的 8 张埋牌底牌也计入闲家捡分,并按牌型翻倍 | +| 亮牌 | 庄家埋牌后、出牌前,若手中固定主牌达到门槛(总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6 任一),向两个闲家**亮出自己全部固定主牌的具体牌面**(见第 8.2 节)。不限叫分,仅可查牌模式 | +| 余主公示 | 任一玩家主牌出空(报无主)后,为**全体三人**展示另外两家各自的**剩余主牌数与剩余主对数**(见第 9 节)。只给数量,不给具体牌面 | +| 明牌 | 可查牌模式下,查看**另外两家**手中全部未出主牌的具体牌面(见第 9 节)。与"亮牌"同样给牌面,区别在:亮牌是**庄家公开自己的**,明牌是**去看别人的** | +| 摸底 | 庄家坐定后把那 8 张**底牌**摸起查看并并入手牌(见第 4 节)。**非 70 分坐庄时只有庄家看得到这 8 张** | +| 开底 | **70 分坐庄**时,在庄家摸底**之前**,先把这 8 张底牌向**所有玩家**翻开 3 秒,之后才由庄家摸入手牌(见第 4 节)。给的是具体牌面 | +| 查底牌 | 对局中随时回看那 8 张底牌的功能(前端底栏按钮)。庄家全程可用;闲家仅在**开底**过的局(即 70 分坐庄)可用 | +| 拖拉机 | 同一花色(含主牌)里点数相邻的连续对子,如 K K Q Q 为"两托"(两连对) | +| 甩牌 | 首家出牌时,一次性打出多组主牌组合(单张 / 对子 / 拖拉机自由搭配);仅主牌可以甩牌,副牌禁止甩牌,只能分单张 / 对子 / 拖拉机依次打出(见第 5.4 节) | +| 毙牌 | 跟牌时手中没有本轮花色(缺门),改用主牌打出、且牌面大过本轮目前所有人的牌(见第 5.1 / 5.2 节) | +| 垫牌 | 跟牌时手中没有本轮花色(缺门)、且不用主牌毙牌,随意出一张其他副牌,不参与、不争夺本轮的出牌权(见第 5.1 节) | +| 混合出牌 | 首家出副牌,你手中有该花色副牌但张数不够凑齐首家总张数,又没有(或不用)别的副牌补差额,只能用主牌补足缺口——打出的是"该花色副牌 + 主牌"的混合牌。这既不是垫牌(不是缺门、不是纯副牌),也不是毙牌(不是纯主牌牌型、压不过首家),服务端判定其牌面为 0,**与垫牌等效:不参与、不争夺本轮出牌权**(见第 5.1 / 5.2 节) | +| 子 | 结算时用于计算输赢筹码的单位,与"倍数"是两个独立概念(见第 7 节) | +| 冲关 | 局末对固定组合(三王/四王、六 2/七 2/八 2、六 7/七 7/八 7、连续对子链等)的额外奖励结算,**只计庄家**(见第 8.1 节)。旧称"常规算奖",现统一叫**冲关**;服务端字段名 `chongguan` 与此同源 | +| 算奖 | **上位概念** = 冲关(8.1,只计庄家)+ 傍王(8.3,可选、庄闲都算)。两个分量求和后才是某玩家的总奖数 `N`,据此结算(见第 8.4 节)。**说"冲关"指的是庄家那一份,说"算奖"指的是合计** | +| 傍王 | 可选房间规则:勾选后每张王额外算一奖,庄家、闲家都要算;这是在 8.1 冲关之外**额外叠加**的奖励,与冲关互不冲突(见第 8.3 节) | +| 爬坡 | 可选房间规则:改变叫分对应的基础子数与保小光分数线,未勾选时按"常规算子"结算(见第 7 节) | + +> **术语变更记录(2026-08-25)**:原「**暗牌**」(发牌留桌的 8 张)改称「**底牌**」;原「**底牌**」(庄家埋下的 8 张)改称「**埋牌底牌**」。 +> +> 改名原因:前端界面上「底牌」按钮查看的正是发牌留桌那 8 张,旧命名与界面倒挂、极易写反。 +> +> **本文已全文改用新术语。** 但 `docs/protocol/packet_protocol.md` 与服务端代码里的 `bottomcards` 字段名**尚未拆分**——两批牌目前仍共用这一个字段名,只靠「出现在哪个包」区分语义:`shangzhuang` / `ChooseMain` / `BuryCards` 里的是**底牌**,`maipai` / `PushCards` / 结算 `bottom` 分组里的是**埋牌底牌**。拆分计划见 `docs_dev/二七王-UI资源与精灵清单.md` §7.5 的 S-4。 + +> **易混术语对照(都带"亮/明"字,但方向与粒度各不相同,务必分清)**: +> +> | 术语 | 谁公开给谁 | 给的是什么 | 触发条件 | 协议字段 | +> | --- | --- | --- | --- | --- | +> | **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后手牌达门槛(§8.2) | `liangpai` | +> | **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(§9) | `seatlist[seat][4]` + `baozhu` | +> | **明牌** | 他家 → 请求者 | **具体牌面**(他家全部未出主牌) | 可查牌 + 报无主,主动点击(§9) | `mingpai.others[].zhucards` | +> | **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(§4) | `ancard3s` + `bottomcards` | +> +> 一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。 +> 另有一组「底」字族专管那 8 张底牌:**摸底**(庄家摸起看)、**开底**(70 分时全场看 3 秒)、**查底牌**(随时回看)、**扣底**(末轮翻开埋牌底牌计分)。 +> 另注:**「亮主」不是本文档的术语**——它是前端选主界面的标题文案(庄家选主牌花色那一步),与上表四项无关。 + +--- + +## 2. 牌局构成 + +两副扑克牌共 108 张,去掉两副牌中的"3"和"4"(2 种点数 × 4 花色 × 2 副 = 16 张),剩余 **92 张**: + +- 点数 5、6、7、8、9、10、J、Q、K、A、2:共 11 种点数,每种 8 张(2 副 × 4 花色) +- 大王、小王:各 2 张(每副各 1 对) + +花色为黑桃 ♠、红桃 ♥、梅花 ♣、方块 ♦。 + +--- + +## 3. 主牌顺序 + +每局开局前庄家选择一个花色作为本局主牌,其余三个花色为副牌。 + +固定主牌(大王、小王、所有花色的 2 和 7)不论选择哪个花色为主,恒定属于主牌;选定花色里的其余普通牌也算主牌。 + +主牌从大到小排列: + +| 顺序 | 牌 | +| --- | --- | +| 1 | 大王 | +| 2 | 小王 | +| 3 | 正 7(选定花色的 7) | +| 4 | 副 7(其余花色的 7) | +| 5 | 正 2(选定花色的 2) | +| 6 | 副 2(其余花色的 2) | +| 7 及以下 | 选定花色的其余普通牌:A、K、Q、J、10、9、8、6、5 | + +副牌(非选定花色,且非 2、7、王)从大到小:A、K、Q、J、10、9、8、6、5。 + +> **关于"对子"的判定**:一副牌里同一张牌有两副(deck1 / deck2),"对子"指**同一张具体牌的两副**(如两张♦7、两张大王)。正 7、副 7、正 2、副 2 虽在上表中各占一个**大小等级**(用于排序与比大小时,同级的三张副 7 视为等大),但**成对只认同一花色同点数的两副**——即两张**不同花色**的副 7(如♠7 + ♥7)**不构成一对**,两张不同花色的副 2 同理。拖拉机的连对判定也以此为基础(每一节连对里的每个对子都须是同花色同点数两副)。 + +--- + +## 4. 开局与坐庄流程 + +1. 每人摸 28 张牌,剩余 8 张扣在桌面作为"底牌"。 +2. 由暂定庄家开始,按逆时针方向依次叫分: + - 叫分范围 5 ~ 70 分,步进 5 分。 + - 叫分数值代表"庄家承诺闲家捡分总数将严格低于该数值"(即闲家捡分只要达到或超过这个数值,庄家就算没达成承诺,见第 7 节);数值越低代表叫分者越有信心,因此后叫的人必须叫出比当前更低的分数才能压过前者,不能叫相同或更高的分数。 + - **暂定庄家必须叫分,不能"不叫"**:叫分从暂定庄家开始,他必须报出一个 5 ~ 70 之间的具体分数,不存在"暂定庄家弃权、三家都不叫"这种情况;起始叫分确定之后,后面的玩家才可以选择"叫出更低的分数"或"不叫"。 + - 若某玩家直接叫出 5 分,则无人能再压过,立即确定为庄家。 + - 若一家叫分后,另外两家都选择"不叫",则该叫分玩家成为本局庄家。 +3. **摸底**:庄家确定后把 8 张底牌摸入手牌(摸完后庄家共 36 张): + - **非 70 分坐庄**:桌面 8 张底牌**只有庄家可见**,庄家查看后摸入手牌。 + - **70 分坐庄**(触发**开底**):在庄家**摸底**之前,先把这 8 张底牌**向所有玩家(含两个闲家)翻开 3 秒**,然后才由庄家把它们摸入手牌。 +4. **选主 / 投降(同一决策点,二选一,互斥)**: + - **选主**:庄家在 4 个花色中选定一个作为本局主牌(见第 3 节)→ 进入下一步埋牌、随后出牌。 + - **投降**:仅当**叫分为 70 分**时,此决策点才额外提供"投降"选项;选择投降表示放弃本局,**直接结束、不再选主、不埋牌、不出牌**,按 7.1 节"70 分坐庄 · 投降"的固定结果结算(基础子数 1 个,庄家直接输;算奖仍照常,见 8.4 节)。选主与投降互斥——**选了花色就等于放弃投降、正常打牌;选了投降就不再选主**。 + - 叫分 < 70 分时没有投降选项,只能选主。 + - (**开底**——70 分坐庄的 8 张底牌向所有玩家翻开 3 秒,发生在上一步"庄家摸底之前",见步骤 3。) + - **前端表现**:此阶段界面显示 4 个花色选主按钮,且**每个花色按钮上要显示该花色在庄家手中有多少对**(供庄家判断选哪门为主);70 分坐庄时并列再显示一个投降按钮。 +5. **埋牌**(仅"选主/打牌"路径):庄家从 36 张里选出 8 张牌重新扣下("埋牌"),作为"埋牌底牌",供最后判断"扣底"使用;埋牌后庄家保留 28 张。(**本局先选主、后埋牌**——庄家先定主牌花色,知道主副后再决定埋哪 8 张。) +6. 坐庄轮换规则(决定下一局的"暂定庄家"): + - 第一局的暂定庄家为 **0 号座位**的玩家(此前没有打过任何牌局、还不存在"上一局的庄家",故取固定座位)。 + - 若庄家本局获胜(庄赢),下一局暂定庄家仍为该玩家(连庄)。 + - 若庄家本局失败(闲家捡分达标 / 庄家投降),下一局暂定庄家变为该玩家的下家(按逆时针顺延)。 + +--- + +## 5. 出牌规则 + +### 5.1 跟牌基本原则 + +- 每一轮由上一轮牌面最大的一方先出牌;**每一局(每一手牌)的第一轮固定由庄家先出**——此时还没有"上一轮"可以比较,只能由庄家开局。 +- 首家出什么花色,其余两家必须跟出同花色的牌;若手中没有该花色的牌或数量不够(缺门),可以任选以下一种方式补齐: + - **毙牌**:改用主牌打出,且牌面要大过本轮目前所有人的牌,即可抢下本轮的出牌权; + - **垫牌**:随意出一张其他副牌,不参与、不争夺本轮出牌权。 +- 首家若出主牌,其余两家必须跟出主牌,规则同上(此时缺门只能垫副牌,不存在再用主牌毙牌的情况)。 + +### 5.2 按牌型跟牌 + +跟牌的总原则:**在本轮首家花色内,尽量凑出与首家相同的牌型结构;能凑出的高规格部分必须优先打出,凑不齐的部分才用低规格牌补足。** 服务端按此层级精确计算每名跟牌者的"必出牌"与"可选牌",玩家无法跳过更高规格的强制部分。 + +| 首家牌型 | 跟牌要求 | +| --- | --- | +| 单张 | 跟同花色任意单张;同花色缺门则任意牌补齐 | +| 对子 | 有同花色对子必须打出对子;没有对子则打两张同花色单牌;同花色牌不足两张时,现有的同花色牌必须打出、不足部分任意补齐 | +| 拖拉机(N 连对) | 见下方「拖拉机跟牌的强制层级」 | + +#### 拖拉机跟牌的强制层级(同花色牌足够时) + +首家出 N 连对拖拉机,跟牌者手中该花色牌数量 ≥ 首家张数时,按以下优先级**从高到低**确定必出部分,高一级能凑出就必须先凑,不允许跳过: + +1. **有同长(N 连对)的同花色拖拉机** → 必须打出该拖拉机;若手中有多组等长拖拉机,可任选其一打出。 +2. **没有 N 连对,但持有更短的同花色拖拉机(仅 3 连对及以上首家才会走到这一步)** → 必须先拆出手中能凑到的**最长**拖拉机作为强制部分,再用次长拖拉机 / 对子 / 单张继续补足到相同张数——即"能凑多长的拖拉机,就必须先凑多长",不允许放着更长的拖拉机不出而只出零散对子。 +3. **没有任何同花色拖拉机,但对子数量足够(对子数 × 2 ≥ 首家张数)** → 用现有对子凑够张数打出(多余的对子可自选保留)。 +4. **对子数量不够(对子数 × 2 < 首家张数)** → 手中现有的同花色对子**全部**必须打出,剩余缺口用同花色单张补齐。 +5. **完全没有同花色对子** → 用同花色单张随意补齐张数。 + +**同花色牌数量不足**首家张数时(手中还有该花色的牌,但张数不够——无论首家出的是单张、对子还是 N 连对拖拉机):手中该花色的牌**全部必须打出**(不论它们是否成对、是否成拖拉机),不足的缺口用**任意其他花色的牌**补齐——补齐的牌**不要求成对、不要求成拖拉机、也不要求是主牌**,随意垫即可。特别地,**即便首家出的是拖拉机,缺口部分也不强制补成对子或拖拉机**(服务端只校验总张数,不校验缺口部分的结构)。若缺口只能用主牌补(手中没有别的副牌可垫),打出的就是"该花色副牌 + 主牌"的**混合出牌**:它不是缺门、不是毙牌,牌面判为 0,**与垫牌等效,不争夺本轮出牌权**(见术语表「混合出牌」)。 + +若**完全缺门**(手中没有该花色的任何一张牌):整手都可任意出,可选择垫牌或用主牌毙牌(见下)。 + +毙牌的前提是**完全缺门**——手中**没有本轮首家花色的任何一张牌**。只要手里还剩该花色的牌(哪怕凑不成对子、凑不成拖拉机),都必须优先打出这些同花色牌(按上文 5.2 层级),**不允许留着该花色的牌改用主牌毙**。缺门后用主牌毙牌时,牌型与张数须与首家一一对应,才算"压过"、抢下出牌权: + +- 若首家出副牌单张,其余玩家**完全缺门**该副牌花色,可以任意出牌(含主牌);但要压过首家,出的主牌也必须是单张(数量对应)。 +- 若首家出副牌对子,其余玩家**完全缺门**该副牌花色,要压过首家,出的主牌必须也是对子(不能用任意两张主牌顶替)。 +- 若首家出副牌拖拉机(N 连对),其余玩家**完全缺门**该副牌花色,要压过首家,出的主牌必须也是同样连对组数(N 连对)的主拖拉机,总张数完全对应;单张主牌或零散的主对子都不能顶替拖拉机。(注意:只要手里还有该副牌花色的牌,哪怕凑不成同长度拖拉机,也必须先出这些同花色牌,不能改用主牌毙。) + +### 5.3 拖拉机定义与相邻关系 + +**拖拉机 = 在各自的大小序列里「位置相邻」的两个(或以上)对子**,如 K K Q Q 为两连对(两托),K K Q Q J J 为三连对(三托)。副牌与主牌各有自己的序列,相邻规则不同,分开说: + +#### 副牌拖拉机(非主花色) + +**必须同一花色**,点数在下面这条序列里相邻: + +``` +A - K - Q - J - 10 - 9 - 8 - 6 - 5 +``` + +序列里没有 7 和 2(它们是固定主牌,见第 3 节),所以 **7 不与 6、8 相连,而 6 与 8 相连**(中间隔的 7 已被抽走);同理 4、3 不在牌堆里(见第 2 节),5 是副牌序列的最小一档。**跨花色不构成连对**——♥K 对 + ♣Q 对不是拖拉机。 + +#### 主牌拖拉机(主花色 + 固定主牌) + +主牌只有**一条**完整的大小序列(即第 3 节的主牌顺序): + +``` +大王 - 小王 - 正7 - 副7 - 正2 - 副2 - 主A - 主K - 主Q - 主J - 主10 - 主9 - 主8 - 主6 - 主5 +``` + +只要在这条序列里**位置相邻**的对子,就构成拖拉机,**不限花色**。因此「正7 对 + 副7 对」「副7 对 + 正2 对」「副2 对 + 主A 对」「大王对 + 小王对」都是合法的两连对,尽管两个对子的花色不同。序列末段(主A 到 主5)都是**选定花色**的普通牌,其中同样是 6 与 8 相连、7 不入此段。 + +> **同一档位的两个对子不相邻**:「副7」在序列里只占**一个**位置,所以 ♥7 对 + ♣7 对是**两个平级的对子、不是拖拉机**;副2 同理。这与第 3 节「成对只认同花色同点数的两副」是同一条原则的两面——先按同花色同点数成对,再看这些对子在序列里的位置是否相邻。 + +### 5.4 甩牌详细规则 + +#### 5.4.1 基础定义 + +**甩牌**:当你是本轮首家出牌人时,一次性打出多组主牌混合牌型(单张、对子、拖拉机自由组合),一轮打完多张主牌,无需分多轮依次打出。 + +两条硬性基础禁令: + +1. **仅主牌允许甩牌,所有副牌完全禁止甩牌**:无论外面是否剩余该花色副牌,副牌只能分开单出、出对子、出拖拉机,不能一次性打包甩出。 +2. **只有本轮先手才能执行甩牌**:若他人先出牌,你只能跟牌、垫牌、用主牌毙牌,不具备甩牌资格。 + +#### 5.4.2 生效条件(最大性原则)与报无主 + +想要成功甩牌,必须同时满足:**剩余两名对手手中,不存在任何一张、一对、一组拖拉机能压制你甩出的整套主牌组合**。简单说,全场所有更大的主牌、更大的主对子、更长的主拖拉机必须全部在你手中,两家闲家没有任何牌型能盖过你的甩牌组合。 + +**合法性由服务端在甩牌提交时自动判定**:服务端掌握全部玩家的手牌,甩牌一旦提交,立即检查另外两名玩家手中是否"持有"(不要求对方真正打出)能压过这套组合任意一部分的主牌;只要有人持有,就直接判定为甩错(见 5.4.5),不需要、也不会等到对方实际出牌反制才判定。 + +"报无主"是给玩家参考的辅助信息,**不是合法性判定的依据**(合法性判定始终由服务端按全部手牌精确计算):对局中非最后一轮,任意玩家手里没有主牌了,必须口头报无主,方便其他玩家在决定要不要甩牌前,大致判断风险: + +- **另外两家都报了无主**:全场只剩你自己持有主牌,甩牌必然合法,无需记牌。 +- **另外两家只有一家报了无主、还有一家没报**:那一家仍持有主牌,你若甩牌需要自行记牌估算风险;但即使判断失误,最终是否算甩错仍由服务端精确判定,不取决于你的记牌是否正确。 + +#### 5.4.3 甩牌可包含的牌型组合 + +甩牌仅限本局主牌(固定主牌 + 选定主花色的普通牌),内部可自由混搭任意数量、任意种类牌型: + +- **纯单张**:大王、小王、正 7、副 7、正 2、副 2、主花色单牌混合; +- **纯对子**:大王对 / 小王对(各自成对,大王与小王点数不同,不能混成一对)、正七对、副七对、正二对、主花色数字对子; +- **主拖拉机**:大小王拖拉机、七七连对、二二连对、主花色数字连对; +- **混合搭配**:单张 + 对子 + 多连拖拉机一同甩出,无组合数量限制。 + +#### 5.4.4 其余两家应对甩牌的强制跟牌规则 + +甩牌一旦被判定合法(见 5.4.2:服务端已确认两名闲家都不持有能盖压这套组合任何一部分的主牌),闲家就不可能再用主牌把它压下去——"能不能盖压"在甩牌提交的那一刻就已经由服务端裁定完毕,不会走到"闲家出牌盖压"这一步。 + +甩牌本身可能是单张、对子、多个不同长度拖拉机自由混搭的复杂组合(见 5.4.3)。闲家跟牌沿用 5.2「拖拉机跟牌的强制层级」同一原则(高规格能凑就必须先凑:拖拉机 → 对子 → 单张),只是这里要把整套甩牌**拆解成一个个独立的分量,逐个分量分别匹配**,而不是把整套甩牌当成一个笼统的整体来对待: + +1. **每一组拖拉机分量**:闲家手中若有同等连对数的主拖拉机,必须优先拆出来跟这一组;没有同等连对数的拖拉机,退而求其次用同等张数的主对子顶替;主对子也不够,才能拆主单张顶替。甩牌里若有多组不同长度的拖拉机,逐组分别按此规则处理。 +2. **每一组单独的对子分量**:闲家手中若有主对子,必须优先拆出来跟这一组;没有主对子,才能拆主单张顶替。 +3. **每一组单独的单张分量**:用主单张顶替即可。 +4. **闲家手中的主牌拆解完仍不够覆盖甩牌剩余的分量**:不够的那部分只能随意垫副牌。 +5. **闲家完全没有主牌**:只能随意垫任意副牌,本轮无法压制你的甩牌,本轮出牌权归你。 + +无论怎么拆解跟牌,都不影响这套甩牌本身的合法性——合法性在提交时已经由服务端按 5.4.2 判定完毕,闲家的跟牌动作只是"凑够同等张数、同等结构"的手牌垫上去,不可能真正反超压制。 + +#### 5.4.5 甩错判定与惩罚规则 + +**甩错判定**:甩牌提交时,服务端按全部玩家的真实手牌自动检查——只要两名对手任意一人**持有**(不要求已经打出)能盖压你甩牌内任意一组牌型的主牌,本次甩牌立即判定为甩错,对方是否愿意打出来压制不影响判定结果。 + +**统一惩罚标准**: + +- 本次甩牌全部收回手牌; +- 本轮仅强制打出你甩出组合里最小的那一张牌; +- 本轮失去再次甩牌、一次性多出牌的资格,剩余手牌只能分轮正常打出; +- 线下娱乐、线上棋牌平台均统一执行该惩罚,不额外扣分。 + +甩错判罚后,本轮实际只算打出了那一张最小的单张主牌,其余两家按 5.2 节的"单张"跟牌规则正常应对即可,不再涉及甩牌相关规则。 + +--- + +## 6. 捡分与扣底 + +### 6.1 分牌 + +牌面 5 记 5 分,牌面 10 记 10 分,牌面 K 记 10 分,其余牌不计分。 + +### 6.2 捡分规则 + +每一轮出完牌后比较牌面大小: + +- 若闲家最大,则本轮牌面上的分牌全部计入闲家捡分(两个闲家谁大不影响,都算闲家捡到)。 +- 若庄家最大,则本轮牌面上的分牌作废——不计入闲家捡分总数,也不属于庄家的个人分数(庄家本身没有"捡分"这个概念,庄家的输赢只看闲家捡分总数是否达到叫分,见第 7 节)。 + +### 6.3 扣底 + +所有手牌出完后,若最后一轮闲家**用主牌**(不论单张、对子、拖拉机)**牌面压过庄家**、赢下最后一轮,才视为"扣底"(若闲家赢了最后一轮但用的不是主牌,则不算扣底)。触发后,庄家埋下的 8 张埋牌底牌翻开,若有分则计入闲家捡分。 + +埋牌底牌的分要不要翻倍、翻几倍,取决于闲家扣底所用的那手牌(赢下最后一轮的牌)本身是什么牌型: + +| 闲家扣底所用牌型 | 倍数 | +| --- | --- | +| 单张主牌 | 不翻倍,按原始分值计入 | +| 主对子(一对主牌) | 2 倍 | +| 两连对拖拉机 | 4 倍 | +| 三连对拖拉机 | 6 倍 | +| N 连对拖拉机 | 2 × N 倍,以此类推递增 | + +若最后一轮闲家是用第 5.4 节的**甩牌**(单张 + 对子 + 拖拉机混合的组合)扣底,取这套组合里**最高规格牌型**对应的倍数计算,其余较低规格的部分不额外叠加。例如甩出"单张 + 一对 + 两连对拖拉机"并以此扣底,最高规格是两连对拖拉机,整手按 4 倍计算。 + +> **重要区分**:扣底翻的是**闲家的捡分得分(grade)**本身,属于"算分"阶段;这与第 7 节里"大光 ×3""小光 ×2""升 N 级 ×N"这些子数倍率是完全不同的两件事——那些乘的是**子数**,属于"算子"阶段。两者的先后顺序是:扣底先把埋牌底牌的分(可能翻倍)计入闲家的捡分总数,这个最终捡分总数再拿去跟庄家叫分比较,判定过庄 / 小光 / 大光 / 闲家赢(升级),最后才由判定结果决定子数乘几倍。 + +--- + +## 7. 结算:子数与升级 + +叫分与结算涉及两套并存的数值:**倍数**(用于计算"捡分得分" grade)与**子数**(用于计算最终筹码输赢),两者是独立概念,不能混用。 + +> **重要区分**:本节里提到的倍率(大光 ×3、小光 ×2、过庄 ×1、升 N 级 ×N),乘的都是**子数**(最终结算给玩家的筹码单位),不是对局过程中闲家捡到的分数(捡分得分)本身。捡分得分只用来判定庄家的过庄 / 小光 / 大光,或闲家的升级级数,一旦等级判定完成,实际相乘的对象是这一局最终要结算的子数。 + +> **算子模式的选择**:7.1 + 7.2 描述的"常规算子"是默认结算方式;开房时勾选"爬坡"后,才改用 7.3 的子数梯度和保小光分段;未勾选"爬坡"时,一律按常规算子(7.1 + 7.2)结算,两套规则不会同时生效。 + +### 7.0 四层关系总览 + +结算分四层,前三层是"算子"(本节),第四层是"算奖"(第 8 节),一层的输出是下一层的输入: + +1. **叫分 → 基础子数**:叫分档位本身决定这一局的基础子数(见 7.1 / 7.3 的对照表),叫分越低(庄家承诺越苛刻),基础子数越高。 +2. **捡分 vs 叫分 → 判定结果**:局末闲家的捡分总数(含扣底,见 6.3 节)与庄家叫分作比较,只会落入互斥的两个分支之一: + - **闲家没达到叫分**(庄家赢)→ 结果记在**庄家**名下,分三级:**过庄 / 小光 / 大光**。 + - **闲家达到或超过叫分**(闲家赢,庄家倒庄)→ 结果记在**闲家**名下,只有一个概念:**升级**(第几级)。 + - **大光 / 小光和升级是两套完全独立的命名体系,分别只属于庄家一侧和闲家一侧,不能混用或类比**:庄家赢的时候没有"升级",闲家赢的时候也没有"大光 / 小光"。 +3. **判定结果 → 子数倍率**:过庄 / 小光 / 大光 / 升级第几级,各自对应一个子数倍率,乘上第 1 层的基础子数,得到这一局的**子数结算结果 `X`**(前三层,即"算子",到此为止)。`X` 是庄家与**每一个**闲家之间"一对一"结算的基础金额:庄家赢(过庄 / 小光 / 大光)时,两个闲家各输给庄家 `X` 子;闲家赢(升级)时,庄家各输给两个闲家 `X` 子。 +4. **算奖 → 结算倍率**:第 8 节算出的"奖数"(8.1 冲关 + 8.3 傍王,各玩家分别求和)**是"持有奖数的这个人"从其余两名玩家那里各自多收一笔钱的独立结算线,作用对象是这个人本身,不是整场结算的统一系数**——某玩家 P 这一局总共有 `N_P` 奖,则**另外两名玩家(不分庄闲)都要各自额外付给 P:`X × N_P` 子**。三名玩家可能同时都持有各自的奖数,会产生最多 3 条互相独立的额外支付线,与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。算奖的计算过程本身不受叫分、过庄 / 小光 / 大光、升级影响(只看手牌结构),详见 8.3 节的完整举例。 + +> **例外**:70 分坐庄选择投降时不走第 1~3 层模型——直接固定基础子数 1 个、庄家输掉,不比较捡分、不判定过庄 / 小光 / 大光,见 7.1 末尾说明;但第 4 层算奖仍然照常叠加(投降是选主阶段的选择、未选主也未埋牌,按庄家"发牌 + 底牌"共 36 张、无主牌花色计算,见 8.4 节)。 + +**查表索引**:本节按叫分档位给出了完整判定表,位置如下: + +| 叫分 | 常规算子(未勾选"爬坡") | 爬坡(勾选"爬坡") | +| --- | --- | --- | +| 70 分 | 7.2.1 末尾"70 分坐庄" | 与常规算子一致,见 7.3.3 末尾说明 | +| 65 / 60 / 55 / 50 / 45 分 | 7.2.1 对应表格 | 与常规算子数值一致(45 分子数不同),见 7.3.3 末尾说明 | +| 40 / 35 / 30 / 25 / 20 / 15 / 10 分 | 7.2.1"40 分及以下坐庄"通用规则 | 7.3.3 对应表格(子数、`Q` 值均与常规算子不同) | +| 5 分 | 7.2.1"40 分及以下坐庄"内的"5 分坐庄没有小光"说明 | 7.3.3"5 分坐庄"(没有小光) | + +### 7.1 常规算子:叫分 → 基础子数(未勾选"爬坡"时的默认规则) + +叫分范围是 5 ~ 70 分(见第 4 节),常规算子下完整的"叫分 → 基础子数"对照: + +| 叫分 | 65 分 | 60 分 | 55 分 | 50 分及以下(50 / 45 / 40 / 35 / 30 / 25 / 20 / 15 / 10 / 5) | +| --- | --- | --- | --- | --- | +| 基础子数 | 2 个子 | 3 个子 | 4 个子 | 统一 6 个子,不再随叫分继续增加 | + +即叫分从 65 分往下每降 5 分子数依次是 2 → 3 → 4,但**到 50 分封顶**:50 分及以下所有档位(50、45、40、35、30、25、20、15、10、5)基础子数都固定是 6 个子,不会像爬坡那样继续往上加(爬坡的区别见 7.3)。 + +70 分叫庄是特例,基础子数不接着上面的阶梯往下走,而是在"投降"和"打牌"两条路径下分别取值: + +- **选择投降**:基础子数按 **1 个**算,庄家直接输 1 个子(此时局面还没有真正出牌捡分,不经过 7.2 的大光 / 小光 / 升级判定,是一次性的固定结算)。 +- **选择打牌**:基础子数按 **2 个**算(与 65 分相同),之后**照样按 7.2 节的方法,用捡分总数判定大光 / 小光 / 过庄 / 升级**,不是不细分——完整判定表见 7.2.1。 + +70 分叫庄时,发牌时桌面留下的那 8 张底牌(见第 4 节;不是埋牌时埋下的那 8 张「埋牌底牌」)需要**在庄家把它们摸入手牌之前、向所有玩家亮出 3 秒**,之后庄家才摸入手中;投降和打牌两种情况都适用(因为亮牌发生在选主 / 投降决策之前)。 + +### 7.2 捡分 vs 叫分:庄家的过庄 / 小光 / 大光,闲家的升级(常规算子) + +#### 7.2.0 判定方法(对任意叫分档位通用) + +**大光的判定条件是固定的、不随叫分或算子模式变化**:闲家捡分 `grade == 0`(一分未捡到)就是大光,常规算子和爬坡完全一样。会随叫分变化的,是**小光 / 过庄**之间的分界线——常规算子下这条分界线**固定为 40 分**,不随叫分变化(这是与爬坡最核心的区别,爬坡的分界线 `Q` 会随叫分变化,见 7.3.2)。设叫分为 `call`,闲家捡分为 `grade`,按以下顺序依次判断(**必须先判断第 1 步,即是否达标,再判断大光 / 小光 / 过庄**,顺序不能颠倒): + +1. **先判断是否达标**:若 `grade ≥ call`,闲家赢,判为**升级**——先升 1 级,此后每再多捡 40 分再升 1 级;子数倍率就是当前的级数本身:升 1 级 ×1、升 2 级 ×2、升 3 级 ×3……逐级 **+1**,不是逐级翻倍。 +2. **未达标(`grade < call`)时,再看捡分具体落在哪个区间**: + - `grade == 0`(一分未捡到)→ **庄家:大光**,子数倍率 ×3。 + - `0 < grade < 40`(捡了一点,但没满 40 分)→ **庄家:小光**,子数倍率 ×2。 + - `40 ≤ grade < call`(捡满 40 分,但仍没达标)→ **庄家:过庄**,子数倍率 ×1(即基础子数,不翻倍)。 + +之所以要先判断"是否达标":当叫分本身 ≤ 40 时(比如叫 35 分坐庄),闲家只要捡到 35 分就已经达标升级了,根本不可能出现"捡到 40 分却还没达标"这种情况——"过庄"这一档这时候是空的,直接从"小光"跳到"升级"(完整例子见 7.2.1"40 分及以下坐庄")。 + +**大光 / 小光只描述庄家(闲家没达标时,庄家赢得有多彻底);升级只描述闲家(闲家达标之后,赢得有多彻底),两套命名不通用、不能互换。** + +#### 7.2.1 常规算子完整判定表 + +按上面的方法,叫分 > 40 分的档位(65 / 60 / 55 / 50 / 45)"过庄"区间是存在的;叫分 ≤ 40 分的档位(40 / 35 / 30 / 25 / 20 / 15 / 10 / 5)"过庄"区间不存在,闲家捡分只要达到叫分就直接升级。 + +**65 分坐庄(基础 2 个子)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 6 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 4 个子 | +| 40 ~ 64 分 | 庄家:过庄 | ×1 | 2 个子 | +| 65 ~ 104 分 | 闲家:升 1 级 | ×1 | 2 个子 | +| 105 ~ 144 分 | 闲家:升 2 级 | ×2 | 4 个子 | +| 145 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +2 个子 | + +**60 分坐庄(基础 3 个子)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 9 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 6 个子 | +| 40 ~ 59 分 | 庄家:过庄 | ×1 | 3 个子 | +| 60 ~ 99 分 | 闲家:升 1 级 | ×1 | 3 个子 | +| 100 ~ 139 分 | 闲家:升 2 级 | ×2 | 6 个子 | +| 140 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +3 个子 | + +**55 分坐庄(基础 4 个子)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 12 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 8 个子 | +| 40 ~ 54 分 | 庄家:过庄 | ×1 | 4 个子 | +| 55 ~ 94 分 | 闲家:升 1 级 | ×1 | 4 个子 | +| 95 ~ 134 分 | 闲家:升 2 级 | ×2 | 8 个子 | +| 135 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +4 个子 | + +**50 分坐庄(基础 6 个子)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 18 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 12 个子 | +| 40 ~ 49 分 | 庄家:过庄 | ×1 | 6 个子 | +| 50 ~ 89 分 | 闲家:升 1 级 | ×1 | 6 个子 | +| 90 ~ 129 分 | 闲家:升 2 级 | ×2 | 12 个子 | +| 130 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 | + +**45 分坐庄(基础 6 个子——从这一档开始进入"50 分及以下统一 6 个子"的范围)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 18 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 12 个子 | +| 40 ~ 44 分 | 庄家:过庄 | ×1 | 6 个子 | +| 45 ~ 84 分 | 闲家:升 1 级 | ×1 | 6 个子 | +| 85 ~ 124 分 | 闲家:升 2 级 | ×2 | 12 个子 | +| 125 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 | + +**40 分及以下坐庄(40 / 35 / 30 / 25 / 20 / 15 / 10 / 5,基础都是 6 个子;此时 `call ≤ 40`,没有"过庄"这一档)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 18 个子 | +| 1 分 ~(叫分 − 1)分 | 庄家:小光 | ×2 | 12 个子 | +| 达到叫分 | 闲家:升 1 级 | ×1 | 6 个子 | +| 达到叫分 + 40 分 | 闲家:升 2 级 | ×2 | 12 个子 | +| 此后每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 | + +例如叫 10 分坐庄(基础 6 个子):0 分为大光(18 子);1 ~ 9 分为小光(12 子);捡到 10 分起升 1 级(6 子);捡到 50 分起升 2 级(12 子);之后每再多 40 分再升 1 级(每级再 +6 子)。 + +> **5 分坐庄没有"小光"**:分牌只有 5 分、10 分两种面值(见 6.1 节),闲家捡分永远是 5 的倍数。上面"1 分 ~(叫分 − 1)分为小光"这一行,在叫分为 5 分时对应区间是 `1 ~ 4 分`,里面不存在任何 5 的倍数,是空区间。所以叫 5 分坐庄时,闲家捡分只有 0(大光)和达到 5 分及以上(直接升 1 级,之后每再多 40 分再升 1 级)两种结果,没有小光这一档;这是"叫分 ≤ 40 分"这组档位里唯一的特例,其余档位(40/35/30/25/20/15/10)小光区间都至少包含一个 5 的倍数,正常存在。 + +**70 分坐庄** + +- **投降**:基础子数按 1 个算,庄家直接输 1 个子,不经过大光 / 小光 / 过庄 / 升级判定(还没出牌捡分,直接放弃)。 +- **打牌**:基础子数按 2 个算,之后按 7.2.0 的方法正常判定,`Q` 同样固定为 40 分——判定表和 65 分坐庄完全一致,只是把"基础 2 个子"原样代入(数值恰好和 65 分相同): + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 6 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 4 个子 | +| 40 ~ 69 分 | 庄家:过庄 | ×1 | 2 个子 | +| 70 ~ 109 分 | 闲家:升 1 级 | ×1 | 2 个子 | +| 110 ~ 149 分 | 闲家:升 2 级 | ×2 | 4 个子 | +| 150 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +2 个子 | + +### 7.3 爬坡:叫分 → 基础子数与保小光分段(可选规则,开房时勾选"爬坡"生效) + +#### 7.3.1 基础子数:叫分 → 子数的完整对照 + +叫分从 65 分(2 个子)开始,每叫低 5 分加 1 个子,直到 50 分(6 个子)——这一段与常规算子的四档数值完全相同;50 分以下,继续按每叫低 5 分加 1 个子延伸,一直到叫分范围的下限 5 分: + +| 叫分 | 65 | 60 | 55 | 50 | 45 | 40 | 35 | 30 | 25 | 20 | 15 | 10 | 5 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 基础子数 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | + +#### 7.3.2 保小光分段:叫分 → 小光 / 过庄分界线(Q 值) + +判定方法与 7.2.0 完全相同,只是把固定的 `Q = 40` 换成按叫分区间取值的 `Q`;**大光的判定条件不受 `Q` 影响,永远是 `grade == 0`**,常规算子和爬坡完全一样——`Q` 只决定"小光"和"过庄"之间的分界线: + +| 叫分区间 | 小光 / 过庄的分界线(Q) | +| --- | --- | +| ≥ 45 分(含 65 / 60 / 55 / 50 / 45) | 40 分 | +| 40 / 35 分 | 20 分 | +| 30 / 25 分 | 15 分 | +| 20 / 15 分 | 10 分 | +| 10 分 | 5 分 | +| 5 分 | 5 分(升级级距仍是 5,只是"小光"这一档因为区间为空而不存在,见下方说明) | + +`≥ 45 分`这一档的 `Q = 40`,与 7.2 节常规算子沿用的分界线数值相同,只是子数改按 7.3.1 的公式取值(65/60/55/50 与常规算子数值一致,45 分是爬坡独有的新增档位)。 + +**5 分档没有"小光"**:分牌只有牌面 5(5 分)和牌面 10 / K(10 分)两种面值(见 6.1 节),闲家的捡分总数(`grade`)因此永远是 5 的倍数(0、5、10、15…),不可能出现 1 ~ 4 这样的中间值。当叫分是 5 分时,"小光"原本对应的区间是 `0 < grade < 5`,这个开区间里不存在任何 5 的倍数,也就是说这个区间**必然是空的**——闲家捡分要么是 0(大光),要么一旦捡到任何分牌(最少 5 分)就已经达到叫分、直接升级,中间不存在"捡到了一点、但没达标"的"小光"状态。所以 5 分档只有**大光**和**升级**两种结果,没有"小光"这一档;但 `Q` 值本身仍然是 5(与 10 分档一样),只是在"小光 / 过庄分界线"这个用途上区间恰好收窄为空,`Q = 5` 仍然继续作为"升级级距"生效(见 7.3.3 的 5 分坐庄判定表)。 + +**升级级距的通用规则**:不管哪个叫分档位,闲家赢了之后,"超过叫分多少分算升一级",用的分数差**就是该档位的 `Q` 值**——即上表里每一档的 `Q` 值,既是"小光 / 过庄"的分界线,也是"升级"的级距单位,两者共用同一个数字。例如叫分 40 分时 `Q = 20`:闲家捡分达到 40 分先升 1 级,之后每再多捡 20 分(60、80、100…)就再升一级。 + +#### 7.3.3 各分段的完整判定表(按 7.2.0 的方法代入对应 `call`、`Q`、基础子数) + +**40 分坐庄(基础 8 个子,Q = 20)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 24 个子 | +| 1 ~ 19 分 | 庄家:小光 | ×2 | 16 个子 | +| 20 ~ 39 分 | 庄家:过庄 | ×1 | 8 个子 | +| 40 ~ 59 分 | 闲家:升 1 级 | ×1 | 8 个子 | +| 60 ~ 79 分 | 闲家:升 2 级 | ×2 | 16 个子 | +| 80 分起,每再多 20 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +8 个子 | + +**35 分坐庄(基础 9 个子,Q = 20)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 27 个子 | +| 1 ~ 19 分 | 庄家:小光 | ×2 | 18 个子 | +| 20 ~ 34 分 | 庄家:过庄 | ×1 | 9 个子 | +| 35 ~ 54 分 | 闲家:升 1 级 | ×1 | 9 个子 | +| 55 ~ 74 分 | 闲家:升 2 级 | ×2 | 18 个子 | +| 75 分起,每再多 20 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +9 个子 | + +**30 分坐庄(基础 10 个子,Q = 15)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 30 个子 | +| 1 ~ 14 分 | 庄家:小光 | ×2 | 20 个子 | +| 15 ~ 29 分 | 庄家:过庄 | ×1 | 10 个子 | +| 30 ~ 44 分 | 闲家:升 1 级 | ×1 | 10 个子 | +| 45 ~ 59 分 | 闲家:升 2 级 | ×2 | 20 个子 | +| 60 分起,每再多 15 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +10 个子 | + +**25 分坐庄(基础 11 个子,Q = 15)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 33 个子 | +| 1 ~ 14 分 | 庄家:小光 | ×2 | 22 个子 | +| 15 ~ 24 分 | 庄家:过庄 | ×1 | 11 个子 | +| 25 ~ 39 分 | 闲家:升 1 级 | ×1 | 11 个子 | +| 40 ~ 54 分 | 闲家:升 2 级 | ×2 | 22 个子 | +| 55 分起,每再多 15 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +11 个子 | + +**20 分坐庄(基础 12 个子,Q = 10)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 36 个子 | +| 1 ~ 9 分 | 庄家:小光 | ×2 | 24 个子 | +| 10 ~ 19 分 | 庄家:过庄 | ×1 | 12 个子 | +| 20 ~ 29 分 | 闲家:升 1 级 | ×1 | 12 个子 | +| 30 ~ 39 分 | 闲家:升 2 级 | ×2 | 24 个子 | +| 40 分起,每再多 10 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +12 个子 | + +**15 分坐庄(基础 13 个子,Q = 10)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 39 个子 | +| 1 ~ 9 分 | 庄家:小光 | ×2 | 26 个子 | +| 10 ~ 14 分 | 庄家:过庄 | ×1 | 13 个子 | +| 15 ~ 24 分 | 闲家:升 1 级 | ×1 | 13 个子 | +| 25 ~ 34 分 | 闲家:升 2 级 | ×2 | 26 个子 | +| 35 分起,每再多 10 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +13 个子 | + +**10 分坐庄(基础 14 个子,Q = 5——这一档同样没有"小光")** + +分牌只有 5 分、10 分两种面值,捡分永远是 5 的倍数。`Q = 5` 时,"小光"对应的区间 `0 < grade < 5` 里不存在任何 5 的倍数,同样是空区间:闲家捡分不可能停在 1 ~ 4 分,只要捡到分就已经是 5 分(直接落入"过庄"区间)。 + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 42 个子 | +| 5 ~ 9 分 | 庄家:过庄(没有小光这一档) | ×1 | 14 个子 | +| 10 ~ 14 分 | 闲家:升 1 级 | ×1 | 14 个子 | +| 15 ~ 19 分 | 闲家:升 2 级 | ×2 | 28 个子 | +| 20 分起,每再多 5 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +14 个子 | + +**5 分坐庄(基础 15 个子,没有"小光",见 7.3.2 的说明)** + +5 分是叫分范围的下限,捡分只能是 5 的倍数:一旦捡到任何分牌就已经是 5 分,等于直接达标(`grade ≥ call = 5`)。所以"小光"和"过庄"这两个区间都不存在,闲家捡分只有 0(大光)和达到 5 分及以上(直接升 1 级)两种结果。 + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 45 个子 | +| 5 ~ 9 分 | 闲家:升 1 级(没有小光、过庄这两档) | ×1 | 15 个子 | +| 10 ~ 14 分 | 闲家:升 2 级 | ×2 | 30 个子 | +| 15 分起,每再多 5 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +15 个子 | + +`≥ 45` 分档(65 / 60 / 55 / 50 / 45)判定逻辑与 7.2.1 完全相同(Q = 40),只是 45 分档基础子数按 7.3.1 取 7 个子,其余数值套用 7.2.1 对应叫分的表即可,此处不重复列出。 + +70 分叫庄时与常规算子规则一致(见 7.2.1 末尾"70 分坐庄":投降固定基础子数 1 个,打牌基础子数 2 个并正常套用大光 / 小光 / 过庄 / 升级判定),不适用本节的保小光分段。 + +--- + +## 8. 算奖规则 + +算奖在**每个小局结算时**计算(与第 7 节的子数结算属于同一次小局结算,不是等大局所有局数打完才统一算,也不是在出牌过程中途算)。8.1 冲关只计入庄家;8.3 傍王是额外叠加的独立奖励,庄家、闲家都要算——两者的计入范围不同,注意区分。 + +**8.1 冲关只认庄家的身份,不看闲家的手牌是否达标**:即使某位闲家的手牌结构凑巧也满足 8.1 表里的条件(比如手里也有 3 张王),这份冲关也**不会**计入这位闲家——冲关从规则上就只针对庄家一人。闲家想要获得算奖,只能靠 8.3 傍王(且必须房间勾选了这条可选规则)。 + +**算奖依据的手牌快照时间点**:不管是 8.1(只看庄家)还是 8.3 傍王(庄闲都算),都按**静态初始手牌**计算——庄家是埋牌完成后的那一刻手牌(28 张),闲家是发牌完成后的那 28 张手牌;这个快照全程固定,不随之后的出牌、被吃、被打出而改变。**例外**:70 分投降局庄家未选主也未埋牌,其快照为"发牌 + 底牌"共 36 张(无主牌花色),见 8.4 节。 + +**算奖(8.1 / 8.3)与亮牌(8.2)是两个互不干涉的独立概念**:算奖是结算阶段"该给多少额外结算分"的规则;亮牌只是出牌开始前"要不要向对手公开一部分手牌"的展示规则。两者各自有自己的触发条件,只是恰好都以庄家埋牌后的手牌结构为依据,因此部分门槛数值相同——但达成亮牌的条件不代表一定触发算奖,反之亦然,具体对照见 8.2 末尾的说明。 + +### 8.1 冲关 + +> **旧称「常规算奖」**,现统一叫**冲关**(与服务端字段 `chongguan`、界面「冲关分」「冲关牌型」同口径)。 +> 它只是「算奖」的**其中一个分量**——另一个是 8.3 傍王;两者求和才是某玩家的总奖数 `N`(见 8.4)。 + + +| 组合 | 奖数 | +| --- | --- | +| 三个王 | 1 奖 | +| 四个王 | 3 奖 | +| 六个 7 | 1 奖 | +| 七个 7 | 2 奖 | +| 八个 7 | 3 奖 | +| 六个 2 | 1 奖 | +| 七个 2 | 2 奖 | +| 八个 2 | 3 奖 | + +三王 / 四王在拥有"正 7"及以后连续对子的情况下,每多一对连续对子再加 1 奖,连续顺序为: + +``` +正7 - 副7 - 正2 - 副2 - A - K - Q - J - 10 - 9 - 8 - 6 - 5 +``` + +其中从 A 到 5(即 A、K、Q、J、10、9、8、6、5 这一段普通点数)都必须是选定花色的主牌,才能计入连续对子——非主花色的普通对子不算数,链条到这里就断了。 + +### 8.2 亮牌规则 + +**亮牌与算奖(8.1 / 8.3)是两个独立概念,互不干涉**:亮牌只是"要不要向对手公开一部分手牌"的展示规则,本身不产生任何奖数、不影响结算;算奖该怎么算、算多少,完全按 8.1 / 8.3 的规则来,不因为触发了亮牌而增加或减少。 + +**亮牌要求**(仅针对庄家):庄家**埋牌后**、**出牌开始前**,若手中剩余的**固定主牌**满足下表任一条件,就要向两个闲家**亮出自己全部固定主牌的具体牌面**。 + +| 触发条件(任一满足即亮牌) | +| --- | +| 固定主牌(双王 + 全部 2 + 全部 7)总数 ≥ 10 张 | +| 王 ≥ 3 张 | +| 7 ≥ 6 张 | +| 2 ≥ 6 张 | + +**亮出的内容是固定的一份**——**庄家手中全部固定主牌的具体牌面**(双王 + 全部 2 + 全部 7;**不含**主花色的普通牌 A/K/Q/J/10/9/8/6/5)。不论是被上表哪一条触发、还是同时满足多条,亮出的都是这同一份牌,不因触发条件不同而增减。 + +- **不限叫分**:任何叫分档位都适用,不是 70 分坐庄专有。 +- **仅可查牌模式**:房间勾选了"不查牌"时,闲家看不到亮牌(见第 9 节)。 +- **只亮庄家的**:闲家不亮牌。 +- 亮牌依据的是**埋牌后**的手牌,与 8.1 冲关用的是同一份静态快照(见第 8 节开头),全局固定、不随之后出牌缩水。 + +> **亮牌给的是牌面,不是统计**。这一点与「余主公示」(只给剩余主牌数与对子数,见第 9 节)不同;与「明牌」(查看**他家**全部未出主牌的牌面,见第 9 节)也不同——亮牌是**庄家主动公开自己的**,明牌是**闲家去看别人的**。 + +**亮牌门槛与 8.1 冲关起点的数值巧合,不代表两者是同一条规则**:王 ≥ 3 张 / 7 ≥ 6 张 / 2 ≥ 6 张这三个门槛,数值上恰好分别与 8.1 表里"三个王""六个 7""六个 2"的冲关起点相同,达到时会同时触发"亮牌"和"冲关"这两件独立的事——但它们是各自独立判定后凑巧同时发生,不是"亮牌导致冲关"或者"冲关导致亮牌"。这一点在"固定主牌 ≥ 10 张"这一档表现得最清楚:它只在亮牌这条规则里有定义、会触发亮牌,但 8.1 的冲关表里根本没有为这一档定义奖数,所以**不产生任何冲关奖**——如果亮牌和冲关是同一回事,这里就该矛盾;现在不矛盾,正说明两者互不干涉。 + +### 8.3 傍王(可选规则) + +开房时勾选"傍王"后,每张王额外算一奖:这个奖是**庄家、闲家都要各自计算**(每个人按自己手里的王算,不限庄家)。 + +**傍王奖是在 8.1 冲关之外额外叠加的一份奖励,与冲关互不冲突、不互相替代**:庄家照常按 8.1 的规则算冲关(只计庄家),同时如果勾选了傍王,庄家、闲家还要**各自另外**再按自己手里的王数算一份傍王奖;两份奖励分开计算,最后相加。 + +### 8.4 算奖如何影响最终结算 + +**算奖是"持有奖数的那个人"从其余两名玩家那里各自多收一笔钱的独立结算线,不是把第 7 节算出的输赢结果整体重新乘一遍**:设第 7 节前三层算出的结算子数为 `X`(庄家与每个闲家之间"一对一"结算的基础金额,见 7.0 第 3 层),玩家 P 这一局总共积累了 `N_P` 奖(8.1 冲关 + 8.3 傍王,各自求和后相加;只有庄家可能有 8.1 的分量,8.3 傍王则庄闲都可能有): + +- 若 `N_P > 0`,**另外两名玩家(不分是庄家还是闲家)都要各自额外付给 P:`X × N_P` 子**; +- 若 `N_P = 0`,P 没有额外收入。 + +三名玩家可能同时都持有各自的奖数,这时会产生最多 3 条互相独立、方向不同的额外支付线,互不冲突、按各自的 `N` 值同时生效,最后与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。 + +**举例**(`X = 6` 个子,庄家为 A,闲家为 B、C): + +- 若庄家 A 本局有 1 奖(`N_A = 1`):B、C 各自额外付给 A `6 × 1 = 6` 子。 +- 若闲家 B 同时勾选了"傍王"、本局有 2 奖(`N_B = 2`):A、C 各自额外付给 B `6 × 2 = 12` 子。 +- 这两条支付线同时成立、互不影响:A 从 B、C 各多收 6 子,B 从 A、C 各多收 12 子——包括"闲家 C 要付给闲家 B"这样的支付,即使 B、C 之间在第 1~3 层的基础输赢结算里并不直接结算(基础结算只发生在"庄家 vs 单个闲家"之间),算奖这一层是单独叠加在所有玩家两两之间的。 + +以下三点已确认: + +- **庄家倒庄(闲家达标升级)时,8.1 冲关仍然生效**:冲关只看庄家埋牌后的手牌结构,与本局谁输谁赢无关。 +- **70 分投降时,仍叠加算奖倍率**:投降只是跳过了第 1~3 层的叫分 / 捡分判定(此时 `X = 1`)。投降是选主阶段的选择,庄家**未选主、未埋牌**,故算奖按庄家"发牌 + 底牌"共 36 张手牌计算——因没有选定主牌花色,**没有连对链**,只有三 / 四王、六~八个 7、六~八个 2 这些组合,以及(勾选傍王时)按王数的傍王奖计入;8.1(及勾选傍王时的 8.3)照常计算并叠加。 +- **算奖对象的身份限制会带进 `N` 的计算**:8.1 只算庄家、8.3 傍王勾选后才不分庄闲(见第 8 节开头说明),把这名玩家实际适用的分量求和,才是他自己的 `N`;"其余两人向他支付 `X × N`"这条支付规则本身不因身份而改变,但闲家的 `N` 从一开始就不可能包含 8.1 的分量。 + +--- + +## 9. 牌局查看模式 + +"查牌"指的是**回看那些不该随时可见的信息**:已经打过去的牌、他家手里还剩多少主牌、他家主牌的具体牌面。房间的"查牌模式"开关决定下列四项功能**整体**是否提供。 + +- **可查牌模式**(四项功能全部提供): + 1. **出牌历史**:当前牌局中,所有玩家已经打出的牌(含之前每一轮打出、已被收走的牌),任何时候都可以回看。 + 2. **余主公示**:一旦有玩家的主牌全部打空(即 5.4.2 节所说的"报无主"状态;"主牌"指大王、小王、所有花色的 2、所有花色的 7,以及主花色的其余普通牌,见第 3 节),系统就为**全体三人**(含刚刚报无主的这位玩家自己)显示另外两家各自的主牌数量、以及这些主牌里有多少对子。**只给数量、不给具体牌面**——要看具体牌面是下一条的"明牌"。 + 3. **明牌**:同时**为这三个人都**出现一个"明牌"按钮——判定依据是「**场上有人**报无主」,不是「**自己**报无主」,所以报无主的那位和另外两位一样都能点。点击后可以查看另外两家手中全部主牌的**具体牌面**(不只是数量 / 对子结构),再点一次取消查看、恢复隐藏;**不限次数,整个出牌阶段随时可查**。 + 4. 庄家若触发了 8.2 节的"亮牌要求",两个闲家可以看到**庄家全部固定主牌的具体牌面**(见 8.2 节)。 + +- **不可查牌模式**:以上四项**一项都不提供**—— + - **不提供出牌历史**:之前各轮打出过什么牌一律不能回看,只能靠玩家自己记牌; + - **不提供余主公示**:即使有玩家报无主,也不会显示任何人的主牌数量、对子结构,也没有"明牌"按钮; + - 庄家即使触发了 8.2 节的亮牌条件,闲家也看不到那些牌(见 8.2 节)。 + +> **"查牌"与"当前这一轮桌面上的牌"是两回事**:本轮已经出过牌的玩家、打在桌面上的这几张牌,**两种模式下都必须对全体可见**——否则后出的人无从跟牌、也无从判断本轮谁最大。查牌模式管的是**已经收走的往轮牌**与**他家手牌信息**能不能回看,不影响当前这一轮的正常出牌展示(断线重连回到牌桌时同理:当前轮桌面上的牌照常恢复,往轮历史则按本节的模式开关决定给不给)。 + +--- + +## 10. 房间设置选项 + +创建页依次提供:**局数、扣卡方式、查牌模式、傍王、爬坡**。前三项各自为独立的单选必选组,每组必须且只能选一项;傍王与爬坡为两个独立可选开关,可以都不选、只选一个或同时勾选。 + +### 10.1 局数与牌局扣卡方式 + +局数:6 局 / 12 局,单选必选。扣卡方式:房主扣卡 / AA 制(每人扣卡),单选必选。两个选项组分别选择,费用按下表联动展示。 + +| 扣卡方式 | 6 局 | 12 局 | +| --- | --- | --- | +| AA 制(每人扣卡) | 每人 1 张 | 每人 2 张 | +| 房主扣卡 | 2 张 | 4 张 | + +### 10.2 查牌模式 + +可查牌 / 不查牌,单选必选(见第 9 节)。 + +### 10.3 附加规则 + +傍王、爬坡:分别为可选开关,允许不勾选;两者可以同时勾选(互不冲突),不属于互斥单选组。未勾选傍王时不计算傍王奖励;未勾选爬坡时使用常规算子。 + +### 10.4 创建参数与默认选择 + +默认选择为:6 局、房主扣卡、可查牌;傍王和爬坡均不勾选。单选必选表示始终保留一个有效选项,不要求用户额外点击已默认选中的选项。 + +`roomtype` 为 **11 位字符串**,前 5 位依次表示局数、扣卡方式、查牌模式、傍王、爬坡;后 6 位为预留,不显示为创建选项,也不赋予玩法含义。具体编码以 [协议 §0.5](../protocol/packet_protocol.md#05-roomtype-房间选项位串) 为唯一来源。预留位的固定填充值尚待确认。 + +--- + +## 11. 牌局交互提示 + +- 选主阶段:4 个花色选主按钮上各显示"该花色在庄家手中的对子数"(供庄家判断选哪门为主);70 分坐庄时并列再显示一个投降按钮——选主与投降是同一决策点的互斥选择(见第 4 节)。 +- 首家出主牌时可以甩牌,甩牌的生效条件、跟牌规则、甩错惩罚详见 5.4 节。 +- 牌局结束需要亮出埋牌底牌。 +- 闲家在出牌时需要有 3 个提示选项: + 1. **踩**:提示对家"我能大过庄家"。 + 2. **没分**:提示对家"我手上没分了"。 + 3. **有分**:提示对家"我手上有分"。 +- **倒计时只作展示,不触发任何自动操作**:叫分 / 选主 / 埋牌 / 出牌四个阶段都会给出一个倒计时秒数,用于界面提醒当前该谁操作。**倒计时归零后不做任何代打**——不自动叫分、不自动选主、不自动埋牌、不自动出牌,也不判负、不跳过该玩家;轮到谁而谁不操作,牌局就停在这一步一直等。 + +> **"无超时托管"是确认过的规则,不是待实现的缺口**:本局不设服务端代打/AI 托管,也不因超时改变任何对局状态。若玩家长时间不操作导致牌局停住,由玩家走房间的**解散**流程收场,按当前累计分结算(见 12.2)。**后续核对不要把"没有超时动作"当成缺陷去补"超时托管/自动出牌"。** + +--- + +## 12. 完整牌局游玩流程 + +本节把前面各节的规则串成一局(一个"小局")从头到尾的完整流程,供整体理解与实现参照。这里只给出**步骤与先后顺序**,每一步的具体细则以其对应章节为准(避免与细则重复而产生分歧)。 + +### 12.1 一个小局的完整流程 + +1. **开局准备**:房间按第 10 节的设置(局数、扣卡方式、傍王、爬坡、查牌)建好后开战。第一局的暂定庄家为 0 号座位;之后每局按 4.6 的轮庄规则确定暂定庄家。 + +2. **发牌**(第 2 节):92 张牌洗匀,三家各摸 28 张,剩余 8 张扣在桌面作为"底牌"。 + +3. **叫分坐庄**(4.2):由暂定庄家起、按逆时针依次叫分。暂定庄家必叫(5~70 分、步进 5,不能"不叫");其后每家只能叫比当前更低的分,或选择"不叫";有人叫出 5 分即立即坐庄,或一家叫分后另外两家都"不叫"也即坐庄。叫分越低表示庄家对"压住闲家捡分"越有信心,对应的基础子数越高(第 7 节)。 + +4. **摸底**:庄家坐定后把 8 张底牌摸入手牌(摸完共 36 张)。非 70 分:这 8 张仅庄家可见。**70 分坐庄先触发「开底」:庄家摸底之前,先把这 8 张向所有玩家翻开 3 秒,再摸入手牌**(4.4 / 第 4 节)。 + +5. **选主 / 投降(同一决策点,二选一,互斥)**(第 4 节): + - **选主**:庄家选定一门花色为本局主牌(大王、小王、所有花色的 2、7 恒为主牌)→ 进入下一步埋牌。前端此阶段每个花色按钮上要显示该花色在庄家手中的对子数;70 分坐庄时并列显示投降按钮。 + - **投降**:仅 70 分坐庄才有此选项;选投降即放弃本局,**不选主、不埋牌、不出牌**,直接按 7.1"70 分 · 投降"结算(算奖仍按下方 8 计,但用庄家 36 张、无主牌花色)。选花色就等于放弃投降、正常打牌。 + - (**开底**——70 分的 8 张底牌翻开 3 秒,发生在步骤 4"摸底之前"。) + +6. **埋牌**(第 4 节,仅"选主/打牌"路径):庄家从 36 张里选 8 张重新扣下作为"埋牌底牌"(供最后"扣底"判断),保留 28 张。(**先选主、后埋牌**。) + +7. **出牌对局**(第 5、6 节): + - 第一轮固定由庄家先出;此后每轮由上一轮牌面最大的一方先出。 + - 首家出什么花色 / 牌型,其余两家按 5.1~5.3 跟牌(同花色不够时可用主牌"毙牌"抢权,或"垫牌"不争)。 + - 首家出主牌时可"甩牌"(5.4):一次性打出多组主牌组合;服务端按全场手牌判定"最大性",甩错则按 5.4.5 收回、只强制打出最小一张。副牌一律不能甩。 + - 每轮比大小定出胜者:闲家赢下的那一轮,牌面上的分牌(5→5 分、10→10 分、K→10 分)计入闲家捡分;庄家赢的轮分牌作废(6.1~6.2)。 + - 过程中:有玩家主牌出空需"报无主",可查牌模式下触发**余主公示**(为全体展示他家主牌数量 / 对子结构,第 9 节);庄家手牌达到 8.2 的门槛时触发**亮牌**。 + +8. **末轮扣底**(6.3):所有手牌出完后,若最后一轮由闲家**用主牌**赢下,则庄家埋的 8 张埋牌底牌翻开,其中的分按闲家赢牌的牌型翻倍(单张 ×1、主对 ×2、N 连对 ×2N)后计入闲家捡分。 + +9. **小局结算**(第 7、8 节,两部分同时结算): + - **算子**:闲家总捡分(含扣底)与庄家叫分比较 → 判定庄家的**过庄 / 小光 / 大光**,或闲家的**升级第几级** → 结合叫分档的基础子数,得到庄家与每个闲家"一对一"的结算子数 `X`(勾选爬坡则改用 7.3 的梯度与分段)。 + - **算奖**:以静态初始手牌(正常局:庄家埋牌后 28 张、闲家发牌后 28 张;**70 分投降局**:庄家用发牌+底牌共 36 张、无主牌花色)计算奖数 `N` —— 冲关(8.1)**只计庄家**;勾选傍王(8.3)则庄闲每张王各算一奖。持有 `N` 奖的玩家从另外两人各多收 `X × N`(8.4)。 + - 以上两部分加总即各家本局盈亏;牌局结束需亮出埋牌底牌(第 11 节)。 + +10. **进入下一局**(4.6):庄家本局赢则连庄(暂定庄家仍是他);庄家输(闲家捡分达标 / 庄家投降)则暂定庄家顺延到其下家。回到第 2 步开新的一小局。 + +### 12.2 大局与结束 + +按房间设定的局数(6 局或 12 局,见第 10 节)打满全部小局后,进行大局结算(累计各家总分并记录战绩);若中途解散,则按当前累计分结算。 + +### 12.3 阶段速览 + +``` +建房(§10) → 发牌(§2) → 叫分坐庄(§4.2) → 摸底[70分先开底] → 选主(§3) → 埋牌(§4) + → [70分:投降/打牌决策(§4.4)] → 出牌对局(§5,§6.1-6.2) → 末轮扣底(§6.3) + → 小局结算=算子(§7)+算奖(§8) → 轮庄(§4.6) → 下一局 …… 打满局数 → 大局结算 +``` + diff --git a/cocoscreator_projects/games/erqiwang/docs/protocol/packet_protocol.md b/cocoscreator_projects/games/erqiwang/docs/protocol/packet_protocol.md new file mode 100644 index 0000000..42160cd --- /dev/null +++ b/cocoscreator_projects/games/erqiwang/docs/protocol/packet_protocol.md @@ -0,0 +1,717 @@ +# 二七王协议包列表 + +说明:`data` 均为发送方与接收方之间约定的 JSON 内容;成败判定只看推送包里的 `data.success`。 + +--- + +## 0. 通用约定(所有包适用,下文各表不再逐一重复) + +### 0.0 术语:底牌 vs 埋牌底牌(先读这一条) + +本项目于 2026-08-25 修订了这两个术语(`design.md` §1 已同步,本文亦已改用新称): + +| 术语 | 指哪 8 张 | 旧称 | +| --- | --- | --- | +| **底牌** | 发牌时**没有发给玩家**、扣在桌面的 8 张 | ~~暗牌~~ | +| **埋牌底牌** | 庄家**埋牌**时从手里扣下的 8 张 | ~~底牌~~ | + +**两批牌的字段名已拆分**(2026-08-25,原先共用 `bottomcards`): + +| 字段名 | 含义 | 出现在 | +| --- | --- | --- | +| **`bottomcards`** | **底牌** | `shangzhuang`、`deskinfo.ChooseMain`、`deskinfo.BuryCards` | +| **`burycards`** | **埋牌底牌** | `maipai`、`deskinfo.PushCards` | +| `bottom.cards` | **埋牌底牌** | 结算包的 `bottom` 分组(分组名已表明是抠底相关,字段未改名) | + +同一个包里不会同时出现这两个字段。**下发面**:`burycards` 与 `bottomcards`(非 70 分时)都**只发给庄家**,闲家两者皆无——已由 `test/test_leak.js` 的泄露审计覆盖。 + +### 0.0b 四类「公开信息」术语(都带亮/明字,别读混) + +| 术语 | 谁公开给谁 | 给的是什么 | 触发 | 本文相关字段 | +| --- | --- | --- | --- | --- | +| **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后固定主牌达门槛(design §8.2) | `liangpai.cards` | +| **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(design §9) | `seatlist[seat][4]`、`baozhu` | +| **明牌** | 他家 → 请求者 | **具体牌面**(他家全部未出主牌) | 可查牌 + **任一**玩家报无主 → **三家均可点、随时可查**(design §9) | `mingpai.others[].zhucards` | +| **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(design §4) | `ancard3s` + `bottomcards` | + +一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。 + +另有一组「底」字族专管发牌留桌的那 8 张:**摸底**(庄家摸起看,仅庄家)、**开底**(70 分时全场看 3 秒)、**查底牌**(对局中随时回看)、**扣底**(末轮翻开埋牌底牌计分)。 +(「亮主」不是术语,是前端选主界面的标题文案,与上表无关。) + + +### 0.0c 算奖 = 冲关 + 傍王 + +| 术语 | 范围 | 相关字段 | +| --- | --- | --- | +| **冲关** | design §8.1,**只计庄家**。旧称"常规算奖",现统一叫冲关 | `chongguan`(奖数)、`cards`(冲关牌型) | +| **傍王** | design §8.3,可选规则,勾选后**庄闲都算**,每张王 1 奖 | `wang`(王数)、`bangwang`(开关) | +| **算奖** | **上位概念** = 冲关 + 傍王 | `naward`(总奖数 N)、`grade_aw`(算奖得分) | + +说「**冲关**」指庄家那一份,说「**算奖**」指两者合计。界面上小局结算显示「冲关分」、按钮叫「冲关牌型」。 + +> ⚠️ `grade_aw` 是按合计后的 `N` 算出来的,**冲关与傍王的贡献事后无法拆分**。大局结算若要分别显示「冲关分」「傍王分」,须服务端在算钱时就分开代入公式——见前端清单 §7.5 **S-6**。 + +### 0.1 成败标志 `data.success` + +**每一个「服务器 → 客户端」的包,`data` 都必带 `success`**(布尔): + +- `success: true` —— 操作成功 / 正常推送。下文各包的字段表描述的都是这种情况,表里不再重复列 `success` 这一行。 +- `success: false` —— 操作失败,见 0.2。 + +前端一律 `if (!data.success)` 判成败,**不看 `status` / `code`,也不写 `status` 兼容兜底**。 + +### 0.2 失败回包 + +服务端受理请求时,任一校验不通过都会**回一个失败包**(此前是静默丢弃、前端只能干等倒计时): + +- **rpc 与请求包同名**(如 `chupai` 失败回 `chupai`,而不是 `chupai1/2/3`); +- **只回发给请求者**(`conmode`/`fromid` 取自请求包),其他两家收不到; +- `data` 只含 `success: false` 与 `errcode`,无业务字段。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| success | 布尔 | 恒为 `false` | +| errcode | 整数 | 失败原因,见下表 | + +`errcode` 取值(服务端 `youle_erqiwang.ERR`,`mod.js`): + +| 值 | 名称 | 含义 | +| --- | --- | --- | +| 1 | PLAYER | 玩家/房间/座位校验不通过(平台 `check_player` 返回 null) | +| 2 | NODESK | 牌桌或牌局不存在 | +| 3 | STEP | 当前阶段不允许该操作(如非出牌阶段发 `chupai`) | +| 4 | SEAT | 位置不符,或还没轮到该玩家操作 | +| 5 | PARAM | 参数非法:类型/范围/张数不对、牌不在手上、牌id 重复 | +| 6 | RULE | 规则不允许:叫分未更低、出牌不合法、投降条件不满足、房间模式禁止等 | + +> **牌id 列表的入参约束**(`cards` 字段,`maipai`/`chupai`):必须是**非空数组**,元素必须是 `0 ~ 107` 的**整数**牌id,且**互不重复**。字符串形式的数字(如 `"5"`)、小数、越界值、重复值一律按 `PARAM` 拒绝,服务端不做类型兜底转换。 + +> **例外:`tishi` 成功时不回执**。该包是闲家给对家的主观提示、不含任何对局状态,服务端只转发给对家(design §11),发送者收不到成功包;只有失败才回给发送者。 + +### 0.3 `countdown` 只是展示用的秒数 + +多个包带 `countdown`(叫分 / 选主 / 埋牌 / 出牌倒计时,取自 `class.desk.js` 的四个常量)。它**只供客户端显示提醒,服务端不据此做任何事**: + +- 服务端**没有**对应的定时器,倒计时归零后**不会**自动叫分 / 选主 / 埋牌 / 出牌,也不判负、不跳过该玩家; +- 轮到谁而谁不操作,牌局就停在该阶段一直等,`step` 与 `playproc` 都不变; +- 这是 design §11 确认过的规则(无超时托管),不是未实现的功能。牌局因此停住时,由玩家走平台的**房间解散**流程收场(平台回调子游戏 `get_disbandRoom` → 解散结算,见 §14)。 + +因此**客户端不要在倒计时归零时做任何乐观的界面推进**(不要自行跳阶段、不要清控制权),一切仍以收到服务端推送为准。 + +### 0.4 断线重连包不在此约定内 + +`deskinfo` 由平台组装在 `pack.data.deskinfo` 下(见文末「断线重连」),`data.success` 由**平台**填写,子游戏不注入。 + +### 0.5 `roomtype` 房间选项位串 + +`roomtype` 是建房时由客户端生成、经平台建房流程存入 `o_room.roomtype` 的 **11 位字符串**。前 5 位依次为:**局数、扣卡方式、查牌模式、傍王、爬坡**;后 6 位预留。下表的位索引从 0 开始,与 `charAt` 一致,“前 5 位”即索引 0~4。 + +| 位索引 | 含义 | '0' | '1' | 创建页控件 | design | +| --- | --- | --- | --- | --- | --- | +| 0 | 局数 | 6 局(默认) | 12 局 | 单选必选 | §10.1 | +| 1 | 扣卡方式 | 房主扣卡(默认) | AA 每人扣卡 | 单选必选 | §10.1 | +| 2 | 查牌模式 | 可查牌(默认) | 不查牌 | 单选必选 | §9 / §10.2 | +| 3 | 傍王 | 关(默认) | 开 | 独立可选 | §8.3 / §10.3 | +| 4 | 爬坡 | 常规算子(默认) | 爬坡 | 独立可选 | §7.3 / §10.3 | +| 5~10 | 预留 | 尚未定义 | 尚未定义 | 无控件 | §10.4 | + +前三组各自必须且只能选中一个选项;傍王与爬坡互不排斥,允许均不选、任选其一或同时选择。可选开关未勾选也必须编码为对应位的 '0',不能省略该位或缩短字符串。 + +默认选项的**前 5 位**为 `"00000"`,表示 6 局 / 房主扣卡 / 可查牌 / 无傍王 / 常规算子。例:前 5 位 `"10010"` 表示 12 局 / 房主扣卡 / 可查牌 / 傍王开 / 常规算子;`"00100"` 表示 6 局 / 房主扣卡 / 不查牌 / 无傍王 / 常规算子。这些仅是有效选项部分,**不是可直接发送的完整 roomtype**。 + +**待确认:后 6 位预留位的固定填充值。** 目前已确认总长度与预留位置,不能把未确认的全零填充写成已生效协议,也不能将预留位解释为额外规则。 + +本节为二七王位编码的唯一文档定义;玩法侧解析入口为 `class.config.js` 的 `parse()`,其他消费方使用解析结果。`multiple` / `bangwang` / `climb` / `baozhu` / `seatlist` / `liangpai` / `pushlist` 按相应选项取值或门控。 + +此次按规则设计者更正了长度和位序;尚未核验服务端实现是否已采用该定义。旧文档关于缺失、非字符串或过短值自动按 '0' 处理的描述,不构成前端补齐或静默容错的授权。前端应在编码入口生成完整合法参数,错误显式暴露,不改变服务器协议。 + +对应的房卡与局数(`export.get_asetcount` / `get_needroomcard` / `get_needroomcard_joinroom`,design §10.1): + +| 扣卡方式(位1) | 局数(位0) | 房主开房扣 | 加入者扣 | +| --- | --- | --- | --- | +| 房主扣卡 `'0'` | 6 局 | 2 张 | 0 | +| 房主扣卡 `'0'` | 12 局 | 4 张 | 0 | +| AA 每人 `'1'` | 6 局 | 1 张 | 1 张 | +| AA 每人 `'1'` | 12 局 | 2 张 | 2 张 | + +### 0.6 「一手出牌」的顺序口径 + +同一份业务数据「某一手打出的牌」会出现在**三个下发位置**: + +| 位置 | 字段 | +|---|---| +| 出牌推送 | `chupai1` / `chupai2` / `chupai3` 的 `data.cards` | +| 本轮进行态 | `playproc.cards[座位]`(`chupai*` 与重连包 `PushCards` 都带)| +| 重连出牌历史 | `PushCards.pushlist[轮次-1][座位]` | + +**这三处的数组内容逐元素完全相等**——顺序统一为「按**本局主牌花色**从大到小」的**权威顺序**。 +服务端只在落牌那一刻归一化一次(`class.paiju.js` 的 `order_playcards`),随后: +出牌推送读它、`playproc.cards` 存它、出牌历史 `playhistory` 归档它,下游一律**只读、不重排**。 + +因此前端**「增量回放」与「重连重建」得到的出牌历史必定一致**,两条路径可以复用同一份解析。 + +> ⚠️ **请求包里的 `cards` 顺序无意义**:那是玩家的点击顺序,同一手牌每次都可能不同。 +> 服务端不采信、不回显、也不据它判定牌型(server dev-guide 04 §8「前端不是数据源」)。 +> 一手单张时看不出差别;**一手多张(甩牌 / 对子 / 拖拉机)时才会显形**,尤其是同编码的一对牌 +> ——排序本身区分不了它们,只有「出牌当场归档」这一个写入处才能保证两条路径给出同一个数组。 + +--- + +## 1. 发牌(fapai) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:fapai + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| asetidx | 整数 | 当前局数 | +| asetcount | 整数 | 总局数 | +| cards | 数组 | 自己得到的牌id列表 | +| step | 整数 | **本局阶段**(发完牌恒为 `1` 叫分;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 | +| seat | 整数 | **控制权**:当前等待叫分者的位置。与重连包 `CallRun.seat` 同源(`get_callgrade_seat()`)| +| countdown | 整数 | 叫分倒计时 | + +--- + +## 2. 叫分或不叫(jiaofen,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:jiaofen + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 叫分者的位置序号 | +| call | 整数 | 分数,0表示不叫 | + +--- + +## 3. 叫分或不叫(jiaofen,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiaofen + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 叫分者的位置序号 | +| call | 整数 | 分数,0表示不叫 | +| currcall | 整数 | 当前叫到的分数 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| step | 整数 | **本局阶段**(叫分尚未结束,恒为 `1`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 | +| nextseat | 整数 | **控制权**:下一个叫分者的位置序号,即本包之后「轮到谁」。与重连包 `CallRun.seat` 同源(`get_callgrade_seat()`)| +| countdown | 整数 | 叫分倒计时 | + +--- + +## 4. 上庄(shangzhuang) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:shangzhuang + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 叫分者的位置序号 | +| call | 整数 | 分数,0表示不叫 | +| banker | 整数 | 庄家的位置序号 | +| grade | 整数 | 庄家的叫分 | +| step | 整数 | **本局阶段**(叫分结束、进入选主/投降,恒为 `2`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 | +| nextseat | 整数 | **控制权**:本包之后「轮到谁」——恒为 `banker`,因为选主与投降都只由庄家做(服务端 `mod.xuanzhu`/`mod.touxiang` 的座位校验同样以 `banker` 为准)。
⚠️ **别和本包的 `seat` 混用**:`seat` 是**最后一个叫分者**,不是控制权。与重连包 `ChooseMain.seat` 同源同值 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| bottomcards | 数组 | 8 张**底牌**(发牌时没发给玩家、扣在桌面的 8 张)。**庄家恒有**;**闲家仅当 70 分坐庄时才有**(供 3 秒亮牌用),非 70 分时闲家没有该属性(底牌只有庄家可见,design §4)。
**顺序在发牌结束时即冻结**(服务端 `paiju.bottomcards`,按「还没有主牌」的口径排一次):底牌是在**选主之前**翻给庄家看的,那时主牌花色尚不存在,故不按主牌花色排。本包与重连包 `ChooseMain.bottomcards` / `BuryCards.bottomcards` 三处**同序**,客户端存一次即可全程复用(含「查底牌」回看)| +| ancard3s | 整数 | **开底**标志。仅 70 分坐庄时出现且为 `1`:表示庄家**摸底**之前,需将 `bottomcards` 这 8 张**底牌**向所有玩家翻开 3 秒(design §4/§7.1);非 70 分无此属性 | +| cards | 数组 | 拿了底牌后手上的牌(36 张,含底牌),庄家才有此属性,闲家没有该属性 | +| countdown | 整数 | 选主倒计时 | +| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同为选主阶段(step2)的决策,见 touxiang 包与 design §4 | +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):按此刻累计捡分实时算出的判定倍率,**带符号**,与结算包 `aset.upgrade` 同口径——`3`/`2`/`1` = 庄家 大光/小光/过庄,`-N` = 闲家升 N 级,`0` = 叫分未定。上庄时捡分恒为 0,故必为 `3`。三家同值(捡分本就公开)。**不含扣底**——扣底要到末轮才产生。
顶部「抓分」角标只关心倍数大小,取 `Math.abs` 即可;**符号供客户端的判定动画分辨该播哪个**(不能取绝对值,否则 3 大光与 -3 升3级会撞在一起)| + +--- + +## 5. 投降(touxiang,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:touxiang + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 庄家的位置序号。投降是**选主阶段(step 2)与选主互斥**的选择:`mod.touxiang` 仅在 `step==2`、`banker==seat`、`call==70` 时受理;点投降即直接结算,不选主、不埋牌、不出牌(design §4) | + +--- + +## 6. 选主(xuanzhu,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:xuanzhu + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 选主者的位置序号 | +| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 | + +--- + +## 7. 选主(xuanzhu,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:xuanzhu + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| banker | 整数 | 庄家的位置序号 | +| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 | +| step | 整数 | **本局阶段**(选主完成、进入埋牌,恒为 `3`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 | +| nextseat | 整数 | **控制权**:本包之后「轮到谁」——恒为 `banker`,埋牌只由庄家做(服务端 `mod.maipai` 的座位校验以 `banker` 为准)。与重连包 `BuryCards.seat` 同源同值 | +| countdown | 整数 | 埋牌倒计时 | +| cards | 数组 | 选主后自己手上的牌id列表 | + +--- + +## 8. 埋牌(maipai,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:maipai + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 埋牌者的位置序号 | +| cards | 数组 | 埋牌的id列表 | + +--- + +## 9. 埋牌(maipai,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:maipai + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| cards | 数组 | 埋牌后手上的牌,去掉了埋牌,庄家才有此属性,闲家没有该属性 | +| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),庄家才有此属性,闲家没有该属性。注意与 `shangzhuang.bottomcards`(**底牌**,发牌留桌 8 张)是两批不同的牌,见 §0.0。
**取服务端权威快照 `get_burycard()`,不是客户端请求包里 `cards` 的原序**:按本局主牌花色从大到小排好,与重连包 `PushCards.burycards` **同源同序** | +| seatlist | 数组 | 三家座位牌况,**仅可查牌模式下发**(不查牌无此属性,design §9),结构与门控同 `chupai1/2/3.seatlist` 与重连包 `PushCards.seatlist`。
埋牌完成时它是刚初始化的**空表**(每家 `[[0,0],[0,0],[0,0],[0,0],[-1,-1]]`)——下发它是为了让「埋牌完成 → 庄家首出」这段窗口内,增量路径与重连路径拿到同一张表,客户端无需为这段窗口特判 | +| playproc | json | **本轮进行态**(design §5.1),**恒有、三家同值**,结构与门控同 [`chupai1.playproc`](#11-第一个玩家出牌chupai1)/重连包 `PushCards.playproc`(同一个快照函数 `get_playproc()`)。`do_burycard` 内部已 `new_playround` 就地初始化好 round-1 的进行态,此包带的正是这份初值:`round=1`、`start`/`currseat` 均为即将首出的庄家、`cards` 全空、`shuai_demand` 为 `null`。
**可见性**:全部字段由桌面公开信息推出(此刻尚无一张牌打出),三家整体下发、不逐座位裁剪 | +| step | 整数 | **本局阶段**(埋牌完成、进入出牌,恒为 `5`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 | +| seat | 整数 | **控制权**:出牌者的位置序号(即将首出的庄家)。服务端取自权威的 `playproc.currseat`,与重连包 `PushCards.playproc.currseat` 同源同值 | +| countdown | 整数 | 出牌倒计时 | +| liangpai | json | **亮牌**(design §8.2)。**只有闲家、且可查牌模式、且庄家达门槛时才有**;不达标或不查牌则无此属性。结构:`{ cards: [牌id...] }`——**庄家手中全部固定主牌的具体牌面**,按本局主牌序从大到小排好。
**固定主牌 = 双王 + 全部花色的 2 + 全部花色的 7**,**不含**主花色的普通牌 A/K/Q/J/10/9/8/6/5。
**门槛**(任一满足即下发):固定主牌总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6。**不限叫分**。
亮出的是**固定的一份**,不随被哪条门槛触发而增减;也**不给数量统计**——数量前端自己数 `cards.length` 即可。
口径是庄家**埋牌后的静态快照**(排除已埋的 8 张,但包含之后已打出的牌),全局固定不随出牌缩水,故重连包里取值一致 | + +--- + +## 10. 出牌(chupai,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:chupai + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 出牌者的位置序号 | +| cards | 数组 | 出牌的id列表 | + +--- + +## 11. 第一个玩家出牌(chupai1) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai1 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 出牌者的位置序号 | +| cards | 数组 | 实际打出的牌id列表;**甩错时**(见 `shuaicuo`)为被强制打出的那一张最小主牌单张。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序——请求包里的 `cards` 顺序服务端不采信、不回显(见 [§0.6](#06-一手出牌的顺序口径))| +| shuaicuo | 整数 | 甩错标志,仅甩错时出现且为 `1`(design §5.4.5:甩牌未通过最大性判定,整套甩牌收回,本轮只强制打出最小一张、失去本轮甩牌资格);正常出牌无此属性 | +| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` | +| count | 整数 | 出牌数量(甩错时为 1) | +| flower | 整数 | 出牌花色 | +| cardtype | 整数 | 出牌牌型:`>100` 单张(101 一张、102 两张…)、`>200` 对子(201 一对、202 两对…)、`>300` 拖拉机(302 两连对、303 三连对…),见 `class.pai.js` 顶部注释。**注意:cardtype 只能表达单一牌型**,而甩牌是单张/对子/拖拉机自由混搭(design §5.4.3),会被牌型推导压平成 1xx 或 2xx(例如「主K对 + 主5」得 103、「两连对 + 一散对」得 203)。**甩牌的真实结构请读 `shuai`,不要据 cardtype 反推** | +| shuai | json | 甩牌分量构成,**仅本次出牌是合法甩牌时才有**(非甩牌、以及甩错退化为单张时都没有该属性)。结构 `{ tractors: [连对数...], pairs: 独立对子数, singles: 单张数 }`,例如「主K对 + 主5」为 `{tractors:[],pairs:1,singles:1}`。与服务端跟牌时逐分量强制匹配用的 `shuai_demand` 同源(design §5.4.3/§5.4.4)| +| nextseat | 整数 | 下一个出牌者的位置序号 | +| countdown | 整数 | 下一个出牌者的出牌倒计时 | +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)| +| cardsinhand | 数组 | 出牌者出牌后手上剩下的牌id列表,只有出牌者才有此属性 | +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与结算包 `aset.upgrade` 同口径(`3`/`2`/`1` 庄家大光/小光/过庄,`-N` 闲家升 N 级,`0` 叫分未定)。同源同算法,差别仅在此处**不含扣底**。三家同值,随本包下发、不另开推送。符号是「谁赢」的区分,客户端判定动画靠它分辨 | +| playproc | json | **本轮进行态**(design §5.1)。**`chupai1/2/3` 三个包恒有,三家同值**;结构与重连包 [`PushCards.playproc`](#断线重连deskinfo) **完全一致**(服务端同一个快照函数 `get_playproc()`,前端增量回放与重连重建复用同一份解析)。
字段:`round` 第几轮、`start` 本轮首出位置、`currseat` 当前该谁出、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮暂时最大者、`maxcard` 其牌编码、`cards` 本轮三家各自出的牌(**定长 3,下标 = 座位序号**,未出的位置为 `null`)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 `null`)。
⚠️ **`chupai3` 带的是【下一轮】的进行态**(`round+1`、`cards` 全空、`currseat == nextseat == maxseat`):本轮第三家一出完,服务端就地开了新一轮。这与「此刻断线重连拿到的 `PushCards.playproc`」完全相同——本轮那三手牌客户端已由 `chupai1/2/3` 各自的 `seat`+`cards` 收到,收牌动画后即清台。**唯一例外**:`chupai3` 打完最后一张牌时本包会转成 `jiesuan`(见 §14),那种情况下**不带** `playproc`。
**可见性**:全部字段都由桌面公开信息推出(`cards` 就是已摊在桌上的牌,`shuai_demand` 与 `chupai1.shuai` 等价且甩出的牌本身已公开),故三家整体下发、不逐座位裁剪 | +| mustcard | 数组 | **下一个出牌者本轮跟牌的必出牌**(design §5.2),供其客户端自动选中。**只发给 `nextseat` 那一家**,其余两家无此属性——它是该玩家自己手牌的子集,整表下发会泄露他家手牌结构。以下情形不下发:`nextseat` 是本轮首家、首家为**甩牌**(甩牌跟牌走逐分量匹配,见 design §5.4.4)、或算出的必出牌为空 | + +--- + +## 12. 第二个玩家出牌(chupai2) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai2 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 出牌者的位置序号 | +| cards | 数组 | 出的牌id列表。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序(见 [§0.6](#06-一手出牌的顺序口径))| +| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` | +| nextseat | 整数 | 下一个出牌者的位置序号 | +| countdown | 整数 | 出牌倒计时 | +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)| +| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 | +| playproc | json | **本轮进行态**,恒有、三家同值,结构见 [§11 `playproc`](#11-第一个玩家出牌chupai1)。此时 `cards` 已含首家与本家两手牌,`currseat` 指向第三家 | +| mustcard | 数组 | **下一个出牌者本轮跟牌的必出牌**(design §5.2),供其客户端自动选中。**只发给 `nextseat` 那一家**,其余两家无此属性——它是该玩家自己手牌的子集,整表下发会泄露他家手牌结构。以下情形不下发:`nextseat` 是本轮首家、首家为**甩牌**、或算出的必出牌为空 | + +--- + +## 13. 第三个玩家出牌(chupai3) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai3 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 出牌者的位置序号 | +| cards | 数组 | 出的牌id列表。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序(见 [§0.6](#06-一手出牌的顺序口径))| +| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` | +| nextseat | 整数 | 下一个出牌者的位置序号 | +| countdown | 整数 | 出牌倒计时 | +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)| +| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 | +| maxseat | 整数 | 本轮出牌谁最大,只有本轮最后一个玩家出牌后才有此属性 | +| grade | 整数 | 本轮闲家得分,只有本轮最后一个玩家出牌后且闲家有得分才有此属性 | +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,口径同 `aset.upgrade`。同源同算法,差别仅在此处**不含扣底**。三家同值,随本包下发、不另开推送 | +| playproc | json | **本轮进行态**,三家同值,结构见 [§11 `playproc`](#11-第一个玩家出牌chupai1)。⚠️ 本包带的是**下一轮**的进行态(`round+1`、`cards` 全空、`currseat == nextseat == maxseat`),与此刻重连拿到的 `PushCards.playproc` 一致。**牌局在本包打完(转为 `jiesuan`)时不带此属性** | + +--- + +## 13.5 明牌(mingpai,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:mingpai + +查看另外两家手中全部主牌的具体牌面(design §9)。服务端仅在**可查牌模式、出牌阶段(step5)、且已有【任一】玩家报无主**(`have_baofu()`)时受理——注意判定的是「场上有人报无主」而非「请求者本人报无主」,报无主那位与另外两位同样有权查看(design §9.3);不满足时按 0.2 回 `mingpai` 失败包(不查牌 / 未报无主 → `RULE`,非出牌阶段 → `STEP`)。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 请求者的位置序号 | + +## 13.6 明牌(mingpai,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:mingpai (只回发给请求者) + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 请求者的位置序号 | +| others | 数组 | 另外两家各自未出的全部主牌,元素 `{ seat: 位置序号, zhucards: [主牌id列表] }` | + +> "再点一次取消查看"是客户端的显示开关,无需再请求服务端。 + +## 13.7 出牌提示(tishi,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:tishi + +闲家在出牌阶段向对家(另一闲家)发"踩/没分/有分"提示(design §11)。服务端仅在**出牌阶段(step5)、且发起者为闲家**(`seat != banker`)、**tip 合法(1/2/3)** 时受理;**不校验提示真实性**(玩家可主观发送);庄家无对家,不受理。不满足时按 0.2 回 `tishi` 失败包给发送者(庄家发 → `RULE`,非出牌阶段 → `STEP`,tip 非法或缺失 → `PARAM`)。**成功时不给发送者回执**,只转发给对家。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 发出提示的闲家位置序号 | +| tip | 整数 | 提示类型:1=踩(我能大过庄家)、2=没分(我手上没分了)、3=有分(我手上有分) | + +## 13.8 出牌提示(tishi,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:tishi (**只转发给对家**,即另一闲家 `3 - banker - seat`) + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 发出提示的闲家位置序号 | +| tip | 整数 | 提示类型:1=踩、2=没分、3=有分 | + +--- + +## 14. 结算(jiesuan) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiesuan + +结算包由以下几个子结构拼装而成。三种结算来源的组成不同: + +- **正常出牌结算**(`mod.chupai` 中最后一张牌出完,`get_paiju_account(0, ...)`):含 `chupai` + `bottom` + `aset`(末局再加 `account`)。 +- **投降结算**(`mod.touxiang`,`get_paiju_account(1, ...)`):只含 `aset`(末局再加 `account`),无 `chupai`、无 `bottom`。 +- **解散结算**(`export.get_disbandRoom`,`get_paiju_account(2, ...)`):只含 `aset` + `account`,无 `chupai`、无 `bottom`。**投递方式与上面两种不同,见 §14.1。** + +上面这张表头(`route: erqiwang / rpc: jiesuan`)**只适用于前两种**——正常出牌结算与投降结算,由子游戏自己 `sendpack_toother` 广播,客户端在 `rpc == "jiesuan"` 的分支里按 `data.aset` 取值。 + +### 14.1 解散结算的投递方式(与前两种不同,客户端需单独处理) + +解散**不是**由子游戏发包,而是平台在解散流程里回调 `youle_erqiwang.export.get_disbandRoom(o_room)`,把**返回值整个对象**塞进**房间路由**的解散包里下发(平台 `server_room/rpc.js`、`server_room/class.room.js`)。因此客户端收到的是: + +```json +{ + "app": "youle", "route": "room", "rpc": "free_room", + "data": { + "seats": [], + "deskfree": { + "rpc": "jiesuan", + "data": { "success": true, "aset": {}, "account": [] } + } + } +} +``` + +要点(照 §14 的表去 `data.aset` 取值会取空): + +- **rpc 是平台的 `free_room`(route 也是 `room`),不是 `erqiwang/jiesuan`**;子游戏返回的 `rpc: "jiesuan"` 只是嵌在 `deskfree` 里的一个普通字段。 +- **多一层 `data` 包装**:真实取值路径是 `data.deskfree.data.aset` 与 `data.deskfree.data.account`。 +- 本包的成败标志有两层:外层 `data.success` 由**平台**填写(解散流程本身成功与否),子游戏结算自带的 `success` 在 `data.deskfree.data.success`。§0.1「每个包 `data` 必带 `success`」说的是子游戏自己发的包;本包外层归平台。 +- `aset` / `account` 的字段结构与下面 §14 的表完全一致(`aset.multiple = 0`、`upgrade = 0`、各家 `grade = 0`,即解散局不结算子数与算奖;`account` 恒有,见 design §12.2「按当前累计分结算」)。 +- 若解散发生在**开战后、首局牌发出前**的窗口内(本游戏首局由 `makewar` 延迟 1 秒创建),`get_disbandRoom` 返回 `null`,平台走「不带 `deskfree`」分支——客户端需容忍 `data.deskfree` 缺失。 + +**顶层字段(三种结算来源恒有)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| success | 布尔 | 恒 `true`(§0.1)| +| step | 整数 | **本局阶段**,结算包恒为 `6`。三种结算来源(正常/投降/解散)统一由 `get_paiju_account` 给出——解散在此之前 `step` 可能还停在 1/2/3/5,由它落定为 6。客户端**不得按 rpc 名硬编码**,一律读此字段 | + +**chupai(出牌包,仅正常出牌结算存在)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 出牌者的位置序号,投降和解散无此属性 | +| cards | 数组 | 出的牌id列表,投降和解散无此属性 | +| maxseat | 整数 | 本轮出牌谁最大,投降和解散无此属性 | +| grade | 整数 | 本轮闲家得分,投降和解散无此属性 | +| gradecards | 数组 | 本轮闲家得分分牌列表,投降和解散无此属性。**注意:当前源码中赋值语句被注释(`class.paiju.js`/`mod.js` 中相关行均被注释掉),该字段实际永远不会出现在下发的包里,属于失效字段** | + +**bottom(抠底包,仅正常出牌结算存在;`cards` 恒有,其余仅闲家抠底时有)** + +> 本分组的 `cards` 是**埋牌底牌**(庄家埋下的 8 张),不是发牌留桌的底牌。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| cards | 数组 | **埋牌底牌**(庄家埋下的 8 张),投降和解散无此属性 | +| multiple | 整数 | 闲家抠底倍数,闲家没抠底无此属性,投降和解散无此属性 | +| grade1 | 整数 | 埋牌底牌的分数,闲家没抠底无此属性,投降和解散无此属性 | +| grade2 | 整数 | 闲家抠底得分,闲家没抠底无此属性,投降和解散无此属性 | + +**aset(单局结算包)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| banker | 整数 | 庄家的位置序号,-1:无庄 | +| call | 整数 | 庄家叫分,-1:无叫分 | +| multiple | 整数 | 基础子数 `get_base_bycall(call, climb)`(design §7.1/§7.3.1):常规算子 65→2、60→3、55→4、50 及以下→6、70打牌→2;投降固定 1、解散 0;无叫分 0 | +| flower | 整数 | 主牌花色,-1:无主牌花色 | +| grade | 整数 | 闲家捡分(含抠底) | +| upgrade | 整数 | 判定倍率(带符号,design §7.2.0):3大光 / 2小光 / 1过庄 / -N升N级(倒庄)/ -99投降 / 0解散或无判定 | +| bangwang | 整数 | 本局是否启用傍王规则 0否 1是(roomtype 位3) | +| climb | 整数 | 本局是否启用爬坡规则 0否 1是(roomtype 位4) | +| seatlist | 数组 | 玩家列表,元素结构见下 | + +`seatlist` 数组元素结构: + +```json +{ + "cards": [], // 冲关牌型:参与冲关的牌(王 + 冲关组合牌)列表,供「冲关牌型」界面展示 + "chongguan": 0, // 冲关奖数(design §8.1,旧称"常规算奖";只有庄家才计入自己的 N) + "wang": 0, // 手牌中的王数(傍王按此计奖) + "naward": 0, // 该家总奖数 N =(庄家?冲关奖数:0)+(傍王?王数:0) + "grade_aw": 0, // 算奖得分 = X×(2Ni−Nj−Nk),X 为每对子子数。= grade_cg + grade_bw + "grade_cg": 0, // 其中的【冲关】分量(design §8.1,不含傍王) + "grade_bw": 0, // 其中的【傍王】分量(design §8.3;未勾傍王时恒 0) + "grade_jf": 0, // 捡分子数得分 + "grade": 0, // 本局总分 = grade_aw + grade_jf + "score": 0 // 累计得分 +} +``` + +> 结算数值模型(design §7~§8):每「庄–闲」对子的基础金额 `X = multiple × |upgrade|`(投降 X=1、解散 X=0)。捡分子数:庄赢时两闲家各付庄家 X、庄家收 2X;闲赢(升级)时庄家各付两闲家 X。算奖:持有 N 奖的玩家从另外两人各多收 `X×N`,三家两两独立叠加(含闲–闲),即 `grade_aw = X×(2Ni−Nj−Nk)`。 +> +> **算奖的两个分量**(供大局结算分项展示):`N = N_冲关 + N_傍王`(冲关只计庄家、傍王勾选后庄闲都算)。 +> 服务端把两个分量**分别代入同一公式**各算一次,得到 `grade_cg` 与 `grade_bw`。 +> 公式对 `N` 是线性的,因此恒有 **`grade_cg + grade_bw == grade_aw`**,且两个分量各自零和。 +> 之所以必须在算钱时就拆,是因为两个 `N` 一旦相加就再也分不开——事后无法从 `grade_aw` 反推各自占比。 + +**account(大局结算包)** + +> 仅当打到最后一局(`o_paiju.idx >= o_room.asetcount`)或房间中途解散(解散结算)时,`jiesuan` 包才会带上此分组;普通的中间局结算包没有 `account` 字段(`class.paiju.js` `get_paiju_account`)。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| account | 数组 | 玩家列表(长度 3,下标 = 座位序号),元素结构见下 | + +```json +[ + { "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 }, + { "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 }, + { "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 } +] +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| score | 整数 | 累积总分(全部小局 `aset.seatlist[i].grade` 之和) | +| grades | 数组 | 每局总分列表,按局序 | +| grade_jf_total | 整数 | **基础分**累计:各局 `grade_jf`(捡分子数得分)之和 | +| grade_cg_total | 整数 | **冲关分**累计:各局 `grade_cg` 之和(design §8.1,**不含**傍王) | +| grade_bw_total | 整数 | **傍王分**累计:各局 `grade_bw` 之和(design §8.3;未勾傍王时恒 0) | + +> 恒等式:`grade_jf_total + grade_cg_total + grade_bw_total == score`,供大局结算面板分项展示(基础分 / 冲关分 / 傍王分 / 总分)。 +> +> ⚠️ **结构变更(2026-08-26)**:原为 `[[累积得分, [每局得分...]], ...]` 的二元数组,现改为**对象数组**并增加三项分解。数组下标语义不清,且前端尚未开工,此时改代价最小。 + +--- + +## 15. 准备(zhunbei,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:zhunbei + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 位置序号 | + +--- + +## 16. 准备(zhunbei,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:zhunbei + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 位置序号 | + +--- + +## 断线重连(deskinfo) + +> 由平台在玩家进入房间/断线重连时回调 `youle_erqiwang.export.get_deskinfo(o_room, seat)` 生成并下发(服务器→客户端);下发的 app/route/rpc 由平台的进房/重连流程决定,不在本子游戏代码内固定。平台把它挂在 `pack.data.deskinfo` 下,**`data.success` 由平台填写、子游戏不注入**(见 0.4)。下列字段即该函数返回的 `deskinfo` 对象结构,按当前 `paiju.step` 只带对应阶段的分组。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| count | 整数 | 总局数 | +| idx | 整数 | 当前局数 | +| PlayerInfo | 数组 | 三个玩家目前的总积分 `[0,0,0]` | +| step | 整数 | 牌桌状态:1发完牌叫分 2选主/投降 3埋牌 5出牌 6结算(无独立投降阶段——投降在 step2 与选主互斥;埋牌后直接进入 step5 出牌) | +| MyCards | 数组 | 自己手上的牌 | + +**CallRun(不在叫分阶段无此属性)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 当前叫分位置 | +| countdown | 整数 | 叫分倒计时 | +| nowcall | 整数 | 当前叫分 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| call | 数组 | 三家叫分 `[null,0,65]`,null还未叫分,0不叫,>0叫了多少分 | + +**ChooseMain(step 2 选主/投降阶段,不在此阶段无此属性)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | **控制权**:此刻轮到谁操作——恒为 `banker`(选主与投降都只由庄家做)。字段名与 `CallRun.seat` 一致:**各阶段分组里的 `seat` 统一表示「轮到谁」**。与增量推送 `shangzhuang.nextseat` 同源同值;客户端据此设控制权,**不要自己用 `banker` 反推**(那是把「选主=庄家」这条规则搬到前端)| +| banker | 整数 | 庄家 | +| call | 整数 | 叫分 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| countdown | 整数 | 选主倒计时 | +| bottomcards | 数组 | 8 张**底牌**(发牌留桌的 8 张),**只有庄家有此属性**(底牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)。顺序取发牌时冻结的快照,与 `shangzhuang.bottomcards`、`BuryCards.bottomcards` **完全同序**(见 §4)| +| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同一决策点,见 touxiang 包与 design §4 | +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `shangzhuang.curmultiple` 同源同值。选主阶段一张牌都还没出、捡分恒为 0,故**必为 `3`**(大光)。三家同值。有此字段,重连后顶部「抓分」角标才不会掉回 0 | + +> 选主阶段前端需在每个花色按钮上显示"该花色在庄家手中的对子数"(design §4/§11)——庄家的完整手牌由 `MyCards` 提供(庄家为 36 张),对子数由前端据此计算,服务端不额外下发。 + +**BuryCards(step 3 埋牌阶段,不在此阶段无此属性)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | **控制权**:此刻轮到谁操作——恒为 `banker`(埋牌只由庄家做)。与增量推送 `xuanzhu.nextseat` 同源同值 | +| banker | 整数 | 庄家 | +| call | 整数 | 叫分 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| flower | 整数 | 主牌花色 | +| countdown | 整数 | 埋牌倒计时 | +| bottomcards | 数组 | 8 张**底牌**(发牌留桌的 8 张),**只有庄家有此属性**(底牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)。顺序取发牌时冻结的快照,与 `shangzhuang.bottomcards`、`ChooseMain.bottomcards` **完全同序**——**不**按本局主牌花色重排(见 §4)| +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `shangzhuang.curmultiple` 同源同值。埋牌阶段同样一张牌未出、捡分恒为 0,故**必为 `3`**(大光)。三家同值 | + +> 埋牌阶段无投降(投降是 step2 与选主互斥的选择,选主后即不可再投降)。 + +**PushCards(step 5 出牌阶段,不在此阶段无此属性)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| banker | 整数 | 庄家 | +| call | 整数 | 叫分 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位4):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| flower | 整数 | 主牌花色 | +| countdown | 整数 | 出牌倒计时 | +| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),只有庄家有此属性。注意与 `ChooseMain`/`BuryCards.bottomcards`(**底牌**)是两批不同的牌,见 §0.0。与 `maipai.burycards` **同源同序**(同一个 `get_burycard()`,按本局主牌花色从大到小)| +| grade | 整数 | 当前的捡分分数 | +| gradecards | 数组 | 当前的捡分分牌,只有闲家有此属性 | +| playproc | json | **本轮进行态**,与 `chupai1/2/3.playproc` **同源同结构**(服务端同一个 `get_playproc()` 快照函数,见 [§11 `playproc`](#11-第一个玩家出牌chupai1)):`round` 第几轮、`start` 本轮首出位置、`currseat` 当前出牌位置、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮最大者位置、`maxcard` 最大牌编码、`cards` 本轮三家各自出的牌(**定长 3**,下标=位置序号,未出为 `null`)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 null,供跟牌逐分量强制匹配,design §5.4.4)。**两种查牌模式下恒有此属性**——`cards` 是当前这一轮桌面上的牌,不属于「查牌」,屏蔽了后出的人就无从跟牌(design §9 末尾)| +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是,**恒有此属性**;与 `chupai1/2/3` 的 `baozhu` **同源同值同门控**(同一个 `have_baofu()` + 同一个查牌位)。它是**余主公示**与“明牌”按钮的开关,重连必须一并恢复,否则重连后按钮凭空消失;**不查牌模式恒为 0**(design §9)| +| seatlist | 数组 | 三家座位牌况,与上文 `maipai` / `chupai1/2/3` 包的 `seatlist` 同名同结构(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`)。**仅可查牌模式下有此属性**(design §9)| +| liangpai | json | **亮牌**,结构同 maipai 包的 `liangpai`(`{ cards: [牌id...] }`);**仅可查牌模式、且请求者为闲家、且庄家达标时有**(供闲家重连后仍能看到,design §8.2)| +| pushlist | 数组 | 出牌历史 `[[[], [], []], [[], [], []], ...]`,外层下标=轮次(从第 1 轮起,含进行中的当前轮),内层**恒为 3 个数组**、按位置序号存该轮各家出的牌id。**每一手与当时那个 `chupai1/2/3` 包的 `cards` 逐元素完全相等**(同一份数据、同一个顺序口径,见 [§0.6](#06-一手出牌的顺序口径)),因此前端“增量回放”与“重连重建”得到的出牌历史必定一致。**仅可查牌模式下有此属性**——往轮打出、已被收走的牌属于「查牌」范畴,不查牌模式一律不下发(design §9)。注意当前这一轮桌面上的牌由 `playproc.cards` 恢复,**两种模式下都有**,否则后出的人无从跟牌 | +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `chupai1/2/3` 同源同值,供重连后顶部「抓分」角标与判定动画状态立即正确 | +| mustcard | 数组 | **本轮跟牌的必出牌**(design §5.2)。**仅当 `playproc.currseat == 请求者座位` 时才有**,且只算请求者自己的手牌;未轮到本家、本轮首家、甩牌局面、必出牌为空时均无此属性 | + +**Balance(不在结算阶段无此属性)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| readystate | 数组 | 所有玩家的准备状态 | +| aset | json | 单局结算包,同上面结算包中的 `aset`,只有当自己的准备状态为0时才有此属性 | +| bottom | json | **抠底包**,结构同 §14 的 `bottom`,与该局 `jiesuan` 推送的 `data.bottom` **同源同值**(服务端在 `get_paiju_account` 末尾冻结的同一份快照)。**仅正常出牌结算才有**(投降/解散没有抠底这一步);同样受「自己的准备状态为 0」门控。
**为什么必须有**:结算面板还开着时断线重连/硬刷新,只恢复 `aset` 会让抠底明细整块空白——同一份数据两条路径给的不一样(server 红线「发全下发面」)。
**可见性**:埋牌底牌在本局结算时已随 `jiesuan` 广播给三家(design §11 结束亮底),此处不构成额外泄露 | +| account | json | **大局结算包**,结构同 §14 的 `account`,与该局 `jiesuan` 推送的 `data.account` **同源同值**。**仅末局或中途解散才有**,与 `jiesuan` 的取舍完全一致(有就带、没有就不带)| + +--- + +## 战绩(大局列表)gameinfo1 + +> 非客户端收发包:大局结束/解散时由 `class.desk.js` `get_desk_account` 组装,经 `import.save_grade(o_room, o_gameinfo1, o_gameinfo2, 1)` 传给平台战绩服务持久化(服务器→平台)。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| roomcode | 整数 | 房号 | +| asetcount | 整数 | 实际局数 | +| createtime | 字符 | 开房时间 | +| makewartime | 字符 | 开战时间 | +| players | 数组 | 玩家列表,元素结构见下 | + +`players` 数组元素结构: + +```json +{ + "seat": 0, // 座位 + "playerid": 100001, // 玩家ID + "name": "", // 昵称 + "avatar": "", // 头像 + "score": 0 // 成绩 +} +``` + +--- + +## 战绩(大局)gameinfo2 + +> 非客户端收发包:与 gameinfo1 同批,由 `import.save_grade` 的第 3 个参数传给平台战绩服务(服务器→平台)。 + +牌局列表,数组元素结构: + +```json +{ + "starttime": "", // 开始时间 + "endtime": "", // 结束时间 + "seatlist": [6, -6, 0], // 玩家成绩 + "callproc": [], // 叫分过程,详情见代码中的注释 + "banker": 0, // 庄 + "call": 0, // 叫分 + "flower": 0, // 主牌花色 + "result": 0, // 牌局结果 + "cards": [] // 发牌出牌情况,详情见代码中的注释 +} +``` diff --git a/cocoscreator_projects/scripts/verify-feedback-split-preview.mjs b/cocoscreator_projects/scripts/verify-feedback-split-preview.mjs new file mode 100644 index 0000000..fa76f49 --- /dev/null +++ b/cocoscreator_projects/scripts/verify-feedback-split-preview.mjs @@ -0,0 +1,36 @@ +/** Test extracted panels with local clipboard, image and command fixtures only. */ +export async function verifyFeedbackSplitPreview(e){ + const checks=[],assert=(v,m)=>{if(!v)throw Error(m);checks.push(m);},sleep=ms=>new Promise(r=>setTimeout(r,ms)); + const load=id=>new Promise((r,j)=>e.assetManager.loadAny(id,(err,a)=>err?j(err):r(a))); + const refs=(await load('1c10c2ee-adc2-496f-9005-d3e0e6ebbb56')).scene.getComponentInChildren('LobbyAssets'); + assert(refs.redeemCode.data.name==='RedeemCode'&&refs.redeemCode.data.children.length===1&&refs.redeemCode.data.children[0].name==='group-74','redeem prefab contains only group-74'); + assert(refs.qrCode.data.name==='QrCode'&&refs.qrCode.data.children.length===1&&refs.qrCode.data.children[0].name==='group-42','QR prefab contains only group-42'); + assert(!('notices' in refs),'obsolete shared asset field is removed'); + const C=Array.from(System.entries()).map(([,m])=>m).find(m=>m.LobbyPanel).LobbyPanel,panel=Object.create(C.prototype); + const host=new e.Node('FeedbackSplitTest');host.parent=e.director.getScene(); + const redeemed=[],shares=[],tips=[],images=[];const loadRemote=e.assetManager.loadRemote; + panel.assets=refs;panel.model={canAct:true};panel.modal=null;panel.modalReleases=[];panel.disposed=false; + panel.extras={paste:async()=>'00Ab-12',redeem:code=>redeemed.push(code)};panel.nativeCall=(name,data)=>shares.push({name,data});panel.tip=message=>tips.push(message); + let qrEnabled=true; + panel.config={identity:{gameid:'test',agentid:'test',channelid:'test',marketid:'test'},remoteConfig:{getValue:key=>({WechatPublicNo_ewm:qrEnabled?'https://example.invalid/fixture.png':'',sharename:'测试',shareDesc:'说明'})[key]}}; + panel.close=()=>{for(const off of panel.modalReleases.splice(0))off();panel.modal?.destroy();panel.modal=null;}; + panel.open=(asset,kind)=>{panel.close();panel.modal=e.instantiate(asset);panel.modal.parent=host;panel.modalKind=kind;return panel.modal;}; + e.assetManager.loadRemote=(url,options,callback)=>images.push({url,callback}); + try{ + panel.openCdKey();let g=panel.modal.getChildByName('group-74'),edit=g.getChildByName('CardInput').getComponent(e.EditBox); + assert(edit.string==='','redeem input starts empty'); + g.getChildByName('Spirit3266').emit(e.Button.EventType.CLICK);assert(redeemed.length===0&&panel.modal!==null,'empty redeem is ignored'); + g.getChildByName('Spirit3265').emit(e.Button.EventType.CLICK);await sleep(20);assert(edit.string==='00Ab-12','paste preserves case and leading zeroes'); + g.getChildByName('Spirit3266').emit(e.Button.EventType.CLICK);assert(redeemed[0]==='00Ab-12'&&panel.modal===null,'redeem command receives exact input and closes'); + panel.openCdKey();g=panel.modal.getChildByName('group-74');assert(g.getChildByName('CardInput').getComponent(e.EditBox).string==='','redeem reopening clears input'); + g.getChildByName('反馈遮罩').emit(e.Button.EventType.CLICK);assert(panel.modal===null,'redeem shade closes'); + qrEnabled=false;panel.openQrCode();assert(panel.modal===null&&images.length===0,'missing QR config retains no-op behavior'); + qrEnabled=true;panel.openQrCode();g=panel.modal.getChildByName('group-42');assert(images[0].url==='https://example.invalid/fixture.png','QR request uses configured URL'); + const bitmap=document.createElement('canvas');bitmap.width=bitmap.height=2;bitmap.getContext('2d').fillRect(0,0,2,2);const image=new e.ImageAsset(bitmap); + images[0].callback(null,image);const sprite=g.getChildByName('Spirit343').getComponent(e.Sprite);assert(sprite.spriteFrame?.texture.image===image,'QR image callback populates sprite'); + g.getChildByName('Spirit345').emit(e.Button.EventType.CLICK);assert(shares[0].name==='friendsSharetypeUrlToptitleDescript'&&shares[0].data.webpageUrl===images[0].url,'QR share retains existing bridge payload'); + g.getChildByName('二维码遮罩').emit(e.Button.EventType.CLICK);assert(panel.modal===null&&sprite.spriteFrame===null,'QR close releases image frame'); + panel.openQrCode();panel.close();images[1].callback(null,image);assert(panel.modal===null&&tips.length===0,'late image callback cannot reopen a closed panel');image.destroy(); + return {checks}; + }finally{panel.close();host.destroy();e.assetManager.loadRemote=loadRemote;} +} diff --git a/cocoscreator_projects/scripts/verify-invite-preview.mjs b/cocoscreator_projects/scripts/verify-invite-preview.mjs new file mode 100644 index 0000000..5535074 --- /dev/null +++ b/cocoscreator_projects/scripts/verify-invite-preview.mjs @@ -0,0 +1,64 @@ +/** Run through Cocos MCP in an isolated Game View. Records packets; never binds a real account. */ +export async function verifyInvitePreview(){ + const e=await System.import('cc'),modules=Array.from(System.entries()).map(([,m])=>m); + const exported=name=>{const m=modules.find(m=>m[name]);if(!m)throw Error(`Missing ${name}`);return m[name];}; + const source=e.director.getScene().getChildByName('PlatformServices').getComponent('PlatformStartup'); + const config=source.config,assets=source.getComponent('LobbyAssets'); + source.unsubscribe?.();source.runtime?.stop();source.panel?.dispose();source.panel=null; + await new Promise((resolve,reject)=>e.director.loadScene('PlatformLobby',err=>err?reject(err):resolve())); + const sent=[],tips=[],checks=[];let receive,panel,pasteResolve; + const assert=(ok,message)=>{if(!ok)throw Error(message);checks.push(message);}; + const runtime=new (exported('PlatformRuntime'))({ + gameEntry:exported('createLoginStageGameEntry')(config.identity.gameid,()=>{}),resolveRuntimeConfig:async()=>config, + createWireClient:()=>({subscribe:cb=>{receive=cb;return ()=>{};},start:()=>{},stop:()=>{},send:packet=>sent.push(packet),switchServer:()=>{},reconnectCurrent:()=>{}}), + scene:{showLoading:()=>{},showLogin:()=>{},showLobby:()=>{},showRoom:()=>{},showReconnect:()=>{},showKicked:()=>{},showServerDenial:()=>{},showFatal:err=>{throw err;}, + showTips:(msg,time,kind)=>{if(kind==='show_message')panel?.cancelInviteRequest();tips.push(msg);}}, + loadResources:async()=>{},waitForMinimumDisplay:async()=>{},getLoginDeviceSnapshot:()=>{throw Error('Inbound fixture only');}, + }); + await runtime.start();receive({type:'open',server:config.servers[0]}); + const packet=(rpc,data)=>receive({type:'message',message:{route:'agent',rpc,data}}); + const login={state:0,playerid:7,nickname:'邀请码接线验证',avatar:'',sex:0,bean:0,roomcard:3,taskstate:0,score:0,sign:null,ip:'127.0.0.1',openid:'invite-openid',unionid:'invite-unionid',invitecode:null}; + packet('player_login',login); + const canvas=e.director.getScene().getChildByName('Canvas'),popup=new e.Node('InviteTestPopup');popup.parent=canvas;popup.layer=canvas.layer;popup.addComponent(e.UITransform).setContentSize(1600,720); + const remote=exported('createRemoteConfig')({gameNoticeShow:-1,agentmode:2},config.identity); + panel=new (exported('LobbyPanel'))(canvas,popup,assets,config,exported('resolveLobbyPolicy')(remote),runtime.state,msg=>tips.push(msg),()=>{},()=>null,()=>{}, + exported('resolveLobbyMenuConfig')(remote),{versionState:0,launchMode:0},()=>{},()=>{},()=>{},undefined, + {query:()=>runtime.getPlayerInvitecode(),bind:code=>runtime.bindInvitecode(code),paste:()=>new Promise(resolve=>{pasteResolve=resolve;})}); + popup.setSiblingIndex(canvas.children.length-1); + const off=runtime.subscribeState(state=>panel.render(state),err=>{throw err;}); + const click=()=>{const node=panel.menuNodes.get(235);if(!node.parent.active)panel.menuNodes.get(3120).emit(e.Button.EventType.CLICK);assert(node.activeInHierarchy,'invite entry is visible');node.emit(e.Button.EventType.CLICK);}; + const button=id=>panel.modal.getChildByPath(`group-80/Spirit${id}`).emit(e.Button.EventType.CLICK); + click();click();assert(sent.length===1&&sent[0].rpc==='get_player_invitecode','query suppresses duplicate clicks'); + assert(JSON.stringify(sent[0].data)===JSON.stringify({agentid:config.identity.agentid,playerid:7,unionid:'invite-unionid',openid:'invite-openid'}),'query preserves exact identity fields'); + assert(panel.modal===null,'query waits for response'); + packet('get_player_invitecode',{invitecode:''});assert(panel.modalKind==='invite','query opens authored Bind prefab'); + let edit=panel.modal.getChildByPath('group-80/Spirit365/InputEditBox').getComponent(e.EditBox); + assert(edit.textLabel&&edit.placeholderLabel&&edit.maxLength===-1,'native input has persisted label references and no six-digit limit'); + for(const l of [edit.textLabel,edit.placeholderLabel]){const anchor=l.node.getComponent(e.UITransform).anchorPoint;assert(anchor.x===0&&anchor.y===1,'EditBox labels use native top-left anchors');} + edit.string='00x1234567';edit.node.emit(e.EditBox.EventType.TEXT_CHANGED);assert(edit.string==='001234567','input removes non-digits while retaining leading zeros'); + button(366);pasteResolve('00123');await Promise.resolve();await Promise.resolve();assert(edit.string==='00123','paste callback updates input'); + const submit=panel.modal.getChildByPath('group-80/Spirit367');submit.emit(e.Button.EventType.CLICK);submit.emit(e.Button.EventType.CLICK); + assert(panel.modal===null&&sent.length===2,'submit closes dialog and sends once'); + assert(JSON.stringify(sent[1].data)===JSON.stringify({agentid:config.identity.agentid,playerid:7,invitecode:'00123'}),'binding preserves exact fields and leading zeros'); + packet('binding_invitecode',{state:'1',error:'邀请码无效'});assert(tips.includes('邀请码无效')&&runtime.state.players.entities[7].invitecode==='','string failure state displays server text without changing code'); + click();packet('get_player_invitecode',{invitecode:''});assert(panel.modalKind==='invite','unchanged query result reopens dialog'); + edit=panel.modal.getChildByPath('group-80/Spirit365/InputEditBox').getComponent(e.EditBox);edit.string='00123';button(367); + packet('binding_invitecode',{state:'0',invitecode:'00123',error:'绑定成功'});assert(runtime.state.players.entities[7].invitecode==='00123'&&tips.includes('绑定成功'),'string success state updates canonical player and shows server text'); + click();packet('get_player_invitecode',{invitecode:'00123'}); + assert([365,366,367].every(id=>!panel.modal.getChildByPath(`group-80/Spirit${id}`).active),'bound state hides input paste and submit'); + assert(panel.modal.getChildByPath('group-80/Spirit368').getComponent(e.Label).string==='您已绑定邀请码,所绑定邀请码为:00123','bound state shows exact code'); + await new Promise(resolve=>e.director.once(e.Director.EVENT_AFTER_DRAW,resolve)); + const messageBox=panel.modal.getChildByPath('group-80/Spirit368').getComponent(e.UITransform).getBoundingBoxToWorld(); + const backgroundBox=panel.modal.getChildByPath('group-80/Spirit363').getComponent(e.UITransform).getBoundingBoxToWorld(); + assert(messageBox.x>=backgroundBox.x&&messageBox.xMax<=backgroundBox.xMax,'bound message stays inside authored background'); + button(362);assert(panel.modal===null,'mask closes dialog'); + click();packet('show_message',{msg:'暂不可用',time:0});assert(panel.invitePending===null,'generic denial releases pending request'); + click();packet('get_player_invitecode',{invitecode:''});button(366);button(362);pasteResolve('late');await Promise.resolve();assert(panel.modal===null,'late paste cannot reopen a closed dialog'); + click();packet('get_player_invitecode',{invitecode:''}); + edit=panel.modal.getChildByPath('group-80/Spirit365/InputEditBox').getComponent(e.EditBox);edit.string='00123'; + panel.requestInvite('query',()=>runtime.getPlayerInvitecode());receive({type:'slow'});assert(panel.invitePending===null,'disconnect releases pending request'); + const count=sent.length;panel.menuNodes.get(235).emit(e.Button.EventType.CLICK);assert(sent.length===count,'disconnect blocks sending'); + await new Promise(resolve=>e.director.once(e.Director.EVENT_AFTER_DRAW,resolve)); + globalThis.__inviteVerification={panel,runtime,checks,close:()=>{off();panel.dispose();runtime.stop();popup.destroy();}}; + return {checks,packets:sent.map(({rpc,data})=>({rpc,fields:Object.keys(data)}))}; +} diff --git a/cocoscreator_projects/scripts/verify-loading-preview.mjs b/cocoscreator_projects/scripts/verify-loading-preview.mjs new file mode 100644 index 0000000..889f675 --- /dev/null +++ b/cocoscreator_projects/scripts/verify-loading-preview.mjs @@ -0,0 +1,52 @@ +/** Cocos Game View verification: mocked wire, no real ranking/card requests. */ +export async function verifyLoadingPreview(){ + const e=await System.import('cc'),modules=Array.from(System.entries()).map(([,m])=>m); + const exported=name=>{const m=modules.find(m=>m[name]);if(!m)throw Error(`Missing ${name}`);return m[name];}; + const source=e.director.getScene().getChildByName('PlatformServices').getComponent('PlatformStartup'); + const config=source.config,assets=source.getComponent('LobbyAssets'); + source.unsubscribe?.();source.runtime?.stop();source.panel?.dispose();source.panel=null; + await new Promise((resolve,reject)=>e.director.loadScene('PlatformLobby',err=>err?reject(err):resolve())); + const sent=[],tips=[],checks=[];let receive,panel; + const assert=(ok,msg)=>{if(!ok)throw Error(msg);checks.push(msg);}; + const runtime=new (exported('PlatformRuntime'))({ + gameEntry:exported('createLoginStageGameEntry')(config.identity.gameid,()=>{}),resolveRuntimeConfig:async()=>config, + createWireClient:()=>({subscribe:cb=>{receive=cb;return ()=>{};},start:()=>{},stop:()=>{},send:p=>sent.push(p),switchServer:()=>{},reconnectCurrent:()=>{}}), + scene:{showLoading:()=>{},showLogin:()=>{},showLobby:()=>{},showRoom:()=>{},showReconnect:()=>{},showKicked:()=>{},showServerDenial:()=>{},showFatal:err=>{throw err;}, + showTips:(msg,time,kind)=>{if(kind==='show_message')panel?.cancelRankRequest();tips.push(msg);}}, + loadResources:async()=>{},waitForMinimumDisplay:async()=>{},getLoginDeviceSnapshot:()=>{throw Error('Fixture only');}, + }); + await runtime.start();receive({type:'open',server:config.servers[0]}); + const packet=(rpc,data)=>{const message={route:'agent',rpc,data};panel?.requestReceived(message);receive({type:'message',message});}; + packet('player_login',{state:0,playerid:7,nickname:'接线验证',avatar:'',sex:0,bean:0,roomcard:3,taskstate:0,score:0,sign:null,ip:'127.0.0.1',openid:'test',unionid:'test',invitecode:null}); + const canvas=e.director.getScene().getChildByName('Canvas'),popup=new e.Node('OwnedVipTestPopup');popup.parent=canvas;popup.layer=canvas.layer;popup.addComponent(e.UITransform).setContentSize(1600,720); + const remote=exported('createRemoteConfig')({gameNoticeShow:-1,rankList:2,cdKey:2,vipRankLimit:3},config.identity); + const feedback=new (exported('FeedbackPanels'))(canvas,{tips:source.tipsPrefab,loading:source.waitPrefab,reconnect:source.reconnectPrefab,kick:source.kickPrefab}); + panel=new (exported('LobbyPanel'))(canvas,popup,assets,config,exported('resolveLobbyPolicy')(remote),runtime.state,msg=>tips.push(msg),()=>{},()=>null,()=>{}, + exported('resolveLobbyMenuConfig')(remote),{versionState:0,launchMode:0},action=>{if(action==='tasks')runtime.getPlayerTasks();},id=>runtime.claimTask(id),()=>{},undefined,undefined, + {rank:()=>runtime.getTreasureList(),redeem:card=>runtime.redeemCard(card),paste:async()=>'00Ab-12',loading:show=>feedback.loading(show),vipRank:limit=>runtime.getVipRankList(limit),joinOwned:code=>runtime.joinOwnedRoom(code,{city:'test'})}); + popup.setSiblingIndex(canvas.children.length-1); + const off=runtime.subscribeState(state=>panel.render(state),err=>{throw err;}); + const click=id=>panel.menuNodes.get(id).emit(e.Button.EventType.CLICK); + + + const overlay=feedback.loadingNode,animation=overlay.getChildByPath('group-40/载入动画').getComponent(e.Animation); + assert(animation&&animation.defaultClip?.name==='RequestLoading','Layer614 owns serialized RequestLoading clip'); + click(17);assert(overlay.activeInHierarchy&&sent.at(-1).rpc==='get_player_task','task send shows Layer614'); + assert(animation.getState('RequestLoading').isPlaying,'loading starts native Animation'); + const angle=animation.node.angle;await new Promise(r=>setTimeout(r,200));assert(animation.node.angle!==angle,'animation advances rotation'); + packet('get_player_task',{tasks:[{taskid:1,title:'test',award:1,finish:1,total:1,state:1}]}); + assert(!overlay.active&&!animation.getState('RequestLoading').isPlaying,'task response hides overlay and stops clip'); + panel.requestTask('award',()=>runtime.claimTask(1));assert(overlay.active&&sent.at(-1).rpc==='get_task_award','award send shows Layer614'); + packet('get_task_award',{taskid:1,taskstate:0});assert(!overlay.active,'award response hides Layer614'); + panel.close();click(3232);assert(overlay.active&&sent.at(-1).rpc==='getVipRankList','VIP rank send shows Layer614'); + packet('getVipRankList',{state:1,error:'失败'});assert(!overlay.active,'VIP failure response hides Layer614'); + panel.cancelRankRequest();click(3232);packet('getVipRankList',{list:[]});assert(!overlay.active,'empty VIP success hides Layer614'); + click(3232);packet('show_message',{msg:'服务拒绝',time:0});assert(!overlay.active,'generic failure closes request loading'); + click(17);panel.cancelTaskRequest();assert(!overlay.active,'timeout cancellation hides Layer614'); + let caught=false;try{panel.requestTask('list',()=>{throw Error('send failed');});}catch{caught=true;} + assert(caught&&!overlay.active,'synchronous send failure hides Layer614'); + click(17);panel.requestReceived({route:'agent',rpc:'get_player_task',data:null});assert(!overlay.active,'malformed task reply closes before parsing');panel.cancelTaskRequest(); + click(3232);assert(overlay.active,'loading can restart'); + globalThis.__loadingVerification={panel,runtime,feedback,checks,close:()=>{off();panel.dispose();runtime.stop();feedback.dispose();popup.destroy();}}; + return {checks}; +} diff --git a/cocoscreator_projects/scripts/verify-notice-tabs-preview.mjs b/cocoscreator_projects/scripts/verify-notice-tabs-preview.mjs new file mode 100644 index 0000000..37a814d --- /dev/null +++ b/cocoscreator_projects/scripts/verify-notice-tabs-preview.mjs @@ -0,0 +1,70 @@ +/** Run in the Cocos Game View; uses local text fixtures and never requests remote content. */ +export async function verifyNoticeTabsPreview(e,boardId) { + const checks=[],assert=(value,message)=>{if(!value)throw Error(message);checks.push(message);}; + const load=id=>new Promise((resolve,reject)=>e.assetManager.loadAny(id,(err,a)=>err?reject(err):resolve(a))); + const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms)); + const asset=await load(boardId),root=e.instantiate(asset);root.parent=e.director.getScene(); + const host=new e.Node('NoticeFlowVerification');host.parent=e.director.getScene(); + let panel; + try { + assert(root.children.length===1&&root.children[0].name==='group-12','standalone prefab contains only group-12'); + const view=root.getChildByPath('group-12/NoticeTabs').getComponent('NoticeTabsView'); + assert(view&&view.tabs.length===3,'three native tabs are serialized'); + assert(view.getComponent(e.ToggleContainer)?.allowSwitchOff===false,'container enforces single selection'); + const selected=[];let release=view.bindPages([true,true,true],index=>selected.push(index));await sleep(60); + const state=index=>{ + assert(view.tabs.every((tab,i)=>tab.isChecked===(i===index)),'exactly the selected tab is checked'); + assert(view.backgrounds.every((s,i)=>s.spriteFrame.name===(i===index?'00216_1':'00216_2')),'backgrounds follow original selected frames'); + assert(view.titles.every((s,i)=>s.spriteFrame.name==='00217_'+(i*2+(i===index?2:1))),'titles follow original selected frames'); + }; + state(0);assert(selected.join(',')==='0','opening shows first page once'); + view.tabs[1].node.emit(e.Button.EventType.CLICK);state(1);assert(selected.at(-1)===1,'native click selects second content page'); + view.tabs[2].node.emit(e.Button.EventType.CLICK);state(2);assert(selected.at(-1)===2,'native click selects third content page'); + view.tabs[2].node.emit(e.Button.EventType.CLICK);state(2); + release();const count=selected.length;view.tabs[0].node.emit(e.Button.EventType.CLICK);assert(selected.length===count,'released callback does not run'); + release=view.bindPages([true,false,true],index=>selected.push(index));await sleep(60);state(0); + assert(!view.tabs[1].node.active,'empty second page hides its entire tab'); + const first=view.tabs[0].node,third=view.tabs[2].node; + assert(Math.abs(first.position.y-third.position.y-first.getComponent(e.UITransform).height-view.getComponent(e.Layout).spacingY)<.1,'hidden middle tab leaves no layout gap'); + view.tabs[2].node.emit(e.Button.EventType.CLICK);state(2);assert(selected.at(-1)===2,'hidden tab does not change page identity');release(); + const startup=await load('1c10c2ee-adc2-496f-9005-d3e0e6ebbb56'),refs=startup.scene.getComponentInChildren('LobbyAssets'); + assert(refs.noticeBoard.uuid===boardId,'startup references the standalone board'); + assert(refs.ownedRoomList.data.getChildByName('group-98')&&refs.qrCode.data.getChildByName('group-42'),'independent room list and QR page remain wired'); + assert(!refs.qrCode.data.getChildByName('group-12'),'QR prefab does not duplicate group-12'); + const LobbyPanel=Array.from(System.entries()).map(([,module])=>module).find(module=>module.LobbyPanel)?.LobbyPanel; + assert(LobbyPanel,'LobbyPanel module is available');panel=Object.create(LobbyPanel.prototype); + panel.assets={noticeBoard:asset};panel.model={canAct:true};panel.disposed=false;panel.modal=null;panel.modalReleases=[]; + panel.policy={notices:[{type:0,text:'第一页',image:'',url:''},{type:0,text:'',image:'',url:''},{type:0,text:'第三页',image:'',url:''}]}; + panel.close=()=>{for(const off of panel.modalReleases.splice(0))off();panel.modal?.destroy();panel.modal=null;panel.modalKind='';}; + panel.open=(prefab,kind)=>{panel.close();panel.modal=e.instantiate(prefab);panel.modal.parent=host;panel.modalKind=kind;return panel.modal;}; + panel.openNotice();await sleep(60); + const board=panel.modal,g=board.getChildByName('group-12'),actual=g.getChildByName('NoticeTabs').getComponent('NoticeTabsView'); + const text=g.getChildByPath('NoticeViewport/NoticeContent/通知内容').getComponent(e.Label); + assert(text.string==='第一页','actual notice entry renders page one'); + actual.tabs[2].node.emit(e.Button.EventType.CLICK);assert(text.string==='第三页','actual tab event renders matching content'); + assert(!actual.tabs[1].node.active,'actual entry uses source visibility rules'); + g.getChildByName('通知关闭').emit(e.Button.EventType.CLICK);assert(panel.modal===null,'close button closes independent board'); + panel.openNotice();await sleep(40); + assert(panel.modal.getChildByPath('group-12/NoticeTabs').getComponent('NoticeTabsView').tabs[0].isChecked,'reopening resets selection'); + // Route coordinate touches through Cocos hit testing, including the background behind controls. + root.active=false; + const canvas=e.director.getScene().getComponentInChildren(e.Canvas); + const mount=async()=>{panel.openNotice();panel.modal.parent=canvas.node;panel.modal.setPosition(0,0,0);const widget=panel.modal.getComponent(e.Widget);widget.target=canvas.node;widget.updateAlignment();await sleep(70);}; + let touchId=100; + const tap=async(node,dx=0,dy=0)=>{ + const position=node.worldPosition.clone();position.x+=dx;position.y+=dy; + const screen=canvas.cameraComponent.worldToScreen(position),touch=new e.Touch(screen.x,screen.y,touchId++); + for(const type of [e.Input.EventType.TOUCH_START,e.Input.EventType.TOUCH_END])e.input._dispatchEventTouch(new e.EventTouch([touch],false,type,[touch])); + await sleep(25); + }; + await mount();let surface=panel.modal.getChildByName('group-12'); + await tap(surface.getChildByName('通知背景'),-350,-200);assert(panel.modal!==null,'panel blank area does not close the board'); + await tap(surface.getChildByName('通知标题内容'));assert(panel.modal!==null,'title does not close the board'); + await tap(surface.getChildByPath('NoticeViewport/NoticeContent/通知内容'),30,-10);assert(panel.modal!==null,'content does not close the board'); + await tap(surface.getChildByPath('NoticeTabs/NoticeTab3'));assert(panel.modal!==null&&surface.getChildByPath('NoticeTabs').getComponent('NoticeTabsView').tabs[2].isChecked,'coordinate tab touch selects without closing'); + await tap(surface.getChildByName('通知关闭'));assert(panel.modal===null,'coordinate close-button touch closes the board'); + await mount();surface=panel.modal.getChildByName('group-12'); + await tap(surface.getChildByName('通知遮罩'),-700,0);assert(panel.modal===null,'coordinate backdrop touch closes the board'); + panel.close();return {checks}; + } finally {panel?.close();root.destroy();host.destroy();} +} diff --git a/cocoscreator_projects/scripts/verify-optional-animation-preview.mjs b/cocoscreator_projects/scripts/verify-optional-animation-preview.mjs new file mode 100644 index 0000000..1c96e1d --- /dev/null +++ b/cocoscreator_projects/scripts/verify-optional-animation-preview.mjs @@ -0,0 +1,114 @@ +/** Run in Cocos Game View, with the imported cc module and prefab UUIDs from asset-db. + * Uses real Animation states and isolated nodes; no wire messages or application settings. + */ +export async function verifyOptionalAnimationPreview(e, prefabIds) { + const checks=[]; + const assert=(ok,message)=>{if(!ok)throw Error(message);checks.push(message);}; + const load=uuid=>new Promise((resolve,reject)=>e.assetManager.loadAny(uuid,(err,asset)=>err?reject(err):resolve(asset))); + const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms)); + const root=new e.Node('OptionalAnimationVerification');root.parent=e.director.getScene(); + const disposable=[]; + const restoreStorage=[]; + try { + const menu=await load(prefabIds.menu),notice=await load(prefabIds.notice),settings=await load(prefabIds.settings); + const find=(node,name)=>{if(node.name===name)return node;for(const child of node.children){const found=find(child,name);if(found)return found;}return null;}; + const cases=[[menu,['Spirit479','Spirit3120','Spirit76','Spirit11','Spirit12','Spirit14','Spirit17','Spirit13','Spirit7']], + [notice,['Spirit3121','Spirit3017','Spirit5','Spirit235','Spirit604','Spirit3057','Spirit15']]]; + for(const [asset,names] of cases)for(const name of names){ + const node=find(asset.data,name); + assert(node?.getComponent(e.Animation)&&node.getComponent('OptionalUiAnimation'),`${asset.name}/${name} has authored animation support`); + } + for(const name of ['音乐底','音效底','Spirit357','Spirit388']){ + const node=find(settings.data,name); + assert(node?.getComponent(e.Animation)&&node.getComponent('OptionalSwitchAnimation'),`settings/${name} has authored switch animation support`); + } + const Switch=e.js.getClassByName('OptionalSwitchAnimation'); + const bareSwitch=new e.Node('BareSwitch');bareSwitch.addComponent(Switch);bareSwitch.parent=root; + assert(bareSwitch.getComponent(e.Animation)&&bareSwitch.getComponent(e.Sprite),'mounting switch support provides both required components'); + const horn=find(menu.data,'Spirit479'),hornClip=horn.getComponent(e.Animation).defaultClip; + assert(hornClip&&hornClip.wrapMode===e.AnimationClip.WrapMode.Loop,'horn owns a looping clip'); + const idle=e.instantiate(find(menu.data,'Spirit11'));idle.parent=root; + const idleSprite=idle.getComponent(e.Sprite),original=idleSprite.spriteFrame; + const idleAnimation=idle.getComponent(e.Animation); + await sleep(80); + assert(idleSprite.spriteFrame===original&&!idleAnimation.defaultClip,'unassigned clip preserves the authored sprite'); + idle.active=false;idleAnimation.defaultClip=hornClip;idle.active=true; + await sleep(100); + const idleState=idleAnimation.getState(hornClip.name); + assert(idleState.isPlaying&&idleState.time>0,'assigned clip starts on show and advances'); + idle.active=false; + assert(!idleState.isPlaying,'hiding stops the animation'); + const stoppedTime=idleState.time;await sleep(80); + assert(idleState.time===stoppedTime,'hidden animation does not advance'); + idle.active=true;await sleep(40); + assert(idleState.isPlaying&&idleState.time<0.3,'showing again restarts the clip'); + // Manual sampling verifies the authored frame schedule, independent of wall-clock jitter. + const sampleNode=e.instantiate(horn);sampleNode.parent=root; + const sampleAnimation=sampleNode.getComponent(e.Animation);sampleAnimation.stop(); + const hornState=sampleAnimation.getState(hornClip.name),sampleSprite=sampleNode.getComponent(e.Sprite); + const samples=[]; + for(const time of [0,0.49,0.5,0.99,1,1.99]){hornState.time=time;hornState.sample();samples.push(sampleSprite.spriteFrame);} + assert(samples[0]===samples[1]&&samples[0]!==samples[2]&&samples[2]===samples[3]&&samples[0]===samples[4]&&samples[0]===samples[5], + 'horn preserves the original two-second frame schedule'); + const onFrame=samples[0],offFrame=samples[2]; + const switchNode=e.instantiate(find(settings.data,'音乐底'));switchNode.parent=root; + const view=switchNode.getComponent(Switch),sprite=switchNode.getComponent(e.Sprite),animation=switchNode.getComponent(e.Animation); + view.setState(true,onFrame,false);view.setState(false,offFrame,true); + assert(sprite.spriteFrame===offFrame,'switch without clips changes immediately'); + // Intentionally author the wrong final frame and Loop: the state remains authoritative. + const onClip=e.AnimationClip.createWithSpriteFrames([offFrame,offFrame],20);onClip.name='TestSwitchOn';onClip.wrapMode=e.AnimationClip.WrapMode.Loop; + const offClip=e.AnimationClip.createWithSpriteFrames([onFrame,onFrame],20);offClip.name='TestSwitchOff'; + disposable.push(onClip,offClip);view.onClip=onClip;view.offClip=offClip; + view.setState(true,onFrame,false); + assert(sprite.spriteFrame===onFrame,'initial state does not play a transition'); + view.setState(false,offFrame,true);await sleep(35); + assert(animation.getState('TestSwitchOff').isPlaying,'off transition starts'); + view.setState(true,onFrame,true);await sleep(250); + assert(sprite.spriteFrame===onFrame&&!animation.getState('TestSwitchOn').isPlaying,'rapid reversal finishes at latest state and plays once'); + view.setState(false,offFrame,true);switchNode.active=false; + assert(sprite.spriteFrame===offFrame&&!animation.getState('TestSwitchOff').isPlaying,'hide cancels transition and restores latest state'); + switchNode.active=true;await sleep(60); + assert(sprite.spriteFrame===offFrame&&!animation.getState('TestSwitchOff').isPlaying,'reopen preserves state without replaying stale transition'); + view.offClip=null;view.setState(true,onFrame,true);view.setState(false,offFrame,true);await sleep(180); + assert(sprite.spriteFrame===offFrame,'missing destination clip cancels the old transition and keeps the new state'); + // Exercise the real settings click handlers against isolated storage keys. + const LobbyPanel=Array.from(System.entries()).map(([,m])=>m).find(m=>m.LobbyPanel)?.LobbyPanel; + assert(LobbyPanel,'LobbyPanel is loaded for settings integration'); + const panel=Object.create(LobbyPanel.prototype),tips=[]; + panel.assets={settings,switches:[onFrame,offFrame]};panel.config={identity:{gameid:987654321,agentid:765432109}}; + panel.context={launchMode:0};panel.model={canAct:true};panel.disposed=false;panel.modalReleases=[]; + panel.tip=message=>tips.push(message); + panel.open=()=>{panel.modal=e.instantiate(settings);panel.modal.parent=root;return panel.modal;}; + panel.close=()=>{for(const off of panel.modalReleases)off();panel.modalReleases=[];panel.modal.destroy();}; + const musicKey='tsgame_bgm_987654321',soundKey='tsgame_sound_987654321765432109'; + for(const key of [musicKey,soundKey]){ + const previous=localStorage.getItem(key);restoreStorage.push(()=>previous===null?localStorage.removeItem(key):localStorage.setItem(key,previous)); + localStorage.setItem(key,'1'); + } + panel.openSettings(); + let group=panel.modal.getChildByName('group-6'); + const music=group.getChildByName('音乐底'),sound=group.getChildByName('音效底'); + const musicView=music.getComponent(Switch);musicView.onClip=onClip;musicView.offClip=offClip; + assert(music.getComponent(e.Sprite).spriteFrame===onFrame&&sound.getComponent(e.Sprite).spriteFrame===onFrame,'settings initialize from persisted state'); + music.emit(e.Button.EventType.CLICK); + assert(localStorage.getItem(musicKey)==='0'&&music.getComponent(e.Animation).getState('TestSwitchOff').isPlaying,'music click persists state and starts configured transition'); + music.emit(e.Button.EventType.CLICK);await sleep(200); + assert(localStorage.getItem(musicKey)==='1'&&music.getComponent(e.Sprite).spriteFrame===onFrame,'rapid music clicks retain the latest persisted state'); + sound.emit(e.Button.EventType.CLICK); + assert(localStorage.getItem(soundKey)==='0'&&sound.getComponent(e.Sprite).spriteFrame===offFrame,'sound click without a clip keeps immediate switching'); + for(const name of ['Spirit357','Spirit388']){ + const node=group.getChildByName(name),frame=node.getComponent(e.Sprite).spriteFrame; + node.emit(e.Button.EventType.CLICK); + assert(node.getComponent(e.Sprite).spriteFrame===frame&&tips.at(-1)==='原生震动与语音开关尚未接入',`${name} does not invent an unsupported switch state`); + } + assert(tips.length===2,'settings click handlers produce no unexpected error tips'); + panel.close();panel.openSettings();group=panel.modal.getChildByName('group-6'); + assert(group.getChildByName('音乐底').getComponent(e.Sprite).spriteFrame===onFrame&&group.getChildByName('音效底').getComponent(e.Sprite).spriteFrame===offFrame, + 'reopening settings restores the latest music and sound states'); + panel.close(); + return {checks}; + } finally { + for(const restore of restoreStorage)restore(); + root.destroy();await sleep(50);for(const clip of disposable)clip.destroy(); + } +} diff --git a/cocoscreator_projects/scripts/verify-owned-vip-preview.mjs b/cocoscreator_projects/scripts/verify-owned-vip-preview.mjs new file mode 100644 index 0000000..e523cfe --- /dev/null +++ b/cocoscreator_projects/scripts/verify-owned-vip-preview.mjs @@ -0,0 +1,61 @@ +/** Cocos Game View verification: mocked wire, no real ranking/card requests. */ +export async function verifyOwnedVipPreview(){ + const e=await System.import('cc'),modules=Array.from(System.entries()).map(([,m])=>m); + const exported=name=>{const m=modules.find(m=>m[name]);if(!m)throw Error(`Missing ${name}`);return m[name];}; + const source=e.director.getScene().getChildByName('PlatformServices').getComponent('PlatformStartup'); + const config=source.config,assets=source.getComponent('LobbyAssets'); + source.unsubscribe?.();source.runtime?.stop();source.panel?.dispose();source.panel=null; + await new Promise((resolve,reject)=>e.director.loadScene('PlatformLobby',err=>err?reject(err):resolve())); + const sent=[],tips=[],checks=[];let receive,panel; + const assert=(ok,msg)=>{if(!ok)throw Error(msg);checks.push(msg);}; + const runtime=new (exported('PlatformRuntime'))({ + gameEntry:exported('createLoginStageGameEntry')(config.identity.gameid,()=>{}),resolveRuntimeConfig:async()=>config, + createWireClient:()=>({subscribe:cb=>{receive=cb;return ()=>{};},start:()=>{},stop:()=>{},send:p=>sent.push(p),switchServer:()=>{},reconnectCurrent:()=>{}}), + scene:{showLoading:()=>{},showLogin:()=>{},showLobby:()=>{},showRoom:()=>{},showReconnect:()=>{},showKicked:()=>{},showServerDenial:()=>{},showFatal:err=>{throw err;}, + showTips:(msg,time,kind)=>{if(kind==='show_message')panel?.cancelRankRequest();tips.push(msg);}}, + loadResources:async()=>{},waitForMinimumDisplay:async()=>{},getLoginDeviceSnapshot:()=>{throw Error('Fixture only');}, + }); + await runtime.start();receive({type:'open',server:config.servers[0]}); + const packet=(rpc,data)=>receive({type:'message',message:{route:'agent',rpc,data}}); + packet('player_login',{state:0,playerid:7,nickname:'接线验证',avatar:'',sex:0,bean:0,roomcard:3,taskstate:0,score:0,sign:null,ip:'127.0.0.1',openid:'test',unionid:'test',invitecode:null}); + const canvas=e.director.getScene().getChildByName('Canvas'),popup=new e.Node('OwnedVipTestPopup');popup.parent=canvas;popup.layer=canvas.layer;popup.addComponent(e.UITransform).setContentSize(1600,720); + const remote=exported('createRemoteConfig')({gameNoticeShow:-1,rankList:2,cdKey:2,vipRankLimit:3},config.identity); + panel=new (exported('LobbyPanel'))(canvas,popup,assets,config,exported('resolveLobbyPolicy')(remote),runtime.state,msg=>tips.push(msg),()=>{},()=>null,()=>{}, + exported('resolveLobbyMenuConfig')(remote),{versionState:0,launchMode:0},()=>{},()=>{},()=>{},undefined,undefined, + {rank:()=>runtime.getTreasureList(),redeem:card=>runtime.redeemCard(card),paste:async()=>'00Ab-12',loading:()=>{},vipRank:limit=>runtime.getVipRankList(limit),joinOwned:code=>runtime.joinOwnedRoom(code,{city:'test'})}); + popup.setSiblingIndex(canvas.children.length-1); + const off=runtime.subscribeState(state=>panel.render(state),err=>{throw err;}); + const click=id=>panel.menuNodes.get(id).emit(e.Button.EventType.CLICK); + + const key='tsgame_roomlist_'+config.identity.gameid+config.identity.agentid,previous=localStorage.getItem(key); + try{ + const before=tips.length;for(const id of [10,286,287,3161,508,509,496,3162])click(id); + assert(tips.length===before&&sent.length===0&&panel.modal===null,'retired payment entries neither notify nor send'); + localStorage.setItem(key,JSON.stringify({data:[100001,100002,100003,100004,100005,100006]})); + click(99);assert(panel.modalKind==='my-rooms','Spirit99 opens authored owned room panel'); + let group=panel.modal.getChildByName('group-98');const rows=panel.modal.getComponent('OwnerRoomListView').content.children; + assert(rows.length===6,'owned rooms show all cached entries'); + assert(rows[0].getChildByName('Digits').children.length===6,'owned room uses six original digit sprites'); + rows[1].emit(e.Button.EventType.CLICK);assert(panel.modal===null,'owned room click closes list'); + const join=sent.at(-1);assert(join.rpc==='self_join_room'&&join.data.roomcode===100002&&join.data.joinmode===1,'owned room click sends original join mode'); + assert(join.data.location.city==='test'&&join.data.ip==='127.0.0.1','owned room join retains native location and login IP'); + localStorage.setItem(key,JSON.stringify({data:[]}));click(99);assert(panel.modalKind==='my-rooms','empty owned room list still opens'); + panel.modal.getChildByPath('group-98/Spirit103').emit(e.Button.EventType.CLICK);assert(panel.modal===null,'owned room shade closes'); + const count=sent.length;click(3232);click(3232); + assert(sent.length===count+1&&sent.at(-1).rpc==='getVipRankList','VIP rank suppresses duplicate request'); + assert(JSON.stringify(sent.at(-1).data)===JSON.stringify({agentid:config.identity.agentid,limit:3}),'VIP rank uses configured limit and exact fields'); + packet('get_treasurelist',{list:[]});assert(panel.rankPending==='vip-rank','wealth response cannot consume pending VIP request'); + const list=[{name:'VIP一',headimg:'',playerid:1,star:88,signature:'签名一',destwo:'附注',notice:'公告一'},{name:'VIP二',headimg:'',playerid:2,star:66,signature:'签名二',notice:'公告二'}]; + packet('getVipRankList',{list});assert(panel.modalKind==='vip-rank','VIP response opens shared rank view'); + group=panel.modal.getChildByName('group-84');assert(group.getChildByName('Spirit3234').getComponent(e.Sprite).spriteFrame===assets.vipRankTitle,'VIP title uses original frame'); + assert(group.getChildByName('Spirit640').getComponent(e.Label).string==='公告:\n(附注)\n公告一','VIP detail renders notice and supplemental text'); + group.getComponentInChildren(e.ScrollView).content.children[1].emit(e.Button.EventType.CLICK); + assert(group.getChildByName('Spirit640').getComponent(e.Label).string==='公告:\n公告二','VIP row selection updates announcement'); + panel.close();click(3232);packet('getVipRankList',{});assert(panel.modal===null&&!panel.rankPending,'missing VIP list completes silently like legacy'); + click(3232);packet('getVipRankList',{list:[]});assert(panel.modal===null&&!panel.rankPending,'empty VIP list stays closed'); + click(3232);packet('getVipRankList',{list}); + await new Promise(resolve=>e.director.once(e.Director.EVENT_AFTER_DRAW,resolve)); + globalThis.__ownedVipVerification={panel,runtime,checks,close:()=>{off();panel.dispose();runtime.stop();popup.destroy();}}; + return {checks,packets:sent.map(p=>({rpc:p.rpc,fields:Object.keys(p.data)}))}; + }finally{if(previous===null)localStorage.removeItem(key);else localStorage.setItem(key,previous);} +} diff --git a/cocoscreator_projects/scripts/verify-owner-room-preview.mjs b/cocoscreator_projects/scripts/verify-owner-room-preview.mjs new file mode 100644 index 0000000..2d983d2 --- /dev/null +++ b/cocoscreator_projects/scripts/verify-owner-room-preview.mjs @@ -0,0 +1,47 @@ +/** Real Cocos layout and coordinate-input checks. Only a dedicated local fixture history is written. */ +export async function verifyOwnerRoomPreview(e,listId){ + const checks=[],assert=(v,m)=>{if(!v)throw Error(m);checks.push(m);},sleep=ms=>new Promise(r=>setTimeout(r,ms)); + const load=id=>new Promise((r,j)=>e.assetManager.loadAny(id,(err,a)=>err?j(err):r(a))); + const source=await load('1c10c2ee-adc2-496f-9005-d3e0e6ebbb56'),refs=source.scene.getComponentInChildren('LobbyAssets'); + const asset=await load(listId),canvas=e.director.getScene().getComponentInChildren(e.Canvas); + assert(refs.ownedRoomList.uuid===listId,'startup points at independent room list'); + assert(!refs.qrCode.data.getChildByName('group-98')&&refs.qrCode.data.getChildByName('group-42'),'room and QR groups use separate prefabs'); + const view=asset.data.getComponent('OwnerRoomListView'); + assert(asset.data.children.length===1&&asset.data.children[0].name==='group-98','room prefab contains only group-98'); + assert(view.itemPrefab.data.name==='OwnerRoomItem','list references the requested OwnerRoomItem prefab'); + const LobbyPanel=Array.from(System.entries()).map(([,m])=>m).find(m=>m.LobbyPanel).LobbyPanel,panel=Object.create(LobbyPanel.prototype),joined=[]; + panel.assets={ownedRoomList:asset,ownedRoomDigits:refs.ownedRoomDigits};panel.model={canAct:true};panel.config={identity:{gameid:'__owner_preview_',agentid:'local'}}; + panel.disposed=false;panel.modal=null;panel.modalReleases=[];panel.extras={joinOwned:code=>joined.push(code)}; + panel.close=()=>{for(const off of panel.modalReleases.splice(0))off();panel.modal?.destroy();panel.modal=null;}; + panel.open=(prefab,kind)=>{panel.close();const n=e.instantiate(prefab);n.parent=canvas.node;n.setPosition(0,0,0);const w=n.getComponent(e.Widget);w.target=canvas.node;w.updateAlignment();panel.modal=n;panel.modalKind=kind;return n;}; + const key='tsgame_roomlist___owner_preview_local',previous=localStorage.getItem(key),codes=Array.from({length:9},(_,i)=>100001+i); + let id=200; + const send=(touch,type)=>e.input._dispatchEventTouch(new e.EventTouch([touch],false,type,[touch])); + const touchAt=(node,dx=0,dy=0)=>{const p=node.worldPosition.clone();p.x+=dx;p.y+=dy;const s=canvas.cameraComponent.worldToScreen(p);return new e.Touch(s.x,s.y,id++);}; + const tap=async(node,dx=0,dy=0)=>{const t=touchAt(node,dx,dy);send(t,e.Input.EventType.TOUCH_START);send(t,e.Input.EventType.TOUCH_END);await sleep(30);}; + try{ + localStorage.setItem(key,JSON.stringify({data:codes}));panel.openOwnedRooms();await sleep(100); + let v=panel.modal.getComponent('OwnerRoomListView'),rows=v.content.children; + assert(rows.length===9,'more than five cached records are displayed'); + assert(v.scroll.getMaxScrollOffset().y>0,'long room list is scrollable'); + assert(rows.every((n,i)=>n.getChildByName('Digits').children.map(d=>refs.ownedRoomDigits.indexOf(d.getComponent(e.Sprite).spriteFrame)).join('')===String(codes[i])),'all room numbers retain digit artwork and ordering'); + const layout=v.content.getComponent(e.Layout); + assert(rows.slice(1).every((n,i)=>Math.abs(rows[i].position.y-n.position.y-80-layout.spacingY)<.1),'room spacing comes from Layout'); + await tap(panel.modal.getChildByPath('group-98/房间列表背景'),0,240);assert(panel.modal!==null,'panel header blank area does not close'); + const t=touchAt(rows[1]);send(t,e.Input.EventType.TOUCH_START);const p=t.getLocation(); + t.setTouchInfo(t.getID(),p.x,p.y+120);send(t,e.Input.EventType.TOUCH_MOVE);send(t,e.Input.EventType.TOUCH_END);await sleep(80); + assert(panel.modal!==null&&joined.length===0,'dragging a row never joins or closes'); + assert(v.scroll.getScrollOffset().y>0,'coordinate drag moves scroll content'); + v.scroll.stopAutoScroll();v.scroll.scrollToBottom(0);await sleep(60); + await tap(rows.at(-1));assert(joined.at(-1)===codes.at(-1)&&panel.modal===null,'last scrolled row joins its own room and closes'); + panel.openOwnedRooms();await sleep(60);v=panel.modal.getComponent('OwnerRoomListView'); + assert(Math.abs(v.scroll.getScrollOffset().y)<.1,'reopening starts at top'); + v.render([],refs.ownedRoomDigits);await sleep(40);assert(v.content.children.length===0,'empty update removes all rows'); + assert(v.content.getComponent(e.UITransform).height===0,'empty update removes stale scroll height'); + v.render(codes,refs.ownedRoomDigits);await sleep(40);assert(v.content.children.length===9&&v.scroll.getMaxScrollOffset().y>0,'refilling empty list restores layout and scrolling'); + panel.close();localStorage.setItem(key,JSON.stringify({data:[]}));panel.openOwnedRooms();await sleep(60); + assert(panel.modal.getComponent('OwnerRoomListView').content.children.length===0,'empty history opens an empty list'); + await tap(panel.modal.getChildByPath('group-98/Spirit103'),600,0);assert(panel.modal===null,'outside shade closes room list'); + return {checks}; + }finally{panel.close();if(previous===null)localStorage.removeItem(key);else localStorage.setItem(key,previous);} +} diff --git a/cocoscreator_projects/scripts/verify-profile-preview.mjs b/cocoscreator_projects/scripts/verify-profile-preview.mjs new file mode 100644 index 0000000..bbc8072 --- /dev/null +++ b/cocoscreator_projects/scripts/verify-profile-preview.mjs @@ -0,0 +1,72 @@ +/** Execute in Cocos Game View. All input fixtures stay local; no server submission. */ +export async function verifyProfilePreview(e,profileId) { + const checks=[],assert=(ok,message)=>{if(!ok)throw Error(message);checks.push(message);}; + const load=id=>new Promise((resolve,reject)=>e.assetManager.loadAny(id,(err,a)=>err?reject(err):resolve(a))); + const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms)); + const asset=await load(profileId),root=e.instantiate(asset);root.parent=e.director.getScene(); + const host=new e.Node('ProfileFlowVerification');host.parent=e.director.getScene(); + try { + const view=root.getComponent('ProfileInfoListView'); + assert(view,'profile prefab owns ProfileInfoListView'); + assert(view.textItemPrefab&&view.authPrefab,'profile owns text-item and auth prefab references'); + view.render(['昵称','ID:42','IP:127.0.0.1','积分:0','星星:10','城市','个性签名:'],'短签名'); + await sleep(80); + const content=view.content,layout=content.getComponent(e.Layout); + const checkSpacing=()=>{ + const children=content.children.filter(n=>n.active); + for(let i=1;in.active).length===4,'removed fields leave no empty rows'); + assert(content.children[0].getComponent(e.UITransform).height>30,'long nickname wraps and increases its row height'); + assert(view.signatureBox.height>=view.signature.node.getComponent(e.UITransform).height+20,'signature background grows with its text'); + assert(content.getComponent(e.UITransform).height>initialHeight&&view.scroll.getMaxScrollOffset().y>0,'long content becomes scrollable'); + view.scroll.scrollToBottom(0);await sleep(30); + view.render(['昵称','个性签名:'],'短签名');await sleep(60);checkSpacing(); + assert(content.getComponent(e.UITransform).heightm).find(m=>m.LobbyPanel)?.LobbyPanel; + assert(LobbyPanel,'LobbyPanel is loaded'); + const panel=Object.create(LobbyPanel.prototype),tips=[]; + panel.assets={profile:asset};panel.disposed=false;panel.modal=null;panel.modalReleases=[]; + panel.policy={starName:'星星'};panel.model={nickname:'资料接线测试',playerid:42,ip:'127.0.0.1',score:0,bean:10,sign:'签名测试',canAct:true}; + panel.avatarFrame=asset.data.getChildByPath('group-18/Spirit118').getComponent(e.Sprite).spriteFrame; + panel.location=()=>null;panel.tip=message=>tips.push(message); + panel.close=()=>{for(const off of panel.modalReleases.splice(0))off();panel.modal?.destroy();panel.modal=null;panel.modalKind='';}; + panel.open=(prefab,kind)=>{panel.close();panel.modal=e.instantiate(prefab);panel.modal.parent=host;panel.modalKind=kind;return panel.modal;}; + panel.openProfile(); + const profile=panel.modal,profileView=profile.getComponent('ProfileInfoListView'); + assert(profileView.content.children[0].getComponent(e.Label).string==='资料接线测试'&&profileView.signature.string==='签名测试','profile renders data through the new hierarchy'); + const auth=()=>{ + panel.modal.getChildByPath('group-18/Spirit484').emit(e.Button.EventType.CLICK); + assert(panel.modal?.name==='Layer18_Auth'&&panel.modalKind==='auth','real-name button opens the auth prefab'); + const group=panel.modal.getChildByName('group-57'); + return {group,name:group.getChildByName('NameInput').getComponent(e.EditBox),id:group.getChildByName('IdInput').getComponent(e.EditBox),submit:group.getChildByName('Spirit493')}; + }; + let inputs=auth();await sleep(40); + assert(!e.isValid(profile),'opening auth closes the profile'); + assert(inputs.name.string===''&&inputs.id.string==='','auth opens with empty inputs'); + assert(inputs.id.inputMode===e.EditBox.InputMode.SINGLE_LINE&&inputs.id.inputFlag===e.EditBox.InputFlag.DEFAULT,'ID input allows the X checksum character'); + for(const edit of [inputs.name,inputs.id])for(const label of [edit.textLabel,edit.placeholderLabel]){ + const ui=label.node.getComponent(e.UITransform); + assert(Math.abs(label.node.position.y+(0.5-ui.anchorY)*ui.height)<0.1&&label.verticalAlign===e.Label.VerticalAlign.CENTER,'input text stays vertically centered inside its hit area'); + } + inputs.name.string='张';inputs.id.string='11010519491231002X';inputs.submit.emit(e.Button.EventType.CLICK); + assert(tips.at(-1)==='姓名不正确!'&&panel.modal!==null,'invalid name shows source tip and keeps auth open'); + inputs.name.string='测试';inputs.id.string='110105194912310020';inputs.submit.emit(e.Button.EventType.CLICK); + assert(tips.at(-1)==='身份证信息不正确!'&&panel.modal!==null,'invalid ID shows source tip and keeps auth open'); + inputs.id.string='11010519491231002X';inputs.submit.emit(e.Button.EventType.CLICK); + assert(tips.at(-1)==='认证成功!'&&panel.modal===null,'valid local input closes auth and shows source success tip'); + panel.openProfile();inputs=auth(); + assert(inputs.name.string===''&&inputs.id.string==='','reopening does not retain identity inputs'); + inputs.group.getChildByName('Spirit485').emit(e.Button.EventType.CLICK); + assert(panel.modal===null,'auth backdrop closes the panel'); + assert(tips.length===3,'flow produced only the three expected validation tips'); + return {checks}; + } finally {root.destroy();host.destroy();await sleep(50);} +} diff --git a/cocoscreator_projects/scripts/verify-rank-card-preview.mjs b/cocoscreator_projects/scripts/verify-rank-card-preview.mjs new file mode 100644 index 0000000..86f095a --- /dev/null +++ b/cocoscreator_projects/scripts/verify-rank-card-preview.mjs @@ -0,0 +1,64 @@ +/** Cocos Game View verification: mocked wire, no real ranking/card requests. */ +export async function verifyRankCardPreview(){ + const e=await System.import('cc'),modules=Array.from(System.entries()).map(([,m])=>m); + const exported=name=>{const m=modules.find(m=>m[name]);if(!m)throw Error(`Missing ${name}`);return m[name];}; + const source=e.director.getScene().getChildByName('PlatformServices').getComponent('PlatformStartup'); + const config=source.config,assets=source.getComponent('LobbyAssets'); + source.unsubscribe?.();source.runtime?.stop();source.panel?.dispose();source.panel=null; + await new Promise((resolve,reject)=>e.director.loadScene('PlatformLobby',err=>err?reject(err):resolve())); + const sent=[],tips=[],checks=[];let receive,panel; + const assert=(ok,msg)=>{if(!ok)throw Error(msg);checks.push(msg);}; + const runtime=new (exported('PlatformRuntime'))({ + gameEntry:exported('createLoginStageGameEntry')(config.identity.gameid,()=>{}),resolveRuntimeConfig:async()=>config, + createWireClient:()=>({subscribe:cb=>{receive=cb;return ()=>{};},start:()=>{},stop:()=>{},send:p=>sent.push(p),switchServer:()=>{},reconnectCurrent:()=>{}}), + scene:{showLoading:()=>{},showLogin:()=>{},showLobby:()=>{},showRoom:()=>{},showReconnect:()=>{},showKicked:()=>{},showServerDenial:()=>{},showFatal:err=>{throw err;}, + showTips:(msg,time,kind)=>{if(kind==='show_message')panel?.cancelRankRequest();tips.push(msg);}}, + loadResources:async()=>{},waitForMinimumDisplay:async()=>{},getLoginDeviceSnapshot:()=>{throw Error('Fixture only');}, + }); + await runtime.start();receive({type:'open',server:config.servers[0]}); + const packet=(rpc,data)=>receive({type:'message',message:{route:'agent',rpc,data}}); + packet('player_login',{state:0,playerid:7,nickname:'接线验证',avatar:'',sex:0,bean:0,roomcard:3,taskstate:0,score:0,sign:null,ip:'127.0.0.1',openid:'test',unionid:'test',invitecode:null}); + const canvas=e.director.getScene().getChildByName('Canvas'),popup=new e.Node('RankCardTestPopup');popup.parent=canvas;popup.layer=canvas.layer;popup.addComponent(e.UITransform).setContentSize(1600,720); + const remote=exported('createRemoteConfig')({gameNoticeShow:-1,rankList:2,cdKey:2},config.identity); + panel=new (exported('LobbyPanel'))(canvas,popup,assets,config,exported('resolveLobbyPolicy')(remote),runtime.state,msg=>tips.push(msg),()=>{},()=>null,()=>{}, + exported('resolveLobbyMenuConfig')(remote),{versionState:0,launchMode:0},()=>{},()=>{},()=>{},undefined,undefined, + {rank:()=>runtime.getTreasureList(),redeem:card=>runtime.redeemCard(card),paste:async()=>'00Ab-12'}); + popup.setSiblingIndex(canvas.children.length-1); + const off=runtime.subscribeState(state=>panel.render(state),err=>{throw err;}); + const click=id=>panel.menuNodes.get(id).emit(e.Button.EventType.CLICK); + click(3233);click(3233);assert(sent.length===1&&sent[0].rpc==='get_treasurelist','rank query suppresses duplicate clicks'); + assert(panel.modal===null,'rank waits for server'); + const list=Array.from({length:8},(_,i)=>({name:`测试${i+1}`,headimg:'',playerid:i+1,star:100-i,signature:`签名${i+1}`})); + packet('get_treasurelist',{list});assert(panel.modalKind==='rank','server opens authored rank prefab'); + let group=panel.modal.getChildByName('group-84'),rows=group.getComponentInChildren(e.ScrollView).content; + assert(rows.children.length===8,'all rank rows rendered'); + assert(rows.children.every(n=>n._prefab?.asset===assets.rankItem),'all rank rows instantiate VipRankItem prefab'); + assert(rows.children.every(n=>n.getChildByPath('Spirit630/Spirit631')),'prefab avatar hierarchy is preserved'); + const authoredLayout=rows.getComponent(e.Layout);assert(authoredLayout&&rows.getComponent(e.UITransform).height>511,'authored layout grows scroll content'); + const viewport=group.getComponentInChildren(e.ScrollView);viewport.scrollToBottom(0);assert(viewport.getScrollOffset().y>0,'authored ScrollView reaches lower rows');viewport.scrollToTop(0); + assert(group.getComponentInChildren(e.ScrollView).content===rows,'native scroll content is bound'); + rows.children[1].emit(e.Button.EventType.CLICK); + assert(group.getChildByName('Spirit637').getComponent(e.Label).string==='ID:2','row selection updates detail ID'); + assert(group.getChildByName('Spirit641').getComponent(e.Label).string==='个性签名:\n签名2','row selection updates signature'); + group.getChildByName('Spirit605').emit(e.Button.EventType.CLICK);assert(panel.modal===null,'rank shade closes'); + click(3233);packet('get_treasurelist',{list:[]});assert(panel.modal===null,'empty rank does not open'); + click(3233);packet('show_message',{msg:'暂不可用',time:0});assert(!panel.rankPending,'denial releases rank request'); + click(3264);assert(panel.modalKind==='cdkey','card entry opens original feedback group'); + group=panel.modal.getChildByName('group-74');const edit=group.getChildByName('CardInput').getComponent(e.EditBox); + assert(edit.textLabel&&edit.maxLength===-1,'native card input is bound'); + const anchor=edit.textLabel.node.getComponent(e.UITransform).anchorPoint;assert(anchor.x===0&&anchor.y===1,'card label uses native input anchor'); + const count=sent.length;edit.string=' ';group.getChildByName('Spirit3266').emit(e.Button.EventType.CLICK);assert(sent.length===count,'blank card is not sent'); + group.getChildByName('Spirit3265').emit(e.Button.EventType.CLICK);await Promise.resolve();await Promise.resolve(); + assert(edit.string==='00Ab-12','paste preserves original card text'); + group.getChildByName('Spirit3266').emit(e.Button.EventType.CLICK);assert(panel.modal===null,'redeem closes original dialog'); + assert(sent.at(-1).rpc==='topup_card'&&sent.at(-1).data.cardno==='00Ab-12','redemption sends exact card'); + packet('topup_card',{});packet('update_bean',{bean:200});assert(panel.model.bean===200,'server bean update reaches lobby'); + const sentBeforeConfig=sent.length; + const cfgRemote=exported('createRemoteConfig')({rankList:1,rankList1:list,nameImgFrame_2:2},config.identity); + panel.config={...config,remoteConfig:cfgRemote};panel.menuConfig=exported('resolveLobbyMenuConfig')(cfgRemote); + click(3233);assert(panel.modalKind==='rank'&&sent.length===sentBeforeConfig,'config rank opens without server request'); + assert(panel.modal.getChildByPath('group-84/Spirit639').getComponent(e.Sprite).spriteFrame===assets.rankSymbols[1],'configured currency symbol is used'); + await new Promise(resolve=>e.director.once(e.Director.EVENT_AFTER_DRAW,resolve)); + globalThis.__rankCardVerification={panel,runtime,checks,close:()=>{off();panel.dispose();runtime.stop();popup.destroy();}}; + return {checks,packets:sent.map(p=>({rpc:p.rpc,fields:Object.keys(p.data)}))}; +} diff --git a/cocoscreator_projects/scripts/verify-room-entry-preview.mjs b/cocoscreator_projects/scripts/verify-room-entry-preview.mjs new file mode 100644 index 0000000..73e1ecf --- /dev/null +++ b/cocoscreator_projects/scripts/verify-room-entry-preview.mjs @@ -0,0 +1,96 @@ +/** Run in Cocos Game View. The actual socket is stopped before all local fixtures. */ +export async function verifyRoomEntryPreview(e,loginFixture){ + const checks=[],assert=(ok,text)=>{if(!ok)throw Error(text);checks.push(text);}; + const sleep=ms=>new Promise(r=>setTimeout(r,ms)); + const load=id=>new Promise((r,j)=>e.assetManager.loadAny(id,(err,a)=>err?j(err):r(a))); + const exported=name=>Array.from(System.entries()).map(([,m])=>m).find(m=>m[name])?.[name]; + const startup=e.director.getScene().getComponentInChildren('PlatformStartup'); + assert(startup,'persistent Startup loaded');startup.unsubscribe?.();startup.unsubscribe=null;startup.runtime.stop();startup.panel?.dispose();startup.panel=null; + const canvas=e.director.getScene().getChildByName('Canvas'); + let node=null;const tips=[],sent=[];let cancel=0; + const mount=async id=>{if(node){node.destroy();await sleep(50);}node=e.instantiate(await load(id));node.parent=canvas;node.setPosition(0,0,0);const w=node.getComponent(e.Widget);if(w){w.target=canvas;w.updateAlignment();}await sleep(100);return node;}; + let pointer=30; + const click=async n=>{const camera=canvas.getComponent(e.Canvas).cameraComponent,p=camera.worldToScreen(n.worldPosition);const touch=new e.Touch(p.x,p.y,pointer++); + e.input._dispatchEventTouch(new e.EventTouch([touch],false,e.Input.EventType.TOUCH_START,[touch])); + e.input._dispatchEventTouch(new e.EventTouch([touch],false,e.Input.EventType.TOUCH_END,[touch]));await sleep(25);}; + const keys=['tsgame_roomtype_','tsgame_shortcode_'].map(k=>k+startup.config.identity.gameid+startup.config.identity.agentid); + const old=keys.map(k=>localStorage.getItem(k)); + let runtime=null; + try{ + await mount('98e42432-0731-4b9e-a158-72032b2c157d');const join=node.getComponent('JoinRoomView'); + assert(join&&join.slots.every(s=>!!s.font),'join prefab has wired BitmapFont slots'); + join.open({initialCode:'',submit:n=>{sent.push(n);join.setBusy(true);},cancel:()=>cancel++,paste:async()=> '房号123-456789',reportError:t=>tips.push(t)}); + for(const d of [0,1,2,3,4,5])await click(join.digitButtons[d].node); + assert(sent.length===1&&sent[0]===12345,'coordinate keypad submits numeric code on sixth digit'); + assert(join.slots.map(s=>s.string).join('')==='012345','join slots render all six digits including leading zero'); + await click(join.submitButton.node);assert(sent.length===1,'busy submit does not duplicate request'); + await click(join.cancelButtons[1].node);assert(cancel===1,'close callback works while busy'); + join.setBusy(false);await click(join.deleteButton.node);assert(join.slots.filter(s=>s.node.active).length===5,'delete hides final slot'); + await click(join.clearButton.node);assert(join.slots.every(s=>!s.node.active),'clear hides all slots'); + await click(join.pasteButton.node);assert(sent[1]===123456,'clipboard strips separators and truncates to six digits'); + let finishPaste;const late=[]; + join.open({initialCode:'',submit:n=>late.push(n),cancel(){},paste:()=>new Promise(r=>finishPaste=r),reportError:t=>tips.push(t)}); + await click(join.pasteButton.node); + const panel=Object.create(exported('LobbyPanel').prototype);panel.roomPage=join;panel.modal=node;panel.modalKind='join';panel.modalReleases=[]; + panel.cancelRankRequest=panel.cancelInviteRequest=panel.cancelTaskRequest=()=>{}; + panel.cancelRoomPage();node=null;finishPaste('123456');await sleep(40); + assert(late.length===0&&panel.roomPage===null,'disconnect closes room page and invalidates deferred clipboard auto-submit'); + await mount('584dbb85-4acc-4bea-9d9b-e0cbeb8741da');const create=node.getComponent('CreateRoomPage'); + assert(create&&create.constructor.name==='TemplateCreateRoomView','platform resolves subgame through base page component'); + const packets=[];create.open({previousRoomtype:null,submit:r=>{packets.push(r);create.setBusy(true);},cancel:()=>cancel++,reportError:t=>tips.push(t)}); + assert(create.options[0].isChecked&&create.options[2].isChecked,'template initial options match source'); + await click(create.options[1].node);await click(create.options[3].node); + assert(!create.options[0].isChecked&&create.options[1].isChecked&&!create.options[2].isChecked&&create.options[3].isChecked,'Cocos ToggleContainer enforces each option group'); + await click(create.options[1].node);assert(create.options[1].isChecked,'clicking the selected option keeps it selected like the source'); + await click(create.submitButton.node);await click(create.submitButton.node); + const roomtype=[1,4,1,2,2,[1,1,[1,2000,10],null,null,1],[1,0,5]]; + assert(packets.length===1&&JSON.stringify(packets[0])===JSON.stringify(roomtype),'create page submits source fixed array once'); + create.close();create.open({previousRoomtype:roomtype,submit:()=>{},cancel:()=>{},reportError:t=>tips.push(t)}); + assert(!create.options[2].isChecked&&!create.options[3].isChecked,'saved template index 4 does not invent a matching option'); + node.destroy();node=null;await sleep(50); + // Exercise the real runtime + GameEntry + Startup scene callback with a local wire. + const game=exported('createTemplateGame')(startup.config.identity.gameid),listeners=new Set(),wire={sent:[],subscribe:fn=>{listeners.add(fn);return()=>listeners.delete(fn);},start(){},stop(){},send(p){this.sent.push(p);},switchServer(){},reconnectCurrent(){},emit(ev){for(const fn of [...listeners])fn(ev);}}; + startup.game=game;startup.loginResponse=null;let roomPromise=Promise.resolve(),lobbies=0;const fatal=[]; + runtime=new (exported('PlatformRuntime'))({gameEntry:game.entry,resolveRuntimeConfig:async()=>startup.config,createWireClient:()=>wire, + scene:{showLoading(){},showLogin(){},showLobby(){lobbies++;},showRoom(){roomPromise=startup.showRoom();},showReconnect(){startup.endRoomRequest();},showTips:t=>tips.push(t),showServerDenial(){startup.endRoomRequest();},showKicked(){},showFatal:error=>fatal.push(String(error))}, + loadResources:async()=>{},waitForMinimumDisplay:async()=>{},getLoginDeviceSnapshot:()=>({ip:'127.0.0.1',location:null,machineid:'preview',machineroom:'',deviceLogin:{enabled:false},loginPlayerId:{enabled:false}})}); + startup.runtime=runtime;await runtime.start();wire.emit({type:'open'}); + runtime.login({openid:'o1',unionid:'test_unionid',nickname:'测试号',avatar:'',sex:0,province:'江西',city:'南昌'}); + wire.emit({type:'message',message:{...loginFixture,data:{...loginFixture.data,avatar:''}}}); + assert(runtime.state.room.kind==='outside'&&lobbies===1,'local login commits lobby state'); + const originalTimer=window.setTimeout;let expire; + try{window.setTimeout=(fn,ms,...args)=>ms===15000?(expire=fn,999999):originalTimer(fn,ms,...args); + startup.requestRoom('create_room',()=>{}); + }finally{window.setTimeout=originalTimer;} + const originalTip=startup.tip;startup.tip=t=>tips.push(t);expire();startup.tip=originalTip; + assert(startup.roomPending===null&&tips.pop()==='房间请求超时,请重试','room timeout releases guard and reports retry'); + startup.requestRoom('create_room',()=>runtime.createRoom({roomtype,ip:'127.0.0.1',location:null})); + startup.requestRoom('create_room',()=>{throw Error('duplicate request invoked');}); + assert(wire.sent.filter(p=>p.rpc==='create_room').length===1,'Startup suppresses duplicate room requests'); + wire.emit({type:'message',message:{route:'agent',rpc:'create_room',data:{state:7,showerror:1,error:'测试拒绝'}}}); + assert(runtime.state.room.kind==='outside'&&startup.roomPending===null,'denial releases wait and keeps lobby'); + startup.requestRoom('create_room',()=>runtime.createRoom({roomtype,ip:'127.0.0.1',location:null})); + const created={state:0,roomcode:'654321',seat:0,roomtype,makewar:0,asetcount:10};startup.roomReceipt={rpc:'create_room',data:created}; + wire.emit({type:'message',message:{route:'agent',rpc:'create_room',data:created}});await roomPromise;await sleep(100); + assert(fatal.length===0&&runtime.state.room.kind==='inside'&&e.director.getScene().name==='TemplateRoom','create success crosses real Startup scene load'); + const roomRoot=e.director.getScene().getChildByName('Canvas').getChildByName('TemplateRoom'),roomView=roomRoot.getComponent('TemplateRoomView'); + assert(roomView.roomLabel.string.includes('654321')&&roomView.playersLabel.string.includes('测试号'),'room view consumes module snapshot'); + wire.emit({type:'message',message:{route:'agent',rpc:'create_room',data:created}});await sleep(50); + assert(e.isValid(roomRoot),'duplicate successful reply does not remount room'); + roomView.prepareButton.node.emit(e.Button.EventType.CLICK);assert(wire.sent.at(-1).rpc==='player_prepare','subgame prepare travels through GameHost'); + wire.emit({type:'message',message:{route:'room',rpc:'player_prepare',data:{seat:0}}}); + roomView.exitButton.node.emit(e.Button.EventType.CLICK);assert(wire.sent.at(-1).rpc==='self_exit_room','subgame exit travels through GameHost'); + wire.emit({type:'message',message:{route:'room',rpc:'self_exit_room',data:{seat:0}}}); + assert(runtime.state.room.kind==='outside'&&lobbies===2&&roomView.actions===null,'exit clears canonical room and disposes view binding'); + startup.requestRoom('self_join_room',()=>runtime.joinRoom({roomcode:123456,ip:'127.0.0.1',location:null})); + assert(wire.sent.at(-1).data.roomcode===123456,'manual join preserves numeric wire roomcode'); + const players=Array(10).fill(null);players[2]={...loginFixture.data,avatar:'',isprepare:0,onstate:0}; + const deskinfo={round:2,cards:[1,2,3]}; + wire.emit({type:'message',message:{route:'agent',rpc:'self_join_room',data:{state:0,roomcode:'123456',seat:2,roomtype,players,deskinfo,deskwar:1,needprepare:1,infinite:0}}}); + await roomPromise;await sleep(80); + assert(fatal.length===0&&runtime.state.room.selfSeat===2&&game.restoredSnapshot===deskinfo,'join and reconnect snapshot reach template module unchanged'); + assert(e.director.getScene().getChildByName('Canvas').getChildByName('TemplateRoom').getComponent('TemplateRoomView').roomLabel.string.includes('123456'),'join success binds its own room scene'); + assert(tips.length===0&&fatal.length===0,'no unexpected fixture UI errors'); + return {checks,count:checks.length}; + }finally{node?.destroy();runtime?.stop();startup.endRoomRequest();keys.forEach((key,i)=>old[i]===null?localStorage.removeItem(key):localStorage.setItem(key,old[i]));} +} diff --git a/docs/protocol/02-协议-agent路由.md b/docs/protocol/02-协议-agent路由.md index 1b750ef..a9fe38e 100644 --- a/docs/protocol/02-协议-agent路由.md +++ b/docs/protocol/02-协议-agent路由.md @@ -1,411 +1,411 @@ -# 02 · 平台层协议 — agent 路由(大厅服务器) - -> 信封:`{ app:"youle", route:"agent", rpc:"<下列名称>", data:{...} }` -> 本篇覆盖 **route=agent 的全部平台层数据包**(双向)。每个 rpc 给出 C→S 请求字段与 S→C 响应/推送字段。子游戏对局包(route=room)不在此篇。 -> 通用身份字段:`agentid`(代理ID)、`gameid`(游戏ID)、`playerid`(玩家ID)。多数请求由各调用点手工拼装,未必三者齐全,下文逐条标注实际字段。 -> 统一条目格式: -> > `rpc名` · route=agent · <方向> -> > 场景 / 请求字段(C→S) / 响应或推送字段(S→C) / 源码 发送·接收 / 备注(⚠️待后端确认,🐛源码 bug) - ---- - -## 登录 / 账号 - -### `player_login` · route=agent · C→S 请求 + S→C 响应 -- 接收路由补充(2026-09-06 实包核对):C→S 仍为 `agent/player_login`;S→C 可以是 `agent`、`room` 或 `platform` 下的 `player_login`。原工程 `12_Logic.js:238-260` 按 rpc 解除登录等待,再把这三类平台路由交给 `Net[rpc]`。已实测短信验证码失败为 `room/player_login`,data 为 `{msg:"短信验证码不正确",state:-1,time:0}`;必须解除等待并交给登录失败处理,不能因响应 route 与发包 route 不同而忽略。 -- 场景:连接建立后 `onopen` 自动发送;断线重连、切服后重发(12_Logic.js:35、:61 构造,09_Net.js:122 `Send_login` 注入)。 -- 请求字段(C→S): - | 字段 | 类型 | 说明 | - |------|------|------| - | agentid | string/int | 代理ID | - | gameid | string/int | 游戏ID | - | openid | string | 微信 openid(游客可为空/特殊值) | - | nickname | string | 昵称 | - | avatar | string | 头像 URL | - | sex | int | 性别 0未知/1男/2女 | - | province | string | 省份 | - | city | string | 城市 | - | unionid | string/int | 微信开放平台 unionid | - | version | string | 客户端版本号(GameData.versionCode) | - | channelid | string/int | 渠道ID | - | marketid | string/int | 市场ID | - | ip | string | 客户端IP,`Send_login` 注入 `returnCitySN.ip`(09_Net.js:125,仅 returnCitySN 存在时) | - | location | object | 定位对象,`Send_login` 注入 `C_Player.addr`(09_Net.js:132,可为 null) | - | machineid | string | 机器标识 `Logic.getMachineId()`(09_Net.js:139) | - | machineroom | string | 机房标识 `Utl.getRoomcode()`(09_Net.js:140) | - | telphone | string | 绑定手机号,仅 `GameData.sysConfig.deviceLogin` 开启时随 `telphoneAuto:true` 一起携带(12_Logic.js:48-50) | - | telphoneAuto | bool | 设备号自动登录标记,同上条件下=true | - | playerid | int | 可选,本地缓存 playerid(`GameData.loginPlayerid` 开启时由 `Logic.readPlayerId()` 注入,09_Net.js:143-148,用于复用账号) | -- 响应字段(S→C):`state`(int 0成功/非0失败) + 大量字段,分两组: - - **A 组 账号资产**:`roomcard`、`bean`、`bank`(仓库星星)、`bankpower`、`bankpwd`、`charm`、`sign`、`tel`、`invitecode` 等(06_Player.js:88 `SetMyInfo` 读取,详见 [04-数据结构.md → 登录响应](./04-数据结构.md#登录响应-deskloginplayer_login))。 - - **B 组 房间恢复**:在房时附带房间快照(roomcode/seat/players/deskinfo 等),由 `Desk.login` 处理,详见 doc04。 -- 源码:发送 `09_Net.js:236`(`Net.Send_login`→`Net._SendData`);接收 `09_Net.js:240`(`Net.player_login`→`Desk.login`)。 -- 备注:— (请求侧字段以本表为准;响应数据结构引用 doc04) - -### `query_player2` · route=agent · C→S 请求 + S→C 响应 -- 场景:仓库转账前查询目标玩家信息。 -- 请求字段(C→S):`agentid`、`playerid`(目标玩家ID)。 -- 响应字段(S→C):`avatar`(string)、`nickname`(string)、`playerid`(int);昵称/头像均空时提示"未找到对应玩家"。 -- 源码:发送 `09_Net.js:870`;接收 `09_Net.js:873`→`Desk.query_player2`(`07_Desk.js:1368`)。 -- 备注:— - -### `binding_phone` · route=agent · C→S 请求 + S→C 响应 -- 场景:绑定手机号。 -- 请求字段(C→S):`agentid`、`playerid`、`phonenum`、`smmcode`(短信验证码)。 -- 响应字段(S→C):`phonenum`(string,回写 `C_Player.tel`)。 -- 源码:发送 `09_Net.js:830`;接收 `09_Net.js:834`→`Desk.binding_phone`(`07_Desk.js:1337`)。 -- 备注:— - -### `send_phone_checkcode` · route=agent · C→S 请求 + S→C 响应 -- 场景:发送手机短信验证码。 -- 请求字段(C→S):`agentid`、`phonenum`(构造点未集中定位,至少含手机号)。 -- 响应字段(S→C):无业务字段(`Desk.send_phone_checkcode` 为空实现,07_Desk.js:1342)。 -- 源码:发送 `09_Net.js:838`;接收 `09_Net.js:842`。 -- 备注:⚠️ 请求字段以后端实现为准。 - -### `send_phone_code_wechat` · route=agent · C→S 请求 + S→C 响应 -- 场景:微信渠道发送手机验证码(绑定手机流程的另一入口)。 -- 请求字段(C→S):`agentid`、`phonenum`(唯一调用点 11_GameUI.js:2620-2623 当前被注释,按注释代码为 agentid+phonenum)。 -- 响应字段(S→C):无业务字段(`Desk.send_phone_code_wechat` 为空实现,07_Desk.js:1345)。 -- 源码:RpcList 定义 `02_Const.js:81`;发送 `09_Net.js:846`(`Send_send_phone_code_wechat`);接收 `09_Net.js:850`→`Desk.send_phone_code_wechat`(`07_Desk.js:1345`)。 -- 备注:⚠️ 框架完整定义并接线,但**当前前端唯一调用点(11_GameUI.js:2623)被注释,实际不发送**;字段以后端实现为准。 - -### `setSign` · route=agent · C→S 请求 + S→C 响应 -- 场景:设置个性签名。 -- 请求字段(C→S):`agentid`、`playerid`、`sign`。 -- 响应字段(S→C):`sign`(string,回显写入 `C_Player.sign`)。 -- 源码:发送 `09_Net.js:736`;接收 `09_Net.js:740`→`C_Player.setSign`(`06_Player.js:525`)。 -- 备注:— - ---- - -## 房间创建 / 进入(大厅侧) - -### `create_room` · route=agent · C→S 请求 + S→C 响应 -- 场景:玩家创建房间。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`roomtype`(房间类型配置数组,见 doc04);`Send_create_room` 自动注入 `ip`=`C_Player.ip`、`location`=`C_Player.addr`(09_Net.js:106-107)。 -- 响应字段(S→C):`state`(0成功)、`roomcode`、`seat`、`roomtype`、`makewar`、`asetcount`、`shortcode`、`infinite`;失败 `showerror`/`error`。详见 [04 → 房间响应公共字段](./04-数据结构.md#房间创建--进入响应公共字段)。 -- 源码:发送 `09_Net.js:104`;接收 `09_Net.js:111`→`Desk.create_room` + `Game_Modify.createRoom`。 -- 备注:— - -### `self_join_room` · route=agent · C→S 请求 + S→C 响应 -- 场景:输入房号 / 快速加入 / H5 唤起 / 进 VIP 配置房。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`roomcode`(房号);`Send_self_join_room` 自动注入 `location`=`C_Player.addr`、`ip`=`C_Player.ip`(09_Net.js:254-255);进 VIP 配置房入口额外带 `vipMatch:1`(12_Logic.js:2171);比赛进房入口可带 `match_id`。 -- 响应字段(S→C):`state`、`roomcode`、`seat`、`isowner`、`players[]`、`roomtype`、`makewar`、`asetcount`、`deskwar`、`deskinfo`(重连快照)。详见 [04 → 房间响应公共字段](./04-数据结构.md#房间创建--进入响应公共字段)。 -- 源码:发送 `09_Net.js:251`;接收 `09_Net.js:259`→`Desk.self_join_room`。 -- 备注:— - -### `quick_enter_share_room` · route=agent · C→S 请求 + S→C 响应 -- 场景:快速进入分享/星星场房间。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`type`(房间类型)、`roomtype`(可选)。 -- 响应字段(S→C):走 self_join_room 进房流程(无独立 `Net.quick_enter_share_room` 接收函数,结果通过 self_join_room/show_message 等回包)。 -- 源码:发送 `09_Net.js:652`。 -- 备注:⚠️ 无对应接收处理函数,进房结果依赖其它推送。 - -### `advanced_roomlist` · route=agent · C→S 请求 + S→C 响应 -- 场景:拉取 VIP/高级房间列表。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 -- 响应字段(S→C):房间列表对象,整包写入 `GameData.snrRoomList` 并渲染(07_Desk.js:1176)。 -- 源码:发送 `09_Net.js:660`;接收 `09_Net.js:665`→`Desk.advanced_roomlist`。 -- 备注:— - -### `advanced_createroom` · route=agent · C→S 请求 + S→C 响应 -- 场景:创建 VIP/高级房间。 -- 请求字段(C→S):`agentid`、`gameid`、`playerid`、`tea`(茶水费)、`infinite`(0/1无限局)、`roomtype`、`videoConfig`(可选)、`rebateLimit`(可选)、`rebateType`(可选)。 -- 响应字段(S→C):`tea`、`rebateLimit` 及房间配置(整包写入 `GameData.snrRoomList`,并回拉 advanced_roomlist,07_Desk.js:1180)。 -- 源码:发送 `09_Net.js:669`;接收 `09_Net.js:674`→`Desk.advanced_createroom`。 -- 备注:— - -### `get_share_room` · route=agent · C→S 请求 + S→C 响应 -- 场景:获取分享/星星场房间列表(仅非大厅环境发送)。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 -- 响应字段(S→C):房间数组(`Desk.get_share_room` / `Game_Modify.getShareRoom` 处理)。 -- 源码:发送 `09_Net.js:641`(`ConstVal.isGameHall` 为 true 时不发);接收 `09_Net.js:648`。 -- 备注:— - -### `getInfoByShortCode` · route=agent · C→S 请求 + S→C 响应 -- 场景:按短码批量查询房间信息(VIP 房列表)。 -- 请求字段(C→S):`agentid`、`gameid`、`shortcodeList`(短码列表)。 -- 响应字段(S→C):`roomInfo`(短号房间信息) → `GameUI.setVipRoomListData`(07_Desk.js:1284)。 -- 源码:发送 `09_Net.js:754`;接收 `09_Net.js:758`→`Desk.getInfoByShortCode`。 -- 备注:— - -### `switchRoomList` · route=agent · C→S 请求 + S→C 响应 -- 场景:开关房间在列表中的可见/可进入状态。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`isClose`(0开/1关)。 -- 响应字段(S→C):`state`(0成功)、`isClose`;失败 `error`(07_Desk.js:1267)。 -- 源码:发送 `09_Net.js:744`;接收 `09_Net.js:748`→`Desk.switchRoomList`。 -- 备注:— - ---- - -## 战绩 / 排行 / 财富 - -### `get_player_grade1` · route=agent · C→S 请求 + S→C 响应 -- 场景:拉取战绩(类型1)。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`type`(可选)、`direction`(可选,翻页)、`gradeidx`(可选,分页索引)。 -- 响应字段(S→C):战绩数据(`gameCombat.get_player_grade1` 渲染)。 -- 源码:发送 `09_Net.js:359`;接收 `09_Net.js:364`。 -- 备注:— - -### `get_player_grade2` · route=agent · C→S 请求 + S→C 响应 -- 场景:拉取战绩(类型2)。 -- 请求字段(C→S):透传调用方 `_data`,构造点未集中定位;至少含 `agentid`/`playerid`/`gameid`。 -- 响应字段(S→C):战绩数据(`gameCombat.get_player_grade2` 渲染)。 -- 源码:发送 `09_Net.js:372`;接收 `09_Net.js:377`。 -- 备注:⚠️ 请求字段构造点未定位,待核对。 - -### `get_treasurelist` · route=agent · C→S 请求 + S→C 响应 -- 场景:财富榜。 -- 请求字段(C→S):`agentid`、`gameid`。 -- 响应字段(S→C):`list`(排行数组) → `Desk.get_treasurelist`。 -- 源码:发送 `09_Net.js:686`;接收 `09_Net.js:691`。 -- 备注:— - -### `getShortCodeRankList` · route=agent · C→S 请求 + S→C 响应 -- 场景:短号场排行榜。 -- 请求字段(C→S):`agentid`、`playerid`、`shortcode`。 -- 响应字段(S→C):成功为排行数据(整包写入 `GameData.vipRank.data`);失败 `error:true` + `message`(07_Desk.js:1311)。 -- 源码:发送 `09_Net.js:770`;接收 `09_Net.js:774`→`Desk.getShortCodeRankList`。 -- 备注:— - -### `getVipRankList` · route=agent · C→S 请求 + S→C 响应 -- 场景:VIP 排行榜。 -- 请求字段(C→S):`agentid`、`limit`(条数)。 -- 响应字段(S→C):`list`(VIP排行数组) → `GameData.rankList`(07_Desk.js:1328)。 -- 源码:发送 `09_Net.js:788`;接收 `09_Net.js:792`→`Desk.getVipRankList`。 -- 备注:— - ---- - -## 任务系统 - -### `get_player_task` · route=agent · C→S 请求 + S→C 响应 -- 场景:获取任务列表。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 -- 响应字段(S→C):`tasks`(任务数组) → `Desk.get_player_task`。 -- 数值兼容:实际回包的 `tasks[].award` 可为数字字符串;原 UI 对 `finish/total` 做算术、对 `state` 用宽松数值比较。新前端在任务列表入库时将这四个字段的数字/十进制数字字符串统一为 number;空值、非数字、非有限值仍报错,state 必须为整数。 -- 源码:发送 `09_Net.js:409`;接收 `09_Net.js:414`。 -- 备注:— - -### `player_finish_task` · route=agent · C→S 请求 + S→C 响应 -- 场景:上报任务完成(如分享成功触发,06_Player.js:414)。 -- 请求字段(C→S):`agentid`、`playerid`、`taskid`。 -- 响应字段(S→C):`state`(int)——`state==1` 且当前 `taskstate==0` 时把 `C_Player.taskstate` 置 1(06_Player.js:476)。 -- 源码:发送 `09_Net.js:421`;接收 `09_Net.js:425`→`C_Player.player_finish_task`(`06_Player.js:474`)。 -- 备注:原文档"响应含 `taskstate`"无源码依据,已删除——接收函数仅读 `_msg.data.state`。 - -### `get_task_award` · route=agent · C→S 请求 + S→C 响应 -- 场景:领取任务奖励。 -- 请求字段(C→S):`agentid`、`playerid`、`taskid`。`11_GameUI.js` 的任务领取点击只构造这三个字段,`Net.Send_get_task_award` / `_SendData` 原样发送,不补 `gameid`(2026-09-06 源码复核修正)。 -- 响应字段(S→C):`taskid`(对应任务 state 置 2=已领取)、`taskstate`(写入 `C_Player.taskstate`)(06_Player.js:482)。 -- 源码:发送 `09_Net.js:429`;接收 `09_Net.js:435`→`C_Player.get_task_award`。 -- 备注:— - -### `refresh_task_state` · route=agent · C→S 请求(发送侧 `Send_can_award`) -- 场景:刷新任务可领取状态。 -- 请求字段(C→S):透传 `_data`(构造点未集中定位,至少含 `agentid`/`playerid`)。 -- 响应字段(S→C):服务器推送 `can_award`(见下条)。 -- 源码:发送 `09_Net.js:440`(`Net.Send_can_award`)。 -- 备注:⚠️ 发送函数名为 `Send_can_award`,但**实际发出的 rpc 是 `"refresh_task_state"`**(硬编码字面量,**不在 RpcList**)。文档以实际 rpc 名为准。 - -### `can_award` · route=agent · S→C 推送 -- 场景:服务器通知有任务可领取。 -- 请求字段:无(纯推送)。 -- 推送字段(S→C):任务可领取标记(载荷字段待后端确认)。 -- 源码:接收 `09_Net.js:444`(`Net.can_award`)→ 调 `C_Player.can_award(_msg)`(`09_Net.js:445`)。 -- 备注:🐛 `06_Player.js` **未定义** `Player.prototype.can_award`(全文无此方法)。推送一旦到达,`C_Player.can_award` 为 undefined,调用即抛 TypeError。当前为 bug,不可当正常协议使用。 - ---- - -## 支付 / 充值 / 资产 - -### `get_paylist` · route=agent · C→S 请求 + S→C 响应 -- 场景:拉取支付项列表。 -- 请求字段(C→S):`agentid`。 -- 响应字段(S→C):`paylist`(支付项数组) → `GameData.payList`,并打开支付界面(09_Net.js:567)。 -- 源码:发送 `09_Net.js:562`;接收 `09_Net.js:567`。 -- 备注:— - -### `pay_succ` · route=agent · C→S 请求(经 HTTP) -- 场景:支付成功后通知服务器入账。 -- 请求字段(C→S):`agentid`、`playerid`、`channelid`、`productid`、`payid`、`amount`、`money`、`paytype`(05_Func.js:2240-2254 构造)。 -- 响应字段(S→C):无显式 WS 回包;资产变化通过 `update_bean`/`update_roomcard` 推送(充房卡场景前端还会本地 `UpdateRoomcard`,05_Func.js:2263)。 -- 源码:WS 发送函数 `Net.Send_pay_succ`(`09_Net.js:573`) **被注释**(05_Func.js:2249、:3199);实际改用 `Func.AjaxHttp` 以同样的 `{app,route:agent,rpc:pay_succ,data}` 信封走 **HTTP** 提交(05_Func.js:2250-2255、:3200-3205)。 -- 备注:⚠️ WebSocket 通道当前不发;该包以 HTTP POST 形式上行,rpc 名仍为 `pay_succ`,待后端确认接收端一致。 - -### `topup_card` · route=agent · C→S 请求 + S→C 响应 -- 场景:充值卡兑换。 -- 请求字段(C→S):`agentid`、`playerid`、`cardno`(卡号)。 -- 响应字段(S→C):`Desk.topup_card` 为空实现(07_Desk.js:1365),资产变化经 update_bean/update_roomcard 推送。 -- 源码:发送 `09_Net.js:863`;接收 `09_Net.js:867`。 -- 备注:— - -### `giveCoin` · route=agent · C→S 请求 + S→C 响应 -- 场景:仓库面板向他人转账豆豆/金币。 -- 请求字段(C→S):`agentid`、`playerid`(转出)、`toPlayerid`(转入目标ID)、`gameid`、`count`(数量)、`password`(仓库密码)(11_GameUI.js:2132-2139)。 -- 响应字段(S→C):`state`(0成功)、`star2`(转出后**仓库星星**数 → `setWareHouseStarCOunt`,**非豆豆**);失败 `showerror`/`error`(07_Desk.js:1381)。 -- 源码:发送 `09_Net.js:876`;接收 `09_Net.js:879`→`Desk.giveCoin`。 -- 备注:响应 `star2` 为仓库星星数,注意区别于豆豆余额。 - ---- - -## 仓库 / 星星 / 魅力 - -### `set_bankpwd` · route=agent · C→S 请求 + S→C 响应 -- 场景:设置仓库密码。 -- 请求字段(C→S):`agentid`、`playerid`、`unionid`、`password`。 -- 响应字段(S→C):`state`(0成功)、`password`;失败 `showerror`/`error`(07_Desk.js:1228)。 -- 源码:发送 `09_Net.js:697`;接收 `09_Net.js:701`→`Desk.set_bankpwd`。 -- 备注:— - -### `change_star` · route=agent · C→S 请求 + S→C 响应 -- 场景:仓库存/取(豆豆 ↔ 仓库星星)。 -- 请求字段(C→S):`agentid`、`playerid`、`mode`(0存入/1取出,源码 `safeInputType-1`)、`password`(仓库密码,字符串)、`count`(数量)(11_GameUI.js:2065-2069 调用点齐全携带此 5 字段)。 -- 响应字段(S→C):`state`(0成功)、`star1`(更新后豆豆余额 → `update_bean2`)、`star2`(更新后仓库星星数 → `setWareHouseStarCOunt`)、`msg`(可选提示)、`count`(可选,回填安全输入);失败 `showerror`/`error`(07_Desk.js:1238)。 -- 源码:发送 `09_Net.js:707`;接收 `09_Net.js:711`→`Desk.change_star`。 -- 备注:原审计疑虑"`mode`/`password` 未见"——经核对仓库存/取调用点(11_GameUI.js:2068-2069)**确含** `mode` 与 `password`,文档正确,疑虑解除。审计提到的"agentid/playerid/toPlayerid/gameid/count"实为相邻的 `giveCoin` 转账包(11_GameUI.js:2132-2139),并非本 rpc。 - -### `update_charm` · route=agent · S→C 推送 -- 场景:座位魅力值更新。 -- 请求字段:`Net.Send_update_charm`(`09_Net.js:727`) 已定义但**无任何调用点**;实际只作服务器→客户端推送。 -- 推送字段(S→C):`seatlist`:[ {`seat`(int), `charm`(number)} ](07_Desk.js:1260 遍历 setCharm)。 -- 源码:接收 `09_Net.js:731`→`Desk.update_charm`(`07_Desk.js:1260`)。 -- 备注:— - -### `setAllCharm` · route=agent · C→S 请求 + S→C 响应 -- 场景:批量设置总魅力。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`value`(11_GameUI.js:1269-1275)。 -- 响应字段(S→C):无显式业务字段,`Desk.setAllCharm` 仅本地存储并提示成功(07_Desk.js:1321)。 -- 源码:发送 `09_Net.js:779`;接收 `09_Net.js:783`→`Desk.setAllCharm`。 -- 备注:— - ---- - -## 邀请码 / 绑定 - -### `binding_invitecode` · route=agent · C→S 请求 + S→C 响应 -- 场景:绑定邀请码。 -- 请求字段(C→S):`agentid`、`playerid`、`invitecode`(11_GameUI.js:1367-1369)。 -- 响应字段(S→C):`state`(0成功时写入 `invitecode`)、`invitecode`、`error`(提示文案)(06_Player.js:251)。 -- 源码:发送 `09_Net.js:587`;接收 `09_Net.js:592`→`C_Player.binding_invitecode`。 -- 备注:— - -### `get_player_invitecode` · route=agent · C→S 请求 + S→C 响应 -- 场景:获取自己的邀请码(打开绑定界面)。 -- 请求字段(C→S):`agentid`、`playerid`、`unionid`、`openid`(11_GameUI.js:1378-1382)。 -- 响应字段(S→C):`invitecode`(string) → `C_Player.setInvitecod` 并打开绑定界面(09_Net.js:607)。 -- 源码:发送 `09_Net.js:603`;接收 `09_Net.js:607`→`Net.get_player_invitecode`。 -- 备注:修正原文档——请求字段为 `agentid/playerid/unionid/openid`,**无 `gameid`**,新增 `unionid`/`openid`(11_GameUI.js:1378)。 - ---- - -## VIP 管理 / 黑白名单 - -### `optBanList` · route=agent · C→S 请求 + S→C 响应 -- 场景:黑名单查看/添加/移除(多入口)。 -- 请求字段(C→S):随入口不同,公共字段 `agentid`、`playerid`、`type`: - - `type=1` 查看黑名单列表(agentid/playerid/type,11_GameUI.js:2401-2405) - - `type=3` 按 ID 添加(agentid/playerid/optId/type,11_GameUI.js:2376-2381) - - `type=4` 按 ID 移除(agentid/playerid/optId/type,11_GameUI.js:2389-2394 及列表项删除 2453-2458) - - `type=6` 一键全部添加(agentid/playerid/type,11_GameUI.js:1262-1267) - - 另有带 `breakRoom`(0/1) 与 `gameid` 的 `type=3` 添加入口(agentid/gameid/playerid/optId/breakRoom/type,11_GameUI.js:2417-2428;breakRoom 由 `GameData.blackList.breakRoom` 决定,开关在 case 3260) - - 服务器响应中亦见 `type=5`(另一种列表返回,07_Desk.js:1299) -- 响应字段(S→C):`type`(1/3/4/5/6)、`banList`(黑名单数组)、`message`(可选提示)(07_Desk.js:1289)。 -- 源码:发送 `09_Net.js:762`;接收 `09_Net.js:766`→`Desk.optBanList`。 -- 备注:— - -### `getPlayerWhiteList` · route=agent · C→S 请求 + S→C 响应 -- 场景:获取白名单。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 -- 响应字段(S→C):`whiteList`(数组) → `GameData.whiteList.data`(07_Desk.js:1412)。 -- 源码:发送 `09_Net.js:882`;接收 `09_Net.js:886`→`Desk.getPlayerWhiteList`。 -- 备注:— - -### `optWhiteList` · route=agent · C→S 请求 + S→C 响应 -- 场景:白名单添加/修改/删除。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`shortcode`、`mode`、`userid`(目标ID): - - `mode=1` 添加/修改,并带 `value`(设置的魅力值) - - `mode=2` 删除(仅 `userid`) -- 响应字段(S→C):`whiteList`(更新后数组)、`mode`(可选)、`message`(可选提示)(07_Desk.js:1398)。 -- 源码:发送 `09_Net.js:890`;接收 `09_Net.js:894`→`Desk.optWhiteList`。 -- 备注:— - -### `setVipForbidSelect` · route=agent · C→S 请求 + S→C 响应 -- 场景:VIP 房禁止玩家选桌开关。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`forbidSelect`(1开/0关,11_GameUI.js:2631-2639)。 -- 响应字段(S→C):`state`(0成功)、`forbidSelect`(1开/0关);失败 `error`(07_Desk.js:1348)。 -- 源码:发送 `09_Net.js:855`;接收 `09_Net.js:859`→`Desk.setVipForbidSelect`。 -- 备注:— - ---- - -## 其它 agent 协议 - -### `submit_opinion` · route=agent · C→S 请求 + S→C 响应 -- 场景:提交反馈/意见。 -- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`content`。 -- 响应字段(S→C):`state`(0成功)(07_Desk.js:1100)。 -- 源码:发送 `09_Net.js:547`;接收 `09_Net.js:551`→`Desk.submit_opinion`。 -- 备注:— - -### `submit_location` · route=agent · C→S 请求 + S→C 响应 -- 场景:提交定位信息(仅非大厅环境发送)。 -- 请求字段(C→S):`agentid`、`playerid`、`info`(定位对象)。 -- 响应字段(S→C):经 `Game.submit_location` 处理(09_Net.js:583)。 -- 源码:发送 `09_Net.js:577`(`ConstVal.isGameHall` 为 true 时不发);接收 `09_Net.js:583`。 -- 备注:— - -### `submit_phoneinfo` · route=agent · C→S 请求(无响应处理) -- 场景:提交手机/通讯录信息(仅非大厅环境发送)。 -- 请求字段(C→S):`agentid`、`playerid`、`info`{ `phoneInfo`(手机信息), `addrBook`(通讯录) }。 -- 响应字段(S→C):`Net.submit_phoneinfo` 接收函数为空实现(09_Net.js:722)。 -- 源码:发送 `09_Net.js:717`;接收 `09_Net.js:722`。 -- 备注:— - -### `submit_error` / `submit_log` · route=agent · C→S 上行(硬编码包) -- 场景:上报前端异常/日志。异常捕获时(12_Logic.js:270)、获取 HTML 失败(12_Logic.js:1990/2009/2014)、收到踢下线包(07_Desk.js:963)等处调用。 -- 请求字段(C→S):`packet`(出错数据包字符串)、`msg`(错误/堆栈信息)、`playerid`、`agentid`、`gameid`(09_Net.js:66-72)。 -- 响应字段(S→C):服务器若回 rpc `"submit_error"`,前端在分发处直接 `return` 忽略(12_Logic.js:235)。 -- 源码:发送 `Net.submit_error`(`09_Net.js:55`) / `Net.submit_log`(`09_Net.js:81`)。 -- 备注:均为**硬编码** `route:"agent", rpc:"submit_error"`,不经 `Send_*`/RpcList。注意 `Net.submit_log`(`09_Net.js:81`) 实际发出的 rpc 同样是 `"submit_error"`(与 submit_error 同包结构)。两者带去重:`submit_error` 对相同 `msg` 只发一次(09_Net.js:57-61),`submit_log` 不去重。 - -### `kick_server` · route=agent · C→S 请求 + S→C 推送 -- 场景:管理端踢出玩家 / 被踢下线弹窗。 -- 请求字段(C→S):`agentid` 等(构造点未集中定位)。 -- 推送字段(S→C):`msg`(踢出提示) → `GameUI.OpenKick`(07_Desk.js:1108)。 -- 源码:发送 `09_Net.js:555`;接收 `09_Net.js:558`→`Desk.kick_server`。 -- 备注:⚠️ 请求字段待补。 - -### `broadcast` · route=agent · C→S 请求 + S→C 推送 -- 场景:广播消息/滚动公告(主要为服务器推送)。 -- 请求字段(C→S):`agentid` 等(`Send_broadcast` 存在,09_Net.js:530)。 -- 推送字段(S→C):`msgtype`(0消息框/1滚动公告,可选,缺省 0)、`msgcontent`(内容)(07_Desk.js:1112)。 -- 源码:发送 `09_Net.js:530`;接收 `09_Net.js:534`→`Desk.broadcast`。 -- 备注:— - -### `connect_agentserver` · route=agent · 双向(切服) -- 场景:切换到大厅服务器。 -- 请求字段(C→S):`Send_connect_agentserver`(`09_Net.js:515`) 透传 `_data`(切服时使用)。 -- 推送字段(S→C):`agentserver`(新大厅服地址)、`opt`(切换原因,如 `other_break_room`/`free_room`)(09_Net.js:518)。 -- 源码:发送 `09_Net.js:515`;接收 `09_Net.js:518`→`Net.connect_agentserver`(关闭当前连接并重连新地址)。 -- 备注:— - -### `playerBehavior` · route=agent(实际走 HTTP GET) -- 场景:玩家行为埋点。 -- 请求字段:原 WS 路径(agentid/gameid/playerid/tag)**被注释**(09_Net.js:799-806),实际改走独立 HTTP GET 上报 `http://test3.1888day.com/api/gamedo/gamedo?agentid=&gameid=&playerid=&tag=`(09_Net.js:807-815)。 -- 响应字段(S→C):HTTP 回调 `playerBehavior_Succ`/`_Fail`(09_Net.js:818/822);WS 接收函数 `Net.playerBehavior`(`09_Net.js:826`) 当前无触发。 -- 源码:发送 `09_Net.js:797`(`Send_playerBehavior`)。 -- 备注:**非 WebSocket 协议**,RpcList 中虽有定义,实际不走 agent 路由。 - ---- - -## 仅接收的 agent 推送(无对应主动请求) - -| rpc | 推送 data 字段 | 说明 | 接收源码 | -|-----|---------------|------|---------| -| `update_roomcard` | `roomcard`、`text`(可选) | 房卡变化(仅 `change` 未定义时更新,09_Net.js:383→06_Player.js:141) | 09_Net.js:383 | -| `update_bean` | `bean`、`change`(可选)、`seat`(可选)、`type`(可选)、`text` | 豆豆变化(09_Net.js:598→Desk.update_bean,07_Desk.js:1201) | 09_Net.js:598 | -| `can_award` | 任务可领取标记 | 🐛 接收即抛异常,见上文 任务系统 章 | 09_Net.js:444 | -| `kick_offline` | `fromOther`(可选)、`gameid`(可选) | 被踢下线,弹 OpenKick;同时本地 `Net.submit_error` 上报"收到踢下线包"(07_Desk.js:945-963) | 09_Net.js:448 | -| `show_message` | `msg`、`time` | 通用消息提示 → `GameUI.OpenTips`(07_Desk.js:1173) | 09_Net.js:656 | +# 02 · 平台层协议 — agent 路由(大厅服务器) + +> 信封:`{ app:"youle", route:"agent", rpc:"<下列名称>", data:{...} }` +> 本篇覆盖 **route=agent 的全部平台层数据包**(双向)。每个 rpc 给出 C→S 请求字段与 S→C 响应/推送字段。子游戏对局包(route=room)不在此篇。 +> 通用身份字段:`agentid`(代理ID)、`gameid`(游戏ID)、`playerid`(玩家ID)。多数请求由各调用点手工拼装,未必三者齐全,下文逐条标注实际字段。 +> 统一条目格式: +> > `rpc名` · route=agent · <方向> +> > 场景 / 请求字段(C→S) / 响应或推送字段(S→C) / 源码 发送·接收 / 备注(⚠️待后端确认,🐛源码 bug) + +--- + +## 登录 / 账号 + +### `player_login` · route=agent · C→S 请求 + S→C 响应 +- 接收路由补充(2026-09-06 实包核对):C→S 仍为 `agent/player_login`;S→C 可以是 `agent`、`room` 或 `platform` 下的 `player_login`。原工程 `12_Logic.js:238-260` 按 rpc 解除登录等待,再把这三类平台路由交给 `Net[rpc]`。已实测短信验证码失败为 `room/player_login`,data 为 `{msg:"短信验证码不正确",state:-1,time:0}`;必须解除等待并交给登录失败处理,不能因响应 route 与发包 route 不同而忽略。 +- 场景:连接建立后 `onopen` 自动发送;断线重连、切服后重发(12_Logic.js:35、:61 构造,09_Net.js:122 `Send_login` 注入)。 +- 请求字段(C→S): + | 字段 | 类型 | 说明 | + |------|------|------| + | agentid | string/int | 代理ID | + | gameid | string/int | 游戏ID | + | openid | string | 微信 openid(游客可为空/特殊值) | + | nickname | string | 昵称 | + | avatar | string | 头像 URL | + | sex | int | 性别 0未知/1男/2女 | + | province | string | 省份 | + | city | string | 城市 | + | unionid | string/int | 微信开放平台 unionid | + | version | string | 客户端版本号(GameData.versionCode) | + | channelid | string/int | 渠道ID | + | marketid | string/int | 市场ID | + | ip | string | 客户端IP,`Send_login` 注入 `returnCitySN.ip`(09_Net.js:125,仅 returnCitySN 存在时) | + | location | object | 定位对象,`Send_login` 注入 `C_Player.addr`(09_Net.js:132,可为 null) | + | machineid | string | 机器标识 `Logic.getMachineId()`(09_Net.js:139) | + | machineroom | string | 机房标识 `Utl.getRoomcode()`(09_Net.js:140) | + | telphone | string | 绑定手机号,仅 `GameData.sysConfig.deviceLogin` 开启时随 `telphoneAuto:true` 一起携带(12_Logic.js:48-50) | + | telphoneAuto | bool | 设备号自动登录标记,同上条件下=true | + | playerid | int | 可选,本地缓存 playerid(`GameData.loginPlayerid` 开启时由 `Logic.readPlayerId()` 注入,09_Net.js:143-148,用于复用账号) | +- 响应字段(S→C):`state`(int 0成功/非0失败) + 大量字段,分两组: + - **A 组 账号资产**:`roomcard`、`bean`、`bank`(仓库星星)、`bankpower`、`bankpwd`、`charm`、`sign`、`tel`、`invitecode` 等(06_Player.js:88 `SetMyInfo` 读取,详见 [04-数据结构.md → 登录响应](./04-数据结构.md#登录响应-deskloginplayer_login))。 + - **B 组 房间恢复**:在房时附带房间快照(roomcode/seat/players/deskinfo 等),由 `Desk.login` 处理,详见 doc04。 +- 源码:发送 `09_Net.js:236`(`Net.Send_login`→`Net._SendData`);接收 `09_Net.js:240`(`Net.player_login`→`Desk.login`)。 +- 备注:— (请求侧字段以本表为准;响应数据结构引用 doc04) + +### `query_player2` · route=agent · C→S 请求 + S→C 响应 +- 场景:仓库转账前查询目标玩家信息。 +- 请求字段(C→S):`agentid`、`playerid`(目标玩家ID)。 +- 响应字段(S→C):`avatar`(string)、`nickname`(string)、`playerid`(int);昵称/头像均空时提示"未找到对应玩家"。 +- 源码:发送 `09_Net.js:870`;接收 `09_Net.js:873`→`Desk.query_player2`(`07_Desk.js:1368`)。 +- 备注:— + +### `binding_phone` · route=agent · C→S 请求 + S→C 响应 +- 场景:绑定手机号。 +- 请求字段(C→S):`agentid`、`playerid`、`phonenum`、`smmcode`(短信验证码)。 +- 响应字段(S→C):`phonenum`(string,回写 `C_Player.tel`)。 +- 源码:发送 `09_Net.js:830`;接收 `09_Net.js:834`→`Desk.binding_phone`(`07_Desk.js:1337`)。 +- 备注:— + +### `send_phone_checkcode` · route=agent · C→S 请求 + S→C 响应 +- 场景:发送手机短信验证码。 +- 请求字段(C→S):`agentid`、`phonenum`(构造点未集中定位,至少含手机号)。 +- 响应字段(S→C):无业务字段(`Desk.send_phone_checkcode` 为空实现,07_Desk.js:1342)。 +- 源码:发送 `09_Net.js:838`;接收 `09_Net.js:842`。 +- 备注:⚠️ 请求字段以后端实现为准。 + +### `send_phone_code_wechat` · route=agent · C→S 请求 + S→C 响应 +- 场景:微信渠道发送手机验证码(绑定手机流程的另一入口)。 +- 请求字段(C→S):`agentid`、`phonenum`(唯一调用点 11_GameUI.js:2620-2623 当前被注释,按注释代码为 agentid+phonenum)。 +- 响应字段(S→C):无业务字段(`Desk.send_phone_code_wechat` 为空实现,07_Desk.js:1345)。 +- 源码:RpcList 定义 `02_Const.js:81`;发送 `09_Net.js:846`(`Send_send_phone_code_wechat`);接收 `09_Net.js:850`→`Desk.send_phone_code_wechat`(`07_Desk.js:1345`)。 +- 备注:⚠️ 框架完整定义并接线,但**当前前端唯一调用点(11_GameUI.js:2623)被注释,实际不发送**;字段以后端实现为准。 + +### `setSign` · route=agent · C→S 请求 + S→C 响应 +- 场景:设置个性签名。 +- 请求字段(C→S):`agentid`、`playerid`、`sign`。 +- 响应字段(S→C):`sign`(string,回显写入 `C_Player.sign`)。 +- 源码:发送 `09_Net.js:736`;接收 `09_Net.js:740`→`C_Player.setSign`(`06_Player.js:525`)。 +- 备注:— + +--- + +## 房间创建 / 进入(大厅侧) + +### `create_room` · route=agent · C→S 请求 + S→C 响应 +- 场景:玩家创建房间。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`roomtype`(房间类型配置数组,见 doc04);`Send_create_room` 自动注入 `ip`=`C_Player.ip`、`location`=`C_Player.addr`(09_Net.js:106-107)。 +- 响应字段(S→C):`state`(0成功)、`roomcode`、`seat`、`roomtype`、`makewar`、`asetcount`、`shortcode`、`infinite`;失败 `showerror`/`error`。详见 [04 → 房间响应公共字段](./04-数据结构.md#房间创建--进入响应公共字段)。 +- 源码:发送 `09_Net.js:104`;接收 `09_Net.js:111`→`Desk.create_room` + `Game_Modify.createRoom`。 +- 备注:— + +### `self_join_room` · route=agent · C→S 请求 + S→C 响应 +- 场景:输入房号 / 快速加入 / H5 唤起 / 进 VIP 配置房。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`roomcode`(房号);`Send_self_join_room` 自动注入 `location`=`C_Player.addr`、`ip`=`C_Player.ip`(09_Net.js:254-255);进 VIP 配置房入口额外带 `vipMatch:1`(12_Logic.js:2171);比赛进房入口可带 `match_id`。 +- 响应字段(S→C):`state`、`roomcode`、`seat`、`isowner`、`players[]`、`roomtype`、`makewar`、`asetcount`、`deskwar`、`deskinfo`(重连快照)。详见 [04 → 房间响应公共字段](./04-数据结构.md#房间创建--进入响应公共字段)。 +- 源码:发送 `09_Net.js:251`;接收 `09_Net.js:259`→`Desk.self_join_room`。 +- 备注:— + +### `quick_enter_share_room` · route=agent · C→S 请求 + S→C 响应 +- 场景:快速进入分享/星星场房间。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`type`(房间类型)、`roomtype`(可选)。 +- 响应字段(S→C):走 self_join_room 进房流程(无独立 `Net.quick_enter_share_room` 接收函数,结果通过 self_join_room/show_message 等回包)。 +- 源码:发送 `09_Net.js:652`。 +- 备注:⚠️ 无对应接收处理函数,进房结果依赖其它推送。 + +### `advanced_roomlist` · route=agent · C→S 请求 + S→C 响应 +- 场景:拉取 VIP/高级房间列表。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 +- 响应字段(S→C):房间列表对象,整包写入 `GameData.snrRoomList` 并渲染(07_Desk.js:1176)。 +- 源码:发送 `09_Net.js:660`;接收 `09_Net.js:665`→`Desk.advanced_roomlist`。 +- 备注:— + +### `advanced_createroom` · route=agent · C→S 请求 + S→C 响应 +- 场景:创建 VIP/高级房间。 +- 请求字段(C→S):`agentid`、`gameid`、`playerid`、`tea`(茶水费)、`infinite`(0/1无限局)、`roomtype`、`videoConfig`(可选)、`rebateLimit`(可选)、`rebateType`(可选)。 +- 响应字段(S→C):`tea`、`rebateLimit` 及房间配置(整包写入 `GameData.snrRoomList`,并回拉 advanced_roomlist,07_Desk.js:1180)。 +- 源码:发送 `09_Net.js:669`;接收 `09_Net.js:674`→`Desk.advanced_createroom`。 +- 备注:— + +### `get_share_room` · route=agent · C→S 请求 + S→C 响应 +- 场景:获取分享/星星场房间列表(仅非大厅环境发送)。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 +- 响应字段(S→C):房间数组(`Desk.get_share_room` / `Game_Modify.getShareRoom` 处理)。 +- 源码:发送 `09_Net.js:641`(`ConstVal.isGameHall` 为 true 时不发);接收 `09_Net.js:648`。 +- 备注:— + +### `getInfoByShortCode` · route=agent · C→S 请求 + S→C 响应 +- 场景:按短码批量查询房间信息(VIP 房列表)。 +- 请求字段(C→S):`agentid`、`gameid`、`shortcodeList`(短码列表)。 +- 响应字段(S→C):`roomInfo`(短号房间信息) → `GameUI.setVipRoomListData`(07_Desk.js:1284)。 +- 源码:发送 `09_Net.js:754`;接收 `09_Net.js:758`→`Desk.getInfoByShortCode`。 +- 备注:— + +### `switchRoomList` · route=agent · C→S 请求 + S→C 响应 +- 场景:开关房间在列表中的可见/可进入状态。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`isClose`(0开/1关)。 +- 响应字段(S→C):`state`(0成功)、`isClose`;失败 `error`(07_Desk.js:1267)。 +- 源码:发送 `09_Net.js:744`;接收 `09_Net.js:748`→`Desk.switchRoomList`。 +- 备注:— + +--- + +## 战绩 / 排行 / 财富 + +### `get_player_grade1` · route=agent · C→S 请求 + S→C 响应 +- 场景:拉取战绩(类型1)。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`type`(可选)、`direction`(可选,翻页)、`gradeidx`(可选,分页索引)。 +- 响应字段(S→C):战绩数据(`gameCombat.get_player_grade1` 渲染)。 +- 源码:发送 `09_Net.js:359`;接收 `09_Net.js:364`。 +- 备注:— + +### `get_player_grade2` · route=agent · C→S 请求 + S→C 响应 +- 场景:拉取战绩(类型2)。 +- 请求字段(C→S):透传调用方 `_data`,构造点未集中定位;至少含 `agentid`/`playerid`/`gameid`。 +- 响应字段(S→C):战绩数据(`gameCombat.get_player_grade2` 渲染)。 +- 源码:发送 `09_Net.js:372`;接收 `09_Net.js:377`。 +- 备注:⚠️ 请求字段构造点未定位,待核对。 + +### `get_treasurelist` · route=agent · C→S 请求 + S→C 响应 +- 场景:财富榜。 +- 请求字段(C→S):`agentid`、`gameid`。 +- 响应字段(S→C):`list`(排行数组) → `Desk.get_treasurelist`。 +- 源码:发送 `09_Net.js:686`;接收 `09_Net.js:691`。 +- 备注:— + +### `getShortCodeRankList` · route=agent · C→S 请求 + S→C 响应 +- 场景:短号场排行榜。 +- 请求字段(C→S):`agentid`、`playerid`、`shortcode`。 +- 响应字段(S→C):成功为排行数据(整包写入 `GameData.vipRank.data`);失败 `error:true` + `message`(07_Desk.js:1311)。 +- 源码:发送 `09_Net.js:770`;接收 `09_Net.js:774`→`Desk.getShortCodeRankList`。 +- 备注:— + +### `getVipRankList` · route=agent · C→S 请求 + S→C 响应 +- 场景:VIP 排行榜。 +- 请求字段(C→S):`agentid`、`limit`(条数)。 +- 响应字段(S→C):`list`(VIP排行数组) → `GameData.rankList`(07_Desk.js:1328)。 +- 源码:发送 `09_Net.js:788`;接收 `09_Net.js:792`→`Desk.getVipRankList`。 +- 备注:— + +--- + +## 任务系统 + +### `get_player_task` · route=agent · C→S 请求 + S→C 响应 +- 场景:获取任务列表。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 +- 响应字段(S→C):`tasks`(任务数组) → `Desk.get_player_task`。 +- 数值兼容:实际回包的 `tasks[].award` 可为数字字符串;原 UI 对 `finish/total` 做算术、对 `state` 用宽松数值比较。新前端在任务列表入库时将这四个字段的数字/十进制数字字符串统一为 number;空值、非数字、非有限值仍报错,state 必须为整数。 +- 源码:发送 `09_Net.js:409`;接收 `09_Net.js:414`。 +- 备注:— + +### `player_finish_task` · route=agent · C→S 请求 + S→C 响应 +- 场景:上报任务完成(如分享成功触发,06_Player.js:414)。 +- 请求字段(C→S):`agentid`、`playerid`、`taskid`。 +- 响应字段(S→C):`state`(int)——`state==1` 且当前 `taskstate==0` 时把 `C_Player.taskstate` 置 1(06_Player.js:476)。 +- 源码:发送 `09_Net.js:421`;接收 `09_Net.js:425`→`C_Player.player_finish_task`(`06_Player.js:474`)。 +- 备注:原文档"响应含 `taskstate`"无源码依据,已删除——接收函数仅读 `_msg.data.state`。 + +### `get_task_award` · route=agent · C→S 请求 + S→C 响应 +- 场景:领取任务奖励。 +- 请求字段(C→S):`agentid`、`playerid`、`taskid`。`11_GameUI.js` 的任务领取点击只构造这三个字段,`Net.Send_get_task_award` / `_SendData` 原样发送,不补 `gameid`(2026-09-06 源码复核修正)。 +- 响应字段(S→C):`taskid`(对应任务 state 置 2=已领取)、`taskstate`(写入 `C_Player.taskstate`)(06_Player.js:482)。 +- 源码:发送 `09_Net.js:429`;接收 `09_Net.js:435`→`C_Player.get_task_award`。 +- 备注:— + +### `refresh_task_state` · route=agent · C→S 请求(发送侧 `Send_can_award`) +- 场景:刷新任务可领取状态。 +- 请求字段(C→S):透传 `_data`(构造点未集中定位,至少含 `agentid`/`playerid`)。 +- 响应字段(S→C):服务器推送 `can_award`(见下条)。 +- 源码:发送 `09_Net.js:440`(`Net.Send_can_award`)。 +- 备注:⚠️ 发送函数名为 `Send_can_award`,但**实际发出的 rpc 是 `"refresh_task_state"`**(硬编码字面量,**不在 RpcList**)。文档以实际 rpc 名为准。 + +### `can_award` · route=agent · S→C 推送 +- 场景:服务器通知有任务可领取。 +- 请求字段:无(纯推送)。 +- 推送字段(S→C):任务可领取标记(载荷字段待后端确认)。 +- 源码:接收 `09_Net.js:444`(`Net.can_award`)→ 调 `C_Player.can_award(_msg)`(`09_Net.js:445`)。 +- 备注:🐛 `06_Player.js` **未定义** `Player.prototype.can_award`(全文无此方法)。推送一旦到达,`C_Player.can_award` 为 undefined,调用即抛 TypeError。当前为 bug,不可当正常协议使用。 + +--- + +## 支付 / 充值 / 资产 + +### `get_paylist` · route=agent · C→S 请求 + S→C 响应 +- 场景:拉取支付项列表。 +- 请求字段(C→S):`agentid`。 +- 响应字段(S→C):`paylist`(支付项数组) → `GameData.payList`,并打开支付界面(09_Net.js:567)。 +- 源码:发送 `09_Net.js:562`;接收 `09_Net.js:567`。 +- 备注:— + +### `pay_succ` · route=agent · C→S 请求(经 HTTP) +- 场景:支付成功后通知服务器入账。 +- 请求字段(C→S):`agentid`、`playerid`、`channelid`、`productid`、`payid`、`amount`、`money`、`paytype`(05_Func.js:2240-2254 构造)。 +- 响应字段(S→C):无显式 WS 回包;资产变化通过 `update_bean`/`update_roomcard` 推送(充房卡场景前端还会本地 `UpdateRoomcard`,05_Func.js:2263)。 +- 源码:WS 发送函数 `Net.Send_pay_succ`(`09_Net.js:573`) **被注释**(05_Func.js:2249、:3199);实际改用 `Func.AjaxHttp` 以同样的 `{app,route:agent,rpc:pay_succ,data}` 信封走 **HTTP** 提交(05_Func.js:2250-2255、:3200-3205)。 +- 备注:⚠️ WebSocket 通道当前不发;该包以 HTTP POST 形式上行,rpc 名仍为 `pay_succ`,待后端确认接收端一致。 + +### `topup_card` · route=agent · C→S 请求 + S→C 响应 +- 场景:充值卡兑换。 +- 请求字段(C→S):`agentid`、`playerid`、`cardno`(卡号)。 +- 响应字段(S→C):`Desk.topup_card` 为空实现(07_Desk.js:1365),资产变化经 update_bean/update_roomcard 推送。 +- 源码:发送 `09_Net.js:863`;接收 `09_Net.js:867`。 +- 备注:— + +### `giveCoin` · route=agent · C→S 请求 + S→C 响应 +- 场景:仓库面板向他人转账豆豆/金币。 +- 请求字段(C→S):`agentid`、`playerid`(转出)、`toPlayerid`(转入目标ID)、`gameid`、`count`(数量)、`password`(仓库密码)(11_GameUI.js:2132-2139)。 +- 响应字段(S→C):`state`(0成功)、`star2`(转出后**仓库星星**数 → `setWareHouseStarCOunt`,**非豆豆**);失败 `showerror`/`error`(07_Desk.js:1381)。 +- 源码:发送 `09_Net.js:876`;接收 `09_Net.js:879`→`Desk.giveCoin`。 +- 备注:响应 `star2` 为仓库星星数,注意区别于豆豆余额。 + +--- + +## 仓库 / 星星 / 魅力 + +### `set_bankpwd` · route=agent · C→S 请求 + S→C 响应 +- 场景:设置仓库密码。 +- 请求字段(C→S):`agentid`、`playerid`、`unionid`、`password`。 +- 响应字段(S→C):`state`(0成功)、`password`;失败 `showerror`/`error`(07_Desk.js:1228)。 +- 源码:发送 `09_Net.js:697`;接收 `09_Net.js:701`→`Desk.set_bankpwd`。 +- 备注:— + +### `change_star` · route=agent · C→S 请求 + S→C 响应 +- 场景:仓库存/取(豆豆 ↔ 仓库星星)。 +- 请求字段(C→S):`agentid`、`playerid`、`mode`(0存入/1取出,源码 `safeInputType-1`)、`password`(仓库密码,字符串)、`count`(数量)(11_GameUI.js:2065-2069 调用点齐全携带此 5 字段)。 +- 响应字段(S→C):`state`(0成功)、`star1`(更新后豆豆余额 → `update_bean2`)、`star2`(更新后仓库星星数 → `setWareHouseStarCOunt`)、`msg`(可选提示)、`count`(可选,回填安全输入);失败 `showerror`/`error`(07_Desk.js:1238)。 +- 源码:发送 `09_Net.js:707`;接收 `09_Net.js:711`→`Desk.change_star`。 +- 备注:原审计疑虑"`mode`/`password` 未见"——经核对仓库存/取调用点(11_GameUI.js:2068-2069)**确含** `mode` 与 `password`,文档正确,疑虑解除。审计提到的"agentid/playerid/toPlayerid/gameid/count"实为相邻的 `giveCoin` 转账包(11_GameUI.js:2132-2139),并非本 rpc。 + +### `update_charm` · route=agent · S→C 推送 +- 场景:座位魅力值更新。 +- 请求字段:`Net.Send_update_charm`(`09_Net.js:727`) 已定义但**无任何调用点**;实际只作服务器→客户端推送。 +- 推送字段(S→C):`seatlist`:[ {`seat`(int), `charm`(number)} ](07_Desk.js:1260 遍历 setCharm)。 +- 源码:接收 `09_Net.js:731`→`Desk.update_charm`(`07_Desk.js:1260`)。 +- 备注:— + +### `setAllCharm` · route=agent · C→S 请求 + S→C 响应 +- 场景:批量设置总魅力。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`value`(11_GameUI.js:1269-1275)。 +- 响应字段(S→C):无显式业务字段,`Desk.setAllCharm` 仅本地存储并提示成功(07_Desk.js:1321)。 +- 源码:发送 `09_Net.js:779`;接收 `09_Net.js:783`→`Desk.setAllCharm`。 +- 备注:— + +--- + +## 邀请码 / 绑定 + +### `binding_invitecode` · route=agent · C→S 请求 + S→C 响应 +- 场景:绑定邀请码。 +- 请求字段(C→S):`agentid`、`playerid`、`invitecode`(11_GameUI.js:1367-1369)。 +- 响应字段(S→C):`state`(整数或整数型字符串,`0`/`"0"` 成功时写入 `invitecode`)、`invitecode`、`error`(提示文案)。原工程 `06_Player.js:270` 使用 `state == 0`;2026-09-06 联调确认服务器也返回字符串状态,前端在绑定响应入口归一化,不能用仅接受 number 的校验直接拒绝。 +- 源码:发送 `09_Net.js:587`;接收 `09_Net.js:592`→`C_Player.binding_invitecode`。 +- 备注:— + +### `get_player_invitecode` · route=agent · C→S 请求 + S→C 响应 +- 场景:获取自己的邀请码(打开绑定界面)。 +- 请求字段(C→S):`agentid`、`playerid`、`unionid`、`openid`(11_GameUI.js:1378-1382)。 +- 响应字段(S→C):`invitecode`(string) → `C_Player.setInvitecod` 并打开绑定界面(09_Net.js:607)。 +- 源码:发送 `09_Net.js:603`;接收 `09_Net.js:607`→`Net.get_player_invitecode`。 +- 备注:修正原文档——请求字段为 `agentid/playerid/unionid/openid`,**无 `gameid`**,新增 `unionid`/`openid`(11_GameUI.js:1378)。 + +--- + +## VIP 管理 / 黑白名单 + +### `optBanList` · route=agent · C→S 请求 + S→C 响应 +- 场景:黑名单查看/添加/移除(多入口)。 +- 请求字段(C→S):随入口不同,公共字段 `agentid`、`playerid`、`type`: + - `type=1` 查看黑名单列表(agentid/playerid/type,11_GameUI.js:2401-2405) + - `type=3` 按 ID 添加(agentid/playerid/optId/type,11_GameUI.js:2376-2381) + - `type=4` 按 ID 移除(agentid/playerid/optId/type,11_GameUI.js:2389-2394 及列表项删除 2453-2458) + - `type=6` 一键全部添加(agentid/playerid/type,11_GameUI.js:1262-1267) + - 另有带 `breakRoom`(0/1) 与 `gameid` 的 `type=3` 添加入口(agentid/gameid/playerid/optId/breakRoom/type,11_GameUI.js:2417-2428;breakRoom 由 `GameData.blackList.breakRoom` 决定,开关在 case 3260) + - 服务器响应中亦见 `type=5`(另一种列表返回,07_Desk.js:1299) +- 响应字段(S→C):`type`(1/3/4/5/6)、`banList`(黑名单数组)、`message`(可选提示)(07_Desk.js:1289)。 +- 源码:发送 `09_Net.js:762`;接收 `09_Net.js:766`→`Desk.optBanList`。 +- 备注:— + +### `getPlayerWhiteList` · route=agent · C→S 请求 + S→C 响应 +- 场景:获取白名单。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 +- 响应字段(S→C):`whiteList`(数组) → `GameData.whiteList.data`(07_Desk.js:1412)。 +- 源码:发送 `09_Net.js:882`;接收 `09_Net.js:886`→`Desk.getPlayerWhiteList`。 +- 备注:— + +### `optWhiteList` · route=agent · C→S 请求 + S→C 响应 +- 场景:白名单添加/修改/删除。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`shortcode`、`mode`、`userid`(目标ID): + - `mode=1` 添加/修改,并带 `value`(设置的魅力值) + - `mode=2` 删除(仅 `userid`) +- 响应字段(S→C):`whiteList`(更新后数组)、`mode`(可选)、`message`(可选提示)(07_Desk.js:1398)。 +- 源码:发送 `09_Net.js:890`;接收 `09_Net.js:894`→`Desk.optWhiteList`。 +- 备注:— + +### `setVipForbidSelect` · route=agent · C→S 请求 + S→C 响应 +- 场景:VIP 房禁止玩家选桌开关。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`forbidSelect`(1开/0关,11_GameUI.js:2631-2639)。 +- 响应字段(S→C):`state`(0成功)、`forbidSelect`(1开/0关);失败 `error`(07_Desk.js:1348)。 +- 源码:发送 `09_Net.js:855`;接收 `09_Net.js:859`→`Desk.setVipForbidSelect`。 +- 备注:— + +--- + +## 其它 agent 协议 + +### `submit_opinion` · route=agent · C→S 请求 + S→C 响应 +- 场景:提交反馈/意见。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`content`。 +- 响应字段(S→C):`state`(0成功)(07_Desk.js:1100)。 +- 源码:发送 `09_Net.js:547`;接收 `09_Net.js:551`→`Desk.submit_opinion`。 +- 备注:— + +### `submit_location` · route=agent · C→S 请求 + S→C 响应 +- 场景:提交定位信息(仅非大厅环境发送)。 +- 请求字段(C→S):`agentid`、`playerid`、`info`(定位对象)。 +- 响应字段(S→C):经 `Game.submit_location` 处理(09_Net.js:583)。 +- 源码:发送 `09_Net.js:577`(`ConstVal.isGameHall` 为 true 时不发);接收 `09_Net.js:583`。 +- 备注:— + +### `submit_phoneinfo` · route=agent · C→S 请求(无响应处理) +- 场景:提交手机/通讯录信息(仅非大厅环境发送)。 +- 请求字段(C→S):`agentid`、`playerid`、`info`{ `phoneInfo`(手机信息), `addrBook`(通讯录) }。 +- 响应字段(S→C):`Net.submit_phoneinfo` 接收函数为空实现(09_Net.js:722)。 +- 源码:发送 `09_Net.js:717`;接收 `09_Net.js:722`。 +- 备注:— + +### `submit_error` / `submit_log` · route=agent · C→S 上行(硬编码包) +- 场景:上报前端异常/日志。异常捕获时(12_Logic.js:270)、获取 HTML 失败(12_Logic.js:1990/2009/2014)、收到踢下线包(07_Desk.js:963)等处调用。 +- 请求字段(C→S):`packet`(出错数据包字符串)、`msg`(错误/堆栈信息)、`playerid`、`agentid`、`gameid`(09_Net.js:66-72)。 +- 响应字段(S→C):服务器若回 rpc `"submit_error"`,前端在分发处直接 `return` 忽略(12_Logic.js:235)。 +- 源码:发送 `Net.submit_error`(`09_Net.js:55`) / `Net.submit_log`(`09_Net.js:81`)。 +- 备注:均为**硬编码** `route:"agent", rpc:"submit_error"`,不经 `Send_*`/RpcList。注意 `Net.submit_log`(`09_Net.js:81`) 实际发出的 rpc 同样是 `"submit_error"`(与 submit_error 同包结构)。两者带去重:`submit_error` 对相同 `msg` 只发一次(09_Net.js:57-61),`submit_log` 不去重。 + +### `kick_server` · route=agent · C→S 请求 + S→C 推送 +- 场景:管理端踢出玩家 / 被踢下线弹窗。 +- 请求字段(C→S):`agentid` 等(构造点未集中定位)。 +- 推送字段(S→C):`msg`(踢出提示) → `GameUI.OpenKick`(07_Desk.js:1108)。 +- 源码:发送 `09_Net.js:555`;接收 `09_Net.js:558`→`Desk.kick_server`。 +- 备注:⚠️ 请求字段待补。 + +### `broadcast` · route=agent · C→S 请求 + S→C 推送 +- 场景:广播消息/滚动公告(主要为服务器推送)。 +- 请求字段(C→S):`agentid` 等(`Send_broadcast` 存在,09_Net.js:530)。 +- 推送字段(S→C):`msgtype`(0消息框/1滚动公告,可选,缺省 0)、`msgcontent`(内容)(07_Desk.js:1112)。 +- 源码:发送 `09_Net.js:530`;接收 `09_Net.js:534`→`Desk.broadcast`。 +- 备注:— + +### `connect_agentserver` · route=agent · 双向(切服) +- 场景:切换到大厅服务器。 +- 请求字段(C→S):`Send_connect_agentserver`(`09_Net.js:515`) 透传 `_data`(切服时使用)。 +- 推送字段(S→C):`agentserver`(新大厅服地址)、`opt`(切换原因,如 `other_break_room`/`free_room`)(09_Net.js:518)。 +- 源码:发送 `09_Net.js:515`;接收 `09_Net.js:518`→`Net.connect_agentserver`(关闭当前连接并重连新地址)。 +- 备注:— + +### `playerBehavior` · route=agent(实际走 HTTP GET) +- 场景:玩家行为埋点。 +- 请求字段:原 WS 路径(agentid/gameid/playerid/tag)**被注释**(09_Net.js:799-806),实际改走独立 HTTP GET 上报 `http://test3.1888day.com/api/gamedo/gamedo?agentid=&gameid=&playerid=&tag=`(09_Net.js:807-815)。 +- 响应字段(S→C):HTTP 回调 `playerBehavior_Succ`/`_Fail`(09_Net.js:818/822);WS 接收函数 `Net.playerBehavior`(`09_Net.js:826`) 当前无触发。 +- 源码:发送 `09_Net.js:797`(`Send_playerBehavior`)。 +- 备注:**非 WebSocket 协议**,RpcList 中虽有定义,实际不走 agent 路由。 + +--- + +## 仅接收的 agent 推送(无对应主动请求) + +| rpc | 推送 data 字段 | 说明 | 接收源码 | +|-----|---------------|------|---------| +| `update_roomcard` | `roomcard`、`text`(可选) | 房卡变化(仅 `change` 未定义时更新,09_Net.js:383→06_Player.js:141) | 09_Net.js:383 | +| `update_bean` | `bean`、`change`(可选)、`seat`(可选)、`type`(可选)、`text` | 豆豆变化(09_Net.js:598→Desk.update_bean,07_Desk.js:1201) | 09_Net.js:598 | +| `can_award` | 任务可领取标记 | 🐛 接收即抛异常,见上文 任务系统 章 | 09_Net.js:444 | +| `kick_offline` | `fromOther`(可选)、`gameid`(可选) | 被踢下线,弹 OpenKick;同时本地 `Net.submit_error` 上报"收到踢下线包"(07_Desk.js:945-963) | 09_Net.js:448 | +| `show_message` | `msg`、`time` | 通用消息提示 → `GameUI.OpenTips`(07_Desk.js:1173) | 09_Net.js:656 | diff --git a/docs/superpowers/plans/2026-09-07-room-entry-template.md b/docs/superpowers/plans/2026-09-07-room-entry-template.md new file mode 100644 index 0000000..4563527 --- /dev/null +++ b/docs/superpowers/plans/2026-09-07-room-entry-template.md @@ -0,0 +1,23 @@ +# Room entry template implementation + +Spec: ../specs/2026-09-07-room-entry-template-design.md (approved). + +Constraints: server zero changes; opaque subgame-owned roomtype; no serialized asset text edits; preserve working tree; main checkout branch; no automatic commits. + +## Task 1: platform create-room lifecycle +Implement exact create_room request and independent response parsing, state construction traced to Desk.create_room, runtime command/session routing and room scene handoff. Test precise fields, denial, duplicate outcomes and module lifecycle. Own framework protocol/platform files and corresponding tests. Do not edit Startup or UI. Expose runtime.createRoom({roomtype,location,ip}). Existing self_join_room remains compatible. Report source evidence and API changes. + +## Task 2: template entry and room UI +Implement pre-room page contract and template rules adapter; JoinRoom input and template create page components with source behavior. Wire LobbyAssets, lobby panel, Startup and usable template room shell through existing GameHost. Use Cocos editor for prefab references/components. No invented gameplay. Test source keypad and exact template array, roomtype validation and host disposal. + +## Task 3: integration verification +Run framework tests, strict Cocos typecheck, import boundaries, local fixture preview for keypad/create options/wait/close and successful room handoff/exit. Review combined feature against spec. Document actual server test limitations. + +## Progress / rulings +- Initial: no implementation yet. Existing working tree snapshot stored in YouleNexus/temp/room-entry-baseline. +- Task 1 produces createRoom consumed by Task 2; no shared edits beyond read-only imports. Task 3 consumes both. Each task matches the approved spec. +- Ruling: use the main checkout branch and preserve dirty state, as required by repository/user preference; no worktree or automatic commit. +- Task 1: complete. Separate create parser/store/session/command paths and numeric join; 223 combined targeted tests pass after review correction for immutable resolver input. +- Task 2: complete. Serialized page controls, subgame CreateRoomPage contract, template entry/room shell, Startup and lobby wiring. Source template capacity is 10 via Desk.Create and Game_Config.Max.PlayerCnt; no roomtype index inference. +- Task 3: complete. Cocos 30 checks pass, both strict typechecks and import boundaries pass. Full suite 770/775; five existing login path/channel fixture failures documented in integration report. Reviewed protocol and integration fixes; no remaining scoped findings. +- Ruling: closed pages cancel callbacks and loading but cannot cancel a server request already sent. The platform continues to accept authoritative success replies; no invented correlation IDs or cancellation RPCs. diff --git a/docs/superpowers/reports/2026-09-06-invite-wiring.md b/docs/superpowers/reports/2026-09-06-invite-wiring.md new file mode 100644 index 0000000..0dfbe37 --- /dev/null +++ b/docs/superpowers/reports/2026-09-06-invite-wiring.md @@ -0,0 +1,30 @@ +# 邀请码接线验证 + +分支:`codex/invite-code-wiring`。 + +## 行为与来源 + +- `Layer8_Notice/Spirit235` 沿用更多菜单显隐规则;点击发送 `agent/get_player_invitecode`,字段仅为 `agentid/playerid/unionid/openid`,收到响应后打开 `Layer16_Bind`。 +- `Spirit365` 原生 EditBox:450×60、20 号字,保留前导零,不限制六位长度。`Spirit366` 粘贴剔除非数字;settings 无参数调用,WVJB 传空字符串并使用回调。纯浏览器调用 Clipboard API,失败显式提示。 +- `Spirit367` 关闭弹窗并发送 `agent/binding_invitecode`,字段仅为 `agentid/playerid/invitecode`。成功才更新玩家邀请码,显示服务器 `error` 提示;已绑定隐藏输入、粘贴、提交并显示绑定码。`Spirit362` 关闭。 +- 身份来源遵循登录模式:非设备登录来自授权账号,设备登录来自回包;重连保留该来源,unionid 保持 string/number 原类型。 +- 查询/提交等待期间抑制重复请求,超时和断线释放等待状态;迟到粘贴不会影响已关闭弹窗。 +- 序列化资源仅经 Cocos MCP、引擎序列化和 asset-db 保存。输入文本采用 EditBox 需要的左上锚点;已绑定文本采用左锚点,左距 403 对应源工程 x=243 加 1600/1280 画布差的一半。 + +权威依据:`docs/protocol/02-协议-agent路由.md` 邀请码章节、`04-数据结构.md` 登录身份说明;原工程 `11_GameUI.js` 的 235/362/366/367 分支和 OpenBind,`06_Player.js` 绑定响应,`05_Func.js` 粘贴接口,`save/Layer00016.xml` 输入尺寸。 + +## 验证 + +- 233 项相关回归通过(platform 全部测试、platform-handlers、lobby-menu、invite-clipboard)。覆盖两种登录模式、数字 unionid、重连、准确出包、失败状态不覆盖和数据非法时不提交。 +- Cocos Game View 中运行 `cocoscreator_projects/scripts/verify-invite-preview.mjs`,29 项断言通过:实际 prefab、Runtime/Router/Store/UI 全链路,记录出包并注入响应。包括输入、粘贴、重复点击、成功/失败提示、反复打开、已绑定状态、文字边界和断线。 +- 未绑定及已绑定截图已人工检查:`YouleNexus/temp/mcp-captures/invite-full.png`、`invite-bound.png`。 +- prefab 的 5 个资源引用全部有效;导入边界检查通过。 +- 框架类型检查通过,命令附加 `--lib es2022,dom`;Cocos 脚本类型检查附加 `--ignoreDeprecations 6.0 --skipLibCheck --lib es2022,dom --types node --strictNullChecks` 后通过。未修改现有 tsconfig;原始配置存在旧库目标/引擎声明兼容问题。 + +预览使用隔离记录端口,没有向真实账号提交绑定。原生桥参数由自动测试验证,尚未进行手机真机剪贴板验收。 + +## 联调修正:字符串 state 导致断线 + +用户实际绑定时收到字符串 `state`,初版 `requireInteger(raw.state)` 拒绝字符串,沿 Router → PlatformRuntime fatal 路径主动关闭连接。原工程的 `state == 0` 支持字符串状态;初版数字回包测试漏掉了这类实际数据。 + +修复限定在 `applyInviteBinding` 的协议入口:整数型字符串先归一化,再校验整数;未改变共享校验器和服务器。新增完整运行时回归覆盖 `0/"0"/1/"1"/-1/"-1"`,断言显示服务器反馈、连接关闭次数保持 0、继续查询可用。空串、null、boolean、非数字和小数字符串仍作为非法状态拒绝,且不提交状态。预览脚本也已改用字符串成功/失败响应。 diff --git a/docs/superpowers/reports/2026-09-07-feedback-prefab-split.md b/docs/superpowers/reports/2026-09-07-feedback-prefab-split.md new file mode 100644 index 0000000..1656469 --- /dev/null +++ b/docs/superpowers/reports/2026-09-07-feedback-prefab-split.md @@ -0,0 +1,10 @@ +# Layer9_Feedback 最后两个分组拆分 + +- `assets/framework/ui/prefabs/widgets/RedeemCode.prefab`:只包含 group-74,保留当前兑换码输入、粘贴、兑换、遮罩关闭逻辑。 +- `assets/framework/ui/prefabs/widgets/QrCode.prefab`:只包含 group-42,保留二维码加载、分享、遮罩关闭及图片资源释放逻辑。 +- `LobbyAssets.redeemCode`、`LobbyAssets.qrCode` 已在 PlatformStartup.scene 中指定对应资源。 +- 已删除不再使用的 `Layer9_Feedback.prefab` 及 meta,并移除共享 `notices` 资源字段。公告数据模型中的 notices 数据字段仍然保留。 + +15 项兑换码/二维码 Game View 检查通过,使用本地粘贴、图片回调、兑换和分享测试替身,没有实际兑换或原生分享。房间列表 18 项、公告 43 项回归检查通过。2 项分享模型测试、严格类型检查和导入边界检查通过;两个 prefab 共 11 个引用均有效。 + +可复用脚本:`cocoscreator_projects/scripts/verify-feedback-split-preview.mjs`。序列化资源均通过编辑器 API 操作。 diff --git a/docs/superpowers/reports/2026-09-07-notice-tabs-prefab.md b/docs/superpowers/reports/2026-09-07-notice-tabs-prefab.md new file mode 100644 index 0000000..2e498a8 --- /dev/null +++ b/docs/superpowers/reports/2026-09-07-notice-tabs-prefab.md @@ -0,0 +1,20 @@ +# 公告独立 prefab 与原生选项卡 + +`assets/framework/ui/prefabs/widgets/NoticeBoard.prefab` 单独承载 `group-12`,原 `Layer9_Feedback.prefab` 保留反馈、二维码、房间列表三个分组。`PlatformStartup.scene` 的 `LobbyAssets.noticeBoard` 已引用新资源;其他入口继续使用原 `notices` 引用。 + +在新 prefab 的 `group-12/NoticeTabs` 中配置: + +- `ToggleContainer`:禁止全部取消,三个 Toggle 直接作为子节点。 +- 纵向 `Layout`:自动排列三个高度 92 的选项,隐藏项不占位。 +- `NoticeTabsView`:指定三个 Toggle、背景及标题 Sprite,以及各自正常/选中 SpriteFrame。 +- 每项分别组合原 `Spirit570/573`、`Spirit571/574`、`Spirit572/575`,统一点击区域,保留美术资源。背景使用 `00216_2/00216_1`;标题正常/选中使用 `00217_1/2`、`00217_3/4`、`00217_5/6`,与原工程相同。 + +公告入口默认选择第一页,后两页沿用原有内容可见性规则。Toggle 事件驱动原公告文字/图片显示逻辑,关闭时释放监听,重新打开重置选择。 + +验证:37 项 Game View 检查通过,覆盖三页互斥与图片状态、隐藏中间项、回调释放、内容切换、关闭与重新打开、启动场景引用和原分组保留;9 项相关单元测试、严格类型检查、导入边界检查通过。新旧 prefab 共 40 个引用全部有效。已检查实际渲染;点击验证采用 Cocos 原生 Button 事件驱动 Toggle,并非物理鼠标实点。 + +可复用脚本:`cocoscreator_projects/scripts/verify-notice-tabs-preview.mjs`。本次资源均通过编辑器 API 保存。 + +后续点击范围修正:打开公告时,为通知背景、通知标题、通知标题内容添加 BlockInputEvents,防止点击穿透到遮罩。只有遮罩和通知关闭按钮负责退出。新增 Cocos 坐标触摸命中测试,覆盖背景空白、标题、正文、选项卡不关闭,关闭按钮和遮罩关闭;现共 43 项检查通过,严格类型检查通过。 + +收尾时,旧 prefab 编辑视图仍显示拆分前的分组,已在该视图移除 group-12;随后 `save_current_scene` 同步调用未返回,已停止等待。上述磁盘资源保存和验证在此之前均已完成,编辑视图是否同步完成未确认,可重新打开 NoticeBoard.prefab 查看新资源。 diff --git a/docs/superpowers/reports/2026-09-07-optional-ui-animation.md b/docs/superpowers/reports/2026-09-07-optional-ui-animation.md new file mode 100644 index 0000000..8f8a2ce --- /dev/null +++ b/docs/superpowers/reports/2026-09-07-optional-ui-animation.md @@ -0,0 +1,35 @@ +# 大厅、通知和设置的可选动画 + +## 配置方法 + +在 Cocos Creator 中打开对应 prefab,选择目标节点。 + +- 大厅和通知节点已经挂好 `Animation` 与 `OptionalUiAnimation`。把制作好的片段拖到 `Animation` 的 **Default Clip**,片段的 **Wrap Mode** 决定单次或循环播放。保持 **Play On Load** 关闭,由组件在节点显示时播放、隐藏时停止,再显示时重新开始。未指定 Default Clip 时保持原来的静态表现。 +- 设置开关已经挂好 `Animation` 与 `OptionalSwitchAnimation`。把片段分别拖到后者的 **开启片段**、**关闭片段**。两个字段各自可空;缺少该方向的片段时立即切换图片。开关过渡只播一次,完成、隐藏或被反向切换时,状态图片服从最新开关状态。 +- 片段以当前节点为绑定根。Sprite 换帧轨道绑定当前节点的 `cc.Sprite.spriteFrame`。制作位移等轨道时,要考虑节点现有 Widget 对布局的约束。 + +## 已配置范围 + +- `Layer4_MainMenu`:Spirit479、Spirit3120、Spirit76、Spirit11、Spirit12、Spirit14、Spirit17、Spirit13、Spirit7。 +- `Layer8_Notice`:Spirit3121、Spirit3017、Spirit5、Spirit235、Spirit604、Spirit3057、Spirit15。 +- `Layer609_Setting`:音乐底、音效底、Spirit357、Spirit388。 + +Spirit479 已指定 `assets/framework/ui/animations/AnnouncementHorn.anim`:沿用原有两张喇叭图片,2 秒循环,在 0.5 秒切到第二张、1 秒切回第一张。`AnnouncementView` 继续负责公告文字移动,喇叭换帧由片段负责。 + +其余节点暂未制作或指定新片段,保持当前表现。音乐和音效已接入状态切换动画;Spirit357、Spirit388 仍保留“原生震动与语音开关尚未接入”的提示,仅预留动画组件。以后业务接通时,由状态拥有方调用 `setState(on, stateFrame, animate)`;初始化用 `animate=false`,用户切换用 `true`。 + +## 验证 + +`cocoscreator_projects/scripts/verify-optional-animation-preview.mjs` 在 Cocos Game View 中执行,使用真实 Animation 组件和独立测试节点,检查资源接线、无片段静态表现、显隐重播、喇叭帧序列、开关中断及实际音乐/音效点击与再次打开。测试使用独立的存储键并在结束时恢复。 + +三个 prefab 通过 Cocos MCP 序列化及 asset-db 保存,原节点 prefab fileId、布局和原有组件属性保留;新组件获得独立 fileId。 + +2026-09-07 验证结果:Game View 44 项检查通过,期间无警告或错误;三个 prefab 的资源引用检查通过,缺失引用为 0;公告、大厅菜单和登录动画的 10 项既有测试通过,导入边界检查通过。 + +工程脚本严格类型检查通过,命令在 `cocoscreator_projects` 执行: + +```powershell +node node_modules/typescript/bin/tsc -p YouleNexus/tsconfig.json --noEmit --ignoreDeprecations 6.0 --skipLibCheck --lib ES2022,DOM --types node --strict +``` + +原始检查入口仍有环境限制:Cocos 内置旧版 TypeScript 不支持工程使用的新语法;未加上述库选项的检查还会报告引擎声明及标准库版本问题。本次未修改这些全局配置。 diff --git a/docs/superpowers/reports/2026-09-07-owner-room-scrollview.md b/docs/superpowers/reports/2026-09-07-owner-room-scrollview.md new file mode 100644 index 0000000..9db8e49 --- /dev/null +++ b/docs/superpowers/reports/2026-09-07-owner-room-scrollview.md @@ -0,0 +1,12 @@ +# 我的开房列表拆分 + +- `assets/framework/ui/prefabs/widgets/OwnedRoomList.prefab` 独立承载原 `group-98`,已绑定 `LobbyAssets.ownedRoomList`。 +- `assets/framework/ui/prefabs/widgets/OwnerRoomItem.prefab` 从用户整理的条目创建,保留背景与数字图片。条目内部坐标改为相对条目中心,房间号数字容器使用横向 Layout。 +- 列表背景保持用户调整后的 280×530 尺寸和位置。其下 `RoomScrollView` 使用 Mask、纵向 ScrollView;边距左/右 8、上 55、下 10。`Content` 使用纵向 Layout,间距 5;条目尺寸 264×80。 +- 根节点 `OwnerRoomListView` 已指定条目 prefab、Content 和 ScrollView。数字图片仍由现有 `LobbyAssets.ownedRoomDigits` 提供。 +- 取消显示前五条的截断,按既有 OwnedRoomHistory 顺序显示全部记录。条目点击继续调用原 joinOwned,拖动不触发加入;背景阻止穿透,遮罩负责关闭。重新打开回到顶部。 +- 空列表明确清空高度并暂停 Layout,重新加入条目恢复布局,避免引擎在零条目时计算出负间距高度。 + +验证:18 项房间列表 Game View 检查通过(包括坐标拖动、滚到底部点击最后一条、遮罩关闭、空列表与重新填充),43 项公告回归检查通过。原房间历史生命周期单元测试、严格类型检查和导入边界检查通过。新 prefab 缺失引用为 0;已检查实际渲染。 + +预览脚本:`cocoscreator_projects/scripts/verify-owner-room-preview.mjs`。序列化资源通过 Cocos 编辑器 API 创建和保存,服务器协议与数据来源未改。 diff --git a/docs/superpowers/reports/2026-09-07-profile-layout-auth.md b/docs/superpowers/reports/2026-09-07-profile-layout-auth.md new file mode 100644 index 0000000..cd0ffce --- /dev/null +++ b/docs/superpowers/reports/2026-09-07-profile-layout-auth.md @@ -0,0 +1,28 @@ +# 资料列表与实名认证接线 + +## 资料列表 + +`Layer17_PlayerInfo` 保留调整后的精灵层级。`InfoText` 作为可滚动视口,`Content` 使用纵向 Layout,根据显示的数据实例化 `PlayerInfoTextItem.prefab`。文字自动换行并调整行高,删除项目后不留空行;签名背景作为最后一项随文字增高,内容超出视口时可以滚动。 + +- 字体、颜色、行宽:在 `assets/framework/ui/prefabs/widgets/PlayerInfoTextItem.prefab` 的 Label 中配置,沿用原文字样式。 +- 项目间距:在 `Layer17_PlayerInfo/InfoText/Content` 对应节点的 Layout 配置(实际位于 group-18 下),当前 `spacingY=14`。 +- 签名背景:最小高度 79,文字上下内边距 10。 +- 根节点 `ProfileInfoListView` 已指定文本 prefab、认证 prefab、容器、滚动组件和签名引用。 +- `lobby-panel.ts` 提供有序文字数组;新增或移除显示字段不需要手工摆放坐标。 + +## 实名认证 + +`Spirit484` 打开 `Layer18_Auth`;姓名、身份证输入框,`Spirit493` 提交,以及 `Spirit485` 退出已接通。背景启用拦截,输入框支持身份证末位 X,重新打开时清空输入。 + +严格沿用原工程 `03_Banwords.js` 的本地姓名长度、身份证地址/日期范围/校验位规则及提示文案。原工程没有服务器身份核验请求,本次也不产生该请求、不保存身份证信息;“认证成功!”仅表示通过原工程的本地校验。 + +## 验证 + +- Game View 中 37 项检查通过,覆盖动态行数、换行、签名增高、滚动回缩、实际 LobbyPanel 接线、输入错误提示、提交和退出事件。 +- 12 项相关单元测试通过,包括与原始 IDValidator 对照的校验测试。 +- Cocos 工程严格类型检查、导入边界检查通过;三个 prefab 的缺失引用均为 0。 +- 已检查资料页、认证页实际渲染,修正 EditBox 文字垂直偏移,并通过原生输入验证中文姓名及含 X 的身份证文本。 +- Electron 鼠标模拟未进入 Game View 的 Cocos 输入系统,因此鼠标实点退出未验证;退出按钮事件接线已通过运行时检查。 +- 核对任务开始时的文件哈希,既有改动中仅预定的 Layer17 prefab 与 lobby-panel.ts 发生变化,其余保留。 + +可复用预览检查脚本:`cocoscreator_projects/scripts/verify-profile-preview.mjs`。所有序列化资源均通过 Cocos 编辑器 API 创建和保存。 diff --git a/docs/superpowers/reports/2026-09-07-room-entry-integration.md b/docs/superpowers/reports/2026-09-07-room-entry-integration.md new file mode 100644 index 0000000..ec76b0a --- /dev/null +++ b/docs/superpowers/reports/2026-09-07-room-entry-integration.md @@ -0,0 +1,35 @@ +# 模板房间接入验收 + +依据:已确认的 `2026-09-07-room-entry-template-design.md`。分支 `codex/room-entry-template`;未提交或合并。服务器零改动。 + +## 已接入 + +- Layer15_JoinRoom:0–9、删除、重输、粘贴、短号确认、6 位自动提交。显示保留前导零,出站房号按原模板为 number。复用 00046 BitmapFont;原有 Sprite 数字槽改为 Label。 +- Layer27_CreateRoom:根节点挂 TemplateCreateRoomView,使用两个 Cocos ToggleContainer。选择与上次 roomtype 的读取遵循原模板;创建按钮仍发原源码固定数组。重新对齐选项、标签和关闭按钮,面板背景阻止点击穿透。 +- create_room 请求、独立创建回包解析和权威房间状态建立;创建回包不要求 players。创建成功的自己资料来自已登录玩家,不向原始回包添加虚构字段。 +- 平台与子游戏双向调用沿用 GameEntry / GameModule / GameHost:成功提交状态后挂载模块,房间视图消费快照,准备/退出经受限 host 发回平台。替换原登录专用拒绝房间入口。 +- 请求防重复、15 秒超时、错误与断线结束等待;断线关闭房间页并使迟到剪贴板回调失效。关闭页不能撤销已经发往服务器的请求,迟到成功仍由平台按服务器状态进入房间。 +- 原样传递 join/login 的 deskinfo。解析人数前传给子游戏的是不可变 roomtype 副本,防止污染服务器原始数据。 + +## 子游戏如何换创建页 + +1. 在自己的 prefab 根节点挂继承 `assets/scripts/platform-login/CreateRoomPage.ts` 的脚本,实现 `open(context)`、`setBusy(busy)`、`close()`。 +2. 子游戏自行管理选项、布局、校验和异步回调,调用 `context.submit(roomtype)` 提交已经验证的数组,调用 `context.cancel()` 关闭。 +3. 在 PlatformStartup 场景的 SubgameAssets.createRoom 指定该 prefab。平台不访问其 group/Spirit 节点。 +4. 子游戏的 GameEntry 提供实际 route、resolveSeatCount 和 GameModule。本次模板注册放在 Startup 的组合入口;不把模板的示例规则作为其他游戏的默认值。 + +模板的 Desk.Create 在没有 getMaxPlayerCount 钩子时明确采用 Game_Config.Max.PlayerCnt=10。本次 TemplateRoomRules 保留该容量;其他游戏必须用自己的真实人数规则。模板没有玩法网络实现,`Game_Surface_3` 模块标识不是任何具体游戏服务器路由的认证结果。TemplateRoom.prefab 是房间接管演示页,不是已完成的牌桌玩法。 + +## 验证结果 + +- 223 项房间相关平台/协议/模型测试通过。 +- Cocos Game View 30 项检查通过:真实坐标点击键盘、数字显示、删除/粘贴、ToggleContainer(再次点击选中项保持选中)、防重复、迟到回调取消、超时、创建拒绝、创建成功真实加载 TemplateRoom 场景、准备/退出 GameHost、重复成功不重挂、加入成功及 deskinfo 原样恢复。脚本:`cocoscreator_projects/scripts/verify-room-entry-preview.mjs`。实际 socket 在测试前停止,测试回包与出站捕获仅在本地运行。 +- Cocos 与 framework 两个严格 TypeScript 检查通过(ES2022/DOM,TypeScript 6);import boundaries 通过。 +- 三个 prefab 共 55 个资源引用校验,无缺失;截图检查创建页和加入页布局。 +- 全量 775 项测试:770 通过,5 失败。3 项旧迁移测试仍读取已在本任务前移走的 Login_Layer.prefab;2 项账号/登录策略测试硬编码旧 agentid/channelid,与本任务前已调整的 defaults 配置不一致。未为测试改回用户的配置或资源名称。 +- 基线哈希核对:原有序列化资源仅 PlatformStartup.scene、Layer15_JoinRoom.prefab、Layer27_CreateRoom.prefab 变化。其余原有 scene/prefab/anim/meta 保持基线。 +- 协议审查发现的可变数组问题、集成审查发现的断线剪贴板问题均已修复并复核。 + +未进行实际服务器创建/加入联调,未实现具体子游戏的规则解释、玩法消息或牌桌操作。本次确认的是模板页面与平台房间交接边界。 + +截图:`cocoscreator_projects/YouleNexus/temp/mcp-captures/room-create.png`、`room-join.png`。 diff --git a/docs/superpowers/reports/2026-09-07-room-protocol-implementation.md b/docs/superpowers/reports/2026-09-07-room-protocol-implementation.md new file mode 100644 index 0000000..fc13376 --- /dev/null +++ b/docs/superpowers/reports/2026-09-07-room-protocol-implementation.md @@ -0,0 +1,45 @@ +# Room protocol implementation — 2026-09-07 + +Implemented Task 1 of the approved room-entry template plan. No server, SDK, Startup, UI, or serialized asset changes were made by this task. Existing dirty work was preserved; no commits were made. + +## Source evidence + +- `docs/protocol/02-协议-agent路由.md`, create_room / self_join_room: outbound agent route and exact identity, roomtype, IP and location fields. +- `projects/Game_Surface_3/js/00_Surface/09_Net.js:104-119`: create request adds C_Player.ip / addr; response calls Desk.create_room, ends loading and calls the subgame hook. +- `projects/Game_Surface_3/js/00_Surface/07_Desk.js:456-536`: create success does not consume players; sets local seat, resets isprepare, seats C_Player, then displays the room. Failure only displays the server error when requested. +- `07_Desk.js:149-156`: Desk.Init starts stage, needprepare and infinite at zero; create response overrides the latter two only when present. These defaults are owned by the create parser, not guessed downstream. +- `06_Player.js:19,69,279-285`: new/local player online state is zero and SetDeskInfo copies it. Created-room canonical self profile comes from the login player, with preparation reset. +- `12_Logic.js:1024-1055`: keypad join submits Number(RoomCode). Numeric roomcode is now preserved by the join request builder while existing string callers remain unchanged. + +## API and state + +- `PlatformRuntime.createRoom({roomtype, location, ip})` delegates to `PlatformCommands`, requires authenticated lobby state and an open wire, sends exactly app/route/rpc plus agentid/playerid/gameid/roomtype/ip/location. +- Reuses the existing defensive roomtype clone to reject non-JSON values without inspecting subgame array indices or rewriting rules. +- Independent `parseCreateRoomResponse` accepts successful create data without players and retains raw data unchanged. `PlatformStore.createRoom` constructs canonical seats from `GameEntry.resolveSeatCount`, places the login player at the server seat, and never manufactures wire players or other raw fields. +- Create denial retains state and reports `ServerDenial.rpc='create_room'`. Successful create opens the leased GameModule, publishes room.entered, and shows the room. The original create path has no deskinfo restoration; join/login restoration behavior is preserved. +- Duplicate create success for the active room and duplicate join success during the active joined session do not reattach or restore a module. Login/switch/exit cleanup resets join deduplication. +- Runtime join/create scene handoff checks that canonical room state is inside; denied lobby join cannot attempt to show an inactive module. + +## Verification + +- Red tests observed missing create command/session methods and duplicate join state/module replacement before implementation. +- 180 tests passed: platform commands, runtime-session, runtime, and all protocol test files, using `node --experimental-transform-types --test`. +- 37 platform-store tests passed separately. +- `node cocoscreator_projects/scripts/check-import-boundaries.mjs`: OK. +- New runtime end-to-end fixture verifies exact create RPC, denial with no module/scene, successful ten-seat entry, duplicate reply with one scene activation/module attachment, and module disposal on stop. +- Framework tsc was attempted with `cocoscreator_projects/node_modules/typescript/bin/tsc`. No new protocol/platform implementation errors; the combined tree currently has pre-existing runtime test Array.at library errors and concurrent template UI/test type errors. Parent owns combined typecheck resolution. +- Outputs: `cocoscreator_projects/YouleNexus/temp/room-protocol-tests.txt` and `room-protocol-store-tests.txt`. + +## Integration boundaries + +Parent owns page waits, repeated submit prevention, timeout/disconnect/close cleanup and user error display. Runtime has no invented request correlation ID. Real server interoperability and Cocos visual acceptance were not performed by this task. Generic startup GameEntry validation already probes and disposes a module; tests distinguish that existing startup probe from actual room entry attachment. + +## Review follow-up: immutable resolver boundary + +Confirmed the initial create-room path passed `parsed.room.roomtype` directly to the subgame seat resolver before canonical state construction. A resolver could mutate the inbound array or its nested objects, changing server data before commit. + +`RuntimeSession.handleCreateRoom` now uses the existing `cloneAndFreezeRoomtype` rules before calling `resolveSeatCount`. The resolver receives a deeply immutable owned copy; raw response data remains untouched. The store still independently constructs canonical state using the original parsed response, with no fabricated input fields. + +Two regressions attempted top-level and nested mutation. Both failed before the fix because no exception occurred. After the fix, both throw TypeError; the raw array and nested object remain unchanged and unfrozen, the prior store root remains identical, no module opens and no partial room state is committed. Re-reviewed create denial, invalid seat count, room command fields, duplicate response handling and scene ordering; existing targeted tests remain green. + +Verification: 149 tests passed across runtime-session, runtime, commands and platform-store. Framework tsc reports only the previously communicated runtime Array.at and concurrent template-game test errors, with no error in this change. Parent edits preserved. diff --git a/docs/superpowers/specs/2026-09-07-room-entry-template-design.md b/docs/superpowers/specs/2026-09-07-room-entry-template-design.md new file mode 100644 index 0000000..7291144 --- /dev/null +++ b/docs/superpowers/specs/2026-09-07-room-entry-template-design.md @@ -0,0 +1,36 @@ +# 模板创建与加入房间接入设计 + +## 已确认依据 + +以 `projects/Game_Surface_3` 为迁移来源。平台不得定义、解释或改写具体玩法的 roomtype;它的生成、业务校验、人数解析属于子游戏。通用页面黑盒改造继续搁置,仅创建房间页采用可替换页面契约。 + +## 职责边界 + +1. 平台加入页:Layer15_JoinRoom 的数字输入、删除、清空、提交、关闭与等待显示。输入限制和提交时机逐项对照原模板。 +2. 子游戏创建页:Layer27_CreateRoom 作为模板默认实现,封装自己的节点、布局、选项与交互。平台仅调用页面生命周期和状态接口,接收提交与取消操作,不依赖该页 group/Spirit 路径。 +3. 子游戏规则适配:由独立的进房前适配对象提供创建页资源和生成配置的能力;内部房间规则模型不进入平台。提交给平台的是已经由子游戏验证的原始 roomtype 数组。平台只检查传输层结构,不补规则默认值。 +4. 平台房间命令:按 docs/protocol/02 的 create_room、self_join_room 契约,补齐已有权威来源的身份、IP、定位字段,管理重复请求、失败、连接状态和页面生命周期。 +5. 进房后模块:沿用 GameEntry / GameModule / GameHost 边界,由平台提交房间状态并分发生命周期事件。创建页不能为了显示选项而提前实例化对局 GameModule。 + +## 模板兼容要求 + +原模板 `01_SubGame_modify.js` 的创建按钮当前发送固定示例数组,而非根据 Type_1/Type_2 选择生成数组。迁移保留这一事实,示例只放在模板适配层,不推导未知索引、不作为其他子游戏的缺省规则。子游戏替换创建页和规则适配后,自行提供完整配置。 + +原模板对局钩子多数为空。本次提供平台房间进入及子游戏接管边界、模板实现和验证,不声称已经实现任何具体玩法。不得继续使用会拒绝所有房间操作的 createLoginStageGameEntry 作为最终房间入口。 + +## 回包与生命周期 + +- 创建成功与加入成功分别解析。创建回包不能直接套用要求 players 数组的现有加入解析器;公共房间状态的建立须逐项追溯原 Desk.create_room 与登录玩家状态,不伪造服务器字段。 +- 失败不进入房间,不创建对局模块,页面按原流程结束等待并显示错误。 +- 成功更新权威状态后,再进入房间表现并通知子游戏;避免重复回包导致重复挂载。 +- deskinfo 原样交给子游戏恢复,平台不解释其玩法内容。保留原工程恢复时机。 +- 子游戏经受限 GameHost 发起平台动作;平台事件和状态快照是反向通知路径,不共享可变全局对象。 +- 页面关闭、切场景、断线和退出时,解绑输入、取消页面等待与异步显示回调。 + +## 工作范围与验证 + +先完成模板创建/加入 UI 与进房前契约,再补平台创建请求/回包、模板 GameEntry 和房间接管;每一段都须可单独测试。已有 SDK、房间状态与座位组件先核对再复用,不另建第二套状态系统。 + +验证包含:原模板输入行为与选项行为;精确出站字段及 roomtype 不变;创建成功、加入成功、拒绝、重复提交、超时/断线、恢复、退出;模块生命周期;真实 Cocos prefab 引用、数字键盘、创建页操作、关闭与等待状态。协议验证使用本地测试数据,实际服务器联调结果单独报告。 + +所有 prefab/scene/meta 通过 Cocos MCP 编辑器操作;主检出分支工作,保留用户现有资源调整,不自动 stash。 diff --git a/server/YouLeGamesServer.html b/server/YouLeGamesServer.html new file mode 100644 index 0000000..3401052 --- /dev/null +++ b/server/YouLeGamesServer.html @@ -0,0 +1,549 @@ + + + + + + + + + 二七王服务器 + + + + +
+ +
+ + + 共 0 条 +
+
+
+ + + + + + + + + diff --git a/server/applist.js b/server/applist.js new file mode 100644 index 0000000..4310f1f --- /dev/null +++ b/server/applist.js @@ -0,0 +1,13 @@ +/////////////////////////////////////////////////// +//////////////////////应用列表///////////////////// +/////////////////////////////////////////////////// +//系统服务 +min_loadJsFile("server/app.js", function (){ +//友乐游戏 +min_loadJsFile("youle/app.js", function (){ +//动态更新模块 +min_loadJsFile("update/app.js", function (){ + +}); +}); +}); \ No newline at end of file diff --git a/server/class/class.app.js b/server/class/class.app.js new file mode 100644 index 0000000..b7cfd77 --- /dev/null +++ b/server/class/class.app.js @@ -0,0 +1,77 @@ +/////////////////////////////////////////////////// +//////////////cls_app: 应用基础类////////////////// +/////////////////////////////////////////////////// +var cls_app = cls_app || { + //构造函数 + new: function(_str_appname, _obj_packet) { + /* + 参数说明: + _str_appname 应用名称 + _obj_packet 所有应用的总包接口对象 + */ + + //创建一个实例 + var app = {}; + + // ================= 实例的公有属性 ================= // + //应用名称 + app.appname = _str_appname; + //总包接口 + app.packet_face = _obj_packet; + //给客户端发包函数 + app.SendPack = app.packet_face.SendPack; + //给http客户端发包函数 + app.SendPack_Http = app.packet_face.SendPack_Http; + //给tcp客户端发包函数 + app.SendPack_Tcp = app.packet_face.SendPack_Tcp; + //将app添加到packet_face的应用列表中 + app.packet_face.applist.push(app); + + //模块列表 [{"face":room_face,"facename":"room_face","routename":"room"}] + app.modlist = []; + + // ================= 实例的公有方法 ================= // + //TCP客户端断开 + app.OnTcpDisConnected = function(_str_tcpid) { + cls_app.OnTcpDisConnected(app, _str_tcpid); + } + + //处理数据包 + app.ReceivePack = function(_pack) { + return cls_app.ReceivePack(app, _pack); + } + + return app; + }, + + // ================= 所有实例的共享方法 ================= // + //TCP客户端断开 + OnTcpDisConnected: function(_obj_app, _str_tcpid) { + for (var i = 0; i < _obj_app.modlist.length; i++) { + if (_obj_app.modlist[i].OnTcpDisConnected) { + _obj_app.modlist[i].OnTcpDisConnected(_str_tcpid); + } + } + }, + + //处理数据包 + ReceivePack: function(_obj_app, _pack) { + if (typeof(_pack) == "string") { + _pack = JSON.parse(_pack); //转换json + } + //路由各个模块 + for (var i = 0; i < _obj_app.modlist.length; i++) { + if (_pack.route == _obj_app.modlist[i].routename) { + if (_obj_app.modlist[i].DoPack) { + var repack = _obj_app.modlist[i].DoPack(_pack); + //发包 + if (repack && _obj_app.SendPack) { + _obj_app.SendPack(repack); + } + return repack; + } + break; + } + } + } +} diff --git a/server/class/class.aset2.js b/server/class/class.aset2.js new file mode 100644 index 0000000..6be96f0 --- /dev/null +++ b/server/class/class.aset2.js @@ -0,0 +1,502 @@ +/*================= cls_aset2: 小局基础类 ================= +注意: +1,此牌类是基础小局类;各子游戏需要继承此基础类后下编写自己的小局类。 +2,子游戏开发人员不能修改该文件。 +=========================================================*/ +var cls_aset2 = { + + //================ 新建一小局 ================ + New: function(){ + //定义小局 + var o_aset = this.declare(); + //初始化牌列表 + this.initcardlist(o_aset); + //除掉不要的牌 + this.deletecard(o_aset); + //设置每张牌的分值 + this.setcardscore(o_aset); + return o_aset; + }, + //小局的数据定义 + declare: function(){ + var o_aset = {}; + //玩家列表 + o_aset.playerlist = []; //在此定义和存储玩家的状态,比如准备状态、叫分的分值、游戏的得分等等,格式如[[], [], ...], [{}, {}, ...], [player, player, ...] + //牌列表 + o_aset.cardlist = []; //格式为[[], [], [], ...],里面的每个小数组就是一张牌 + //庄(位置) + o_aset.zhuang = -1; + //控制权(位置) + o_aset.control = -1; + //当前是第几轮出牌 + o_aset.play = 0; + //当前是本轮的第几个出牌 + o_aset.index = 0; + + return o_aset; + }, + //初始化牌列表 + initcardlist: function(o_aset){ + //几副牌 + var card_count = this.get_cardcount(); + //牌类 + var card_class = this.get_cardclass(); + //初始化 + for (var i = 1; i <= card_count; i++){ //几副牌 + for (var j = 1; j <= 4; j++){ //方块、梅花、红心、黑桃四种花色 + for (var k = 1; k <= 13; k++){ //A到K + var id = (i - 1) * 54 + (j - 1) * 13 + k - 1; //牌的绝对id + //新建一张牌 + var card_object = card_class.New(id); + o_aset.cardlist.push(card_object); + } + } + //小王 + var card_object = card_class.New((i - 1) * 54 + 53 - 1); + o_aset.cardlist.push(card_object); + //大王 + var card_object = card_class.New((i - 1) * 54 + 54 - 1); + o_aset.cardlist.push(card_object); + } + }, + //几副牌 + get_cardcount: function(){ + return 1; + }, + //小局对应的牌类 + get_cardclass: function(){ + return cls_card2; + }, + //除掉不要的牌 + deletecard: function(o_aset){ + for (var i = 0; i < o_aset.cardlist.length; i++){ + var o_card = o_aset.cardlist[i]; + this.get_cardclass().SetDeal(o_card, -1); + } + + //下面的代码是3人二七王除掉3、4的例子。类似功能需要在子游戏中重写该方法 + // for (var i = 0; i < o_aset.cardlist.length; i++){ + // var o_card = o_aset.cardlist[i]; + // var card_number = this.get_cardclass().GetNumber(o_card); + // if (card_number == 3 || card_number == 4){ + // this.get_cardclass().SetDeal(o_card, -2); + // } + // } + }, + //设置每张牌的分值 + setcardscore: function(o_aset){ + for (var i = 0; i < o_aset.cardlist.length; i++){ + var o_card = o_aset.cardlist[i]; + var card_deal = this.get_cardclass().GetDeal(o_card); + if (card_deal != -2){ + this.get_cardclass().SetScore(o_card, 0); + } + } + + //下面的代码是设置5、10、K分值的例子。类似功能需要在子游戏中重写该方法 + // for (var i = 0; i < o_aset.cardlist.length; i++){ + // var o_card = o_aset.cardlist[i]; + // var card_deal = this.get_cardclass().GetDeal(o_card); + // if (card_deal != -2){ + // var card_number = this.get_cardclass().GetNumber(o_card); + // switch (card_number){ + // case 5: + // this.get_cardclass().SetScore(o_card, 5); + // break; + // case 10: + // this.get_cardclass().SetScore(o_card, 10); + // break; + // case 13: + // this.get_cardclass().SetScore(o_card, 10); + // break; + // } + // } + // } + }, + + //===================== 发牌 ===================== + DealCard: function(o_aset, o_desk){ + //需要发牌的位置列表 + var seatlist = this.get_dealseatlist(o_aset, o_desk); + //每人需要发多少张牌 + var dealcount = this.get_dealcount(o_aset, o_desk); + //需要留几张底牌 + var bottomcount = this.get_bottomcount(o_aset, o_desk); + //注意:dealcount * seatlist + bottomcount必须等于除掉不要的牌之后的牌的总数 + + //发牌数组 + var tmplist = []; + for (var i = 0; i < seatlist.length; i++){ + for (var j = 0; j < dealcount; j++){ + tmplist.push(seatlist[i]); + } + } + for (var i = 0; i < bottomcount; i++){ + tmplist.push(-1); + } + + //随机发牌 + for (var i = 0; i < o_aset.cardlist.length; i++){ + var o_card = o_aset.cardlist[i]; + var card_deal = this.get_cardclass().GetDeal(o_card); + if (card_deal != -2){ + var idx = min_random(0, tmplist.length - 1); + this.get_cardclass().SetDeal(o_card, tmplist[idx]); + this.get_cardclass().SetStart(o_card, tmplist[idx]); + this.get_cardclass().SetPlay(o_card, -1); + this.get_cardclass().SetOver(o_card, tmplist[idx]); + //将发牌数组的最后一位移至该位置 + if (idx < tmplist.length - 1) { + tmplist[idx] = tmplist[tmplist.length - 1]; + }; + //发牌数组长度减一 + tmplist.length = tmplist.length - 1; + } + } + }, + DealCardByList: function(o_aset, o_desk){ + //需要发牌的位置列表 + var seatlist = this.get_dealseatlist(o_aset, o_desk); + //每人需要发多少张牌 + var dealcount = this.get_dealcount(o_aset, o_desk); + //需要留几张底牌 + var bottomcount = this.get_bottomcount(o_aset, o_desk); + //注意:dealcount * seatlist + bottomcount必须等于除掉不要的牌之后的牌的总数 + + //发牌数组 + var tmplist = []; + for (var i = 0; i < seatlist.length; i++){ + for (var j = 0; j < dealcount; j++){ + tmplist.push(seatlist[i]); + } + } + for (var i = 0; i < bottomcount; i++){ + tmplist.push(-1); + } + + //随机发牌 + for (var i = 0; i < o_aset.cardlist.length; i++){ + var o_card = o_aset.cardlist[i]; + var card_deal = this.get_cardclass().GetDeal(o_card); + if (card_deal != -2){ + var idx = min_random(0, tmplist.length - 1); + this.get_cardclass().SetDeal(o_card, tmplist[idx]); + this.get_cardclass().SetStart(o_card, tmplist[idx]); + this.get_cardclass().SetPlay(o_card, -1); + this.get_cardclass().SetOver(o_card, tmplist[idx]); + //将发牌数组的最后一位移至该位置 + if (idx < tmplist.length - 1) { + tmplist[idx] = tmplist[tmplist.length - 1]; + }; + //发牌数组长度减一 + tmplist.length = tmplist.length - 1; + } + } + }, + //需要发牌的位置列表 + get_dealseatlist: function(o_aset, o_desk){ + var seatlist = []; + for (var i = 0; i < o_desk.o_room.seatlist.length; i++){ + if (o_desk.o_room.seatlist[i]){ + if (o_desk.o_room.seatlist[i].gameinfo.isbet){ + seatlist.push(i); + } + } + } + return seatlist; + }, + //每人需要发多少张牌 + get_dealcount: function(o_aset, o_desk){ + return 0; + }, + //需要留几张底牌 + get_bottomcount: function(o_aset, o_desk){ + return 0; + }, + + //==================== 拿底牌 ==================== + PutBottomCard: function(o_aset, seat){ + var bottomcardlist = this.GetBottomCards(o_aset); + for (var i = 0; i < bottomcardlist.length; i++) { + var o_card = bottomcardlist[i]; + this.get_cardclass().SetStart(o_card, seat); + } + }, + + //===================== 埋牌 ===================== + BuryCard: function(o_aset, seat, cardidlist){ + /*参数: + seat:哪个位置要埋牌 + cardidlist:要埋的牌的id + 返回值:true表示执行成功,false表示执行失败。*/ + if (this.canBuryCard(o_aset, seat, cardidlist)){ + this.doBuryCard(o_aset, seat, cardidlist); + return true; + } else { + return false; + } + }, + get_burycardcount: function(){ + return 8; + }, + //检查是否可埋牌 + canBuryCard: function(o_aset, seat, cardidlist){ + //检查控制权 + if (seat != o_aset.control){ + return false; + } + //检查埋牌数量 + if (cardidlist.length != this.get_burycardcount()){ + return false; + } + //检查要埋的牌是否在玩家手上 + var inhandcardids = this.GetCardIdsInhand(o_aset, seat); + return min_ary_include(inhandcardids, cardidlist); + }, + //埋牌 + doBuryCard: function(o_aset, seat, cardidlist){ + var cardlist = this.CardIdsToCards(o_aset, cardidlist); + for (var i = 0; i < cardlist.length; i++) { + var o_card = cardlist[i]; + this.get_cardclass().SetPlay(o_card, -2); + } + }, + + //===================== 出牌 ===================== + PlayCard: function(o_aset, seat, cardidlist, mode){ + /*参数: + seat:哪个位置要出牌 + cardidlist:要出的牌的id + mode:出牌模式 0-需要跟随上一家出牌 1-需要跟随第一家出牌 + 返回值:true表示执行成功,false表示执行失败。*/ + var can = this.canPlayCard(o_aset, seat, cardidlist, mode); //检查是否可出 + if (can.result){ //可出 + this.doPlayCard(o_aset, seat, cardidlist); //出下去 + return true; + } else { //不可出 + return false; + } + }, + //检查是否可出牌 + canPlayCard: function(o_aset, seat, cardidlist, mode){ + var can = {}; //返回的结果 + can.result = false; //是否可以出出去 + can.cards = null; //牌(对象) + can.cardtype = null; //牌型 + can.value = null; //大小 + can.flower = null; //花色 + + //检查控制权 + if (seat != o_aset.control){ + can.result = false; + return can; + } + //检查要出的牌是否在玩家手上 + var inhandcardids = this.GetCardIdsInhand(o_aset, seat); + if (!min_ary_include(inhandcardids, cardidlist)){ + can.result = false; + return can; + } + + return can; + }, + //第一个出牌时分析选中的牌是否可以出出去(主要是分析牌型) + canPlayCard_first: function(o_aset, cardidlist){ + var can = {}; //返回的结果 + can.result = false; //是否可以出出去 + can.cards = null; //牌(对象) + can.cardtype = null; //牌型 + can.value = null; //大小 + can.flower = null; //花色 + + + return can; + }, + //不是第一个出牌时分析选中的牌是否可以出出去(主要是分析牌型是否符合前面出牌的牌型) + canPlayCard_second: function(o_aset, cardidlist, mode){ + var can = {}; //返回的结果 + can.result = false; //是否可以出出去 + can.cards = null; //牌(对象) + can.cardtype = null; //牌型 + can.value = null; //大小 + can.flower = null; //花色 + + return can; + }, + //出牌 + doPlayCard: function(o_aset, seat, cardidlist){ + var cardlist = this.CardIdsToCards(o_aset, cardidlist); + for (var i = 0; i < cardlist.length; i++) { + var o_card = cardlist[i]; + this.get_cardclass().SetPlay(o_card, o_aset.play); + this.get_cardclass().SetIndex(o_card, o_aset.index); + } + }, + + //=================== 可跟随的牌 =================== + CanFollowCard: function(o_aset, seat, mode){ + //根据第一个人或上一个人出的牌,分析seat这个位置上的人必出的牌和可出的牌 + + }, + + //==================== 小局结算 ===================== + CloseAccount: function(o_aset, o_desk){ + + }, + + //================ 获取各种情况的牌 ================ + //获取开局时玩家发到手上的牌 + GetDealCardsBySeat: function(o_aset, seat){ + return this.GetCardsByDealstate(o_aset, seat); + }, + GetDealCardIdsBySeat: function(o_aset, seat){ + var cardlist = this.GetDealCardsBySeat(o_aset, seat); + return this.CardsToCardIds(cardlist); + }, + //获取底牌 + GetBottomCards: function(o_aset){ + return this.GetCardsByDealstate(o_aset, -1); + }, + GetBottomCardIds: function(o_aset){ + var cardlist = this.GetBottomCards(o_aset); + return this.CardsToCardIds(cardlist); + }, + //获取玩家当前手上有的牌 + GetCardsInhand: function(o_aset, seat){ + var cardlist = []; + for (var i = 0; i < o_aset.cardlist.length; i++){ + var o_card = o_aset.cardlist[i]; + var card_start = this.get_cardclass().GetStart(o_card); + if (card_start == seat){ + var card_play = this.get_cardclass().GetPlay(o_card); + if (card_play == -1 || card_play == null){ + cardlist.push(o_card); + } + } + } + return cardlist; + }, + GetCardIdsInhand: function(o_aset, seat){ + var cardlist = this.GetCardsInhand(o_aset, seat); + return this.CardsToCardIds(cardlist); + }, + //获取埋牌 参数:seat谁埋的牌,可以不传,默认只有一个人会埋牌 + GetBuryCards: function(o_aset, seat){ + return this.GetCardsByPlaystate(o_aset, -2, seat); + }, + GetBuryCardIds: function(o_aset, seat){ + var cardlist = this.GetBuryCards(o_aset, seat); + return this.CardsToCardIds(cardlist); + }, + //获取上一轮出牌 + GetPriorPlayCard: function(o_aset, round){ + + }, + //获取下一轮出牌 + GetNextPlayCard: function(o_aset, round){ + + }, + + //================== 各种底层函数 ================== + //根据发牌状态获取牌列表 + GetCardsByDealstate: function(o_aset, dealstate){ + var cardlist = []; + for (var i = 0; i < o_aset.cardlist.length; i++){ + var o_card = o_aset.cardlist[i]; + var card_deal = this.get_cardclass().GetDeal(o_card); + if (card_deal == dealstate){ + cardlist.push(o_card); + } + } + return cardlist; + }, + //根据出牌状态获取牌列表 + GetCardsByPlaystate: function(o_aset, playstate, seat){ + var cardlist = []; + for (var i = 0; i < o_aset.cardlist.length; i++){ + var o_card = o_aset.cardlist[i]; + var card_play = this.get_cardclass().GetPlay(o_card); + if (card_play == playstate){ + if (seat != null){ + var card_start = this.get_cardclass().GetStart(o_card); + if (card_start == seat){ + cardlist.push(o_card); + } + } else { + cardlist.push(o_card); + } + } + } + return cardlist; + }, + //将牌对象列表转换成牌id列表 + CardsToCardIds: function(cardlist){ + var cardidlist = []; + for (var i = 0; i < cardlist.length; i++){ + var o_card = cardlist[i]; + var card_id = this.get_cardclass().GetId(o_card); + cardidlist.push(card_id); + } + return cardidlist; + }, + //将牌id列表转换成牌对象列表 + CardIdsToCards: function(o_aset, cardidlist){ + var cardlist = []; + for (var i = 0; i < cardidlist.length; i++){ + var o_card = o_aset.cardlist[cardidlist[i]]; + cardlist.push(o_card); + } + return cardlist; + }, + + //新建小局类 + NewClass: function(){ + var cls = {}; + cls.New = cls_aset2.New; + cls.declare = cls_aset2.declare; + cls.initcardlist = cls_aset2.initcardlist; + cls.get_cardcount = cls_aset2.get_cardcount; + cls.get_cardclass = cls_aset2.get_cardclass; + cls.deletecard = cls_aset2.deletecard; + cls.setcardscore = cls_aset2.setcardscore; + + cls.DealCard = cls_aset2.DealCard; + cls.get_dealseatlist = cls_aset2.get_dealseatlist; + cls.get_dealcount = cls_aset2.get_dealcount; + cls.get_bottomcount = cls_aset2.get_bottomcount; + + cls.PutBottomCard = cls_aset2.PutBottomCard; + + cls.BuryCard = cls_aset2.BuryCard; + cls.get_burycardcount = cls_aset2.get_burycardcount; + cls.canBuryCard = cls_aset2.canBuryCard; + cls.doBuryCard = cls_aset2.doBuryCard; + + cls.PlayCard = cls_aset2.PlayCard; + cls.canPlayCard = cls_aset2.canPlayCard; + cls.canPlayCard_first = cls_aset2.canPlayCard_first; + cls.canPlayCard_second = cls_aset2.canPlayCard_second; + cls.doPlayCard = cls_aset2.doPlayCard; + + cls.CanFollowCard = cls_aset2.CanFollowCard; + + cls.CloseAccount = cls_aset2.CloseAccount; + + cls.GetDealCardsBySeat = cls_aset2.GetDealCardsBySeat; + cls.GetDealCardIdsBySeat = cls_aset2.GetDealCardIdsBySeat; + cls.GetBottomCards = cls_aset2.GetBottomCards; + cls.GetBottomCardIds = cls_aset2.GetBottomCardIds; + cls.GetCardsInhand = cls_aset2.GetCardsInhand; + cls.GetCardIdsInhand = cls_aset2.GetCardIdsInhand; + cls.GetBuryCards = cls_aset2.GetBuryCards; + cls.GetBuryCardIds = cls_aset2.GetBuryCardIds; + cls.GetPriorPlayCard = cls_aset2.GetPriorPlayCard; + cls.GetNextPlayCard = cls_aset2.GetNextPlayCard; + + cls.GetCardsByDealstate = cls_aset2.GetCardsByDealstate; + cls.GetCardsByPlaystate = cls_aset2.GetCardsByPlaystate; + cls.CardsToCardIds = cls_aset2.CardsToCardIds; + cls.CardIdsToCards = cls_aset2.CardIdsToCards; + return cls; + } +} \ No newline at end of file diff --git a/server/class/class.card2.js b/server/class/class.card2.js new file mode 100644 index 0000000..417f9dd --- /dev/null +++ b/server/class/class.card2.js @@ -0,0 +1,742 @@ +/*===================== cls_card2: 牌的基础类 ====================== + 扑克牌的统一定义: + 1,单张牌的定义是一个长度为14的数组 [Id, Flower, Number, ArithF, ArithN, Score, Deal, Start, Play, Index, Over, Tag1, Tag2, Tag3] + 第一位:Id,牌的绝对id,即数组下标,从0开始计数。 + 第二位:Flower,牌的物理花色,5:王 4:黑桃 3:红心 2:梅花 1:方块。 + 第三位:Number,牌的物理大小,1:A 2:2 ... 9:9 10:10 11:J 12:Q 13:K 53:小王 54:大王。 + 第四位:ArithF,牌的算法花色,区别于物理花色,用于牌型计算和比较牌的大小。 + 要求是大于0的整数,不能等于0或小于0,默认等于物理花色。 + 第五位:ArithN,牌的算法大小,区别于物理大小,用于牌型计算和比较牌的大小。 + 要求是大于0的整数,不能等于0或小于0,默认等于物理大小。 + 第六位:Score,牌的分值,如5、10、K。 + 第七位:Deal,发牌状态。 + -2:规则去除的牌; + -1:未发的牌(底牌); + >=0:发牌发到谁手上,即座位号。 + 第八位:Start,开局状态,开局时牌在谁手上,与座位编号对应。 + 第九位:Play,出牌状态。 + -2:埋的牌; + -1:未出的牌(手上的牌); + >=0:牌是第几轮出出去的,从0开始计数。 + 第十位:Index,本轮中的出牌顺序号,从0开始计数。 + 第十一位:Over,牌局结束时被谁得到,与座位编号对应。 + 第十二位:Tag1,扩展属性1。 + 第十三位:Tag2,扩展属性2。 + 第十四位:Tag3,扩展属性3。 + 2,基础牌型的统一定义: + 所有基础牌型都由两个值表示:tong,同,表示一样的牌;shun,顺,表示连续的牌。 + 单张:tong=1,shun=1 + 对子:tong=2,shun=1 + 两连对:tong=2,shun=2 + 三连对:tong=2,shun=3 + 三个:tong=3,shun=1 + 飞机:tong=3,shun=2 + 四个:tong=4,shun=1 + 五连顺:tong=1,shun=5 + 六连顺:tong=1,shun=6 + 其他基础牌型以此类推 + + 注意: + 1,此牌类是基础扑克牌类、通用扑克牌类;各子游戏可以直接使用,也可以继承此基础类后下编写自己的牌类。 + 2,子游戏开发人员不能修改该文件。 + 3,访问牌的属性时要求使用Get和Set方法,不能直接通过数组下标的形式访问。 +===================================================================*/ +var cls_card2 = { + + //新建一张牌 + New: function(id){ + return this.declare(id); + }, + //牌的数据定义 + declare: function(id){ + //id转物理花色 + var IdToFlower = function(cardid){ + var yu = cardid % 54; + if (yu == 52 || yu == 53){ + return 5; + } + return parseInt(yu / 13) + 1; + } + //id转物理数值 + var IdToNumber = function(cardid){ + var yu = cardid % 54; + if (yu == 52){ + return 53; + } + if (yu == 53){ + return 54; + } + return yu % 13 + 1; + } + + var o_card = []; + o_card[0] = id; + o_card[1] = IdToFlower(id); + o_card[2] = IdToNumber(id); + o_card[3] = o_card[1]; + o_card[4] = o_card[2]; + o_card[5] = null; + o_card[6] = null; + o_card[7] = null; + o_card[8] = null; + o_card[9] = null; + o_card[10] = null; + o_card[11] = null; + o_card[12] = null; + o_card[13] = null; + return o_card; + }, + //牌的id,整型,只读 + GetId: function(o_card){ + return o_card[0]; + }, + //牌的物理花色,整型,只读 + GetFlower: function(o_card){ + return o_card[1]; + }, + //牌的物理大小,整型,只读 + GetNumber: function(o_card){ + return o_card[2]; + }, + //牌的算法花色,整型 + GetArithF: function(o_card){ + return o_card[3]; + }, + SetArithF: function(o_card, value){ + o_card[3] = value; + }, + //牌的算法大小,整型 + GetArithN: function(o_card){ + return o_card[4]; + }, + SetArithN: function(o_card, value){ + o_card[4] = value; + }, + //牌的分值,整型 + GetScore: function(o_card){ + return o_card[5]; + }, + SetScore: function(o_card, value){ + o_card[5] = value; + }, + //发牌状态,整型 + GetDeal: function(o_card){ + return o_card[6]; + }, + SetDeal: function(o_card, value){ + o_card[6] = value; + }, + //开局状态,整型 + GetStart: function(o_card){ + return o_card[7]; + }, + SetStart: function(o_card, value){ + o_card[7] = value; + }, + //出牌状态,整型 + GetPlay: function(o_card){ + return o_card[8]; + }, + SetPlay: function(o_card, value){ + o_card[8] = value; + }, + //出牌顺序,整型 + GetIndex: function(o_card){ + return o_card[9]; + }, + SetIndex: function(o_card, value){ + o_card[9] = value; + }, + //结束时被谁得到,整型 + GetOver: function(o_card){ + return o_card[10]; + }, + SetOver: function(o_card, value){ + o_card[10] = value; + }, + //扩展属性 + GetTag1: function(o_card){ + return o_card[11]; + }, + SetTag1: function(o_card, value){ + o_card[11] = value; + }, + GetTag2: function(o_card){ + return o_card[12]; + }, + SetTag2: function(o_card, value){ + o_card[12] = value; + }, + GetTag3: function(o_card){ + return o_card[13]; + }, + SetTag3: function(o_card, value){ + o_card[13] = value; + }, + /* + 以下提供几个针对牌数组的基础算法,这两个方法是基于牌的算法花色和算法大小实现的,各子游戏在调用前需要将牌的算法花色和算法大小先设置好。 + + 设置算法花色的原则是: + 1,如果对花色没要求则需要将所有牌的算法花色统一。比如斗地主中的五连顺是任何花色都可以一起连顺子的,则需要将所有牌的算法花色全部设置成0,表示都是同一花色,然后在同一花色下取顺子。 + 2,如果对花色有要求则需要根据实际情况区分算法花色。比如升级中的两连对是指同一花色下的两连对,则需要将算法花色设置成不同的值,表示不同算法花色之间是不能组成两连对的。 + + 设置算法大小的原则是:算法大小即可表示是否连牌,也可表示牌的大小关系。 + 1,设置算法大小时要求做到数字连续则表示是连牌。比如A的物理大小是1,K的物理大小是13,1和13是不连续的,此时需要将A的算法大小设置成14,将K的算法大小设置成13,14和13是连续的,表示A和K可以连牌。再比如王牌和A是不能作为连牌出现的,则需要将王的算法大小设置成16,将A的算法大小设置成14,16和14不是连续的,表示王和A不能连牌。 + 2,设置算法大小时要求做到数字大小则表示是牌的大小。比如A的物理大小是1,K的物理大小是13,1比13小,但A比K大,此时需要将A的算法大小设置成14,将K的算法大小设置成13,14比13大,表示A比K大。再比如王牌比A大,则需要将王的算法大小设置成16,将A的算法大小设置成14,16大于14,表示王比A大。 + */ + + //根据算法花色筛选牌 + FilterCardListByArithF: function(o_cardlist, ArithF){ + /*参数说明 + o_cardlist:需要进行筛选的牌数组 + ArithF:要筛选的算法花色,默认不筛选 + + 返回值:筛选后的牌数组*/ + var result = []; + for (var i = 0; i < o_cardlist.length; i++) { + var o_card = o_cardlist[i]; + var card_ArithF = this.GetArithF(o_card); + if (!ArithF || card_ArithF == ArithF) { + result.push(o_cardlist[i]); + } + } + return result; + }, + + //根据算法大小筛选牌 + FilterCardListByArithN: function(o_cardlist, min_ArithN, max_ArithN){ + /*参数说明 + o_cardlist:需要进行筛选的牌数组 + min_ArithN:要筛选的算法大小最小值,>=,默认不限制 + max_ArithN:要筛选的算法大小最大值,<=,默认不限制 + 返回值:筛选后的牌数组*/ + var result = []; + for (var i = 0; i < o_cardlist.length; i++) { + var o_card = o_cardlist[i]; + var card_ArithN = this.GetArithN(o_card); + if ((!min_ArithN || card_ArithN >= min_ArithN) && + (!max_ArithN || card_ArithN <= max_ArithN)) { + result.push(o_cardlist[i]); + } + } + return result; + }, + + //根据算法大小对牌数组进行排序(冒泡排序法) + SortCardList: function(o_cardlist, options){ + /*参数说明 + o_cardlist: 需要进行排序的牌数组 + options : 排序选项,长度为2的数组,结构为[大小排序方向,花色排序方向] + 第一位: 算法大小排序方向,0-从小到大排序 1-从大到小排序,默认为0。 + 第二位: 物理花色排序方向,算法大小相同时是否再按物理花色排序,默认为0。 + 0-根据算法大小排序方向默认选择物理花色排序方向, + options[0]=0时,物理花色默认按“方块->梅花->红心->黑桃”排序; + options[0]=1时,物理花色默认按“黑桃->红心->梅花->方块”排序。 + 1-不按物理花色排序,在o_cardlist中是什么顺序就什么顺序。 + 2-按物理花色从小到大,方块->梅花->红心->黑桃 + 3-按物理花色从大到小,黑桃->红心->梅花->方块 + 返回值: 排序后的牌数组*/ + var ArithN_direct = 0; //算法大小排序方向 + var Flower_direct = 0; //物理花色排序方向 + if (options) { + ArithN_direct = parseInt(options[0]); + Flower_direct = parseInt(options[1]); + } + if (!Flower_direct){ + if (ArithN_direct) { + //算法大小从大到小排序时,默认按“黑桃->红心->梅花->方块”排序 + Flower_direct = 3; + } else { + //算法大小从小到大排序时,默认按“方块->梅花->红心->黑桃”排序 + Flower_direct = 2; + } + } + + //j与j+1互换位置 + var doChangej = function(){ + var tmp = o_cardlist[j]; + o_cardlist[j] = o_cardlist[j + 1]; + o_cardlist[j + 1] = tmp; + } + + for (var i = 0; i < o_cardlist.length; i++){ + for (var j = 0; j < o_cardlist.length - i - 1; j++){ + var ArithN_j = this.GetArithN(o_cardlist[j]); + var ArithN_j1 = this.GetArithN(o_cardlist[j + 1]); + if (ArithN_direct == 0 && ArithN_j > ArithN_j1){ + //从小到大排序 + doChangej(); + continue; + } + if (ArithN_direct == 1 && ArithN_j < ArithN_j1){ + //从大到小排序 + doChangej(); + continue; + } + if (ArithN_j == ArithN_j1){ + //算法大小相同时 + if (Flower_direct == 1){ + //不按物理花色排序 + continue; + } + var Flower_j = this.GetFlower(o_cardlist[j]); + var Flower_j1 = this.GetFlower(o_cardlist[j + 1]); + if (Flower_direct == 2 && Flower_j > Flower_j1){ + //按“方块->梅花->红心->黑桃”排序 + doChangej(); + continue; + } + if (Flower_direct == 3 && Flower_j < Flower_j1){ + //按“黑桃->红心->梅花->方块”排序 + doChangej(); + continue; + } + } + } + } + return o_cardlist; + }, + + //获取指定牌型(基础牌型)的各种组合(只组合,无排列) + GetCardListByCardType: function(o_cardlist, cardtype, options){ + /*参数说明 + o_cardlist:在这些牌中获取指定牌型,必须是经过了从小到大排序后的牌数组。 + cardtype :目标牌型,长度为2的数组,结构为[同,顺]。 + 例如单张为[1,1],对子为[2,1],五连顺为[1,5]。 + options :取牌选项,长度为4的数组,结构为[结果数量,取牌方向,拆牌标志,一次标志] + 第一位:结果数量,0-获取所有可能的结果,>0要取的结果数量,默认为0。 + 比如,牌数组为[2,3,5,7]时, + 当options[0]=0时,则取单张的结果为2,3,5,7; + 当options[0]=1时,则取单张的结果为2; + 当options[0]=3时,则取单张的结果为2,3,5。 + 第二位:取牌方向,0-从小到大取结果,1-从大到小取结果,默认为0。 + 第三位:拆牌标志,0-不拆牌,1-拆牌,默认为0。 + 比如,牌数组为[2,2,3,5,5,5,7], + 当options[2]=0时,则取单张时会不取对子的牌和三张的牌,即结果为3,7; + 当options[2]=1时,则取单张时会拆掉对子的牌和三张的牌,即结果为2,3,5,7。 + 第四位:一次标志,0-相同大小的牌只取一次,1-取所有情况,默认为0。 + 比如,牌数组为[红心2, 黑桃2, 方块3, 方块5, 红心5, 黑桃5, 方块7], + 当options[3]=0时,则取单张的结果为:红心2, 方块3, 方块5, 方块7; + 当options[3]=1时,则取单张时结果为:红心2, 黑桃2, 方块3, 方块5, 红心5, 黑桃5, 方块7。 + 返回值:满足牌型要求的牌组合数组。 + 格式如 + [ + [o_card, o_card, o_card, ...], + [o_card, o_card, o_card, ...], + [o_card, o_card, o_card, ...], + ... + ] + 注意:统一用第一张牌的算法大小值表示牌型大小,比如34567是顺子,56789也是顺子,用3表示34567顺子的大小,用5表示56789顺子的大小,5大于3,表示56789的顺子比34567的顺子大*/ + + var cardtype_tong = parseInt(cardtype[0]); //牌型-同 + var cardtype_shun = parseInt(cardtype[1]); //牌型-顺 + var options_count = 0; //结果数量 + var options_direct= 0; //取牌方向 + var options_split = 0; //拆牌标志 + var options_once = 0; //一次标志 + if (options){ + options_count = parseInt(options[0]); + options_direct= parseInt(options[1]); + options_split = parseInt(options[2]); + options_once = parseInt(options[3]); + } + + //将牌按大小分组,即相同大小的牌归为一组。如,将[2,2,3,5,5,5,7]这样的牌数组转成[[2,2],[3],[5,5,5],[7]] + var SameGroupList = []; + var SameGroup = []; + for (var i = 0; i < o_cardlist.length; i++){ + var o_card = o_cardlist[i]; + if (SameGroup.length == 0){ + SameGroup.push(o_card); + } else { + var card_ArithN = this.GetArithN(o_card); + var SameGroup_ArithN = this.GetArithN(SameGroup[0]); + if (card_ArithN == SameGroup_ArithN){ + SameGroup.push(o_card); + } else { + SameGroupList.push(SameGroup); + SameGroup = []; + SameGroup.push(o_card); + } + } + } + if (SameGroup.length > 0){ + SameGroupList.push(SameGroup); + } + + //将牌分组按牌型的“同”获取各自的组合。例如,将[[2,2],[3],[5,5,5],[7]]这样的牌分组按“同”等于2转成[[[2,2]], [[方5,梅5],[方5,红5],[梅5,红5]]] + var TongGroupList = []; + for (var i = 0; i < SameGroupList.length; i++){ + if (SameGroupList[i].length < cardtype_tong){ + // TongGroupList.push([]); + continue; + } + if (SameGroupList[i].length == cardtype_tong){ + TongGroupList.push([SameGroupList[i]]); + continue; + } + if (SameGroupList[i].length > cardtype_tong){ + if (!options_split){ //不允许拆牌 + // TongGroupList.push([]); + continue; + } + if (!options_once){ //同样大小的牌只取一次 + TongGroupList.push([SameGroupList[i].slice(0, cardtype_tong)]); + } else { + TongGroupList.push(min_CombineInAry(SameGroupList[i], cardtype_tong)); + } + } + } + + //返回结果 + var resultlist = []; + var ShunGroup = []; + //检查ShunGroup是否是连顺,如果是连顺则在ShunGroup取结果保存到resultlist中 + var check_return_ShunGroup = function(){ + var isShun = true; + for (var j = 0; j < ShunGroup.length; j++){ + if (ShunGroup[j].length == 0) { + isShun = false; + break; + } + if (j > 0) { + var j1_ArithN = this.GetArithN(ShunGroup[j-1][0][0]); + var j_ArithN = this.GetArithN(ShunGroup[j][0][0]); + //相减等于1表示是顺 + if (j_ArithN - j1_ArithN != 1){ + isShun = false; + break; + } + } + } + if (!isShun){ + return false; + } + var result = min_CombineByArys(ShunGroup); + for (var j = 0; j < result.length; j++) { + var temp = []; + for (var k = 0; k < result[j].length; k++) { + temp = temp.concat(result[j][k]); + } + resultlist.push(temp); + + if (options_count && resultlist.length >= options_count) { + //达到了要取的结果数量 + return true; + } + } + return false; + }.bind(this); + + //按牌型的“顺”获取符合要求的结果 + if (!options_direct){ + //从小到大取结果 + for (var i = 0; i <= TongGroupList.length - cardtype_shun; i++){ + ShunGroup = TongGroupList.slice(i, i + cardtype_shun); + if (check_return_ShunGroup()){ + return resultlist; + } + } + } else { + //从大到小取结果 + for (var i = TongGroupList.length - cardtype_shun; i >= 0; i--){ + ShunGroup = TongGroupList.slice(i, i + cardtype_shun); + if (check_return_ShunGroup()){ + return resultlist; + } + } + } + return resultlist; + }, + GetCardListByCardTypeA: function(o_cardlist, cardtype, options, min_ArithN, max_ArithN){ + /*参数说明 + o_cardlist:同GetCardListByCardType中的参数说明。 + cardtype :同GetCardListByCardType中的参数说明。 + options :同GetCardListByCardType中的参数说明。 + min_ArithN:同FilterCardListByArithN中的参数说明。 + max_ArithN:同FilterCardListByArithN中的参数说明。 + 返回值:同GetCardListByCardType中的参数说明。*/ + var cardlist = this.FilterCardListByArithN(o_cardlist, min_ArithN, max_ArithN); + return this.GetCardListByCardType(cardlist, cardtype, options); + }, + GetCardListByCardTypeB: function(o_cardlist, cardtype, options, ArithF, min_ArithN, max_ArithN){ + /*参数说明 + o_cardlist:可以是未排序的牌数组,该函数会实现排序。 + cardtype :同GetCardListByCardType中的参数说明。 + options :同GetCardListByCardType中的参数说明。 + ArithF :同FilterCardListByArithF中的参数说明。 + min_ArithN:同FilterCardListByArithN中的参数说明。 + max_ArithN:同FilterCardListByArithN中的参数说明。 + 返回值:同GetCardListByCardType中的参数说明。*/ + var cardlist = this.FilterCardListByArithF(o_cardlist, ArithF); + this.SortCardList(cardlist); + cardlist = this.FilterCardListByArithN(cardlist, min_ArithN, max_ArithN); + return this.GetCardListByCardType(cardlist, cardtype, options); + }, + + //获取指定牌型(扩展牌型)的一种组合(只取一种组合) + GetCardListByExtendCardTypeA: function(o_cardlist, cardtypelist){ + /*参数说明 + o_cardlist :同GetCardListByCardTypeA中的参数说明。 + cardtypelist:扩展牌型和取牌选项。 + 结构为[ + [cardtype, options, min_ArithN, max_ArithN], + [cardtype, options, min_ArithN, max_ArithN], + [cardtype, options, min_ArithN, max_ArithN] + ] + 其中cardtype、options、min_ArithN、max_ArithN同GetCardListByCardTypeA中的参数说明。 + 例如3带1,cardtypelist=[ + [[3,1], [1,x,x,x], min_ArithN, max_ArithN], + [[1,1], [1,x,x,x], min_ArithN, max_ArithN] + ] + 3带2,cardtypelist=[ + [[3,1], [1,x,x,x], min_ArithN, max_ArithN], + [[1,1], [1,x,x,x], min_ArithN, max_ArithN], + [[1,1], [1,x,x,x], min_ArithN, max_ArithN] + ] + 3带1对,cardtypelist=[ + [[3,1], [1,x,x,x], min_ArithN, max_ArithN], + [[2,1], [1,x,x,x], min_ArithN, max_ArithN] + ] + 注意:options中第一参数一定为1,表示只取一个结果,如果传的值不等于1,也会按等于1处理。 + + 返回值:同GetCardListByCardTypeA中的参数说明。*/ + + var cardlist = o_cardlist; + var result = []; + for (var i = 0; i < cardtypelist.length; i++){ + var cardtype = cardtypelist[i][0]; + var options = cardtypelist[i][1]; + if (options[0] != 1){ + options[0] = 1; + } + var min_ArithN = cardtypelist[i][2]; + var max_ArithN = cardtypelist[i][3]; + var BaseCardGroup = this.GetCardListByCardTypeA(cardlist, cardtype, options, min_ArithN, max_ArithN); + if (BaseCardGroup.length == 0){ + return []; + } + result = result.concat(BaseCardGroup[0]); + cardlist = min_ary_deduct(cardlist, BaseCardGroup[0]); + } + return [result]; + }, + + //获取牌数组的最大牌型 + GetMaxCardTypeByCardList: function(o_cardlist, option){ + /*参数说明 + o_cardlist: 必须是经过了从小到大排序后的牌数组。 + option: 选项,默认为0 + 0-返回"同"最多的最大牌型(如果"同"相同则取"顺"最多的,如果"顺"也相同则取"值"最大的) + 1-返回"顺"最多的最大牌型(如果"顺"相同则取"同"最多的,如果"同"也相同则取"值"最大的) + 返回值:结构为[cardtype, cardlist]。其中, + cardtype为基础牌型,结构为[同,顺]; + cardlist为满足cardtype的牌数组,牌型相同时取最大值的牌。 + + 例如,当option=0时, + 牌数组为[3,7,7,7,9,9,J,J,J,Q,Q], 返回值为[[3,1], [J,J,J]] + 牌数组为[3,7,7,7,8,8,8,9,9,J,J,J,Q,Q], 返回值为[[3,2], [7,7,7,8,8,8]] + 牌数组为[3,7,7,7,8,8,8,9,9,J,J,J,Q,Q,Q],返回值为[[3,2], [J,J,J,Q,Q,Q]] + 当option=1时, + 牌数组为[2,4,5,6,8,9,J,J,Q,Q,K], 返回值为[[1,3], [J,Q,K]] + 牌数组为[2,4,4,5,5,6,6,8,9,J,J,Q,Q,K], 返回值为[[2,3], [4,4,5,5,6,6]] + 牌数组为[2,4,4,5,5,6,6,8,9,J,J,Q,Q,K,K],返回值为[[3,2], [J,J,Q,Q,K,K]]*/ + var result = []; + + //将o_cardlist按大小分组。 + //如,将[2,2,3,5,5,5,7]这样的牌数组转成[[2,2],[3],[5,5,5],[7]] + var changeto_SameGroupList = function(){ + var SameGroupList = []; + var SameGroup = []; + for (var i = 0; i < o_cardlist.length; i++){ + var o_card = o_cardlist[i]; + if (SameGroup.length == 0){ + SameGroup.push(o_card); + } else { + var card_ArithN = this.GetArithN(o_card); + var SameGroup_ArithN = this.GetArithN(SameGroup[0]); + if (card_ArithN == SameGroup_ArithN){ + SameGroup.push(o_card); + } else { + SameGroupList.push(SameGroup); + SameGroup = []; + SameGroup.push(o_card); + } + } + } + if (SameGroup.length > 0){ + SameGroupList.push(SameGroup); + } + return SameGroupList; + }.bind(this); + + var SameGroupList = changeto_SameGroupList(); + switch (option){ + case 0: //取"同"最多的牌型 [A,2,2,3,4,4,5,5,7,7,8,8,9,9,J,J,Q,Q,K,K] + //检查ShunGroup是否是连顺 + var check_ShunGroup_isShun = function(){ + for (var j = 0; j < ShunGroup.length; j++){ + if (ShunGroup[j].length == 0) { + return false; + } + if (j > 0) { + var j1_ArithN = this.GetArithN(ShunGroup[j-1][0]); + var j_ArithN = this.GetArithN(ShunGroup[j][0]); + //相减等于1表示是顺 + if (j_ArithN - j1_ArithN != 1){ + return false; + } + } + } + return true; + }.bind(this); + + result[0] = [SameGroupList[0].length, 1]; + result[1] = SameGroupList[0]; + for (var i = 1; i < SameGroupList.length; i++) { + if (SameGroupList[i].length > result[0][0]){ + result[0] = [SameGroupList[i].length, 1]; + result[1] = SameGroupList[i]; + } else if (SameGroupList[i].length == result[0][0]){ + var ArithN_i = this.GetArithN(SameGroupList[i][0]); + var ArithN_r = this.GetArithN(result[1][result[1].length - 1]); + if (ArithN_i - ArithN_r == 1){ //相减等于1表示是顺 + result[0][1] = result[0][1] + 1; + result[1] = result[1].concat(SameGroupList[i]); + } else { + if (i + result[0][1] <= SameGroupList.length){ + var ShunGroup = SameGroupList.slice(i, i + result[0][1]); + var isShun = check_ShunGroup_isShun(); + if (isShun){ + var check_Tong = true; + for (var j = 1; j < ShunGroup.length; j++) { + if (ShunGroup[j].length != result[0][0]){ + check_Tong = false; + break; + } + } + if (check_Tong) { + result[1] = []; + for (var j = 0; j < ShunGroup.length; j++) { + result[1] = result[1].concat(ShunGroup[j]); + } + i = i + result[0][1] - 1; + } + } + } + } + } + } + break; + case 1: //取"顺"最多的牌型 [A,3,4,6,8,8,9,9,J,J,J,Q,Q,Q] + //将SameGroupList转成ShunGroupList + //如,将[[2,2],[3],[5,5,5],[6,6],[7],[9,9]]转成[[[2,2],[3]], [[5,5,5],[6,6],[7]], [[9,9]]] + var changeto_ShunGroupList = function(){ + var ShunGroupList = []; + for (var i = 0; i < SameGroupList.length; i++) { + var ArithN_i = this.GetArithN(SameGroupList[i][0]); + var isFound = false; + for (var j = 0; j < ShunGroupList.length; j++) { + var ArithN_j = this.GetArithN(ShunGroupList[j][ShunGroupList[j].length - 1][0]); + if (ArithN_i - ArithN_j == 1) { + ShunGroupList[j].push(SameGroupList[i]); + isFound = true; + break; + } + } + if (!isFound){ + ShunGroupList.push([SameGroupList[i]]); + } + } + return ShunGroupList; + }.bind(this); + + //获取ShunGroup中最小的"同"数 + var get_ShunGroup_minTong = function(){ + var min_Tong = ShunGroup[0].length; + for (var j = 1; j < ShunGroup.length; j++) { + if (ShunGroup[j].length < min_Tong) { + min_Tong = ShunGroup[j].length; + } + } + return min_Tong; + }.bind(this); + + //根据ShunGroup的min_Tong设置result + var set_result_byShunGroup_byMinTong = function(){ + result[0] = [min_Tong, ShunGroup.length]; + result[1] = []; + for (var j = 0; j < ShunGroup.length; j++) { + result[1] = result[1].concat(ShunGroup[j].slice(0, min_Tong)); + } + }.bind(this); + + var ShunGroupList = changeto_ShunGroupList(); + //在ShunGroupList中取最长的"顺" + for (var i = 0; i < ShunGroupList.length; i++) { + var ShunGroup = ShunGroupList[i]; + var min_Tong = get_ShunGroup_minTong(); + if (i == 0){ + set_result_byShunGroup_byMinTong(); + } else { + if (ShunGroup.length > result[0][1]) { + set_result_byShunGroup_byMinTong(); + } else if (ShunGroup.length == result[0][1]) { + if (min_Tong >= result[0][0]){ + set_result_byShunGroup_byMinTong(); + } + } + } + } + break; + } + return result; + }, + GetMaxCardTypeByCardListA: function(o_cardlist){ + /*参数说明 o_cardlist:可以是未排序的牌数组,该函数会实现排序。*/ + this.SortCardList(o_cardlist); + return this.GetMaxCardTypeByCardList(o_cardlist); + }, + + //新建单张牌类 + NewClass: function(){ + var cls = {}; + cls.New = cls_card2.New; + cls.declare = cls_card2.declare; + cls.GetId = cls_card2.GetId; + cls.GetFlower = cls_card2.GetFlower; + cls.GetNumber = cls_card2.GetNumber; + cls.GetArithF = cls_card2.GetArithF; + cls.SetArithF = cls_card2.SetArithF; + cls.GetArithN = cls_card2.GetArithN; + cls.SetArithN = cls_card2.SetArithN; + cls.GetScore = cls_card2.GetScore; + cls.SetScore = cls_card2.SetScore; + cls.GetDeal = cls_card2.GetDeal; + cls.SetDeal = cls_card2.SetDeal; + cls.GetStart = cls_card2.GetStart; + cls.SetStart = cls_card2.SetStart; + cls.GetPlay = cls_card2.GetPlay; + cls.SetPlay = cls_card2.SetPlay; + cls.GetIndex = cls_card2.GetIndex; + cls.SetIndex = cls_card2.SetIndex; + cls.GetOver = cls_card2.GetOver; + cls.SetOver = cls_card2.SetOver; + cls.GetTag1 = cls_card2.GetTag1; + cls.SetTag1 = cls_card2.SetTag1; + cls.GetTag2 = cls_card2.GetTag2; + cls.SetTag2 = cls_card2.SetTag2; + cls.GetTag3 = cls_card2.GetTag3; + cls.SetTag3 = cls_card2.SetTag3; + cls.FilterCardListByArithF = cls_card2.FilterCardListByArithF; + cls.FilterCardListByArithN = cls_card2.FilterCardListByArithN; + cls.SortCardList = cls_card2.SortCardList; + cls.GetCardListByCardType = cls_card2.GetCardListByCardType; + cls.GetCardListByCardTypeA = cls_card2.GetCardListByCardTypeA; + cls.GetCardListByCardTypeB = cls_card2.GetCardListByCardTypeB; + cls.GetCardListByExtendCardTypeA = cls_card2.GetCardListByExtendCardTypeA; + cls.GetMaxCardTypeByCardList = cls_card2.GetMaxCardTypeByCardList; + cls.GetMaxCardTypeByCardListA = cls_card2.GetMaxCardTypeByCardListA; + return cls; + } +} diff --git a/server/class/class.desk.js b/server/class/class.desk.js new file mode 100644 index 0000000..89a6918 --- /dev/null +++ b/server/class/class.desk.js @@ -0,0 +1,225 @@ +/////////////////////////////////////////////////// +/////////////// cls_desk: 牌桌基础类 ////////////// +/////////////////////////////////////////////////// +var cls_desk = cls_desk || { + + //创建单个牌桌实例 + new: function(o_room){ + /* + 参数说明: + o_room 牌桌对应的房间对象 + + 补充说明: + 1,房间对象由框架负责创建和释放。 + 2,房间对象管理的是人,解决诸如谁在房间里,谁在哪个位置上,谁进了房间,谁离开了房间等问题。 + 3,牌桌对象由游戏负责创建,牌桌对象会随着房间的释放而释放,子游戏不用管牌桌的释放。 + 4,牌桌对象管理的是牌,解决诸如哪个位置上发了什么牌,出了什么牌等问题。 + 5,此牌桌类是基础牌桌类,各子游戏需要在继承此基础类的情况下编写自己的牌桌类。 + */ + var desk = {}; + + //牌桌与房间互挂 + o_room.o_desk = desk; //可通过房间对象的o_desk属性访问牌桌对象 + desk.o_room = o_room; //可通过牌桌对象的o_desk属性访问房间对象 + + //数据 + desk.data = {}; //要求data必须是纯数据,不能在data下面写方法、函数、定时器 + //定时器 + desk.timer = {}; //可根据实际需要在desk.timer下定义多个定时器 + //收发包 + desk.pack = []; //调试状态下记录牌桌的收包发包情况 + //流程方法 + desk.flow = {}; + //算法方法 + desk.arith = {}; + + //////////在下面定义实例的属性 + //数据-小局列表 + desk.data.asetlist = []; //每开一小局则往该数组中添加一个aset小局对象,[aset, aset, aset, ...], [{}, {}, {}, ...], [[], [], [], ...],数组长度即可表示当前是第几小局,数组最后一个小局对象即是当前小局 + + + /////////在下面定义实例的方法,要求实例的方法必须调用类的方法,以节省内存资源,要求实例的方法名要与类的方法名保持一致 + //记录游戏的收包情况(不包括框架的收包),要求前端向服务器发包时根据配置参数的设置情况将前端内存数据一起发上来,数据放在pack.data.d里面,服务器收到包后必须调用该方法 + desk.flow.save_receivepack = function(pack, seat, playerid){ + return cls_desk.save_receivepack(desk, pack, seat, playerid); + } + //记录游戏的发包情况(不包括框架的发包),要求服务器向前端发包后一定要调用该方法 + desk.flow.save_sendpack = function(pack, seat, playerid){ + return cls_desk.save_sendpack(desk, pack, seat, playerid); + } + //将当前小局的收发包情况保存成TXT + desk.flow.save_pack_curraset = function(callback_succ, callback_fail){ + return cls_desk.save_pack_curraset(desk, callback_succ, callback_fail); + } + //输出日志 + desk.flow.save_log = function(dir, file, log){ + return cls_desk.save_log(desk, dir, file, log); + } + //清日志 + desk.flow.clear_log = function(dir, file){ + return cls_desk.clear_log(desk, dir, file); + } + //获取当前小局对象 + desk.flow.get_curraset = function(){ + return cls_desk.get_curraset(desk); //必须调用类的方法,且需要将实例对象作为参数传入类方法,这里只能写一句代码 + } + //获取当前小局局数 + desk.flow.get_currasetnum = function(){ + return cls_desk.get_currasetnum(desk); //必须调用类的方法,且需要将实例对象作为参数传入类方法,这里只能写一句代码 + } + + return desk; + }, + + ////////////以下都是类的方法,要求所有的逻辑算法全部都要用类方法来实现 + //记录收包情况 + save_receivepack: function(o_desk, pack, seat, playerid){ + try{ + if (o_desk.o_room.o_game.method.isdebugger()){ //如果是调试状态下,则记录收包情况 + var o_save = {}; + o_save.time = min_now(); //时间 + o_save.type = 0; //收包 + o_save.seat = seat; //位置 + o_save.playerid = playerid; //玩家id + o_save.data = {}; + //前端内存数据 + o_save.data.client = pack.data.d; + //服务器房间内存数据 + o_save.data.room = o_desk.o_room.method.get_roominfo(); + //服务器牌桌内存数据 + o_save.data.desk = min_copyjson(o_desk.data); + for (var i = 0; i < o_save.data.desk.asetlist.length; i++) { + if (i < o_save.data.desk.asetlist.length - 1){ + o_save.data.desk.asetlist[i] = null; + }; + }; + delete pack.data.d; + //接收到的数据包 + o_save.pack = pack; + o_desk.pack.push(o_save); + } + }catch(e){}; + }, + //记录发包情况 + save_sendpack: function(o_desk, pack, seat, playerid){ + try{ + if (o_desk.o_room.o_game.method.isdebugger()){ //如果是调试状态下,则记录发包情况 + var o_save = {}; + o_save.time = min_now(); //时间 + o_save.type = 1; //发包 + o_save.seat = seat; //位置 + o_save.playerid = playerid; //玩家id + o_save.data = {}; + //服务器房间内存数据 + o_save.data.room = o_desk.o_room.method.get_roominfo(); + //服务器牌桌内存数据 + o_save.data.desk = min_copyjson(o_desk.data); + for (var i = 0; i < o_save.data.desk.asetlist.length; i++) { + if (i < o_save.data.desk.asetlist.length - 1){ + o_save.data.desk.asetlist[i] = null; + }; + }; + //发送的数据包 + o_save.pack = pack; + o_desk.pack.push(o_save); + } + }catch(e){}; + }, + //将当前小局的收发包情况保存成TXT + save_pack_curraset: function(o_desk, callback_succ, callback_fail){ + try{ + if (o_desk.o_room.o_game.method.isdebugger()){ + for (var i = 0; i < o_desk.pack.length; i++) { + o_desk.pack[i].saveflag = 1; + }; + + var o_makewartime = new Date(o_desk.o_room.makewartime); + var yyyy = o_makewartime.getFullYear(); + var mm = o_makewartime.getMonth() + 1; + var dd = o_makewartime.getDate(); + var dir = yyyy + "-" + mm + "-" + dd; + var file = o_desk.o_room.o_game.o_agent.agentid + "-" + o_desk.o_room.o_game.gameid + "-" + o_desk.o_room.roomcode + "-" + o_desk.flow.get_currasetnum() + ".txt"; + var data = JSON.stringify(o_desk.pack); + + var cfg = {}; + // cfg.url = "http://120.26.52.206:8080/debug.php"; + cfg.url = "http://10.117.216.66:8080/debug.php"; + cfg.data = "dir=" + dir + "&file=" + file + "&data=" + data; + cfg.type = "POST"; + cfg.success = function(str){ + for (var i = 0; i < o_desk.pack.length; i++) { + if (o_desk.pack[i].saveflag){ + o_desk.pack.splice(i, 1); + i--; + }; + }; + if(callback_succ){ + callback_succ(); + }; + }; + cfg.error = function(str){ + for (var i = 0; i < o_desk.pack.length; i++) { + if (o_desk.pack[i].saveflag){ + delete o_desk.pack[i].saveflag; + }; + }; + console.log(min_now() + " 保存牌局调试数据失败1"); + console.log("dir=" + dir); + console.log("file=" + file); + console.log(str); + if(callback_fail){ + callback_fail(); + }; + }; + min_http(cfg); + } + }catch(e){ + console.log(min_now() + " 保存牌局调试数据失败2"); + console.log("dir=" + dir); + console.log("file=" + file); + console.log(e); + }; + }, + //输出日志 + save_log: function(o_desk, dir, file, log){ + try{ + var cfg = {}; + // cfg.url = "http://120.26.52.206:8080/savelog.php"; + cfg.url = "http://10.117.216.66:8080/savelog.php"; + cfg.data = "dir=" + dir + "&file=" + file + "&data=" + log; + cfg.type = "POST"; + cfg.success = function(str){}; + cfg.error = function(str){ + console.log(min_now() + " 输出日志失败:" + dir + "/" + file); + console.log(str); + }; + min_http(cfg); + }catch(e){} + }, + //清日志 + clear_log: function(o_desk, dir, file){ + try{ + var cfg = {}; + // cfg.url = "http://120.26.52.206:8080/clearlog.php"; + cfg.url = "http://10.117.216.66:8080/clearlog.php"; + cfg.data = "dir=" + dir + "&file=" + file; + cfg.type = "POST"; + cfg.success = function(str){}; + cfg.error = function(str){ + console.log(min_now() + " 清空日志失败:" + dir + "/" + file); + console.log(str); + }; + min_http(cfg); + }catch(e){} + }, + //获取当前小局对象 + get_curraset: function(o_desk){ + return o_desk.data.asetlist[desk.data.asetlist.length - 1]; + }, + //获取当前小局局数 + get_currasetnum: function(o_desk){ + return o_desk.data.asetlist.length; + } +} + + diff --git a/server/class/class.desk2.js b/server/class/class.desk2.js new file mode 100644 index 0000000..c122728 --- /dev/null +++ b/server/class/class.desk2.js @@ -0,0 +1,287 @@ +/*================= cls_desk2: 牌桌基础类 ================= +说明: +1,房间对象由框架负责创建和释放。 +2,房间对象管理的是人,解决诸如谁在房间里,谁在哪个位置上,谁进了房间,谁离开了房间等问题。 +3,牌桌对象由游戏负责创建,牌桌对象会随着房间的释放而释放,子游戏不用管牌桌的释放。 +4,牌桌对象管理的是牌,解决诸如哪个位置上发了什么牌,出了什么牌等问题。 + +注意: +1,此牌桌类是基础类;各子游戏需要在继承此基础类的情况下编写自己的牌桌类。 +2,子游戏开发人员不能修改该文件。 +3,子游戏只能调用实例方法,不能直接调用类方法。 +=========================================================*/ +var cls_desk2 = { + + //debug服务器地址 + // debugserver: "120.26.52.206:8080", //外网地址,开发人员本地调试时使用该地址 + debugserver: "10.117.216.66:8080", //内网地址,正式服务器上使用该地址 + + //创建实例 + new: function(object_room){//参数说明:object_room 牌桌对应的房间对象 + var object = {}; + + //牌桌与房间互挂 + object_room.o_desk = object; //可通过房间对象的o_desk属性访问牌桌对象 + object.o_room = object_room; //可通过牌桌对象的o_desk属性访问房间对象 + + //=============== 在下面定义实例的属性 =============== + //数据 + object.data = {}; //要求data必须是纯数据,不能在data下面写方法、函数、定时器 + //定时器 + object.timer = {}; //可根据实际需要在desk.timer下定义多个定时器 + //收发包 + object.pack = []; //调试状态下记录牌桌的收包发包情况 + //bug调试方法 + object.debug = {}; + //游戏内容方法 + object.method = {}; + + //小局列表。每开一小局则往该数组中添加一个aset小局对象,格式如[aset, aset, aset, ...],数组长度即可表示当前是第几小局,数组最后一个小局对象即是当前小局 + object.data.asetlist = []; + + + //=============== 在下面定义实例的方法 =============== + /*1,要求实例的方法必须调用类的方法,以节省内存资源。 + 2,要求实例的方法必须只用一句代码调用类的方法,并将实例对象作为第一个参数传给类方法。 + 3,要求实例的方法名要与类的方法名保持一致。*/ + + //====================== bug调试 ===================== + //记录游戏的收包情况(不包括框架的收包),要求前端向服务器发包时根据isdebugger参数的配置情况将前端的内存数据一起发上来,数据放在pack.data.d里面,服务器收到前端发来的包后必须调用该方法 + //参数说明:pack前端发来的包,seat哪个位置发来的包,哪个玩家发来的包 + object.debug.save_receivepack = function(pack, seat, playerid){ + return cls_desk2.save_receivepack(object, pack, seat, playerid); + } + //记录游戏的发包情况(不包括框架的发包),要求服务器向前端发包后一定要调用该方法 + /*参数说明:pack向前端发的包,seat给哪个位置发包,给哪个玩家发包。 + 如果是向所有玩家发包则seat传-1,playerid传null, + 如果是向多个玩家发包则seat传位置数组,playerid传玩家id数组*/ + object.debug.save_sendpack = function(pack, seat, playerid){ + return cls_desk2.save_sendpack(object, pack, seat, playerid); + } + //将当前小局的收发包情况保存到debug服务器上,要求小局结算后,下一个小局新建前调用此方法 + //参数说明:callback_succ保存成功的回调函数,callback_fail保存失败的回调函数 + object.debug.save_pack_curraset = function(callback_succ, callback_fail){ + return cls_desk2.save_pack_curraset(object, callback_succ, callback_fail); + } + //输出调试日志到debug服务器上,输入后可在debug服务器上或浏览器中查看日志内容 + //参数说明:dir日志目录名,file日志文件名,log要输出的日志内容 + object.debug.save_log = function(dir, file, log){ + return cls_desk2.save_log(object, dir, file, log); + } + //清空debug服务器上的调试日志 + //参数说明:dir日志目录名,file日志文件名 + object.debug.clear_log = function(dir, file){ + return cls_desk2.clear_log(object, dir, file); + } + + + //====================== 游戏内容 ===================== + //对应的小局类 + object.method.AsetClass = function(){ + return cls_aset2; + } + //新建一小局 + object.method.NewAset = function(){ + return cls_desk2.NewAset(object); + } + //大局结算 + object.method.CloseAccount = function(){ + return cls_desk2.CloseAccount(object); + } + + //获取当前小局对象 + object.method.CurrAset = function(){ + return cls_desk2.CurrAset(object); + } + //获取当前小局是第几小局 + object.method.CurrAsetNum = function(){ + return cls_desk2.CurrAsetNum(object); + } + //获取下一个有人的位置 + object.method.GetNextSeat = function(seat){ + return cls_desk2.GetNextSeat(object, seat); + } + + return object; + }, + + //================ 在下面定义类的方法 ================ + //===================== bug调试 ====================== + //记录服务器收包情况 + save_receivepack: function(o_desk, pack, seat, playerid){ + try{ + if (o_desk.o_room.o_game.method.isdebugger()){ //如果是调试状态下,则记录收包情况 + var o_save = {}; + o_save.time = min_now(); //记录的时间 + o_save.type = 0; //服务器收到前端的发包 + o_save.seat = seat; //哪个位置发来的包 + o_save.playerid = playerid; //哪个玩家发来的包 + o_save.data = {}; + //前端内存数据 + o_save.data.client = pack.data.d; + //服务器房间内存数据 + o_save.data.room = o_desk.o_room.method.get_roominfo(); + //服务器牌桌内存数据 + o_save.data.desk = min_copyjson(o_desk.data); + for (var i = 0; i < o_save.data.desk.asetlist.length; i++) { + if (i < o_save.data.desk.asetlist.length - 1){ + o_save.data.desk.asetlist[i] = null; + }; + }; + delete pack.data.d; + //接收到的数据包 + o_save.pack = pack; + o_desk.pack.push(o_save); + } + }catch(e){}; + }, + //记录服务器发包情况 + save_sendpack: function(o_desk, pack, seat, playerid){ + try{ + if (o_desk.o_room.o_game.method.isdebugger()){ //如果是调试状态下,则记录发包情况 + var o_save = {}; + o_save.time = min_now(); //记录的时间 + o_save.type = 1; //服务器向前端发包 + o_save.seat = seat; //服务器向哪个位置发包 + o_save.playerid = playerid; //服务器向哪个玩家发包 + o_save.data = {}; + //服务器房间内存数据 + o_save.data.room = o_desk.o_room.method.get_roominfo(); + //服务器牌桌内存数据 + o_save.data.desk = min_copyjson(o_desk.data); + for (var i = 0; i < o_save.data.desk.asetlist.length; i++) { + if (i < o_save.data.desk.asetlist.length - 1){ + o_save.data.desk.asetlist[i] = null; + }; + }; + //发送的数据包 + o_save.pack = pack; + o_desk.pack.push(o_save); + } + }catch(e){}; + }, + //将当前小局的收发包情况保存到debug服务器上 + save_pack_curraset: function(o_desk, callback_succ, callback_fail){ + try{ + if (o_desk.o_room.o_game.method.isdebugger()){ + for (var i = 0; i < o_desk.pack.length; i++) { + o_desk.pack[i].saveflag = 1; + }; + + var o_makewartime = new Date(o_desk.o_room.makewartime); + var yyyy = o_makewartime.getFullYear(); + var mm = o_makewartime.getMonth() + 1; + var dd = o_makewartime.getDate(); + var dir = yyyy + "-" + mm + "-" + dd; + var file = o_desk.o_room.o_game.o_agent.agentid + "-" + o_desk.o_room.o_game.gameid + "-" + o_desk.o_room.roomcode + "-" + o_desk.method.CurrAsetNum() + ".txt"; + var data = JSON.stringify(o_desk.pack); + + var cfg = {}; + cfg.url = "http://" + cls_desk2.debugserver + "/debug.php"; + cfg.data = "dir=" + dir + "&file=" + file + "&data=" + data; + cfg.type = "POST"; + cfg.success = function(str){ + for (var i = 0; i < o_desk.pack.length; i++) { + if (o_desk.pack[i].saveflag){ + o_desk.pack.splice(i, 1); + i--; + }; + }; + if(callback_succ){ + callback_succ(); + }; + }; + cfg.error = function(str){ + for (var i = 0; i < o_desk.pack.length; i++) { + if (o_desk.pack[i].saveflag){ + delete o_desk.pack[i].saveflag; + }; + }; + console.log(min_now() + " 保存牌局调试数据失败1"); + console.log("dir=" + dir); + console.log("file=" + file); + console.log(str); + if(callback_fail){ + callback_fail(); + }; + }; + min_http(cfg); + } + }catch(e){ + console.log(min_now() + " 保存牌局调试数据失败2"); + console.log("dir=" + dir); + console.log("file=" + file); + console.log(e); + }; + }, + //输出调试日志到debug服务器上 + save_log: function(o_desk, dir, file, log){ + try{ + if (typeof(log) == "object"){ + log = JSON.stringify(log); + } + var cfg = {}; + cfg.url = "http://" + cls_desk2.debugserver + "/savelog.php"; + cfg.data = "dir=" + dir + "&file=" + file + "&data=" + log; + cfg.type = "POST"; + cfg.success = function(str){}; + cfg.error = function(str){ + console.log(min_now() + " 输出调试日志失败"); + console.log(str); + }; + min_http(cfg); + }catch(e){} + }, + //清空debug服务器上的调试日志 + clear_log: function(o_desk, dir, file){ + try{ + var cfg = {}; + cfg.url = "http://" + cls_desk2.debugserver + "/clearlog.php"; + cfg.data = "dir=" + dir + "&file=" + file; + cfg.type = "POST"; + cfg.success = function(str){}; + cfg.error = function(str){ + console.log(min_now() + " 清空调试日志失败"); + console.log(str); + }; + min_http(cfg); + }catch(e){} + }, + + //===================== 游戏内容 ====================== + //新建一小局 + NewAset: function(o_desk){ + var o_aset = o_desk.method.AsetClass().New(); + o_desk.data.asetlist.push(o_aset); + return o_aset; + }, + //获取当前小局对象 + CurrAset: function(o_desk){ + return o_desk.data.asetlist[o_desk.data.asetlist.length - 1]; + }, + //获取当前小局局数 + CurrAsetNum: function(o_desk){ + return o_desk.data.asetlist.length; + }, + //获取下一个有人的位置 + GetNextSeat: function(o_desk, seat){ + if (seat < 0){ + return -1; + } + var seatcount = o_desk.o_room.o_game.seatcount; + if (seat >= seatcount){ + return -1; + } + var i = seat; + var o_nextplayer = null; + while (!o_nextplayer){ + i = (i + 1) % seatcount; + o_nextplayer = o_desk.o_room.seatlist[i]; + } + return i; + }, + //大局结算 + CloseAccount: function(o_desk){ + + } +} diff --git a/server/class/class.flow2.js b/server/class/class.flow2.js new file mode 100644 index 0000000..abc8f24 --- /dev/null +++ b/server/class/class.flow2.js @@ -0,0 +1,135 @@ +/*=============== cls_flowMgr2: 流程管理类 =============== +注意: +1,子游戏开发人员不能修改该文件。 +=========================================================*/ +var cls_flowMgr2 = { + //创建实例 + new: function(){ + object = {}; + //流程库 + object.lib = []; + //流程配置 + object.steplist = []; + //当前流程,即steplist的数组下标 + object.idx = -1; + + object.method = {}; + + //在流程库中添加一个流程 + object.method.addflow = function(id, name, func_start, func_over, time, flag1, flag2){ + /*参数说明: + id : 流程id,必须唯一 + name : 流程名,主要起到说明和备注的作用 + func_start: 进入该流程时执行的函数(先完成本流程的开始处理,再根据time开启定时器) + func_over : 该流程结束时执行的函数(先完成本流程的结束处理,再进入下一流程) + time : 进入该流程后开启的定时器(自动触发func_over结束该流程)时长,毫秒 + -1: 不需要开启定时器,需要用户触发func_over结束流程 + 0: 不需要开启定时器,执行完func_start后立即执行func_over结束流程 + >0: 需要开启定时器,定时器自动触发func_over结束流程 + flag1: 进入流程时执行完func_start后,是否由子游戏开启本流程的定时器,默认为0 + 0: 由流程管理对象管理 + 1: 由子游戏管理 + flag2: 结束流程时执行完func_over后,是否由子游戏关闭定时器并进入下一流程,默认为0 + 0: 由流程管理对象管理 + 1: 由子游戏管理*/ + return cls_flowMgr2.addflow(object, id, name, func_start, func_over, time, flag1, flag2); + } + //设置流程。参数steplist为流程配置,是一个流程id数组,如[2,1,1,3,2,4] + object.method.set = function(steplist){ + object.steplist = steplist; + } + //获取当前流程 + object.method.currflow = function(){ + cls_flowMgr2.currflow(object); + } + //开始按流程配置执行 + object.method.startflow = function(){ + cls_flowMgr2.startflow(object); + } + //进入下一流程 + object.method.nextflow = function(){ + cls_flowMgr2.nextflow(object); + } + return object; + }, + + //在流程库中添加一个流程 + addflow: function(o_flowMgr, id, name, func_start, func_over, time, flag1, flag2){ + var o_flow = {}; + o_flow.id = id; //流程id + o_flow.name = name; //流程名 + o_flow.func_start = func_start; //进入该流程是执行的函数 + o_flow.func_over = func_over; //该流程结束时执行的函数 + o_flow.time = time; //进入该流程后开启的定时器时长 + o_flow.flag1 = flag1; + o_flow.flag2 = flag2; + o_flow.timer = null; //定时器id + o_flowMgr.lib.push(o_flow); + return o_flow; + }, + //获取当前流程 + currflow: function(o_flowMgr){ + if (!o_flowMgr.steplist || o_flowMgr.steplist.length == 0){ + return null; + } + if (o_flowMgr.idx == -1 || o_flowMgr.idx >= o_flowMgr.steplist.length){ + return null; + } + var flowid = o_flowMgr.steplist[o_flowMgr.idx]; + var flowidx = min_ary_indexof(o_flowMgr.lib, flowid, "id"); + if (flowidx == -1){ + return null; + } + return o_flowMgr.lib[flowidx]; + }, + //开始按流程配置执行程序 + startflow: function(o_flowMgr){ + if (o_flowMgr.lib.length == 0){ + return; + } + if (o_flowMgr.steplist.length == 0){ + return; + } + o_flowMgr.idx = 0; + this.exec(o_flowMgr); + }, + //进入下一流程 + nextflow: function(o_flowMgr){ + var o_flow = this.currflow(o_flowMgr); + if (o_flow){ + if (o_flow.func_over) { + o_flow.func_over(); + } + if (!o_flow.flag2){ + if (o_flow.timer){ + clearTimeout(o_flow.timer); + o_flow.timer = null; + } + } + } + o_flowMgr.idx = o_flowMgr.idx + 1; + this.exec(o_flowMgr); + }, + //执行当前流程 + exec: function(o_flowMgr){ + var o_flow = this.currflow(o_flowMgr); + if (!o_flow){ + return; + } + if (!o_flow.func_start){ + return; + } + o_flow.func_start(); + if (o_flow.time == 0) { + this.nextflow(o_flowMgr); + } else if (o_flow.time > 0) { + //需要开启定时器 + if (!o_flow.flag1) { + if (o_flow.func_over) { + o_flow.timer = setTimeout(o_flow.func_over, o_flow.time); + } + } + } + } +} + diff --git a/server/class/class.mod.js b/server/class/class.mod.js new file mode 100644 index 0000000..a795f47 --- /dev/null +++ b/server/class/class.mod.js @@ -0,0 +1,70 @@ +/////////////////////////////////////////////////// +//////////////cls_mod: 模块基础类////////////////// +/////////////////////////////////////////////////// +var cls_mod = cls_mod || { + //构造函数 + new: function(_str_modname, _str_routename, _obj_app) { + /* + _str_modname 模块名称 + _str_routename 模块的路由名称 + _obj_app 所属的应用对象 + */ + + //创建一个实例 + var mod = {}; + + // ================= 实例的公有属性 ================= // + //模块名称 + mod.modname = _str_modname; + //路由名称 + mod.routename = _str_routename; + //所属的应用 + mod.app = _obj_app; + //将mod对象添加到app的模块列表中 + mod.app.modlist.push(mod); + mod.app[_str_modname] = mod; + + // ================= 实例的公有方法 ================= // + //收包处理 + mod.DoPack = function(_msg) { + return cls_mod.DoPack(mod, _msg); + } + + //输出mod + cls_mod.OutputMod(mod); + + return mod; + }, + + DoPack: function(_obj_mod, _msg) { + if (typeof(_msg) == "string") { + _msg = JSON.parse(_msg); //转换json + } + //执行rpc + if (_msg.rpc) { + if (min_ExitsFunction(_obj_mod[_msg.rpc])) { + return _obj_mod[_msg.rpc](_msg); + } + } + }, + + OutputMod: function(_obj_mod) { + if (typeof global !== "undefined") + {} else { + global = {}; + } + + if (typeof module !== "undefined") + {} else { + module = {}; + module.exports = {}; + } + + global[_obj_mod.modname] = _obj_mod; + global[_obj_mod.modname].DoPack = _obj_mod.DoPack; + + if (module) { + module.exports[_obj_mod.modname] = _obj_mod; + } + } +} diff --git a/server/class/class.mysql.js b/server/class/class.mysql.js new file mode 100644 index 0000000..8c6cc39 --- /dev/null +++ b/server/class/class.mysql.js @@ -0,0 +1,93 @@ +/////////////////////////////////////////////////// +///////////// cls_mysql: 数据库操作 /////////////// +/////////////////////////////////////////////////// +var cls_mysql = cls_mysql || { + + //构造函数 + new: function(url, func_fail_sendpack){ + var mysql = {}; + //php地址 + mysql.url = url; + //发生错误时的回包函数 + mysql.func_fail_sendpack = func_fail_sendpack; + + //执行sql语句 + mysql.execsql = function(method, sql, para, func_succ, pack) { + /* + method: php方法名 execsql/opensql + sql: 要执行的sql语句 + para: 要执行的sql语句的参数数组 + func_succ: sql语句执行成功后的回调函数 + pack: 客户端发来的数据包,执行失败的话会根据pack返回客户端执行失败的结果 + */ + var data = {} + data.m = method; + data.s = sql; + data.p = para; + + var strdata = JSON.stringify(data); + //将=号替换为^,php收到后再替换回来,以避免=号POST传参会截断的问题 + strdata = strdata.replace(/\=/g,"^"); + //将+号替换为#,php收到后再替换回来,以避免+号POST传参会截断的问题 + strdata = strdata.replace(/\+/g,"#"); + //将&号替换为!,php收到后再替换回来,以避免&号POST传参会截断的问题 + strdata = strdata.replace(/\&/g,"!"); + + var cfg = {}; + cfg.url = mysql.url; + cfg.data = "data=" + strdata; + cfg.type = "POST"; + cfg.success = function(str) { + try { + var result = JSON.parse(str); + } catch(e) { + console.log(min_now() + " 数据库执行结果转换json失败."); + console.log(str); + console.log(data); + return; + } + + if (result.state != 0) { + console.log(min_now() + " 请求" + mysql.url + "失败."); + console.log(result); + console.log(data); + + if (pack) { + console.log(JSON.stringify(pack)); + //返回客户端执行失败的结果 + if (mysql.func_fail_sendpack) { + pack.data = {}; + pack.data.state = result.state; + pack.data.error = result.error; + mysql.func_fail_sendpack(pack); + } + } + } else { + //执行sql成功执行回调函数 + // console.log(result); + if (func_succ) { + func_succ(result.data); + } + } + }; + cfg.error = function(str){ + console.log(min_now() + " 请求" + mysql.url + "失败."); + console.log(str); + console.log(data); + + if (pack) { + console.log(JSON.stringify(pack)); + //返回客户端执行失败的结果 + if (mysql.func_fail_sendpack) { + pack.data = {}; + pack.data.state = 99; + pack.data.error = "请求数据库失败."; + mysql.func_fail_sendpack(pack); + } + } + }; + min_http(cfg); + } + return mysql; + } +} diff --git a/server/class/class.pai.js b/server/class/class.pai.js new file mode 100644 index 0000000..73cc085 --- /dev/null +++ b/server/class/class.pai.js @@ -0,0 +1,70 @@ +/////////////////////////////////////////////////// +////////////////// cls_pai: 单张牌 //////////////// +/////////////////////////////////////////////////// +var cls_pai = cls_pai || { + + //创建单张牌实例 + new: function(id, score, dealowner){ + /* + 参数说明: + id 牌的绝对id + score 牌在游戏中的分值 + dealowner 发牌状态 + + 扑克牌的统一编码规则: + 1,牌面花色的定义 5:王 4:黑桃 3:红心 2:梅花 1:方块 + 2,牌面数值的定义 1:A 2:2 3:3 ... 9:9 10:10 11:J 12:Q 13:K 53:小王 54:大王 + */ + var pai = {}; + + //属性 + pai.id = id; //绝对id,即数组下标,从0开始计数 + pai.score = score; //牌在游戏中的分值 + pai.dealowner = dealowner; //发牌状态 + //-1:规则去除的牌 + // 0:未发的牌,即底牌 + //>0:发牌发到谁手上,从1开始计数,即座位编号+1,座位编号是从0开始计数的 + pai.playround = -1; //出牌状态 + //-1:未出的牌 + // 0:埋牌 + //>0:牌是第几轮出出去的,从1开始计数 + pai.playindex = -1; //本轮中的出牌顺序,从1开始计数 + pai.playowner = -1; //出牌后被谁得到,与座位编号对应 + + //方法 + pai.method = {}; + + //牌面花色 + pai.method.get_flower = function(){ + return cls_pai.get_flower(pai); + } + + //牌面数值 + pai.method.get_number = function(){ + return cls_pai.get_number(pai); + } + + return pai; + }, + + //获取牌面花色 + get_flower: function(o_pai) { + var yu = o_pai.id % 54; + if (yu == 52 || yu == 53){ //小王大王 + return 5; + } + return parseInt(yu / 13) + 1; + }, + + //获取牌面数值 + get_number: function(o_pai) { + var yu = o_pai.id % 54; + if (yu == 52){ //小王 + return 53; + } + if (yu == 53){ //大王 + return 54; + } + return yu % 13 + 1; + } +} diff --git a/server/config/update中属性说明.txt b/server/config/update中属性说明.txt new file mode 100644 index 0000000..8475b2f --- /dev/null +++ b/server/config/update中属性说明.txt @@ -0,0 +1,312 @@ +showmessage +1£¬appÆô¶¯Ìáʾ£¬appÓõ½¡£ +2£¬ËùÓв㶼ÓиÃÊôÐԡ£ + +scrollmsg +1£¬ÓÎϷÖеĹö¶¯ÐÅϢÀ¸£¬ÓÎϷÖÐÓõ½¡£ +2£¬ËùÓв㶼ÓиÃÊôÐԡ£ + +noticemsg +1£¬ÓÎϷÖеÄ֪ͨÐÅϢÀ¸£¬ÒÔ#ºűíʾ»»ÐУ¬ÓÎϷÖÐÓõ½¡£ +2£¬ËùÓв㶼ÓиÃÊôÐԡ£ + +topupurl +1£¬·¿¿¨³äֵµÄÁ´½ӣ¬¸ÃÊôÐÔֵ²»Ϊ¿ÕÔòÌøתÁ´½ӣ¬ÓÎϷÖÐÓõ½¡£ +2£¬¶¥²㡢´úÀíagent¡¢ÓÎϷgame¡¢ÇþµÀchannel¶¼ÓиÃÊôÐԡ£ + +logimage +1£¬logͼƬ£¬ÓÎϷÖÐÓõ½¡£ +2£¬´úÀíagent¡¢ÓÎϷgame¡¢ÇþµÀchannel¶¼ÓиÃÊôÐԡ£ + +logimage2 +1£¬´úÀíÉ̵ÄlogͼƬ£¬¹«ÖںÅҳÃæÖÐÓõ½£¬ҳÃæÓÒÉϽÇͼ±ꡣ +2£¬´úÀíagentÓиÃÊôÐԡ£ + +sharename +1£¬·ÖÏíµÄÄÚÈÝÃû³ƣ¬ÓÎϷÖÐÓõ½¡£ +2£¬ÓÎϷgame¡¢ÇþµÀchannel¶¼ÓиÃÊôÐԡ£ + +game_version +1£¬ÓÎϷ°汾ºţ¬ÓÎϷÈÈÉý¼¶¡¢ÓÎϷÖеǼÀ¹½ØÍæ¼ÒʱÓõ½¡£ +2£¬ÓÎϷgame¡¢ÇþµÀchannel¡¢Êг¡market¶¼ÓиÃÊôÐԡ£ + +game_zip +1£¬ÓÎϷzipÎļþ£¬ÓÎϷÈÈÉý¼¶ʱÓõ½¡£ +2£¬ÓÎϷgame¡¢ÇþµÀchannel¡¢Êг¡market¶¼ÓиÃÊôÐԡ£ + +game_size +1£¬ÓÎϷzipÎļþ´óС£¬ÓÎϷ´óÌüÖÐÓõ½¡£ +2£¬ÓÎϷgame¡¢ÇþµÀchannel¡¢Êг¡market¶¼ÓиÃÊôÐԡ£ + +game_hall_image +1£¬´óÌüÖÐ×ÓÓÎϷµÄͼ±ꡣ +2£¬ÓÎϷgameÓиÃÊôÐԡ£ + +game_hall_dir +1£¬´óÌüÖÐ×ÓÓÎϷµÄĿ¼¡£ +2£¬ÓÎϷgameÓиÃÊôÐԡ£ + +game_down_image +1£¬ÏÂÔØҳÃæµÄÓÎϷͼ±ꡣ +2£¬ÓÎϷgameÓиÃÊôÐԡ£ + +game_down_state +1£¬ÏÂÔØҳÃæµÄÓÎϷ״̬£¬0-·¢²¼ÖÐ 1-¿ª·¢ÖÐ 2-²âÊÔÖС£ +2£¬ÓÎϷgameÓиÃÊôÐԡ£ + +game_down_memo +1£¬ÏÂÔØҳÃæµÄÓÎϷһ¾仰ÃèÊö¡£ +2£¬ÓÎϷgameÓиÃÊôÐԡ£ + +ios_defdownload_marketid +1£¬ƻ¹ûϵͳÏÂÔØҳÃæµĶÔӦµÄÊг¡marketid¡£ +2£¬ÇþµÀchannelÓиÃÊôÐԡ£ + +and_defdownload_marketid +1£¬°²׿ϵͳÏÂÔØҳÃæµĶÔӦµÄÊг¡marketid¡£ +2£¬ÇþµÀchannelÓиÃÊôÐԡ£ + +app_version +1£¬app°汾ºţ¬appÉý¼¶ʱÓõ½¡£ +2£¬Êг¡marketÓиÃÊôÐԡ£ + +app_download +1£¬appÏÂÔصØַ£¬appÉý¼¶ʱÓõ½¡£ +2£¬Êг¡marketÓиÃÊôÐԡ£ + +app_size +1£¬app°²װ°ü´óС£¬appÉý¼¶ʱÓõ½¡£ +2£¬Êг¡marketÓиÃÊôÐԡ£ + +url_match +1£¬±ÈÈüµÄÁ´½ӵØַ¡£ +2£¬¶¥²㡢´úÀíagent¡¢ÓÎϷgame¶¼ÓиÃÊôÐԡ£ + +url_match2 +1£¬·þÎñÆ÷±ÈÈüÊý¾ݷ¢Ë͵Øַ¡£ +2£¬¶¥²㡢´úÀíagent¡¢ÓÎϷgame¶¼ÓиÃÊôÐԡ£ + +html_buyroomcard +1£¬¹«ÖںÅҳÃæÊǷñÓйºÂ򷿿¨¹¦ÄÜ 1-ûÓÐ 2-ÓС£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +html_applysales +1£¬ÉêÇë³ÉΪ´úÀíµķ½ʽ 1-Ìîд΢ÐźŲ¢¹ºÂ򷿿¨ 2-Ìîд΢Ðźš£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +sale_qq¡¢sale_wechat¡¢sale_tel +1£¬´úÀíºǫ́ҳÃæÏÔʾµĹٷ½¿ͷþÐÅϢ¡£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +agentmode +1£¬´úÀíģʽ£¬1-ÎÞÑûÇëÂëģʽ 2-ÓÐÑûÇëÂëģʽ¡£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +relagentid +1£¬¹ØÁª´úÀíid£¬ÓÃÓڹØÁªÆäËû´úÀí¡£ +2£¬´úÀíagent¶¼ÓиÃÊôÐԡ£ + +isclose +1£¬¹رշþÎñÆ÷±ê־£¬1-¿ªÆô 2-¹رգ¨²»Äܴ´½¨ºͼÓÈ뷿¼䣩¡£ +2£¬¶¥²㡢´úÀíagent¡¢ÓÎϷgame¶¼ÓиÃÊôÐԡ£ + +pushrate +1£¬ÐÇÐdz¡·¿Ö÷³é³ɰٷֱȣ¬ÕûÊý¡£ +2£¬¶¥²㡢´úÀíagent¡¢ÓÎϷgame¶¼ÓиÃÊôÐԡ£ + +player_server_tcp +1£¬Íæ¼ҷþÎñÆ÷µØַ¼°¶˿ںš£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +player_server_http +1£¬Íæ¼ҷþÎñÆ÷µØַ¼°¶˿ںš£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +game_server_http +1£¬×ÓÓÎϷ·þÎñÆ÷µØַ¼°¶˿ںš£ +2£¬¶¥²㡢´úÀíagent¡¢ÓÎϷgame¶¼ÓиÃÊôÐԡ£ + +visitor_server_tcp +1£¬ÓοͷþÎñÆ÷µØַ¼°¶˿ںš£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +visitor_server_http +1£¬ÓοͷþÎñÆ÷µØַ¼°¶˿ںš£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +system_room +1£¬ÐÇÐÇÕù°Գ¡ϵͳ·¿¼äÁбí¡£ +2£¬ֻÓÐÓÎϷgameÕâ²ãÓиÃÊôÐԡ£ +3£¬½ṹΪ£º +[ + { + "type": 2, + "title": "³õ¼¶³¡", + "image": "", + "roomtype": object, + "online": 99, + "robotGoldRange":[min,max], + "removeRobot":0,//ÊǷñÆÁ±λúÆ÷ÈË + //"forbidEnter":0,//ÊǷñ¿ªս״̬ÏÂÆÁ±λúÆ÷ÈË + roomRate:80,//»úÆ÷ÈËËùռ·¿¼ä°ٷֱÈ + ranMin:1,//Ëæ»ú»úÆ÷ÈË×îСֵ + ranMax:1,//Ëæ»ú»úÆ÷ÈË×î´óֵ + roomLeave:0,//·¿¼äÊǷñÁôһ¸öÕæʵÍæ¼Ò×ùλ0²»Áô1Áô + }, {}, {} +] + + +isdebugger +1£¬ÊǷñÊǵ÷ÊÔ״̬ 0£ºÕýʽ 1£ºµ÷Êԡ£ +2£¬ֻÓÐÓÎϷgameÕâ²ãÓиÃÊôÐԡ£ + +surrender_min +1£¬ÐÇÐdz¡Ͷ½µµÄ×îСÊýÁ¿¡£ +2£¬¶¥²㡢´úÀíagent¡¢ÓÎϷgame¶¼ÓиÃÊôÐԡ£ + +surrender_unit +1£¬ÐÇÐdz¡Ͷ½µµÄ×îСµ¥λÊý¡£ +2£¬¶¥²㡢´úÀíagent¡¢ÓÎϷgame¶¼ÓиÃÊôÐԡ£ + +starName +1£¬ÐÇÐÇÃû³ơ£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +roomCardName +1£¬·¿¿¨Ãû³ơ£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +aset_rebate +1£¬ÐÇÐdz¡·¿¼äС¾Öϵͳ³é³ÉÉèÖà + a£¬ÐÇÐdz¡·¿¼ä²Żᴥ·¢С¾ֳé³ɹ¦Äܡ£ + b£¬Èç¹û´¥·¢ÁËС¾ֳé³ɹ¦ÄÜÔò»ᰴС¾ֽáË㣬´ó¾ֽáÊøº󽫲»»á½øÐдó¾ֽáË㡣 +{ + infinite: [1, 1, 5], -- ÎÞÏ޾ÖС¾ֳé³ÉÉèÖ㬲»ÉèÖñíʾÎÞÏ޾ֲ»½øÐÐС¾ֳé³É + finitude: [1, 2, 10] -- ÓÐÏ޾ÖС¾ֳé³ÉÉèÖ㬲»ÉèÖñíʾÓÐÏ޾ֲ»½øÐÐС¾ֳé³É + + Êý×éÖУ¬ + µÚһλ£º1-¶ԴóӮ¼ҳé³É 2-¶ÔËùÓÐӮµÄÈ˳é³É 3-¶ÔËùÓÐÍæ¼ҳé³É + µڶþλ£º1-°´Íæ¼ҷÖÊýµİٷֱÈ(µÚÈýλÊýֵ)³é³É 2-°´±¶Êýµİٷֱȳé³É + µÚÈýλ£º³é³ÉÊýֵ +} +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +pushtoplayerid +1£¬´ó¾ֳé³ɵÄÐÇÐǸø˭£¬Èç¹ûÉèÖÃÁËÔò¸øÉèÖõÄÍæ¼ң¬Èç¹ûûÉèÖÃÔò¸ø·¿Ö÷¡£ֻ¶ԷÇÐÇÐdz¡ÉèÖÃÁËÐÇÐÇÏ޽øµķ¿¼äÓÐЧ¡£ + ÊÇÊý×飬¸ñʽΪ[ÆÕͨ·¿¼äµijé³ÉÍæ¼Òid, ¶̺ŷ¿¼äµijé³ÉÍæ¼Òid] +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +pushto +1£¬0-´ó¾ֳé³ɵÄÐÇÐǸøµ½pushtoplayeridµÄÍæ¼ÒÕ˺ÅÉϡ£ĬÈÏΪ0¡£ + 1-´ó¾ֳé³ɵÄÐÇÐǸøµ½pushtoplayerid¶ÔӦµĴúÀíÕ˺ÅÉϡ£ + 2-´ó¾ֳé³ɵÄÐÇÐǸøµ½·¿Ö÷¶ÔӦµĴúÀíÕ˺ÅÉϡ£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +xxroomdeduct +1£¬ÐÇÐdz¡ÊǷñҪ¿۷¿¿¨¡£ + 0:ÐÇÐdz¡·¿¼䲻¿۷¿¿¨¡£ĬÈÏΪ0¡£ + 1:ÐÇÐdz¡·¿¼äºͷ¿¿¨·¿¼äһÑù¸ù¾ݾÖÊý¿۳ý·¿¿¨¡£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +noticelimit +1£¬Íæ¼ҷ¢²¼ϵͳÏûϢµĽð±ÒÊýÁ¿ÏÞÖƣ¬ÓÐÕâô¶à½ð±ҲÅÄܷ¢ϵͳÏûϢ¡£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +noticecost +1£¬Íæ¼ҷ¢²¼һ´ÎϵͳÏûϢÏûºĵĽð±ÒÊýÁ¿¡£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +sysroom_playercount +1£¬ϵͳ·¿¼äÀï»úÆ÷ÈËÅãÍæµÄÕæʵÍæ¼ÒÊýÁ¿ãÐֵ¡£ĬÈÏΪ20¡£ + ÕæʵÍæ¼ÒÊýÁ¿СÓڵÈÓڸÃãÐֵʱ£¬һ¸ö·¿¼äÄÚһ¸öÍæ¼ÒÅäN¸ö»úÆ÷Èˣ¬²»»á³öÏÖÕæʵÍæ¼ҺÍÕæʵÍæ¼ÒÔÚͬһ·¿¼äµÄÇé¿ö¡£ + ÕæʵÍæ¼ÒÊýÁ¿´óÓڸÃãÐֵʱ£¬һ¸ö·¿¼äÄڿÉÒԽøÈë¶à¸öÕæʵÍæ¼ҡ£ +2£¬¶¥²㡢´úÀíagent¡¢ÓÎϷgame¶¼ÓиÃÊôÐԡ£ + +showrebate +1£¬ϵͳ·¿¼äÊǷñÌáʾ½ð±ҳé³ɡ£0-²»Ìáʾ 1-Ìáʾ£¬ĬÈÏΪ0¡£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +matchlist +1£¬±ÈÈüÅÅÐаñ +[ + { + "id": 2018010101, -- ±ÈÈüid£¬¸ñʽ±ØÐëΪÄêÔÂÈÕ+ÐòºÅ + "roomtype": [], -- ·¿¼äÀàÐͣ¬¸ù¾Ý×ÓÓÎϷµÄroomtype¶¨Òå + "matchname": "", -- ±ÈÈüÃû³Æ + "imageurl": "", -- ±ÈÈüͼƬµØַ£¬¿ÉÒԲ»ÅäÖ㬲»ÅäÖÃʱÏÔʾĬÈÏͼƬ + "beginTime": "2018-01-01 12:00", -- ±ÈÈü¿ªʼʱ¼䣬¾«ȷµ½·ÖÖÓ + "endTime": "2018-01-01 14:00", -- ±ÈÈü½Øֹʱ¼䣬¾«ȷµ½·ÖÖÓ + "playercount": 100, -- ¿ªʼ±ÈÈüµÄÈËÊýҪÇó + "playercount2": 10, -- ±ÈÈü¿ªʼºóµÚһÅ̴òÍêºóÔٴÎÅŶӴﵽ¶àÉÙÈ˺óÔٿªʼÉÏ×À + "bean": 0, -- ²μӵĽð±ҷÑÓà + "roomcard": 0, -- ²μӵķ¿¿¨·ÑÓã¬ҪÓëroomtypeÖеķ¿¿¨ÊýÁ¿±£³ÖһÖ + "topcount": 10, -- ȡǰ¼¸Ãû + "circulation": 0, -- ±ÈÈüѭ»·´ÎÊý 0:²»ѭ»· >0:ÔÙѭ»·µĴÎÊý + "interval": 0, -- ±ÈÈüѭ»·µļä¸ôʱ³¤£¬¾«ȷµ½Ãë + "gradename": "µ÷Ö", -- µ÷ÖÃû³Æ + "memo1": "", -- ÎÄ×Ö˵Ã÷1£¨µÚһÃû½±Àø˵Ã÷£© + "memo2": "", -- ÎÄ×Ö˵Ã÷2£¨±ÈÈüÏêÇ飬ÒÔ#×÷Ϊ»»Ðзû£© + "memo3": "", -- ÎÄ×Ö˵Ã÷3 + "haverobot": 0, -- 1£ºÓлúÆ÷ÈË <>1£ºûÓлúÆ÷ÈË + "status": 0, -- 0: ÐèҪ´úÀíºǫ́¿ªͨ±ÈÈü 1£ºÅäÖÃÎļþÖÐֱ½ӿªͨ + "ranking": -- ÅÅÐаñÄڵÄϵͳ³ɼ¨ + [ + { + "min": 100, -- ×îµͷÖÊý£¬°üº¬µÈÓÚ + "max": 200, -- ×î¸߷ÖÊý£¬°üº¬µÈÓÚ + "count": 3 -- ϵͳ³ɼ¨¸öÊý + } + ] + }, {}, {} +] +2£¬ֻÓÐÓÎϷgameÏÂÓиÃÊôÐԡ£ + + +reportRobotGold +´¿»úÆ÷ÈËÊǷñ´洢½ð±ұ䶯 +0 or 1(·Ç0Ҳ¿ÉÒÔ) +1.²»Åä»òÕßΪ0²»´桢·Ç0´洢 +2.¶¥²㡢´úÀí²㡢ÓÎϷ²ã + +robotGoldRange +»úÆ÷È˽ð±ÒËæ»ú·¶Χ +[num1,num2] +1.²»Åä»òÕßΪ0°´ÕÕÓëÓÎϷÄÚÍæ¼ҽð±ÒÊýÁ¿Ï൱¡¢ÈôÅäÖÃÔòÔÚnum1¡¢num2ÖÐËæ»ú(ע£ºÁ½¸öֵ²Á¿´óһµã) +2.¶¥²㡢´úÀí²㡢ÓÎϷ²ã + +videoConfig +ÊÓƵ·¿ÅäÖà +{ + type:1·¿¿¨2ÐÇÐÇ + profitMode:1¹̶¨ֵ£¬2°ٷֱÈ + profit:°ٷֱÈģʽÏÂÈ磺50%ÔòΪ50 +} +Î޴ËÊôÐÔΪ·ÇÊÓƵ·¿ +¶¥²㡢´úÀí²㡢ÓÎϷ²ã + +rebateConfig +1£¬´ó¾ֽáËã³é³ÉÅäÖÃ(ֻ֧³ֱ¶Êý°ٷֱȳ鏸ϵͳ) +{ + from:³é˭µÄ 1.´óӮ¼Ò 2.Ӯ¼Ò 3.ËùÓÐÈË + value:³é¶àÉÙ(ֻ֧³ֱ¶Êýµİٷֱȳé³É)£¬Èç50£¬³鱶ÊýµÄ50% +} +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ +brRebateUnit +1£¬°ÙÈËvip·¿ÿ¶àÉÙÈËÊÕȡ²èˮ·Ñ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +smm_message +1£¬Êֻú¶ÌÐÅÑé֤ÂëÏûϢģ°壬ĬÈϣºÄúµÄÑé֤ÂëÊÇX¡£X±íʾÑé֤Â롣 +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +smm_timeout +1£¬Êֻú¶ÌÐÅÑé֤ÂëʱЧ£¬Ã룬ĬÈÏ60¡£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +invitecodetype +1£¬ÔÚÓÎϷÖа󶨵ÄÑûÇëÂ루³¤¶ÈСÓÚ8ʱ£©µÄÀàÐͣ¬0-¸öÈ˴úÀíid 1-Íæ¼Òid£¬ĬÈÏΪ0¡£ +2£¬¶¥²㡢´úÀíagent¶¼ÓиÃÊôÐԡ£ + +ramSeat +1£¬ÊǷñËæ»úÉÏ×ù£¬0-²»Ëæ»ú(˳ÐòÉÏ×ù) 1-Ëæ»ú£¬ĬÈÏΪ0¡£ +2£¬¶¥²㡢´úÀíagent,ÓÎϷgame¶¼ÓиÃÊôÐԡ£ \ No newline at end of file diff --git a/server/debug.php b/server/debug.php new file mode 100644 index 0000000..d5857c9 --- /dev/null +++ b/server/debug.php @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/server/development-guide/01-服务端环境与框架基础.md b/server/development-guide/01-服务端环境与框架基础.md new file mode 100644 index 0000000..07d49e8 --- /dev/null +++ b/server/development-guide/01-服务端环境与框架基础.md @@ -0,0 +1,203 @@ +# 01 · 服务端环境与框架基础 + +本篇建立**全局认知**:子游戏运行在什么环境里、平台框架由哪些对象构成、一个数据包如何被路由到你的代码、一个房间从生到死经历哪些阶段。 + +> 举例以麻将类房卡游戏为主,但本篇所有机制对任意子游戏一致。 + +> **命名说明**:代码示例里的类/文件/变量名(如 `GameStateManager` 等)均为**示例、可自定**;只有平台接缝上的名字(包字段、`export`/`import` 钩子名、平台 API 如 `cls_mod.new`/`min_loadJsFile`/`sendpack_toseat`)是契约。详见 [README「命名约定」](./README.md)。 + +--- + +## 1. 运行环境:一套代码,两个运行时 + +子游戏代码**同时运行在两套环境**,写每一行都要同时考虑: + +| 运行时 | 场景 | 模块加载方式 | 是否有 `require` | +|--------|------|--------------|------------------| +| **Node.js** | 本地、单元测试 | `require()` | 有 | +| **浏览器 / 友乐平台** | 线上部署 | 由 `mod.js` 用 `min_loadJsFile` 统一加载为**全局对象** | **没有** | + +由此引出两条硬约束(详见 04): + +1. **严格 ES5**:用 `var`/`function`,禁止 `let`/`const`/箭头函数/模板串/`class`/`Promise` 等;线上浏览器/友乐运行时不保证 ES6+。 +2. **`require` 只能在文件开头的守卫块内**: + + ```js + if (typeof require !== 'undefined') { + var GameStateManager = require('./dataStructures/GameStateManager.js'); + } + // 之后按全局名引用 GameStateManager.xxx() + ``` + + 浏览器运行时没有 `require`,任何**无守卫的、函数体内的中途 `require`** 都会抛 `ReferenceError: require is not defined`。跨模块统一**按全局名引用**:Node 由守卫块 `var X = require(...)` 得到,浏览器由 `mod.js` 加载的同名全局解析,二者靠共享全局作用域下的同名 `var` 统一。 + +### 前后端是物理分离的两端 + +- 客户端跑在用户浏览器,服务端跑在 Node。两端**不是函数调用,而是 WebSocket/HTTP 收发 JSON 包**,全程异步。 +- 前端代码不走 npm 构建,**前后端共享代码靠文件复制同步**(见 04 的 shared 同步流程)。 +- **服务器权威**:凡房卡、胜负、积分、房间状态等关键数据/操作,一律以服务器为准,前端只负责显示,不能"前端说什么就是什么"。 + +--- + +## 2. 平台代码 vs 子游戏代码 + +``` +server/ ← 平台框架(除子游戏容器目录外,全部禁改) +├── packet.js 总收发包入口、应用列表、最终 SendPack +├── applist.js 加载各应用(server / youle / update) +├── class/ 框架基础类 +│ ├── class.app.js 应用基类 cls_app(按 route 路由到模块) +│ ├── class.mod.js 模块基类 cls_mod(按 rpc 路由到方法) +│ └── class.desk.js / ... 桌、牌等基础类 +├── youle/ 友乐应用(appname = "youle") +│ ├── app.js 创建 youle_app +│ └── server_room/ 房间模块 youle_room(routename = "room") +│ ├── class.room.js 房间对象 o_room(含 seatlist、发包方法) +│ ├── class.player.js 座位/玩家对象(含 conmode/fromid) +│ ├── class.export.js 框架对外服务:check_player / deduct_roomcard / save_grade ... +│ └── class.import.js 框架回调子游戏:makewar_deskwar / get_disbandRoom / player_* ... +└── <游戏容器目录>/ ← 子游戏容器(目录名由接入方自定,非框架强制) + └── <你的游戏>/ ← 子游戏(唯一可编辑目录) + ├── mod.js 模块入口:创建模块、加载文件、定义 RPC 方法 + ├── export.js 子游戏对平台暴露的一组回调接口(按需实现,见 02) + ├── import.js 子游戏对平台服务的封装接口(本项目 4 个) + └── ... 你的玩法业务代码 +``` + +> **容器目录名不是框架约定**:容器目录名由接入方自定。框架**不按目录名找子游戏**——运行时由 `youle_room.app[o_room.o_game.modename]` **按模块名**定位(见 §5)。接入一个新游戏只需两步:① 在 `server/youle/app.js` 里加一行 `min_loadJsFile("<容器目录>/<你的游戏>/mod.js", ...)` 加载它;② 在 `mod.js` 里用唯一的 `modname/routename` 注册。因此容器目录换成任何不与框架冲突的名字都可以(`app.js` 是唯一必须触碰的平台文件,属游戏注册接入点)。 + +**一句话**:平台提供"网络 + 房间 + 玩家 + 房卡 + 战绩"的地基,子游戏只往自己的目录里填"玩法"。两边的接缝就是 **export / import**(详见 02)。 + +--- + +## 3. 核心对象模型 + +理解五个对象的层级关系,就理解了框架的数据骨架: + +``` +app (youle_app) 一个应用,appname = "youle" + └── modlist[] 应用下挂的所有模块 + ├── youle_room 平台房间模块,routename = "room" + └── mod_<你的游戏> 你的子游戏模块,routename = "<你的游戏>" + +o_room 房间对象(平台创建,一桌一个) + ├── roomcode / roomtype / asetcount / battlestate ... 平台基础数据 + ├── seatlist[] 座位列表,长度=满桌人数 + │ └── seatlist[seat] = o_player 座位上的玩家对象 + │ ├── playerid / nickname / onstate + │ └── conmode / fromid 连接方式与连接ID(发包要用) + ├── method.sendpack_toseat(msg, seat) 平台发包方法 + ├── method.sendpack_toother(msg, seat) + └── o_desk ★ 子游戏的牌桌对象(由你在 makewar 里创建并挂上) + ├── o_room 反向引用回房间 + └── data.* ★ 你的对局状态全挂这里(房间隔离的落点) +``` + +要点: + +- **`o_room` 由平台创建并持有**;`seatlist[seat]` 是玩家对象,发包所需的 `conmode`/`fromid` 就在它上面。 +- **`o_desk` 由子游戏创建**(在 `makewar` 中),并与 `o_room` 建立双向引用:`o_room.o_desk = desk; desk.o_room = o_room;`。 +- **对局状态一律挂在 `o_room.o_desk.data.*`**。这是"房间隔离"的物理落点:每桌一份,互不串扰(见 04)。 + +> 举例:麻将把游戏状态放在 `o_room.o_desk.data.roomAdapter.gameState`,把操作队列放在 `o_room.o_desk.data.operationQueue`。换成斗地主、跑得快也是同一套落点,只是 `data.*` 下的字段不同。 + +--- + +## 4. 三层路由:一个包如何到达你的函数 + +平台把客户端发来的包,按 `app → route → rpc` 三级精确投递到子游戏的某个函数。 + +``` +{ app: "youle", route: "<你的游戏>", rpc: "playCard", data: { ... } } +``` + +| 级 | 在哪 | 依据 | 动作 | +|----|------|------|------| +| 1 | `packet.js` `packet_face.ReceivePack` | `pack.app` | 在 `applist` 里找到 `appname == pack.app` 的应用,调 `app.ReceivePack(pack)` | +| 2 | `class/class.app.js` `cls_app.ReceivePack` | `pack.route` | 在 `app.modlist` 里找到 `routename == pack.route` 的模块,调 `mod.DoPack(pack)` | +| 3 | `class/class.mod.js` `cls_mod.DoPack` | `pack.rpc` | 若 `mod[pack.rpc]` 存在,调用 `mod[pack.rpc](pack)` | + +证据: +- 第 1 级:`server/packet.js` `ReceivePack` 按 `pack.app == applist[i].appname` 分发。 +- 第 2 级:`server/class/class.app.js` `ReceivePack` 按 `pack.route == modlist[i].routename` 调 `DoPack`。 +- 第 3 级:`server/class/class.mod.js` `DoPack` 按 `_msg.rpc` 调 `_obj_mod[_msg.rpc](_msg)`。 + +**对开发者的含义**:你在 `mod.js` 里写下 `mod_<游戏>.playCard = function(pack){...}`,前端只要发 `{route:"<你的游戏>", rpc:"playCard", ...}`,框架就会自动调到它——**无需自己写路由分发**,更不要在一个总入口里用 `switch(action)` 做二次路由(一个操作对应一个 RPC 方法)。 + +### ⚠️ 关于 `DoPack` 的返回值(重要) + +`cls_app.ReceivePack` 在拿到 `DoPack` 的返回值后,确实会执行一次 `app.SendPack(repack)`。但这个回发: + +- 只回给**当前请求的那条连接**,且不经过"按座位定向"的处理; +- 在浏览器/友乐链路下**不是子游戏向前端推送状态的可靠通道**。 + +因此本项目的铁律是:**子游戏 RPC handler 一律通过 `o_room.method.sendpack_toseat / sendpack_toother` 主动推送**来下发结果,**不依赖 `return`**。这条直接推导出 03 的"成败标志必须放在主动推送的 `data.success` 里"。 + +--- + +## 5. 模块注册与全局暴露 + +子游戏模块用框架基类创建,并自动注册进应用: + +```js +// cls_mod.new(模块名, 路由名, 所属应用) +var mod_<你的游戏> = mod_<你的游戏> || cls_mod.new("mod_<你的游戏>", "<你的游戏>", youle_app); +``` + +`cls_mod.new` 做了三件事:把模块 push 进 `app.modlist`、在 `app` 上挂 `app[模块名]=mod`、并通过 `OutputMod` 把模块**暴露为全局名**(`global[modname]`)。这套"双运行时全局暴露"是浏览器/友乐能按全局名找到模块的基础。 + +> 双运行时陷阱:凡"构造函数 + 单例实例"式模块,`module.exports` 之后必须**无条件**把全局名暴露出去(不要塞进 `else` 分支),否则线上浏览器拿不到该全局,报 `xxx is not a function`,而 Node 测试却测不出来。 + +`export.js` / `import.js` 在各自文件末尾把接口对象挂到 `mod.export` / `mod.import`,`mod.js` 只负责按顺序加载它们(见 02)。 + +--- + +## 6. 房间生命周期 + +一个房间从创建到回收,平台与子游戏分工如下。**理解每个阶段"谁调用谁",是接入的关键**: + +``` +创建房间 + 平台调 export.get_needroomcard / get_asetcount / get_needroomcard_joinroom + → 决定房卡与总局数(此时还没有 o_desk,子游戏不需要知道谁坐在哪) + +满桌/房主开战 + 平台调 export.makewar(o_room, o_game_config) + → 子游戏创建 o_desk、初始化对局状态、返回开战数据包 + → 平台把开战包下发所有客户端(对应前端 StartWar) + → o_room.battlestate = 1 + +对局进行中 + 客户端发 RPC → mod[rpc](pack) → 子游戏处理 → 主动推送结果 + +断线重连 / 中途加入 + 平台调 export.get_deskinfo(o_room, seat) + → 子游戏返回该座位「当前完整快照」(对应前端 Reconnect) + +每小局结算 + 第一小局结算时:子游戏调 import.deduct_roomcard(o_room) 扣房卡(仅此一次) + +大局(整场)结束 + 子游戏算完最终战绩后调 import.save_grade(o_room, ...) + → 平台保存战绩并「自动释放房间」,子游戏无需再回收房间本身 + +解散房间 + 平台调 export.get_disbandRoom(o_room) 取解散数据包下发 + +玩家中途进出 + 平台调 export.player_enter / player_leave 通知子游戏处理 +``` + +**子游戏自己创建的东西,自己负责回收**:定时器、托管/决策状态、各类缓存等,必须能按房间定位,并在小局结束、解散、开新局时清理干净,禁止泄漏到下一局或别的房间(见 04 的房间隔离)。 + +--- + +## 7. 小结 + +- 一套代码两个运行时 → 强制 ES5 + 守卫块 require。 +- 对象层级:`app → mod`、`o_room → seatlist[seat](o_player) / o_desk(.data.*)`。 +- 三层路由:`app(按app) → route(按route) → rpc(按rpc)`,框架自动投递,你只写 `mod.rpc 方法`。 +- `DoPack` 的 `return` 不是可靠下发通道 → **一律主动 `sendpack_toseat` 推送**。 +- 房间生命周期由 export/import 接缝串起:`makewar` 开局、`get_deskinfo` 重连、`deduct_roomcard` 首局扣卡、`save_grade` 终局保存并自动回收。 + +下一篇 [02-子游戏接入与开发流程](./02-子游戏接入与开发流程.md) 讲:这三个文件具体怎么写、8+4 个接口各做什么、一次操作的完整数据流。 diff --git a/server/development-guide/02-子游戏接入与开发流程.md b/server/development-guide/02-子游戏接入与开发流程.md new file mode 100644 index 0000000..cdae98e --- /dev/null +++ b/server/development-guide/02-子游戏接入与开发流程.md @@ -0,0 +1,287 @@ +# 02 · 子游戏接入与开发流程 + +本篇讲**怎么动手**:一个子游戏由哪三个必需文件构成、`export.js` 的一组回调接口与 `import.js` 的封装接口各做什么、`makewar`/重连怎么写,以及一次玩家操作从收包到推送的完整数据流。 + +> 仍以麻将举例,但"三文件架构 + export/import 接口 + 主动推送"对任意子游戏一致。 + +> **命名说明**:本篇代码里的业务 `rpc` 名(`playCard`、`declareHu` 等)与 handler/类/文件名(`RpcHandler.handlePlayCard`、`OperationManager`、`ResponseBuilder`、`BroadcastManager` 等)均为**示例、可自定**(业务 `rpc` 名只需前后端一致);只有平台接缝上的名字(包字段、`export`/`import` 钩子名、平台 API、`data.success`)是契约。详见 [README「命名约定」](./README.md)。 + +--- + +## 1. 文件结构 + +### 必需三件套(固定文件名) + +``` +server/<游戏容器目录>/<你的游戏>/ (容器目录名由接入方自定,非框架强制,见 01) +├── mod.js 模块入口:创建模块、按序加载文件、定义 RPC 方法 +├── export.js 对平台暴露的一组回调接口(平台按需回调,子游戏按需实现) +└── import.js 对平台服务的封装接口(本项目 4 个) +``` + +### 业务分层(复杂游戏推荐) + +按职责拆分,**一个文件一个明确职责**,被依赖者先加载(见 04 的"模块职责边界"): + +``` +├── game/ 对局编排:状态管理、控制器、对平台的适配器 +├── rpc/ 收发包:各 RPC handler、广播、响应构建、序列化 +├── rules/ 规则解析与规则引擎 +├── shared/ 前后端共享代码(改这里,再用脚本同步到前端) +├── utils/ 日志、错误处理等工具 +└── tests/ 单元/集成测试 +``` + +> 举例(麻将):`game/RoomAdapter.js`(房间↔对局适配)、`game/GameController.js`(对局编排)、`rpc/BroadcastManager.js`(差异化广播)、`rpc/handlers/*`(各操作处理)。换个玩法,分层思路不变。 + +--- + +## 2. `mod.js` —— 模块入口 + +`mod.js` 做三件事:**创建模块 → 按依赖顺序加载文件 → 定义 RPC 方法**。 + +### 2.1 创建模块 + +```js +// cls_mod.new(模块名, 路由名, 所属应用) +var mod_<你的游戏> = mod_<你的游戏> || cls_mod.new("mod_<你的游戏>", "<你的游戏>", youle_app); +``` + +路由名即前端包里的 `route`:**必须与前端包的 `route` 一致、且在应用内唯一**;它与目录名并无绑定关系(`routename` 与目录同名只是约定,非框架要求)。 + +### 2.2 按依赖顺序加载文件 + +被依赖的先加载;`export.js`/`import.js` 通常在业务类之后加载(它们会用到业务模块)。浏览器/友乐用 `min_loadJsFile` 异步链式加载: + +```js +min_loadJsFile("<容器目录>/<你的游戏>/常量与工具.js", function(){ + min_loadJsFile("<容器目录>/<你的游戏>/export.js", function(){ /* ...嵌套加载 import.js、业务与rpc.js... */ }); +}); +``` + +> 推荐分层加载顺序:**常量 → 工具 → 数据结构 → export/import → 收发包层 → 业务层 → 规则引擎**。Node 测试侧用 `require`(写在守卫块内),浏览器侧用 `min_loadJsFile`,两套并存但按全局名统一引用(见 01)。 + +### 2.3 定义 RPC 方法(规范) + +RPC 方法是**前后端交互的服务端入口**。以下是必须遵守的规范(**规则是契约**,示例里的具体方法名/handler 名可自定,见 [README「命名约定」](./README.md))。 + +#### 规则一:函数名 = 前端包的 `rpc` 字段(无二次路由) + +框架的第三层路由(`class.mod.js` 的 `DoPack`)**直接用 `pack.rpc` 当函数名去 `mod` 上取方法调用**——`if (mod[pack.rpc]) mod[pack.rpc](pack)`。所以: + +- 你在 `mod` 上挂一个**与前端 `rpc` 同名**的方法,框架就会自动调到它;这个"同名映射"是**契约**。 +- **一个操作 = 一个 RPC 方法**。**禁止**把多个操作塞进一个入口再用 `switch(action)` 二次路由——那等于绕开框架路由、退化成自建分发。 +- 方法名本身(`playCard`/`declareHu`…)由你和前端约定,只要**两端字符串一致**即可;大小写/风格随子游戏。 + +#### 规则二:方法体只"接住并委托",逻辑放 handler(`mod.js` 保持薄) + +RPC 方法本身**不写业务逻辑**,只把 `pack` 转交给收发包层的 handler;真正的参数校验、业务编排、广播都在 handler 里: + +```js +mod_<你的游戏>.playCard = function(pack) { + // 就绪守卫:handler 由 min_loadJsFile 异步加载,未就绪时防御性返回(见规则四) + if (typeof RpcHandler === 'undefined' || !RpcHandler) { + return { success: false, error: 'RPC处理器未就绪' }; + } + return RpcHandler.handlePlayCard(pack); // 委托到收发包层,真正逻辑在这里 +}; +``` + +> `RpcHandler`、`handlePlayCard` 这些是**本项目的命名示例**;换成任何风格都行,关键是"薄入口 + 委托"的分层。 + +#### 规则三:`return` 不是给前端的下发通道 + +RPC 方法/handler 的 `return` 值**不会可靠地下发到前端**(机制见 01 §4、03 §4)。上面示例里 `return { success:false, ... }` 只是**服务端内部/防御用途**;真正让前端看到的结果(含成功/失败)**必须靠 handler 内部主动推送** `o_room.method.sendpack_toseat/sendpack_toother`,且推送 `data` 自带 `success`(见 03 §5)。 + +#### 规则四:注册时机——handler 就绪后再挂 RPC 方法 + +浏览器/友乐运行时用 `min_loadJsFile` **异步**加载各文件,RPC 方法委托的 handler 可能**晚于** `mod.js` 主体就绪。因此: + +- 把 RPC 方法的定义收敛到一个"**依赖加载完成后再执行**"的注册函数里(本项目为 `defineRpcMethods()`,在加载链回调末尾调用),避免在 handler 尚未加载时就引用它。 +- 每个 RPC 方法开头再加一道**就绪守卫**(如上例 `typeof RpcHandler === 'undefined'`)兜底,防止极端时序下 `undefined` 崩溃。 + +#### 新增一个前后端接口的完整步骤 + +1. 服务端:在 `mod.js` 的 RPC 注册处加 `mod_<游戏>. = function(pack){ return XxxHandler.handleXxx(pack); }`; +2. 服务端:在 handler 层实现 `handleXxx`(安检 → 委托权威业务 → 主动推送,见 03 §2); +3. 前端:发包时带 `rpc: ""`(与服务端方法名一致)。 + +**不要**在受限前端接口文件里新增接口(见 04),也**不要**用 `switch(action)` 二次路由。 + +#### 本项目已定义的 RPC 方法(示例参考) + +`jinxianmahjong` 实际挂了这些 RPC(名字均为示例,仅示范"一操作一方法"的粒度): +`playCard`、`declareHu`、`declareGang`/`mingGang`/`anGang`/`buGang`/`declareJingGang`、`declarePeng`、`declareChi`、`passAction`、`playerReady`、`gameStart`、`getRoomInfo`、`declareBaoding`、`rollDiceSelectJing`/`playerRollDice`、`setHostingState`/`cancelHostingState`/`getHostingInfo`。可见:**每个玩家动作/查询各占一个独立 RPC**,无一处用 `action` 二次分发。 + +--- + +## 3. `export.js` —— 平台回调子游戏的接口(按需实现) + +平台在房间生命周期的各节点回调子游戏 `export` 上的一组接口。**这些接口全部是"可选钩子"**:平台侧每一个调用都写成 `if (mod_game.export.<接口>) { ... }`(见 `youle/server_room/class.import.js`),**没有任何一个是框架强制必须实现的**——子游戏**按玩法需要实现其中的一部分**,未实现的钩子平台会跳过(部分钩子有平台默认行为)。用工厂模式创建,文件末尾挂到 `mod.export`: + +```js +var cls_<游戏>_export = cls_<游戏>_export || { + new: function() { + var exp = {}; + exp.get_needroomcard = function(roomtype, o_game_config) { /* ... */ }; + exp.get_asetcount = function(roomtype, o_game_config) { /* ... */ }; + exp.get_needroomcard_joinroom = function(roomtype, o_game_config) { return 0; }; + exp.makewar_playercount = function(roomtype, o_game_config) { /* ... */ }; + exp.makewar = function(o_room, o_game_config) { /* ... */ }; + exp.get_deskinfo = function(o_room, seat) { /* ... */ }; + exp.get_disbandRoom = function(o_room) { /* ... */ }; + exp.player_enter = function(o_room, seat) { /* ... */ }; + exp.player_leave = function(o_room, seat) { /* ... */ }; + // …按需再实现 restore_room / player_prepare / check_*_permission 等 + return exp; + } +}; +mod_<你的游戏>.export = cls_<游戏>_export.new(); // 文件末尾自动挂载 +``` + +### 3.0 最常用的一批接口 + +绝大多数房卡玩法都会实现下面这批: + +| 接口 | 何时被调 | 返回 | 职责 | +|------|----------|------|------| +| `get_needroomcard` | 创建房间 | Number | 该房型创建需要的房卡数(由你解析 `roomtype`) | +| `get_asetcount` | 创建房间 | Number | 该房型总局数(小局数量) | +| `get_needroomcard_joinroom` | 他人加入 | Number | 加入需要的房卡数(多数玩法返回 0) | +| `makewar` | 开战 | Object | **创建 `o_desk`、初始化对局、返回开战数据包** | +| `get_deskinfo` | 重连/中途加入 | Object | **返回该座位当前完整快照** | +| `get_disbandRoom` | 解散达成 | Object | 返回解散数据包 | +| `player_enter` | 中途进入 | Object? | 处理新玩家加入 | +| `player_leave` | 中途退出 | Object? | 处理玩家离开(如清理其托管/占位) | + +### 3.0.1 其它常见可选钩子(按需实现) + +平台还暴露一批可选钩子,本项目实际用到的有: + +| 接口 | 何时被调 | 职责 | +|------|----------|------| +| `makewar_playercount` | 未满桌但有人准备时 | 返回"达到几人准备即自动开战"的人数(无则等满桌) | +| `restore_room` | **服务器重启后恢复房间** | 用平台持久化的 `o_deskinfo` 重建对局状态 | +| `player_prepare` / `createroom_needprepare` | 准备机制 | 是否需要准备、玩家点准备时的处理 | +| `check_joinroom_permission` / `check_createroom_permission` | 加入/创建房间前 | 自定义准入校验 | + +> 平台的完整钩子清单以 `youle/server_room/class.import.js` 为准(如 `deduct_roomcard_mode`、`owner_beanpush`、`getWinnerByGameInfo` 等);用到哪个就实现哪个,**不要因为"文档列了就全实现"**。 + +> `roomtype` 是一个由子游戏**自定义、自解析**的房型编码(数组或数字串),各位代表局数/人数/扣卡方式/玩法开关等。它的含义只有你的子游戏知道,平台不解释它。 + +### 3.1 `makewar` 是接入的核心 + +`makewar` 必须完成三件事,缺一不可: + +```js +exp.makewar = function(o_room, o_game_config) { + // 1) 创建牌桌对象,与房间建立【双向引用】 + if (!o_room.o_desk) { o_room.o_desk = {}; } + if (!o_room.o_desk.data) { o_room.o_desk.data = {}; } + o_room.o_desk.o_room = o_room; // 反向引用 + + // 2) 创建对局状态,挂到 o_room.o_desk.data.*(房间隔离的落点) + o_room.o_desk.data.gameState = createGameState(o_room, o_game_config); + + // 3) 返回开战数据包(通常按座位差异化下发) + return { + success: true, + sendtype: 1, // 差异化标记:平台逐座位下发 + seatlist: [ { seat: 0, data: {/* 0号位能看到的 */} }, /* ... */ ] + }; +}; +``` + +要点: +- **此时不需要知道"谁"坐在每个位置**,只需知道有几个位置有人——身份由平台管理。 +- **对局状态挂 `o_room.o_desk.data.*`**,不要放模块级单例/全局(见 04)。 +- 开战包对应前端的 `Game_Modify.StartWar`:**改了 `makewar` 下发结构,必须同步改前端 StartWar 解析**(见 03)。 + +### 3.2 `get_deskinfo` 处理重连 + +重连/中途加入时,平台带着 `seat` 来要"当前快照"。你要把该座位此刻该看到的一切(手牌仅本人可见、弃牌区、轮到谁、可用操作、倒计时、比分等)从 `o_room.o_desk.data.*` 组装返回。它对应前端的 `Game_Modify.Reconnect`。 + +> 关键:重连快照应与正常对局推送**复用同一套状态组装逻辑**,避免两份并行实现随规则演化而分叉(数据权威原则,见 04)。 + +--- + +## 4. `import.js` —— 子游戏调用平台的 4 个接口 + +这 4 个接口是对平台 `youle_room.export` 的薄封装,**逻辑由平台实现,你只管在正确时机调用**。每个都形如 `imp.<接口> = function(...args){ return mod_<你的游戏>.app.youle_room.export.<接口>(...args); }`,本项目 4 个: + +```js +mod_<你的游戏>.import = (function() { + var imp = {}; + imp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid) { /* → youle_room.export.check_player(...) */ }; + imp.deduct_roomcard = function(o_room) { /* → youle_room.export.deduct_roomcard(o_room) */ }; + imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag) { /* → youle_room.export.save_grade(...) */ }; + imp.finish_gametask = function(agentid, o_player, taskid, finishamount) { /* → youle_room.export.finish_gametask(...) */ }; + return imp; +})(); +``` + +| 接口 | 调用时机(关键) | 作用 | +|------|------------------|------| +| `check_player` | **每个 RPC handler 开头** | 校验玩家位置/连接,返回 `o_room` 或 `null` | +| `deduct_roomcard` | **第一小局结算时(仅一次)** | 扣房卡。注意**不是开战时扣** | +| `save_grade` | **大局完全结束、战绩算好后** | 保存战绩;调用后**平台自动释放房间** | +| `finish_gametask` | 完成游戏内任务时(可选) | 上报任务进度 | + +> 时机错误是高频 bug:`deduct_roomcard` 放到开战时会导致重复/错误扣卡;`save_grade` 在结算数据没算完就调用会保存到残缺战绩。 + +--- + +## 5. 一次玩家操作的完整数据流 + +把 01 的路由和本篇的接口串起来,一次"出牌"从收包到下发如下(其它操作同构): + +``` +① 客户端发包 { app:"youle", route:"<游戏>", rpc:"playCard", data:{ roomcode, seat, ... } } + │ 三层路由(见 01) + ▼ +② mod.<游戏>.playCard(pack) → 委托 RpcHandler.handlePlayCard(pack) + │ + ▼ +③ handler:参数提取 + 校验 + var o_room = mod.import.check_player(...); // 校验玩家;失败直接 return + if (!o_room) return; + var o_desk = o_room.o_desk; if (!o_desk) return; + // 状态/轮次/合法性校验(轮到该座?这张牌在手里?) + │ + ▼ +④ 执行业务(委托给权威业务模块,handler 不内联规则) + var result = OperationManager.handleOperation(o_room, params); + │ + ▼ +⑤ 构建响应(按需补充摸牌、可用操作、倒计时、比分、游戏状态等) + var responseData = ResponseBuilder.buildPlayCardResponse(...); + │ + ▼ +⑥ 差异化广播:为每个座位定制其「可见数据」并主动推送 + BroadcastManager.broadcastPlayCard(o_room, responseData); + → 内部对每个座位组 msg,调用 o_room.method.sendpack_toseat(msg, seat) + → 前端按既有 playCard 逻辑解析(无需区分触发源) +``` + +每个环节的纪律: + +- **③ 校验必做且失败静默 `return`**:不要给客户端回作弊提示,`check_player` 失败、状态不对、轮次不对都直接 `return`。 +- **④ 不在 handler 内重造规则**:胡牌检测、听牌分析、合法操作枚举等核心算法调用权威模块,handler 只编排(见 04)。 +- **⑥ 主动推送、自带成败**:下发**唯一靠 `sendpack_toseat/toother` 主动推送**;凡有成败语义的推送,`data` 必须带 `success`(见 03)。 +- **服务端代替玩家操作(如 AI 托管)走的是同一条 ④⑤⑥ 链路**,只是触发源从"前端请求"变成"服务端决策",对前端透明(见 04)。 + +--- + +## 6. 接入自检清单 + +新接入或改动子游戏时,逐条核对: + +- [ ] `mod.js` 用 `cls_mod.new` 创建,路由名与前端 `route` 一致;文件按依赖顺序加载。 +- [ ] `export.js` 按玩法需要实现相应回调接口(至少含"最常用的一批",见 §3)并在末尾挂到 `mod.export`。 +- [ ] `makewar` 创建了 `o_desk`、建立 `o_room.o_desk` ↔ `o_desk.o_room` 双向引用、对局态挂 `o_desk.data.*`、返回开战包。 +- [ ] `get_deskinfo` 能给出与正常对局一致的当前快照(复用状态组装,不另写一份)。 +- [ ] `import.js` 4 接口就位;`deduct_roomcard` 在首局结算调、`save_grade` 在终局调。 +- [ ] 每个 RPC handler:先 `check_player` → 取 `o_desk` → 校验 → 委托业务 → 主动推送。 +- [ ] 改了下发结构,前端 `StartWar`/`Reconnect`/对应操作解析同步检查。 + +下一篇 [03-数据收发与通信协议](./03-数据收发与通信协议.md) 详解包结构、发包方式、主动推送与 `success` 成败协议。 diff --git a/server/development-guide/03-数据收发与通信协议.md b/server/development-guide/03-数据收发与通信协议.md new file mode 100644 index 0000000..d204a27 --- /dev/null +++ b/server/development-guide/03-数据收发与通信协议.md @@ -0,0 +1,279 @@ +# 03 · 数据收发与通信协议 + +本篇是**日常手册**:数据包长什么样、收包要做哪些固定步骤、三种发包方式怎么选、为什么"主动推送"是唯一可靠的下发通道,以及本项目最重要的一条——**成败标志只认 `data.success`**。 + +> **命名说明**:本篇代码里的业务 `rpc` 名(`playCard` 等)与 handler/类/函数名(`BroadcastManager`、`deepCopy` 等)均为**示例、可自定**;只有平台接缝上的名字(包四字段、平台 API `sendpack_toseat`/`sendpack_toother`、成败字段 `data.success`)是契约。详见 [README「命名约定」](./README.md)。 + +--- + +## 1. 数据包结构 + +所有前后端数据包统一四字段: + +```js +{ + app: "youle", // 应用名(游戏固定 "youle") + route: "<你的游戏>", // 路由名 = 服务端 cls_mod.new 第二参 routename(见 §1.1) + rpc: "playCard", // 方法名,决定调到 mod 的哪个函数 / 前端走哪个解析分支(见 02 §2.3) + data: { /* 业务数据 */ } +} +``` + +- `app/route/rpc` 三字段驱动三层路由(见 01)。 +- `data` 里**必含平台参数**:`agentid`、`playerid`、`gameid`、`roomcode`、`seat` 等;数值参数收包时要 `parseInt`。 +- **一包多信息**:一个响应/推送包应带全「本次状态变更」所需的核心数据(出了什么牌、轮到谁、各家剩余、倒计时、比分……),使前端**仅凭本包 + 已有本地数据**就能把相关界面刷对——既减少往返,也避免"把一个界面状态拆成几个包拼、中途丢一个就停在自相矛盾的中间态"。("发全哪些"的具体要求见 §1.2。) + +### 1.1 `route` / `routename` 从哪来、在哪定义、怎么匹配 + +`route` 决定包被投到**哪个模块**,是前后端必须对齐的字符串。它的权威定义与匹配链如下(均可在代码验证): + +- **服务端:`routename` 的唯一定义点**是 `mod.js` 里 `cls_mod.new(模块名, 路由名, 所属应用)` 的**第二个参数**: + + ```js + // server/games2/<你的游戏>/mod.js —— 第二参 "<你的游戏>" 即 routename(route 要用的值) + var mod_<你的游戏> = global.mod_<你的游戏> + || cls_mod.new("mod_<你的游戏>", "<你的游戏>", youle_app); + ``` + + `cls_mod.new` 把第二参存进 `mod.routename`,并把模块 `push` 进 `app.modlist`(`server/class/class.mod.js`)。 +- **它是你自定义的字符串**:由你起名,只要**在应用内唯一**即可;与目录名、与模块名 `modname`(第一参,用于全局暴露 `global[modname]`、`app[modname]`)都**无强制绑定**——本项目三者恰好都叫 `jinxianmahjong` 只是约定(见 02 §2.1、01 §5)。 +- **平台按它匹配模块**:收包时 `cls_app.ReceivePack` 用 `pack.route == modlist[i].routename` 找到模块,再 `DoPack` 进第三层按 `rpc` 调方法(`server/class/class.app.js`,见 01 §4)。 +- **前端发包的 `route` 必须与它逐字一致**:前端把该值固化为常量(本项目 `codes/game/network/RpcSender.js` 里 `var ROUTE_NAME = 'jinxianmahjong'`,经 `Utl.sendData(app, route, rpc, data)` 发出)。**两端字符串不一致 → 平台匹配不到模块,包被静默丢弃**(前端也收不到任何响应)。 +- **与平台房间模块区分**:平台自带房间模块 `routename` 是 `"room"`,处理创建/加入/开战(`createRoom`、`self_join_room`、`self_makewar` 等);**你的子游戏自定义 RPC**(`playCard` 等)走**你自己的 `routename`**。两者不要混用:平台流程发 `route:"room"`,玩法操作发 `route:"<你的游戏>"`。 + +> 一句话:**`routename` 在服务端 `cls_mod.new` 第二参定义(自定、应用内唯一),前端发包 `route` 必须与它逐字相同,平台据此把包投到你的模块**;改名要两端同步改。 + +### 1.2 发包必须自带前端界面所需的全部核心数据 + +**前端以服务端数据为权威——只做界面展示与交互、不做权威计算**(见 [前端 05 开发规范与红线](../../client/development-guide/05-开发规范与红线.md))。推论落到服务端:**每个下发包都必须携带前端渲染/刷新该界面所需的全部核心数据**,让前端「据包写入数据 → 直接刷新出界面」,而**不需要前端自行推算、补全或兜底**。 + +- **界面要用的字段都要发全**:凡界面要显示、或前端 set/refresh 要用到的核心字段(出了什么牌、轮到谁、各家剩余张数、手牌/副露、分数/比分、倒计时锚点、庄家/座位、各类状态标志……)都要放进 `data`,不能让前端"猜"或本地推算权威结果。 +- **漏发是服务端的缺陷,不许前端补**:前端遵循数据权威原则——权威字段缺失应显式报错/留空、不用 `|| 0`/`|| []` 兜造(见 client 05)。所以服务端漏发 = 前端界面缺数据,**修在服务端发包处,不在前端补洞**。 +- **落实「一包多信息」(见 §1)**:本包要让前端**仅凭本包 + 已有本地数据**把相关界面刷对,别把一个界面状态拆成几个包拼(中途丢一个就停在自相矛盾的中间态)。 +- **差异化但要发全**:每个座位只发它**该看到**的核心数据(手牌只发本人,见 §3),但"该看到的"必须发全、发准。 +- **重连/中途加入发完整快照**:`get_deskinfo` 必须给出该座恢复整个界面所需的**全量**核心数据,让前端 `Reconnect` 据快照一次重画(见 §6.4、02)。 + +> 一句话:**服务端权威不仅是"服务端算",也是"服务端把界面要用的核心数据发全"——前端只据包渲染,不推算、不兜底。** + +### 1.3 发全 ≠ 发多:按可见性下发(防作弊) + +§1.2 要求"该看到的发全",本节是它的另一半:**不该看到的一律不发**。前端是不可信环境——**下发即泄露**:包到了客户端就能被抓包/改内存看到,"前端拿到但不渲染"等于零防护。 + +- **按座位裁剪可见面**:他人手牌、牌堆剩余序列与顺序、未公开的判定结果(他人是否听牌/能否胡、暗牌内容、未到揭示时机的底牌与亮牌明细)——**对不该看到的座位不放进 `data`**,用差异化下发(`sendtype:1` + `seatlist[]` 逐座位组包,见 §3)实现。 +- **只发"公开面"的统计量**:他人的信息若界面确实要显示,只发**已公开的派生量**(如剩余张数、已亮出的花色/数量),不发原始牌面。 +- **不发未来**:尚未发生或尚未公开的权威结果(下一张要摸的牌、预先算好的胜负、待揭晓的底牌)不提前下发,哪怕前端"只是缓存"。 +- **判别**:问一句 **"这个字段落到一个改过的客户端手里,玩家会不会因此获得优势?"** 会 → 不发(或只发到该看到的座位)。 +- **违例信号**:`deepCopy(baseData)` 后**没有**逐座位裁剪敏感字段就全员广播;把全量 `handCards`/`cardPool` 塞进公共包;重连快照 `get_deskinfo` 直接回整张桌的内部状态。 + +> §1.2 与 §1.3 合起来才是完整的下发面:**该看到的一个不少(否则前端缺数据),不该看到的一个不多(否则等于送作弊入口)。** + +### 1.4 阶段与状态机唯一在服务端,并随包下发 + +前端**不持有对局状态机**(前端侧规范见 [客户端 05 §6](../../client/development-guide/05-开发规范与红线.md)):阶段、轮到谁、该座位可用操作、倒计时基准,前端一概不推导。因此这些字段**必须由服务端唯一维护、并在每个相关下发包里明确给出**: + +- **阶段/状态标志**(当前 `phase`、是否在响应窗口、是否已结束等)——不让前端按"收到了什么包"去反推阶段。 +- **控制权**(下一个该谁操作,如 `nextControlSeat`/`currentPlayer`)——显式字段,不让前端按座位顺序自己算。 +- **该座位可用操作**(`availableActions[seat]`)——按座位下发;它同时是服务端准入白名单(见 [04 §8](./04-开发规范与红线.md#8-操作请求合法性验证不可信客户端)),**同一份权威数据既驱动前端按钮显示、又校验请求合法性**,天然不会两边判得不一致。 +- **倒计时锚点与时长**——前端只做本地插值显示,**超时的裁定与后续推进仍在服务端**,不接受前端上报"我超时了"。 +- **状态变更必须有包**:任何阶段推进都要有一个下发包承载;**没有包的状态变化 = 前端不可能正确显示**,只能靠前端猜——那正是本节要杜绝的。 + +--- + +## 2. 收包处理的固定步骤(在 handler 里) + +`mod` 上的 RPC 方法只是**薄入口**(`return XxxHandler.handleXxx(pack)`,见 02 §2.3);下面这套"安检流程"发生在**它委托到的 handler 内部**,**不可省略、不可简化**: + +```js +XxxHandler.handlePlayCard = function(pack) { // 由 mod_<游戏>.playCard 委托进来 + try { + // 1) 提取并校验参数:必填字段 + 数值型字段类型(本项目用 ValidationHelper.extractAndValidateParams) + var params = extractAndValidateParams( + pack, + ['agentid', 'playerid', 'gameid', 'roomcode', 'seat', 'cardUniqueId'], + { playerid: 'number', roomcode: 'number', seat: 'number' }); + if (!params.success) return { success: false, error: params.error }; + var p = params.params; + + // 2) 校验玩家与房间(必做)——失败直接 return + // ⚠️ conmode / fromid 取自 pack 顶层(不是 pack.data),是发包定向要用的连接信息 + var o_room = mod_<游戏>.import.check_player( + p.agentid, p.gameid, p.roomcode, p.seat, p.playerid, pack.conmode, pack.fromid); + if (!o_room) return { success: false, error: '玩家验证失败' }; + + // 3) 取桌对象与对局状态 + var o_desk = o_room.o_desk; + if (!o_desk) return { success: false, error: '游戏桌不存在' }; + + // 4) 调试记录(若框架提供 o_desk.debug.save_receivepack)——便于复盘 + // 5) 业务校验 + 执行业务:委托权威模块(handler 不内联规则,见 04) + // 6) 构建响应 + 【主动推送】:组包 → 逐座位 sendpack_toseat;推送 data 自带 success(见 §5); + // return 不是下发通道(见 §4) + } catch (e) { /* 记录日志;必要时给该座推送失败包 */ } +}; +``` + +- **参数提取要校验类型**:数值字段(`playerid`/`roomcode`/`seat` 等)必须转成数值再用;缺字段/类型不符即判失败。用统一工具集中做(本项目 `ValidationHelper.extractAndValidateParams`)比每处手写 `parseInt` 更不易漏。 +- **`check_player` 是强制安检**:校验座位、连接、身份,返回 `o_room` 或 `null`;`null` 一律 `return`。它的第 6/7 个参数 `pack.conmode`、`pack.fromid` 来自**包顶层**(框架收包时注入),是后续定向发包的连接凭据。 +- **校验失败不回作弊提示**:对客户端不下发"你作弊了"之类反馈,直接结束(如需可只给本座推送一个通用失败包),避免给作弊者信息。 +- **`return` 值仅服务端内部用**:例中的 `return {success:false,...}` 不会下发前端(见 §4);要让前端看到的结果一律走第 6 步主动推送。 + +--- + +## 3. 三种发包方式 + +| 方式 | 接口 | 用于 | +|------|------|------| +| 点对点 | `o_room.method.sendpack_toseat(msg, seat)` | 只发给某个座位(个人状态、定向数据) | +| 广播其他人 | `o_room.method.sendpack_toother(msg, seat)` | 发给除 `seat` 外所有人(`seat=-1` 即全发) | +| 差异化广播 | 逐座位组 `msg` 后各自 `sendpack_toseat` | 每个玩家看到的内容不同(如手牌只发本人) | + +平台发包内部会**从 `seatlist[seat]` 上取 `conmode`/`fromid`** 填入包,再交给底层 `SendPack` 按 TCP/HTTP 下发——**这两个连接字段不需要你手工设置**,只要座位上有在线玩家即可。 + +### 差异化广播:棋牌最常用 + +牌类游戏里"同一动作、各家可见不同",所以广播时为每个座位**深拷贝一份基础数据再定制**: + +```js +for (var seat = 0; seat < o_room.seatlist.length; seat++) { + if (!o_room.seatlist[seat]) continue; // 空座跳过 + var msg = { + app: "youle", route: "<游戏>", rpc: "playCard", + data: deepCopy(baseData) // 公共信息 + }; + // 敏感信息只发本人:本人给真实手牌,他人给 [] + msg.data.handCards = (seat === actionSeat) ? hands[seat] : []; + o_room.method.sendpack_toseat(msg, seat); +} +``` + +> 敏感信息(手牌、暗牌、未公开的判定)**只发给该看到的人**,这是服务器权威的一部分。裁剪清单与判别标准见 [§1.3 按可见性下发](#13-发全--发多按可见性下发防作弊)——**下发即泄露,前端"拿到但不渲染"不算防护**。 + +--- + +## 4. 主动推送是唯一可靠的下发通道 + +**前端收包的唯一通道是服务端主动推送**(`sendpack_toseat / toother` → 底层 `SendPack`)。 + +而 RPC 方法的 **`return` 值不可靠下发前端**:框架虽会对 `DoPack` 的返回值做一次回发,但它只回当前请求连接、不按座位定向,浏览器/友乐链路下不能当作状态推送通道(机制见 01 §4)。 + +**推论**:凡需要让前端看到的结果(包括操作的成功/失败),都必须放进**主动推送的 `data`** 里。 + +--- + +## 5. 成败标志协议(本项目最重要的一条) + +> 这条覆盖并修正了早期文档里"用 HTTP 风格 `status` 码判成败"的写法。**以本协议为准。** + +### 规则 + +1. **成败唯一权威字段是 `data.success`(boolean)**。前端一律 `if (!data.success)` 判断操作成败。 +2. **禁止用 `data.status`(如 `status === 200`)或 `data.code` 判成败**。`status`/`code` 只能作展示/日志用的细分信息,不参与成败裁定。 +3. **主动推送必须自带 `success`**:因为 `return` 不下发前端,凡有成败语义的**推送包**,其 `data` 必须显式带 `success: true/false`,不能只放 `status`。 +4. **禁止双轨/兼容兜底**:前端不得写 `status !== 200 && !success` 之类的 `status` 兜底;新增/改动的推送一律补齐 `success`,前端一律只认 `success`。 + +### 正反例 + +```js +// ❌ 推送只带 status、少了 success → 前端读 data.success 恒 undefined,误判失败 +data: { status: 200, hosting: true } +// ✅ 成败语义放 success,status 仅作细分 +data: { success: true, status: 200, hosting: true } + +// 前端:只认 success,status/code 仅用于展示或日志 +if (!data.success) { /* 失败处理 */ return; } +``` + +> 典型事故:托管状态推送 `{ status: 200, ... }` 不带 `success`,前端读 `data.success` 恒为 `undefined`,于是"已进入托管却报失败"。根因就是违反了本协议第 3 条。 + +--- + +## 6. 端到端收发全链路(前后端对照) + +本节把**一个包从前端出发、到服务端处理、再推回前端**的完整链路摊开,标出**每一步在谁的哪个文件/函数**,供前端与服务端开发共同对照。下面的锚点里,**平台/契约名**(包字段、`packet_face.ReceivePack`、`Utl.sendData`、`Game_Modify.*`、`sendpack_toseat`、`data.success`)是真实且固定的;**子游戏侧命名**(`RpcSender`、`SubGameHooks`、收包分发器、各 handler)是本项目示例、可自定(见 [README「命名约定」](./README.md))。 + +### 6.1 方向 A:玩家主动操作(请求 → 处理 → 推送) + +``` +前端 client 服务端 server +──────────────── ──────────────── +业务/控制器 + └ 发包封装 / RpcHelper(自动补 agentid/playerid/roomcode/seat…) + └ Utl.sendData(app, route, rpc, data) ──WS/HTTP──▶ packet_face.ReceivePack (按 app 找应用) + (平台 08_Utl_Output.js) └ cls_app.ReceivePack (按 route 找模块,01 §4) + └ cls_mod.DoPack (按 rpc 找方法) + └ mod[rpc](pack) RPC 薄入口(02 §2.3) + └ XxxHandler.handleXxx(pack) + check_player → 取 o_desk → 校验 + → 委托权威业务模块(04) + → o_room.method.sendpack_toseat(msg, seat) + (填 conmode/fromid) + Game_Modify._ReceiveData(_msg) ◀──WS/HTTP── → app.SendPack → packet_face.SendPack + (平台 02_SubGame_Input.js,受限文件) (只定向该座位的那条连接,01 §4) + └ 转交子游戏收包分发器(SubGameHooks._ReceiveData) + └ 按 _msg.rpc 查路由表 → 对应 rpc 处理器 + └ if(!data.success) 失败处理;否则改数据模型 → 表现(04 篇) +``` + +- **前端出口只有一个**:所有请求经发包封装/`RpcHelper` 最终落到平台 `Utl.sendData(app, route, rpc, data)`,业务不手拼包(前端 04 §1)。 +- **服务端入口只有一个**:`packet_face.ReceivePack`,随后三层路由 `app→route→rpc` 精确投递(01 §4)。 +- **服务端出口只有一个**:`o_room.method.sendpack_toseat/sendpack_toother`——它从 `seatlist[seat]` 取 `conmode/fromid` 填入包,交 `app.SendPack` 定向下发(`class.room.js`)。**`return` 不算下发**(§4)。 +- **前端收口只有一个**:平台 `09_Net.js` 收到主动推送后调 `Game_Modify._ReceiveData(_msg)`(受限文件只转交,不写业务),再由子游戏**收包分发器按 `_msg.rpc`** 分到对应处理器(前端 04 §2)。 + +### 6.2 方向 B:服务端主动推送(无前端请求) + +超时、AI 托管、他人操作波及本座、每小局/大局结算等,都是**服务端主动发起**、前端没有对应请求的推送。它**复用方向 A 的后半段**——同样 `sendpack_toseat/toother` → 前端 `_ReceiveData` → 按 `rpc` 分发: + +``` +服务端某处业务(定时器/AI决策/结算) + → o_room.method.sendpack_toseat(msg, seat) (与真人操作完全相同的出口与包结构) + → 前端 _ReceiveData → 收包分发器按 rpc → 对应处理器 +``` + +> 因此前端**无法也无需区分**一个推送是"我请求的响应"还是"服务端主动发的"——两者走同一条收口、同一张分发表。服务端替玩家操作必须保持这种一致(§7、04 §7)。 + +### 6.3 谁在哪:收 / 发 / 路由一览 + +| 环节 | 前端(client) | 服务端(server) | +|------|----------------|------------------| +| **发包出口** | 发包封装/`RpcHelper` → `Utl.sendData(app,route,rpc,data)`(平台 `08_Utl_Output.js`) | `o_room.method.sendpack_toseat` / `sendpack_toother` → `app.SendPack` | +| **传输** | WebSocket/HTTP(平台 `09_Net.js` / `00_minhttp.js`) | 平台 `packet.js`(`SendPack_Tcp` / `SendPack_Http`) | +| **收包入口** | 平台 `09_Net.js` → `Game_Modify._ReceiveData(_msg)`(受限文件转交) | `packet_face.ReceivePack`(`packet.js`) | +| **路由依据** | 子游戏收包分发器按 `_msg.rpc` → 处理器 | 三层 `app→route→rpc`(01 §4),`rpc` 直取同名 `mod` 方法 | +| **处理** | 对应 `rpc` 处理器,先判 `data.success` | RPC handler:`check_player` → 取 `o_desk` → 校验 → 委托权威业务 | +| **成败标志** | 只认推送 `data.success`(§5) | 推送 `data` 必自带 `success`(§5) | +| **开局** | `Game_Modify.StartWar(_msg)`(取本座数据,02 篇/前端 04 §3) | `export.makewar` 返回包(`sendtype:1 + seatlist[]` 差异化) | +| **重连/中途加入** | `Game_Modify.Reconnect(_deskinfo)`(据快照重画) | `export.get_deskinfo` 返回该座完整快照 | + +### 6.4 对接接缝:改一端必核对另一端 + +`rpc` 是前后端的**共同契约字符串**:服务端推送用哪个 `rpc`,前端就必须在收包分发表里注册同名处理器;反之前端请求的 `rpc`,服务端 `mod` 上必须有同名方法。因此: + +| 场景 | 服务端产出 | 前端接收 | +|------|------------|----------| +| 开战 | `export.makewar` 的返回包 | `Game_Modify.StartWar(_msg)` → 开局处理 | +| 重连/中途加入 | `export.get_deskinfo` 的返回 | `Game_Modify.Reconnect(_deskinfo)` → 重画 | +| 对局/推送 | RPC handler 或主动推送(按 `rpc`) | 收包分发表里同名 `rpc` 的处理器 | + +**改服务端下发结构 = 同步核对前端对应 `rpc` 的解析**;**新增一种推送 = 服务端选定 `rpc` + 前端在分发表加同名处理器**。任一端单方面改,另一端必按旧结构解析出错。前端侧的收发细节以 [前端 04 网络对接与启动编排](../../client/development-guide/04-网络对接与启动编排.md) 为权威。 + +--- + +## 7. 服务端代替玩家操作时的透明性 + +服务端自动替玩家执行的操作(如 AI 托管出牌/碰杠胡),**必须复用真人操作的同一套数据包与广播链路**:产生的包结构、`rpc`、`data` 与真人操作**完全一致**,前端无需也禁止为它单开一套接收/解析分支。唯一区别只在触发源(前端请求 → 服务端决策),其后的数据组织、协议、广播路径不变。(详见 [04 §7 自动操作复用真人链路](./04-开发规范与红线.md#7-服务端自动操作复用真人链路) 与 [§8 操作请求合法性验证](./04-开发规范与红线.md#8-操作请求合法性验证不可信客户端)。) + +--- + +## 8. 小结 + +- 包结构恒为 `{app, route, rpc, data}`;收包先 `check_player`,失败静默 `return`。 +- **下发包必须发全前端界面所需的核心数据**(§1.2):前端以服务端为权威、不自算,漏发修在服务端发包处、前端不补洞。 +- **不该看到的一律不发**(§1.3):下发即泄露,按座位裁剪可见面;"该看到的一个不少、不该看到的一个不多"。 +- **阶段/控制权/可用操作/倒计时锚点由服务端唯一维护并显式下发**(§1.4):前端不持有状态机、不反推阶段;状态变更必须有包承载。 +- 三种发包方式按"谁该看到什么"选;差异化广播逐座位定制,敏感信息只发本人。 +- **主动推送是唯一可靠下发通道**,`return` 不算。 +- **成败只认 `data.success`**,推送必自带 `success`,禁止 `status`/`code` 判成败与兼容兜底。 +- **端到端一条链**(§6):前端 `Utl.sendData` → 服务端 `packet_face.ReceivePack`→三层路由→handler→`sendpack_toseat` → 前端 `Game_Modify._ReceiveData`→按 `rpc` 分发;`rpc` 是前后端共同契约,改一端必核对另一端。 +- 改下发结构必同步核对前端 `StartWar`/`Reconnect`/对应 `rpc` 解析。 + +下一篇 [04-开发规范与红线](./04-开发规范与红线.md) 汇总所有必须遵守的工程纪律。 diff --git a/server/development-guide/04-开发规范与红线.md b/server/development-guide/04-开发规范与红线.md new file mode 100644 index 0000000..4792637 --- /dev/null +++ b/server/development-guide/04-开发规范与红线.md @@ -0,0 +1,230 @@ +# 04 · 开发规范与红线 + +本篇汇总所有**必须遵守**的工程纪律。每一条都对应过真实事故或返工,是代码审查的清单来源。改任何代码前,按相关条目自检。 + +> **命名说明**:本篇提到的模块/类/文件名(如 `GameStateManager`、`RoomAdapter` 等)多为**示例、可自定**,红线约束的是**做法**而非具体名字;只有平台接缝上的名字(包字段、`export`/`import` 钩子、平台 API、`data.success`)是契约。详见 [README「命名约定」](./README.md)。 + +--- + +## 1. 可编辑范围 + +### 服务端 + +- **唯一可编辑目录**:子游戏自己的目录 `server/<游戏容器目录>/<你的游戏>/`。容器目录名由接入方自定,**非框架强制**,换任何不与框架冲突的目录皆可(框架按 `modename` 而非目录名定位子游戏,见 01)。 +- **禁改**:`server/` 其余一切均为平台代码——`server/class/`、`server/config/`、`server/server/`、`server/youle/`、`server/update/`、`server/packet.js`、`server/applist.js`、`server/minhttp.js` 等。 +- **唯一例外**:接入新游戏时需在平台文件 `server/youle/app.js` 里加一行 `min_loadJsFile("<容器目录>/<你的游戏>/mod.js", ...)` 完成注册加载——这是必须触碰平台代码的**唯一游戏接入点**,除此之外不得改动 `server/youle/`。 + +### 前端 + +- **平台代码禁改**:`client/js/00_Surface/` 下全部文件。 +- **受限接口文件**(不可新增对外接口,尽量不改,确需则只在现有接口内部加逻辑): + `client/js/01_SubGame/00_SubGame_Config.js`、`01_SubGame_modify.js`、`02_SubGame_Input.js`。 +- **新增前后端交互**一律走 `mod.js` 的 `mod_<游戏>.` 机制,**不在受限文件里新增接口**。 + +--- + +## 2. 语言标准:严格 ES5 + +- 全部 JS **必须严格符合 ES5**:用 `var`/`function`、字符串用 `+` 拼接。 +- **禁止** ES6+:`let`/`const`、箭头函数、模板字符串、解构、默认参数、展开运算符、`class`、`for...of`、`Promise`/`async`/`await`、对象简写等。 +- 原因:线上浏览器/友乐运行时不保证 ES6+ 支持。 + +--- + +## 3. 模块加载:require 双运行时守卫 + +- **所有 `require` 必须写在文件开头的 `if (typeof require !== 'undefined') { ... }` 守卫块内。** +- **禁止函数体内 / 中途 `require`**(含"延迟 require 规避循环依赖"的写法)。浏览器/友乐运行时无 `require`,无守卫的 `require` 会抛 `ReferenceError: require is not defined`。 +- 跨模块运行时**按全局名引用**:Node 由守卫块 `var X = require(...)` 得到,浏览器由 `mod.js` 加载的同名全局解析。 + +```js +// ✅ 标准形态:require 仅在顶部守卫块,函数体内直接用全局名 +if (typeof require !== 'undefined') { + var GameStateManager = require('./dataStructures/GameStateManager.js'); +} +function foo() { GameStateManager.doSomething(); } +// ❌ 函数体内中途 require → 浏览器崩溃:function bar(){ var GSM = require('...'); } +``` + +### 双运行时全局暴露陷阱 + +"构造函数 + 单例实例"式模块,`module.exports` 之后必须**无条件**把全局名暴露出去(**不要放进 `else` 分支**),否则线上浏览器拿不到该全局,报 `xxx is not a function`,而 Node 测试测不出来。 + +--- + +## 4. 数据权威原则 + +- **数据源唯一**:同一业务数据只有一个权威来源;上游计算/写入/校验,下游只读取/消费,**不重复推断、不重复拼装**。 +- **禁止猜测兜底**:不用默认值掩盖缺失数据。**关键权威字段缺失要显式报错或返回 `null`**,由上层决定是否终止,**禁止** `|| 0`、`|| []`、`|| ''`、双源回退等掩盖。 +- **下游不修补**:下游不得为"让流程跑起来"而补写/重算上游本该提供的数据;发现下游在修补,应把责任前移到权威来源。 +- **边界**:只有展示层、纯 UI 兼容、非关键日志字段,才可在明确边界内用安全默认值。 +- **下发面要发全**:因前端以服务端为权威、不自算权威结果,服务端**每个下发包必须携带前端界面所需的全部核心数据**(界面要显示、或前端 set-refresh 要用的字段都发全);漏发 = 前端缺数据,**修在服务端发包处、前端不补洞**(详见 [03 §1.2](./03-数据收发与通信协议.md))。 +- **前端不是数据源**:客户端只提供"意图"(想做什么、对哪个目标),**永远不是任何业务数据的权威来源**。凡进入服务端状态的值——分数、牌面、阶段、控制权、结算——只能由服务端自己算出或从 `o_desk.data.*` 读出,**绝不采信包体里前端算好的同名字段**(见 §8)。对应地,阶段/控制权/`availableActions`/倒计时锚点由服务端唯一维护并显式下发,前端不推导(见 [03 §1.4](./03-数据收发与通信协议.md)、前端 [05 §6](../../client/development-guide/05-开发规范与红线.md))。 + +> 审查信号:看到 `|| []`、`|| 0`、`|| ''`、三元默认、双源字段,先判断它是不是**权威字段**(影响发牌/庄家/手牌/规则/结算/重连/状态流转)。是 → 改成权威读取;只是展示/日志 → 才可保留默认。 + +--- + +## 5. 模块职责边界 + +- **一个职能只在一个模块实现**,其他模块**只调用、不重造**。 +- 需要某能力时,调用对应**权威模块**;权威能力不满足,应在权威模块内扩展,而非在调用方旁路重写。 +- **禁止**:在 A 模块内联 B 模块核心算法的"简化版";同一职能在两处各有一份实现并行演化。 +- 后果:交叉实现产生双份并行逻辑,必随规则演化分叉、互相矛盾——这是数据权威原则在"代码职责"维度的同一条红线。 + +### "自动操作"模块只做决策,不重造规则 + +服务端自动替玩家操作的模块(如 AI 托管)**只负责决策**(出哪张、是否碰/杠/吃/胡/过),**核心算法一律复用权威实现**:胡牌检测、听牌/做牌分析、合法操作枚举、牌型判定等都已在权威模块实现,决策模块只能**读取/调用其结果**,禁止自行重算。 + +- 判别:回答"能否胡/听什么/有哪些合法操作"——属核心算法,必须复用;回答"在合法选项中选哪个更好"——才属决策。 + +--- + +## 6. 房间隔离 + +服务端同进程并发多张牌桌(多个 `o_room`)。**任何随对局变化的状态都必须以房间为单位隔离**: + +- **状态挂房间**:对局数据存 `o_room.o_desk.data.*`,由 `o_room` 携带。**禁止存在模块级单例 / 全局变量 / 静态字段**。 +- **不得只用 `seat` 作 key**:座位号仅 0–3,多房并发必碰撞。缓存、定时器表、决策状态、计数器等**必须用 `房间 + seat` 复合维度**(或每房一份实例)。 +- **定时器随房生命周期**:所有 `setTimeout`/`setInterval` 必须能按房间定位与清理;小局结束、解散房间、开新局时,**清理该房名下全部定时器与残留状态**,禁止泄漏到下一局或别房。 +- 覆盖范围:自动托管/决策表、算法中间态缓存(听牌/胡牌检测)、超时与回合定时器、待响应队列等,任一项跨房共享都会导致 A 房误改 B 房。 + +> 一句话:一切随对局变化的东西都属于某个房间,必须能用 `o_room` 唯一定位、隔离与回收。 + +--- + +## 7. 服务端自动操作复用真人链路 + +服务端代替玩家执行的操作(AI 托管等): + +- **必须复用真人手动操作的同一套数据包链路**:走与真人相同的服务端处理入口与广播下发路径,产生的包结构与真人**完全一致**。 +- **对前端透明**:前端只按既有"玩家操作"逻辑解析表现,**禁止为"自动操作"单开一套接收/解析/表现分支**,无需区分触发源。 +- **唯一区别在触发源**:由"前端请求"变为"服务端决策",其后数据组织、下发协议、广播路径不变。 + +--- + +## 8. 操作请求合法性验证(不可信客户端) + +> 与 §7 配套:§7 保证"自动操作走真人链路",本节保证"到达的每个请求都合法才被链路执行"。 + +**客户端完全不可信**:它可能连点、乱序、丢包重发、篡改包体、发送非当前阶段/越权/当前不允许的操作。服务端**必须对每个到达的请求独立判定合法性**,非法即拒绝且**绝不改动任何对局状态**——就当这个包没来过。这是正确性的**根本防线**;前端的乐观清除/防连点(见前端 [04 §5.6](../../client/development-guide/04-网络对接与启动编排.md#56-受控例外响应交互按钮乐观清除--服务端合法性验证))只是体验层,**不承担正确性**。 + +### 五层验证栈(进业务前依次过闸,任一不过即 `success:false` + 不改状态) + +1. **座位鉴权(防越权)**:操作者座位**由连接身份反查**(连接绑定的 playerid→座位,如平台 `check_player` 以 `fromid/conmode` 绑定),**绝不信任包体 `seat` 字段**;包内 seat 只做一致性校验,不等即拒。否则 A 玩家发 `{seat: B}` 就能替 B 操作。 +2. **阶段/状态门(防非当前阶段包)**:校验当前游戏阶段/状态允许该操作——非出牌阶段发出牌、非掷骰窗口发掷骰、无响应窗口发碰/过 → 拒绝。门控读 `gameState.phase` / `pendingResponse.waiting` / 是否轮到该座位(`currentPlayer === seat`)。 +3. **操作可用性校验(防"当前不允许的操作")——核心闸**:校验该操作**确实在该座位当前权威 `availableActions[seat]` 里**(没可碰的牌却发碰、没有胡机会却发胡 → 拒绝)。 + > 关键:`availableActions[seat]`(听牌/胡牌检测/操作枚举产出的**权威合法操作集**)**既是自动操作模块的决策选项来源,也是校验真人请求的准入白名单**——同一份权威数据兼任"决策依据"与"准入校验",统一了数据源唯一(§4)、自动操作职责边界(§5)、自动操作复用真人链路(§7)三条线。 +4. **幂等 / 去重 / 时序(连点的根治)**:不是限流,而是**状态机的单调推进**——一个操作被处理后立即推进状态并更新 `availableActions`,使**重复包因落在新 `availableActions` 之外而天然非法**;响应窗口对同座位重复响应去重(如 `addPlayerResponse` 记录后再收同座位响应即忽略);对已结束窗口/轮次的迟到包丢弃。 +5. **参数 / 数据合法性**:牌 `uniqueId` 是否真在该玩家手里、`targetCard`/`fromSeat`/`choiceIndex` 是否与权威牌局一致 —— 全按**服务端权威数据**校验,不信客户端传的牌面。 + +### 只接受「意图」入参:客户端回传的结论一律忽略 + +第 5 层的前提是**入参里根本不该出现结论**。请求包只表达"我想做什么"(操作类型 + 目标标识),**不表达"结果是什么"**(前端侧规范见 [客户端 04 §1「请求包只带意图」](../../client/development-guide/04-网络对接与启动编排.md)): + +- **协议层不定义结论入参**:`score`/`isWin`/`multiplier`/`nextSeat`/`phase`/`result`/`handCards` 之类字段**不出现在请求包定义里**——定义了它,就是留了一个可被伪造的洞。 +- **收到也忽略**:即便非正规客户端硬塞这些字段,handler **一律不读、不落地**,全部按服务端权威数据重算。审查信号:handler 里出现 `pack.data.score`、`pack.data.isWin`、`pack.data.phase` 这类读取。 +- **包内 `seat` 只作一致性校验**:身份由连接反查(第 1 层),包内座位不等即拒,**不作身份依据**。 +- **前端 `shared/` 的计算结果不是输入**:前端跑 `shared/` 只为提示与预校验,其结论不回传、服务端也不采信;服务端自己跑同一份 `shared/` 得出权威结果(见 §9)。 + +### 设计形态:统一裁决网关,默认拒绝(fail-closed) + +把五层收敛成**一道所有操作共用的准入网关**(而非每个 handler 各写一遍散点 if)。网关默认 **deny**,仅当请求显式**命中该座位 `availableActions`** 且通过座位/阶段/参数校验才 **allow**。好处: + +- **单点权威**:合法性判定只有一处,各 handler 不会判得不一致(呼应 §5 职责边界); +- **自动操作与真人同闸**:自动操作(AI 托管)本就从 `availableActions` 选,必过同一网关,真正做到 §7 "自动操作 ≈ 服务端模拟一次**合法**真人操作"。 + +> **反模式警示**:若"是否轮到你/是否允许"的通用时序校验只存在于某个**从未被调用**的方法里(死代码),等于没有验证——尤其**出牌入口最易漏掉回合门**,导致任一玩家可在非自己回合越权出牌。验证网关必须**在统一入口真实生效**,并有测试覆盖。 + +### 失败返回:安静且明确 + +- 一律 `success:false` +(可选)`reason` 码,**绝不静默改状态**;失败推送前端只做提示/日志、**不改对局界面**(与前端 `if(!data.success)` 闭环)。 +- 最危险的是**"非法包被误当合法处理"(fail-open)**——默认拒绝就是为杜绝它。 + +### 验收(测试构造非法包,硬断言拒绝) + +按 §11 测试纪律补单测:**非自己回合出牌 / 同回合连发两张 / 非当前阶段各操作 / 响应窗口关闭后迟到碰杠 → 断言 `success:false` 且状态不变**;**合法出牌各时机(摸后/碰后/杠后/报定后)→ 断言通过**(防回合门误伤合法出牌)。 + +> 一句话:**服务端对每个操作请求默认拒绝,仅当命中该座位当前权威 `availableActions` 且通过座位/阶段/参数校验才执行;操作一经消费即推进状态使重复包天然失效。正确性完全由这道网关保证,前端乐观清除只是体验。** + +--- + +## 9. Shared 文件同步流程 + +### `shared/` 是什么:子游戏自己的游戏逻辑,与平台无关 + +- **`shared/` 属于子游戏,不是平台代码**:它存放**本玩法自身**的规则、算法、常量、数据结构(本项目如胡牌检测 `WinDetectionFactory`、精牌 `JingAlgorithm`、计分 `ScoreCalculation`、牌型/比精,以及 `constants/` 下各类常量)。平台**既不提供也不感知** `shared/`——它完全落在子游戏可编辑范围内,是你玩法逻辑的一部分。 +- **为什么要"共享"**:**同一份玩法逻辑需要在两处运行**——服务端(Node)做**权威裁定**,前端(浏览器)做**即时表现/预校验**。若两端各写一份,必随规则演化而分叉、算出不一致结果。因此把这类"前后端必须完全一致"的纯逻辑收进 `shared/`,两端跑**逐字相同**的代码。 +- **它与"数据权威"的关系**:`shared/` 是**逻辑同源**(两端同一套算法),不改变**数据权威**(结果仍以服务端为准,见 §4);前端算出的只是表现/预判,最终以服务端 `shared/` 算的为准。 + +### 同步流程:服务端权威源 → 前端只读副本 + +前后端各持一份 `shared/`,**必须**经同步流程单向更新,禁止直接编辑前端副本: + +| 角色 | 路径 | +|------|------| +| 权威源(服务端,唯一可改) | `server/<游戏容器目录>/<你的游戏>/shared/` | +| 前端副本(只读,脚本生成) | `client/js/01_SubGame/codes/shared/` | + +1. **只改服务端** `shared/` 下文件。 +2. 改完运行根目录的同步脚本(如 `sync-shared.ps1`)同步到前端。 +3. **禁止直接编辑前端 `codes/shared/`**(同步脚本会覆盖)。 + +> 判别一段逻辑该不该进 `shared/`:**它是不是"前后端必须算出完全相同结果"的玩法逻辑**?是(胡牌/听牌/比精/牌型/计分/规则常量)→ 放 `shared/`;只是服务端流程编排或只是前端表现 → 各自放自己那侧,不进 `shared/`。 + +--- + +## 10. 硬编码常量准则 + +**需提取为常量**(满足任一):会随规则变化、跨模块复用、裸值无法自解释。典型:分数数值、状态/类型字符串标识、配置阈值、跨模块共享的 key 名。常量按语义归入 `shared/constants/` 下对应文件(分数类、规则配置类、类型枚举类)。 + +**不必提取**(避免过度设计):单函数内一次性临时值(循环初值 `0`、空数组 `[]`)、框架约定固定串(`require` 路径)、自解释布尔开关、纯展示标点文字。 + +--- + +## 11. 测试纪律 + +- **测试唯一目的是验证业务正确性**。失败是有价值的信号,第一反应是**定位根因**,不是"让测试变绿"。 +- **禁止任何掩盖手段**:skip/条件 return、软化断言、放宽阈值、try-catch 吞异常、把硬断言改成"存在才校验"。 +- **每个失败必须裁定归属**(基于证据):【业务代码缺陷】还是【测试脚本缺陷】,二选一。手段:临时诊断探针、打印中间态、最小复现;拿证据再下结论,**禁止凭猜测定性**(诊断日志定位后清理)。 +- **按归属修复**:业务缺陷 → 修业务、单独提交、写明根因;脚本缺陷 → 修置场/时序,断言保持硬断言。优先级:**确定性构造场景 > 有界重试采样 > 条件跳过**。 +- **flaky 同样是缺陷**:要么业务竞态、要么测试非确定性置场,须根治;验收标准是**连跑 ≥5 次全绿**。 + +### 测试不绑架正式代码 + +- **正式核心代码禁止存在专为测试服务的逻辑**,更禁止为"让测试通过/兼容测试"而新增或修改正式代码。 +- 方向永远是**测试适配正式代码的生产契约**,而非正式代码迁就测试的简化输入/不规范 stub。测试要构造符合生产契约的输入与 stub。 +- 违例信号:正式代码注释出现"仅兼容测试 stub""如单元测试传 X"之类,即是违例。 + +--- + +## 12. Git 提交规范 + +- **及时自动提交**:每完成一个可独立成立的逻辑改动(一个修复/功能/重构/一批测试)就**立即提交**,不堆积工作区。 +- **无需逐次询问**:完成阶段性改动后主动提交(`push` 按需)。 +- **提交信息用中文**,简明说明"做了什么/为什么",一次提交聚焦一件事,结尾保留 `Co-Authored-By` 署名行。 + +--- + +## 13. 审查速查表 + +| 维度 | 红线 | +|------|------| +| 范围 | 只改子游戏目录 `<容器目录>/<游戏>/` 与允许的前端范围 | +| 语言 | 纯 ES5;`require` 仅在顶部守卫块 | +| 成败 | 只认 `data.success`,推送必自带,禁 `status` 兜底 | +| 数据 | 权威唯一、缺失报错/返回 null、禁兜底、下游不修补;下发包发全前端界面所需核心数据 | +| 前端不是数据源 | 请求包只接受「意图」(操作类型+目标标识),协议不定义 `score`/`isWin`/`phase` 等结论入参,收到也忽略并按权威数据重算;包内 `seat` 只作一致性校验 | +| 下发可见性 | 不该看到的不发(他人手牌/牌堆序列/未公开判定/未来结果),按座位差异化裁剪;"下发即泄露",前端不渲染不算防护(03 §1.3) | +| 状态机归属 | 阶段/控制权/`availableActions`/倒计时锚点由服务端唯一维护并显式下发,前端不推导;状态变更必须有包承载(03 §1.4) | +| 职责 | 一职能一模块,调用不重造 | +| 隔离 | 状态挂 `o_desk.data.*`,禁全局;`房间+seat` 作 key;定时器随房清理 | +| 自动操作 | 复用真人链路,对前端透明 | +| 请求验证 | 客户端不可信;每个操作请求过座位鉴权/阶段门/操作可用性/幂等去重/参数五层校验,统一网关默认拒绝、失败 `success:false` 不改状态;出牌回合门尤其不可漏(防死代码) | +| Shared | 只改服务端 `shared/`,跑同步脚本 | +| 测试 | 失败裁定归属、禁掩盖;正式代码不迁就测试 | +| Git | 一事一提交、中文信息、及时提交 | + +--- + +至此,从框架运作(01)、子游戏接入(02)、收发协议(03)到工程红线(04),构成一套完整的服务端子游戏开发指导。回到 [README](./README.md) 查看导航与一页纸模型。 diff --git a/server/development-guide/README.md b/server/development-guide/README.md new file mode 100644 index 0000000..7f3dd42 --- /dev/null +++ b/server/development-guide/README.md @@ -0,0 +1,44 @@ +# 服务端 · 子游戏开发指导文档 + +> 本套文档是**友乐游戏平台**下「子游戏服务端」开发的通用指导与规范。 +> 它讲清楚三件事:**框架怎么运作**、**子游戏怎么接进去**、**开发时必须守哪些规矩**。 +> +> 文中以「麻将」一类房卡棋牌作举例,但所有结论都是**框架通用**的,不绑定任何具体玩法。 +> 新开发者按本套文档即可理解运作流程、动手接入并写出符合规范的子游戏。 + +--- + +## 这套文档写给谁 + +- **新接手子游戏服务端的开发者**:先读完 01、02 建立全局认知,再按 03、04 动手。 +- **正在开发/维护某个子游戏的开发者**:03、04 是日常红线,改任何东西前回查。 +- **做代码审查的人**:04 是审查清单的来源。 + +## 阅读顺序 + +| 篇 | 文档 | 解决什么问题 | +|----|------|--------------| +| 00 | 本文 README | 这套文档是什么、怎么读 | +| — | [红线速查.md](./红线速查.md) | **一页纸运作模型 + 命名约定 + 红线清单**(由 `CLAUDE.md` 常驻加载,改代码前先对照) | +| 01 | [01-服务端环境与框架基础.md](./01-服务端环境与框架基础.md) | 平台与子游戏的关系、核心对象模型、三层路由、房间生命周期 | +| 02 | [02-子游戏接入与开发流程.md](./02-子游戏接入与开发流程.md) | 三文件架构、export/import 接口、makewar/重连、一次操作的完整数据流 | +| 03 | [03-数据收发与通信协议.md](./03-数据收发与通信协议.md) | 包结构、收发包方式、**下发面发全 / 按可见性裁剪 / 状态机唯一在服务端**、主动推送、成败标志协议、前后端对接点 | +| 04 | [04-开发规范与红线.md](./04-开发规范与红线.md) | 可编辑范围、ES5/require、数据权威(含**前端不是数据源**)、模块职责、房间隔离、请求合法性验证、测试纪律 | + +建议第一次**从 01 顺序读到 04**;之后把 03/04 当手册随用随查。 + +--- + +## 一页纸与红线 + +**运作模型、命名约定与红线清单已独立成篇:[红线速查.md](./红线速查.md)。** + +该文件由 `CLAUDE.md` 通过 `@import` 常驻每次会话上下文,是红线的权威源;本 README 只负责导航,不重复抄写红线(避免两处不同步)。红线的完整细节见 [04-开发规范与红线.md](./04-开发规范与红线.md)。 + +--- + +## 与既有文档的关系 + +- 本套文档是平台级收发包/子游戏开发规范的**落地版**:结合本仓库真实代码,给出可直接照做的接入步骤与红线,并对其中已被本项目实践修正的部分(如成败标志由 `status` 收敛为 `success`)以本套为准。 +- 各子游戏内部的架构细节(模块划分、算法)仍以各自 `<游戏容器目录>/<游戏>/docs/` 为准。 +- **平台无关的通用工程与架构规范**(分层、可扩展模式、配置化、数据权威、反模式与审查清单)见 [工程与架构通则](../../games/engineering/):本套讲"平台怎么接、红线是什么",工程通则讲"该怎么设计、怎么长久演进",互补阅读。 diff --git a/server/development-guide/红线速查.md b/server/development-guide/红线速查.md new file mode 100644 index 0000000..488bb1e --- /dev/null +++ b/server/development-guide/红线速查.md @@ -0,0 +1,69 @@ +# 服务端 · 一页纸运作模型与红线速查 + +> 本文是服务端开发**必须常驻在手边**的那一页:运作模型 + 命名约定 + 红线清单。 +> 它由 `CLAUDE.md` 通过 `@import` 常驻每次会话上下文;**红线以本文为权威**。 +> 文档导航、阅读顺序、各篇主题见 [README](./README.md);红线的完整细节见 [04-开发规范与红线.md](./04-开发规范与红线.md)。 + +--- + +## 一页纸:核心运作模型 + +``` +客户端数据包 { app, route, rpc, data } + │ + ▼ +packet_face.ReceivePack 按 pack.app 找到「应用」 + │ + ▼ +app.ReceivePack 按 pack.route 找到「模块」(子游戏) + │ + ▼ +mod.DoPack 按 pack.rpc 调到「方法」 mod[rpc](pack) + │ + ▼ +子游戏 RPC handler 校验玩家 → 取对局状态 → 执行业务 → 主动推送 + │ + ▼ +o_room.method.sendpack_toseat 按座位取连接信息(conmode/fromid) → 下发客户端 +``` + +- **平台框架**负责:网络收发、应用/模块/房间/玩家对象、路由、房卡、战绩、房间生命周期。 +- **子游戏**负责:玩法规则、对局状态、每个操作的处理与广播。两者通过 **export / import** 两组接口对接。 + +> 上图是**入站半程**(前端 → 服务端 handler)。完整的往返链路(含服务端 `sendpack_toseat` 推回 → 前端 `Game_Modify._ReceiveData` 按 `rpc` 分发)见 [03 §6「端到端收发全链路」](./03-数据收发与通信协议.md#6-端到端收发全链路前后端对照),前端侧细节见 [前端 04 网络对接与启动编排](../../client/development-guide/04-网络对接与启动编排.md)。 + +--- + +## 命名约定:哪些是框架契约,哪些只是示例 + +本套文档的代码示例里出现的**具体名字,绝大多数是举例,并非强制标准**。请区分两类: + +- **框架契约(必须照用)**——由平台代码决定,改了就对接不上: + - 数据包四字段 `{ app, route, rpc, data }`,其中 `app` 固定为 `"youle"`; + - 平台真实回调的 export 钩子名、你调用的 import 接口名(如 `makewar`、`get_deskinfo`、`check_player`、`deduct_roomcard`、`save_grade`); + - 成败字段 `data.success`; + - 平台 API 名(`cls_mod.new`、`min_loadJsFile`、`o_room.method.sendpack_toseat` / `sendpack_toother` 等)。 +- **示例命名(可自定)**——本项目的举例,你的子游戏可按自己的风格命名: + - 业务 `rpc` 名(如 `playCard`、`declareHu`,只需**前后端约定一致**即可); + - handler / 类 / 文件 / 变量 / 分层目录名(如 `RpcHandler.handlePlayCard`、`OperationManager`、`ResponseBuilder`、`BroadcastManager`、`RoomAdapter`、`GameController`、`game/`、`rpc/`、`rules/` 等)。 + +> 一句话:**平台接缝上的名字是契约、要照用;接缝之内你自己代码里的名字都是示例、可自定。** 01–04 的代码块同样遵循这条约定,不再逐处重复声明。 + +--- + +## 红线速查(详见 04) + +> 以下每一条都有过真实事故或返工,**改代码前先对照**。 + +- **可编辑范围**:服务端只能改子游戏目录 `server/<游戏容器目录>/<你的游戏>/`(容器目录名由接入方自定,**非框架强制**,换任何不与框架冲突的目录皆可),`server/` 其余皆平台代码,禁改。接入新游戏还需在 `server/youle/app.js` 里加一行 `min_loadJsFile` 加载其 `mod.js`(唯一必须触碰的平台文件,属游戏注册接入点)。 +- **双运行时**:代码同时跑在 Node 与浏览器/友乐平台。必须严格 **ES5**;`require` 只能写在文件开头 `if (typeof require !== 'undefined') {}` 守卫块内,**禁止函数体内/中途 require**。 +- **成败标志唯一是 `data.success`**:前端只认 `if (!data.success)`;**主动推送的 `data` 必须自带 `success`**(mod 的 `return` 值不可靠下发前端);禁止用 `status`/`code` 判成败、禁止 `status` 兼容兜底。 +- **数据权威**:同一业务数据只有一个权威来源,下游只读不重算;权威字段缺失要**显式报错/返回 null**,禁止 `|| 0`、`|| []`、`|| ''` 兜底掩盖。 +- **模块职责边界**:一个职能只在一个模块实现,其他模块**调用而非重造**。 +- **房间隔离**:一切随对局变化的状态都挂 `o_room.o_desk.data.*`;**禁止模块级单例/全局变量存对局态**;缓存/定时器/决策表不得只用 `seat` 作 key,必须 `房间 + seat`;结束/解散/开新局时按房间清理全部定时器与残留状态。 +- **服务端自动操作复用真人链路**:服务端代替玩家做的操作(如 AI 托管)必须复用「真人操作」的同一套处理入口与广播链路,对前端透明,不为它单开一套下发分支。 +- **服务器权威 + 发全下发面**:房卡、胜负、积分、状态等关键数据与操作一律以服务器为准、前端只作显示不自算;因此**服务端每个下发包必须携带前端界面所需的全部核心数据**,漏发修在服务端发包处、前端不补(详见 03 §1.2 / 04 §4)。 +- **状态机唯一在服务端**:前端是**数据驱动**的、不持有对局状态机(见前端 05 §6),所以阶段、控制权(轮到谁)、该座位 `availableActions`、倒计时锚点**必须由服务端唯一维护并在相关下发包里显式给出**;**任何状态变更都要有包承载**——没有包的状态变化等于逼前端去猜。超时裁定在服务端,不接受前端上报"我超时了"(详见 03 §1.4)。 +- **前端不是数据源,只收「意图」**:请求包只接受「操作类型 + 目标标识」;协议**不定义** `score`/`isWin`/`phase`/`nextSeat`/`handCards` 之类**结论入参**,即便客户端硬塞也一律**不读、不落地**,全部按服务端权威数据重算;包内 `seat` 只作一致性校验、身份由连接反查(详见 04 §8)。 +- **发全 ≠ 发多,按可见性下发**:他人手牌、牌堆剩余序列、未公开判定、尚未揭晓的结果**不发给不该看到的座位**(差异化下发逐座位裁剪)。**下发即泄露**——包到了客户端就能被抓包看到,"前端拿到但不渲染"不算防护(详见 03 §1.3)。 +- **测试纪律**:测试失败先用证据裁定「业务缺陷 vs 测试脚本缺陷」,禁止 skip/软化断言/吞异常掩盖;**正式代码不得为兼容测试而加逻辑**。 diff --git a/server/games/erqiwang/class.arith.js b/server/games/erqiwang/class.arith.js new file mode 100644 index 0000000..aa0e224 --- /dev/null +++ b/server/games/erqiwang/class.arith.js @@ -0,0 +1,1170 @@ +/////////////////////////////////////////////////// +////////// cls_youle_erqiwang_arith: 算法 ///////// +/////////////////////////////////////////////////// +//跨模块依赖:Node 走 require 守卫;友乐/浏览器无 require,由 min_loadJsFile 加载为同名全局(dev-guide 01 §1) +if (typeof require !== "undefined"){ + var youle_erqiwang_shared_cards = require("./shared/cards.js"); +} + +var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || { + + //根据叫分计算基础子数(design §7.1 常规算子 / §7.3.1 爬坡) + //climb: 是否勾选爬坡。70 分打牌基础子数 2;投降(基础1)在结算处特殊处理 + get_base_bycall: function(callgrade, climb){ + if (callgrade == 70){ + return 2; + } + if (!climb){ + //常规算子:65→2 60→3 55→4 50及以下统一6 + if (callgrade == 65) return 2; + if (callgrade == 60) return 3; + if (callgrade == 55) return 4; + if (callgrade >= 5 && callgrade <= 50) return 6; + return 0; + } + //爬坡:65→2 起每低5分+1子,直到5分→15 + switch (callgrade){ + case 65: return 2; + case 60: return 3; + case 55: return 4; + case 50: return 6; + case 45: return 7; + case 40: return 8; + case 35: return 9; + case 30: return 10; + case 25: return 11; + case 20: return 12; + case 15: return 13; + case 10: return 14; + case 5: return 15; + } + return 0; + }, + + //小光/过庄分界与升级级距 Q(design §7.2.0 常规固定40 / §7.3.2 爬坡分段) + get_qvalue: function(callgrade, climb){ + if (!climb){ + return 40; + } + if (callgrade >= 45){ + return 40; + } + if (callgrade == 40 || callgrade == 35){ + return 20; + } + if (callgrade == 30 || callgrade == 25){ + return 15; + } + if (callgrade == 20 || callgrade == 15){ + return 10; + } + if (callgrade == 10 || callgrade == 5){ + return 5; + } + return 40; + }, + + //以下 9 项为前后端共享算法,权威实现在 shared/cards.js(经 sync_shared.cmd 同步到前端)。 + //此处只挂引用:对外方法名与签名不变,全部既有调用点无需改动。 + id_to_flower: youle_erqiwang_shared_cards.id_to_flower, + id_to_number: youle_erqiwang_shared_cards.id_to_number, + id_to_code: youle_erqiwang_shared_cards.id_to_code, + order_cards: youle_erqiwang_shared_cards.order_cards, + is_continuous: youle_erqiwang_shared_cards.is_continuous, + get_pairlist: youle_erqiwang_shared_cards.get_pairlist, + group_tractor_runs: youle_erqiwang_shared_cards.group_tractor_runs, + get_tuolaji_list: youle_erqiwang_shared_cards.get_tuolaji_list, + trump_rank: youle_erqiwang_shared_cards.trump_rank, + + //把一组主牌分解为分量列表(design §5.4.3/§5.4.4) + //返回 [{type:'single'|'pair'|'tractor', len, value, cards:[]}...],value 为该分量最大牌的归一化 rank + decompose_trump: function(mainflower, cards){ + var ordered = cls_youle_erqiwang_arith.order_cards(mainflower, cards.concat()); + //先分出对子与单张(对子形状与 shared get_pairlist 一致:[id, id]) + var pairs = []; //[[id,id], ...] + var singles = []; //[id, ...] + var i = 0; + while (i < ordered.length){ + var code_i = cls_youle_erqiwang_arith.id_to_code(mainflower, ordered[i]); + if (i + 1 < ordered.length){ + var code_n = cls_youle_erqiwang_arith.id_to_code(mainflower, ordered[i + 1]); + if (code_i == code_n){ + pairs.push([ordered[i], ordered[i + 1]]); + i = i + 2; + continue; + } + } + singles.push(ordered[i]); + i++; + } + //连续对子组成拖拉机,其余为单独对子。极大连续段的扫描是前后端共享算法(shared/cards.js), + //此处只负责把段翻译成分量,不再自己实现「什么算连续、几对起算拖拉机」 + var runs = youle_erqiwang_shared_cards.group_tractor_runs(mainflower, pairs); + var comps = []; + for (var j = 0; j < runs.length; j++){ + var run = runs[j]; + var tcards = []; + for (var r = 0; r < run.length; r++){ + tcards = tcards.concat(run[r]); + } + if (run.length >= youle_erqiwang_shared_cards.TRACTOR_MIN_PAIRS){ + comps.push({type: "tractor", len: run.length, value: cls_youle_erqiwang_arith.trump_rank(mainflower, run[0][0]), cards: tcards}); + } else { + comps.push({type: "pair", len: 1, value: cls_youle_erqiwang_arith.trump_rank(mainflower, run[0][0]), cards: tcards}); + } + } + for (var s = 0; s < singles.length; s++){ + comps.push({type: "single", len: 1, value: cls_youle_erqiwang_arith.trump_rank(mainflower, singles[s]), cards: [singles[s]]}); + } + return comps; + }, + + //甩牌最大性判定(design §5.4.2):两名对手是否持有能压过甩牌任一分量的主牌/主对/主拖拉机 + //comps:甩牌分量;opp_zhulist:各对手未出的主牌id列表数组。返回 true 表示有人能压(甩错) + opp_can_beat_flush: function(mainflower, comps, opp_zhulist){ + for (var o = 0; o < opp_zhulist.length; o++){ + var zhu = cls_youle_erqiwang_arith.order_cards(mainflower, opp_zhulist[o].concat()); + var pairs = cls_youle_erqiwang_arith.get_pairlist(mainflower, zhu); + for (var c = 0; c < comps.length; c++){ + var comp = comps[c]; + if (comp.type == "single"){ + //对手持有任一 rank 更大的主牌单张即可压 + for (var a = 0; a < zhu.length; a++){ + if (cls_youle_erqiwang_arith.trump_rank(mainflower, zhu[a]) > comp.value){ + return true; + } + } + } else if (comp.type == "pair"){ + //对手持有 rank 更大的主对即可压 + for (var p = 0; p < pairs.length; p++){ + if (cls_youle_erqiwang_arith.trump_rank(mainflower, pairs[p][0]) > comp.value){ + return true; + } + } + } else { + //拖拉机:对手持有同长度、顶牌 rank 更大的主拖拉机即可压 + var tlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairs, 300 + comp.len); + for (var t = 0; t < tlist.length; t++){ + if (cls_youle_erqiwang_arith.trump_rank(mainflower, tlist[t][0]) > comp.value){ + return true; + } + } + } + } + } + return false; + }, + + //从对子列表中移除指定拖拉机(flat牌id数组)用到的对子 + remove_pairs: function(pairlist, tractorcards){ + var result = []; + for (var i = 0; i < pairlist.length; i++){ + var inT = false; + for (var j = 0; j < tractorcards.length; j++){ + if (tractorcards[j] == pairlist[i][0]){ + inT = true; + break; + } + } + if (!inT){ + result.push(pairlist[i]); + } + } + return result; + }, + + //跟甩牌的分量拆解校验(design §5.4.4) + //inhandcards: 跟牌方出牌前的全部手牌;followcards: 提交的跟牌;demand: {tractors:[连对数...], pairs, singles} + //在"同花色凑张数、主牌最大化"之上,追加"拖拉机→对子→单张"的强制分量优先级: + //能凑同长主拖就必须凑、能凑主对就必须凑,不得拆散去垫。返回 true 表示合规。 + flush_follow_ok: function(mainflower, inhandcards, followcards, demand){ + var isTrump = function(cid){ + return cls_youle_erqiwang_arith.id_to_code(mainflower, cid) >= 1000; + } + //手牌与跟牌里的主牌 + var heldTrump = []; + var playedTrump = []; + for (var i = 0; i < inhandcards.length; i++){ + if (isTrump(inhandcards[i])){ heldTrump.push(inhandcards[i]); } + } + for (var i = 0; i < followcards.length; i++){ + if (isTrump(followcards[i])){ playedTrump.push(followcards[i]); } + } + heldTrump = cls_youle_erqiwang_arith.order_cards(mainflower, heldTrump); + playedTrump = cls_youle_erqiwang_arith.order_cards(mainflower, playedTrump); + + //甩牌总张数 + var total = demand.singles + demand.pairs * 2; + for (var t = 0; t < demand.tractors.length; t++){ + total = total + demand.tractors[t] * 2; + } + + //主牌最大化:手上主牌够则必须全打主牌,不够则全部主牌都要打出 + var needTrump = (heldTrump.length >= total) ? total : heldTrump.length; + if (playedTrump.length != needTrump){ + return false; + } + + //按 拖拉机(长→短) → 对子 的优先级,算出跟牌方"必须打出"的拖拉机与对子数 + var heldPairs = cls_youle_erqiwang_arith.get_pairlist(mainflower, heldTrump); + var _tractors = demand.tractors.concat(); + _tractors.sort(function(a, b){ return b - a; }); //降序 + var reqTractors = []; + var workPairs = heldPairs.concat(); + var unmatchedTractorPairs = 0; + for (var t = 0; t < _tractors.length; t++){ + var N = _tractors[t]; + var tl = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, workPairs, 300 + N); + if (tl.length > 0){ + reqTractors.push(N); + workPairs = cls_youle_erqiwang_arith.remove_pairs(workPairs, tl[0]); + } else { + //凑不成同长拖拉机,该拖拉机分量退化为对子需求 + unmatchedTractorPairs = unmatchedTractorPairs + N; + } + } + //对子需求 = 对子分量 + 退化的拖拉机分量;受限于剩余可凑对子数 + var wantedPairs = demand.pairs + unmatchedTractorPairs; + var reqPairs = (workPairs.length < wantedPairs) ? workPairs.length : wantedPairs; + + //校验跟牌里的主牌是否包含这些必须的拖拉机与对子 + var playedPairs = cls_youle_erqiwang_arith.get_pairlist(mainflower, playedTrump); + for (var r = 0; r < reqTractors.length; r++){ + var ptl = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, playedPairs, 300 + reqTractors[r]); + if (ptl.length == 0){ + return false; //必须打出的同长主拖拉机没打 + } + playedPairs = cls_youle_erqiwang_arith.remove_pairs(playedPairs, ptl[0]); + } + if (playedPairs.length < reqPairs){ + return false; //必须打出的主对子没凑够 + } + return true; + }, + + //第一个位置是否可出牌 + can_playcard: function(mainflower, cards, selfseat, seatlist, opp_zhulist){ + /* + 参数: + mainflower: 主牌花色 + cards : 要出的牌id列表 + selfseat : 出牌者的位置 + seatlist : 座位列表,四维数组, + [ //第一层表示玩家,与位置序号对应 + [ //第二层表示花色,与花色-1对应 + [ + 0, //无该花色的牌标志 + 0 //该花色无对标志 + ], [0, 0], [0, 0], [0, 0] + ], + [[0, 0], [0, 0], [0, 0], [0, 0]], + [[0, 0], [0, 0], [0, 0], [0, 0]] + ] + 返回结果json: + result : true可出,false不可出 + flower : 花色,result=false时无此属性,下同 + cardtype : 牌型 + cardvalue: 牌型值,取满足牌型的最小牌的牌编码 + */ + + //返回的结果 + var can = {}; + can.result = true; + can.flower = null; + can.cardtype = null; + can.cardvalue = null; + + //返回不能出牌的结果 + var do_returnfalse = function(){ + can.result = false; + delete can.flower; + delete can.cardtype; + delete can.cardvalue; + } + + //张数上界:design §5.4.3 明确甩牌「无组合数量限制」,故不设规则性上限; + //这里只挡结构性越界——出牌阶段一名玩家最多持 28 张(埋牌后), + //超过即不可能是合法出牌(调用方 mod.chupai 已先做 check_cards_inhand 持牌校验)。 + //注:牌型编码在 28 张内不会溢出各自区间(单张≤128、多对≤214、拖拉机≤314) + if (cards.length > 28){ + do_returnfalse(); + return can; + } + + /////////// 计算牌型 /////////// + //将牌从大到小排序 + cards = cls_youle_erqiwang_arith.order_cards(mainflower, cards); + var i = 0; + while (i < cards.length){ + //牌编码 + var code = cls_youle_erqiwang_arith.id_to_code(mainflower, cards[i]); + //牌的花色 + var flower = parseInt(code / 100); + if (code > 1000){ //大于1000是主牌 + flower = mainflower; + } + + //是第一张牌 + if (i == 0){ + can.flower = flower; + can.cardtype = 101; + can.cardvalue = code; + i++; + continue; + } + + //不是第一张牌 + //花色必须一致 + if (flower != can.flower){ + do_returnfalse(); + return can; + } + + //之前是单张牌型 + if (can.cardtype > 100 && can.cardtype < 200){ + if (can.cardtype == 101 && code == can.cardvalue){ + can.cardtype = 201; //是一对 + } else { + can.cardtype = can.cardtype + 1; //单张甩牌 + can.cardvalue = code; + } + i++; + continue; + } + + //之前是对子或拖拉机牌型,则两张两张牌一取 + //如果是最后一张牌,取不到后面一张牌了 + if (i == cards.length - 1){ + can.cardtype = 100 + i + 1; //单张甩牌 + can.cardvalue = code; + i++; + continue; + } + + //后面一张牌的编码 + var code_2 = cls_youle_erqiwang_arith.id_to_code(mainflower, cards[i + 1]); + //后面一张牌的花色 + var flower_2 = parseInt(code_2 / 100); + if (code_2 > 1000){ //大于1000是主牌 + flower_2 = mainflower; + } + //后面一张牌的花色必须一致 + if (flower_2 != flower){ + do_returnfalse(); + return can; + } + + //当前两张牌不是对子 + if (code_2 != code){ + can.cardtype = 100 + i + 2; //单张甩牌 + can.cardvalue = code_2; + i = i + 2; + continue; + } + + //当前两张牌是对子 + //之前是对子牌型 + if (can.cardtype > 200 && can.cardtype < 300){ + if (can.cardtype == 201 && cls_youle_erqiwang_arith.is_continuous(can.cardvalue, code)){ + //之前是一对,且现在是连牌,则是拖拉机 + can.cardtype = 302; + can.cardvalue = code; + } else { + //是多对 + can.cardtype = can.cardtype + 1; //多对甩牌 + can.cardvalue = code; + } + i = i + 2; + continue; + } + + //之前是拖拉机牌型 + if (can.cardtype > 300 && can.cardtype < 400){ + if (cls_youle_erqiwang_arith.is_continuous(can.cardvalue, code)){ + //现在是连牌,则还是拖拉机 + can.cardtype = can.cardtype + 1; + can.cardvalue = code; + } else { + //是多对 + can.cardtype = 200 + can.cardtype % 100 + 1; //多对甩牌 + can.cardvalue = code; + } + i = i + 2; + continue; + } + } + + /////////// 甩牌处理(design §5.4)/////////// + //把要出的牌分解为分量;>1 个分量即为甩牌 + var comps = cls_youle_erqiwang_arith.decompose_trump(mainflower, cards); + if (comps.length > 1){ + //副牌绝对禁甩(§5.4.1):甩牌必须全是主牌 + var allTrump = true; + for (var ci = 0; ci < cards.length; ci++){ + if (cls_youle_erqiwang_arith.id_to_code(mainflower, cards[ci]) < 1000){ + allTrump = false; + break; + } + } + if (!allTrump){ + //副牌不能甩,属非法出牌(无甩错惩罚,直接拒绝) + do_returnfalse(); + return can; + } + //最大性判定(§5.4.2):按全部手牌,两对手都不持有能压过任一分量的主牌 + if (cls_youle_erqiwang_arith.opp_can_beat_flush(mainflower, comps, opp_zhulist)){ + //甩错(§5.4.5):do_playcard 据此收回甩牌、只强制打出最小一张 + do_returnfalse(); + can.shuaicuo = true; + can.smallest = cards[cards.length - 1]; //cards 已从大到小排序,末位最小 + return can; + } + //合法甩牌:记录分量需求,供跟牌逐分量强制匹配(§5.4.4) + var _dTractors = []; + var _dPairs = 0; + var _dSingles = 0; + for (var ci2 = 0; ci2 < comps.length; ci2++){ + if (comps[ci2].type == "tractor"){ + _dTractors.push(comps[ci2].len); + } else if (comps[ci2].type == "pair"){ + _dPairs = _dPairs + 1; + } else { + _dSingles = _dSingles + 1; + } + } + can.shuai_demand = { tractors: _dTractors, pairs: _dPairs, singles: _dSingles }; + } + + return can; + }, + + //一组同花色牌里,成对部分按相邻拆成的连对 run 长度列表(单张忽略),降序 + //(run 长度=该段连续对子的对数:孤立对子=1、两连对=2、三连对=3……) + tractor_run_lengths: function(mainflower, cards){ + var comps = cls_youle_erqiwang_arith.decompose_trump(mainflower, cards); + var runs = []; + for (var i = 0; i < comps.length; i++){ + if (comps[i].type == "tractor"){ + runs.push(comps[i].len); + } else if (comps[i].type == "pair"){ + runs.push(1); + } + //single 忽略:跟拖拉机时零散单张不参与对子覆盖度 + } + runs.sort(function(a, b){ return b - a; }); + return runs; + }, + + //给定可用连对 run 长度表与要取的对数 k,贪心"最长优先"得到字典序最大的规格档案 + //(每次从最长 run 取 min(run, 剩余对数),即"能凑多长的拖拉机就先凑多长") + max_tractor_profile: function(runlens, k){ + var avail = runlens.concat(); + avail.sort(function(a, b){ return b - a; }); + var profile = []; + var budget = k; + var idx = 0; + while (budget > 0 && idx < avail.length){ + var take = avail[idx]; + if (take > budget){ take = budget; } + profile.push(take); + budget = budget - take; + idx++; + } + return profile; //取用顺序已从大到小 + }, + + //字典序比较:档案 a 是否严格劣于(小于)档案 b + profile_less: function(a, b){ + var n = a.length < b.length ? a.length : b.length; + for (var i = 0; i < n; i++){ + if (a[i] < b[i]){ return true; } + if (a[i] > b[i]){ return false; } + } + //前缀相同,较短者小 + return a.length < b.length; + }, + + //§5.2 拖拉机跟牌"最大拖拉机覆盖度"校验(修复 get_followcard 降级递归 else 分支的欠约束): + //首家出拖拉机、且跟牌方该花色牌足够(跟牌须整手同花色对子)时,跟出的对子按相邻拆成拖拉机后的 + //"规格档案"必须达到该手同花色对子在相同对数下可达的最优(最长拖拉机优先,逐档字典序比较), + //否则等于"放着更长的拖拉机不出、只出零散对子",不合法。 + //返回 true 表示覆盖度达标或本情形不适用(放行,交由其他校验);false 表示不达标(拒绝)。 + follow_tractor_cover_ok: function(mainflower, inhandcards, followcards, startcount, startflower){ + //跟牌必须全是 startflower 花色(主牌算主花色;副7/副2 归主,不算副花色) + var _follow = cls_youle_erqiwang_arith.order_cards(mainflower, followcards.concat()); + for (var i = 0; i < _follow.length; i++){ + var codef = cls_youle_erqiwang_arith.id_to_code(mainflower, _follow[i]); + var flf = (codef > 1000) ? mainflower : parseInt(codef / 100); + if (flf != startflower){ return true; } //含非同花色(垫/混合),不适用 + } + //跟牌必须整手成对 + var followpairs = cls_youle_erqiwang_arith.get_pairlist(mainflower, _follow); + if (followpairs.length * 2 != _follow.length){ return true; } //非全对子(对子不够+补单张等),不适用 + //跟牌方手中该花色的牌 + var suit = []; + for (var h = 0; h < inhandcards.length; h++){ + var codeh = cls_youle_erqiwang_arith.id_to_code(mainflower, inhandcards[h]); + var flh = (codeh > 1000) ? mainflower : parseInt(codeh / 100); + if (flh == startflower){ suit.push(inhandcards[h]); } + } + if (suit.length < startcount){ return true; } //该花色不够,允许混合/垫,本校验不适用 + //该花色可用对子的连对 run,与跟牌自身的 run + var availruns = cls_youle_erqiwang_arith.tractor_run_lengths(mainflower, suit); + var best = cls_youle_erqiwang_arith.max_tractor_profile(availruns, startcount / 2); + var got = cls_youle_erqiwang_arith.tractor_run_lengths(mainflower, _follow); + //跟牌自身档案不得字典序劣于最优档案 + return !cls_youle_erqiwang_arith.profile_less(got, best); + }, + + //根据第一个玩家的出牌获取后面玩家必出的和可出的牌 + get_followcard: function(mainflower, inhandcards, startcount, startflower, startcardtype){ + /* + 参数: + mainflower : 主牌花色 + inhandcards : 手上的牌id列表 + startcount : 第一个玩家出牌的数量 + startflower : 第一个玩家出牌的牌花色 + startcardtype: 第一个玩家出牌的牌型 + 返回结果json: + mustcard: 必出的牌列表 + cancard : 可出的牌列表 + cantype : 可出的牌中需要的牌型 + */ + //返回的结果 + var re = {}; + re.mustcard = []; + re.cancard = []; + re.cantype = null; + + //拷贝数组 + var _inhandcards = inhandcards.concat(); + //将牌从大到小排序 + _inhandcards = cls_youle_erqiwang_arith.order_cards(mainflower, _inhandcards); + + //如果手上的牌数量与要求的出牌数量一样则全是必出的牌 + if (_inhandcards.length == startcount){ + re.mustcard = _inhandcards; + re.cancard = []; + re.cantype = null; + return re; + } + + //获取相同花色的牌与其他花色的牌 + var _startflowercards = []; //相同花色的牌 + var _otherflowercards = []; //其他花色的牌 + for (var i = 0; i < _inhandcards.length; i++){ + //牌编码 + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _inhandcards[i]); + //牌的花色 + var _flower = parseInt(_code / 100); + if (_code > 1000){ //大于1000是主牌 + _flower = mainflower; + } + if (_flower == startflower){ + _startflowercards.push(_inhandcards[i]); + } else { + _otherflowercards.push(_inhandcards[i]); + } + } + + //如果没有相同花色的牌,则全部为可出的牌 + if (_startflowercards.length == 0){ + re.mustcard = []; + re.cancard = _inhandcards; + re.cantype = 100 + startcount; + return re; + } + + //如果相同花色的牌数量不够,则相同花色的牌为必出的牌,其他花色为可出的牌 + if (_startflowercards.length < startcount){ + re.mustcard = _startflowercards; + re.cancard = _otherflowercards; + re.cantype = 100 + startcount - re.mustcard.length; + return re; + } + + //如果相同花色的牌数量与要求的出牌数量一样则全是必出的牌,没有可出的牌 + if (_startflowercards.length == startcount){ + re.mustcard = _startflowercards; + re.cancard = []; + re.cantype = null; + return re; + } + + //如果相同花色的牌数量大于要求的出牌数量 + if (startcardtype == 101){ + //一张单张 + re.mustcard = []; + re.cancard = _startflowercards; + re.cantype = 100 + startcount; + return re; + } else if (startcardtype > 101 && startcardtype < 200){ + //多张单张 + re.mustcard = []; + re.cancard = _startflowercards; + re.cantype = 100 + startcount; + return re; + } else if (startcardtype == 201){ + //一对 + //获取对子列表 + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _startflowercards); + //没有对子 + if (pairlist.length == 0){ + re.mustcard = []; + re.cancard = _startflowercards; + re.cantype = 100 + startcount; + return re; + } + //只有一对 + if (pairlist.length == 1){ + re.mustcard = pairlist[0]; + re.cancard = []; + re.cantype = null; + return re; + } + //有多对 + re.mustcard = []; + for (var i = 0; i < pairlist.length; i++){ + re.cancard = re.cancard.concat(pairlist[i]); + } + re.cantype = 201; + return re; + } else if (startcardtype > 201 && startcardtype < 300){ + //多对 + re.mustcard = []; + re.cancard = _startflowercards; + re.cantype = 100 + startcount; + return re; + } else if (startcardtype > 300 && startcardtype < 400){ + //拖拉机 + //获取对子列表 + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _startflowercards); + + //没有对子 + if (pairlist.length == 0){ + re.mustcard = []; + re.cancard = _startflowercards; + re.cantype = 100 + startcount; + return re; + } + + //对子数量不够 + if (pairlist.length * 2 < startcount){ + for (var i = 0; i < pairlist.length; i++){ + re.mustcard = re.mustcard.concat(pairlist[i]); + } + re.cancard = min_ary_deduct(_startflowercards, re.mustcard); + re.cantype = 100 + startcount - re.mustcard.length; + return re; + } + + //对子数量刚好够 + if (pairlist.length * 2 == startcount){ + for (var i = 0; i < pairlist.length; i++){ + re.mustcard = re.mustcard.concat(pairlist[i]); + } + re.cancard = []; + re.cantype = null; + return re; + } + + //对子有多 + //拖拉机列表 + var tuolaji_list = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairlist, startcardtype); + + //有多个符合条件的拖拉机 + if (tuolaji_list.length > 1){ + re.mustcard = []; + re.cancard = []; + //这里要去重复项 + for (var i = 0; i < tuolaji_list.length; i++){ + for (var j = 0; j < tuolaji_list[i].length; j++) { + var found = false; + for (var k = 0; k < re.cancard.length; k++){ + if (re.cancard[k] == tuolaji_list[i][j]){ + found = true; + break; + } + } + if (!found){ + re.cancard.push(tuolaji_list[i][j]); + } + } + } + re.cantype = startcardtype; + return re; + } + + //只有一个符合条件的拖拉机 + if (tuolaji_list.length == 1){ + re.mustcard = tuolaji_list[0]; + re.cancard = []; + re.cantype = null; + return re; + } + + //没有符合条件的拖拉机 + if (tuolaji_list.length == 0){ + re.mustcard = []; + for (var i = 0; i < pairlist.length; i++){ + re.cancard = re.cancard.concat(pairlist[i]); + } + re.cantype = startcardtype - 100; + //如果是连两对拖拉机 + if (startcardtype == 302){ + return re; + } + + //如果是3连对及以上的拖拉机则降低拖拉机的规格再继续找 + var tlj_type = startcardtype; //需要的牌型 + var tlj_find = startcardtype - 1; //当前查找的牌型 + //再继续找 + while (tlj_type >= 302 && tlj_find >= 302){ + var tlj_list = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairlist, tlj_find); + if (tlj_list.length == 1){ + //找到一个 + re.mustcard = re.mustcard.concat(tlj_list[0]); + re.cantype = re.cantype - (tlj_find - 100) + 200; + if (re.cantype < 201){ + re.cancard = []; + re.cantype = null; + return re; + } + re.cancard = min_ary_deduct(re.cancard, tlj_list[0]); + //删除对子列表 + i = 0; + while (i < pairlist.length){ + if (min_ary_include(tlj_list[0], pairlist[i])){ + pairlist.splice(i, 1); + } else { + i++; + } + } + tlj_type = (tlj_type - tlj_find) + 300; + continue; + } + + if (tlj_list.length > 1){ + //找到多个 + if ((tlj_find - 300) * tlj_list.length <= (tlj_type - 300)){ + re.mustcard = re.mustcard.concat(tlj_list[0]); + re.cantype = re.cantype - (tlj_find - 100) + 200; + if (re.cantype < 201){ + re.cancard = []; + re.cantype = null; + return re; + } + re.cancard = min_ary_deduct(re.cancard, tlj_list[0]); + //删除对子列表 + i = 0; + while (i < pairlist.length){ + if (min_ary_include(tlj_list[0], pairlist[i])){ + pairlist.splice(i, 1); + } else { + i++; + } + } + tlj_type = (tlj_type - tlj_find) + 300; + continue; + } else { + //候选等长拖拉机比剩余需求多(玩家可自选出哪一组): + //此处 cantype 无法表达"任选其一成拖拉机、其余自由",故返回宽松的"任意 N 对", + //由 can_followcard 的 follow_tractor_cover_ok 覆盖度校验兜底拒绝零散对子。 + return re; + } + } + tlj_find = tlj_find - 1; + } + return re; + } + } + }, + + //第二、三个位置是否可跟牌 + can_followcard: function(mainflower, inhandcards, followcards, startcount, startflower, startcardtype){ + /* + 参数: + mainflower : 主牌花色 + inhandcards : 手上的牌id列表 + followcards : 要跟的牌id列表 + startcount : 第一个玩家出牌的数量 + startflower : 第一个玩家出牌的牌花色 + startcardtype: 第一个玩家出牌的牌型 + 返回结果json: + result : true可跟,false不可跟 + cardvalue: 满足第一个玩家出牌的牌型值 + noflower : 出牌后没有了第一个玩家出牌花色的牌标志 + nopair : 出牌后没有了第一个玩家出牌花色的对子标志 + */ + + //返回的结果 + var can = {}; + can.result = true; + can.cardvalue = null; + can.noflower = false; + can.nopair = false; + + //返回不能出牌的结果 + var do_returnfalse = function(){ + can.result = false; + delete can.cardvalue; + delete can.noflower; + delete can.nopair; + } + + //牌的数量必须相等 + if (followcards.length != startcount){ + do_returnfalse(); + return can; + } + + //拷贝数组 + var _inhandcards = inhandcards.concat(); + var _followcards = followcards.concat(); + + //将牌从大到小排序 + _inhandcards = cls_youle_erqiwang_arith.order_cards(mainflower, _inhandcards); + _followcards = cls_youle_erqiwang_arith.order_cards(mainflower, _followcards); + + //已排序跟牌的完整快照:下面的必出/可出校验会用 min_ary_deduct 削减 _followcards, + //而后半段的牌面值(cardvalue)与缺门/无对(noflower/nopair)判定必须基于「完整且已排序」的跟牌。 + //这里绝不能改用入参 followcards——那是客户端提交的原始顺序,而 get_pairlist / get_tuolaji_list + //都按降序相邻取对:乱序提交会漏判对子/拖拉机,把本该压过的主拖拉机算成牌面 0(本轮胜者、 + //捡分归属、扣底倍数全被客户端的数组顺序左右),也能抹掉缺门标志污染 §9 牌况表。 + //(dev-guide server 04 §8:前端不是数据源,服务端不得依赖客户端可控的入参顺序) + var _sortfollow = _followcards.concat(); + + //根据第一个玩家的出牌获取后面玩家必出的和可出的牌 + var get = cls_youle_erqiwang_arith.get_followcard(mainflower, _inhandcards, startcount, startflower, startcardtype); + + ////////// 判断是否可跟牌 /////////// + //如果有必出的牌则必须包含必出的牌 + if (get.mustcard.length > 0){ + if (!min_ary_include(_followcards, get.mustcard)){ + do_returnfalse(); + return can; + } + //除去必出的牌还要出的牌 + _followcards = min_ary_deduct(_followcards, get.mustcard); + } + + if (_followcards.length > 0){ + //必须在可出的牌里面 + if (!min_ary_include(get.cancard, _followcards)){ + do_returnfalse(); + return can; + } + + //必须满足需要的牌型 + if (get.cantype > 100 && get.cantype < 200){ + //单张牌型 + //牌数量要正确 + if (_followcards.length != get.cantype - 100){ + do_returnfalse(); + return can; + } + } else if (get.cantype > 200 && get.cantype < 300){ + //对子牌型 + //牌数量要正确 + if (_followcards.length != (get.cantype - 200) * 2){ + do_returnfalse(); + return can; + } + //对子数量要正确 + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _followcards); + if (pairlist.length != get.cantype - 200){ + do_returnfalse(); + return can; + } + } else if (get.cantype > 300 && get.cantype < 400){ + //拖拉机牌型 + //牌数量要正确 + if (_followcards.length != (get.cantype - 300) * 2){ + do_returnfalse(); + return can; + } + //要是正确的拖拉机牌型 + var _fpairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _followcards); + var _ftljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _fpairlist, get.cantype); + if (_ftljlist.length == 0){ + do_returnfalse(); + return can; + } + } + } + + //§5.2 拖拉机跟牌最大覆盖度校验:修复 get_followcard 降级递归 else 分支的欠约束 + //(同花色对子足够、无同长拖拉机、又有多组等长短拖拉机时,必须尽量长地出拖拉机,不得只出零散对子) + if (startcardtype > 300 && startcardtype < 400){ + if (!cls_youle_erqiwang_arith.follow_tractor_cover_ok(mainflower, inhandcards, followcards, startcount, startflower)){ + do_returnfalse(); + return can; + } + } + + /////////// 可以跟牌 /////////// + //根据跟牌情况获取是否有相同花色的牌,是否有对子 + //如果最大的一张牌的花色与第一家的出牌花色不一样则认为没有了相同花色的牌 + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _sortfollow[0]); + var _flower = parseInt(_code / 100); + if (_code > 1000){ + _flower = mainflower; + } + if (_flower != startflower){ + can.noflower = true; + can.nopair = true; + } + //如果最小的一张牌的花色与第一家的出牌花色不一样则认为没有了相同花色的牌 + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _sortfollow[_sortfollow.length - 1]); + var _flower = parseInt(_code / 100); + if (_code > 1000){ + _flower = mainflower; + } + if (_flower != startflower){ + can.noflower = true; + can.nopair = true; + } + //如果第一家出的是对子或拖拉机牌型,而跟牌没有出对子则认为没有相同花色的对子 + if (startcardtype > 200){ + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _sortfollow); + if (pairlist.length != startcardtype % 100){ + can.nopair = true; + } + } + + //根据第一家的出牌计算牌值大小 + //一张单张 + if (startcardtype == 101){ + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _sortfollow[0]); + var _flower = parseInt(_code / 100); + if (_code > 1000){ //大于1000是主牌 + _flower = mainflower; + } + + if (_flower == startflower || _flower == mainflower){ + if (_code > 7000 && _code < 8000){ //副7 + can.cardvalue = 7000; + } else if (_code > 2000 && _code < 3000){ //副2 + can.cardvalue = 2000; + } else { + can.cardvalue = _code; + } + } else { + can.cardvalue = 0; + } + return can; + } + + //甩多张单张 + if (startcardtype > 101 && startcardtype < 200){ + //第一个玩家是甩牌,则跟牌一定没有第一家玩家的牌大 + can.cardvalue = 0; + return can; + } + + //一对 + if (startcardtype == 201){ + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _sortfollow); + if (pairlist.length != 1){ + can.cardvalue = 0; + return can; + } + + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _sortfollow[0]); + var _flower = parseInt(_code / 100); + if (_code > 1000){ + _flower = mainflower; + } + if (_flower == startflower || _flower == mainflower){ + if (_code > 7000 && _code < 8000){ //副7 + can.cardvalue = 7000; + } else if (_code > 2000 && _code < 3000){ //副2 + can.cardvalue = 2000; + } else { + can.cardvalue = _code; + } + } else { + can.cardvalue = 0; + } + return can; + } + + //甩多对 + if (startcardtype > 201 && startcardtype < 300){ + //第一个玩家是甩牌,则跟牌一定没有第一家玩家的牌大 + can.cardvalue = 0; + return can; + } + + //拖拉机 + if (startcardtype > 300 && startcardtype < 400){ + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _sortfollow); + var tuolaji_list = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairlist, startcardtype); + if (tuolaji_list.length != 1){ + can.cardvalue = 0; + return can; + } + + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _sortfollow[0]); + var _flower = parseInt(_code / 100); + if (_code > 1000){ + _flower = mainflower; + } + if (_flower == startflower || _flower == mainflower){ + can.cardvalue = _code; + } else { + can.cardvalue = 0; + } + return can; + } + }, + + //闲家抠底倍数(design §6.3:单张主 ×1、主对子 ×2、N 连对拖拉机 ×2N) + get_bottom_multiple: function(mainflower, cards){ + //拷贝数组 + var _cards = cards.concat(); + //将牌从大到小排序 + _cards = cls_youle_erqiwang_arith.order_cards(mainflower, _cards); + + //扣底要求赢下最后一轮的牌全是主牌(首末张都 >=1000 即全为主牌) + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[0]); + if (_code < 1000){ + return 0; + } + _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[_cards.length - 1]); + if (_code < 1000){ + return 0; + } + + //单张主牌不翻倍 + var _pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _cards); + if (_pairlist.length == 0){ + return 1; + } + + //取牌中最长的连对数 N,倍数为 2N(主对子按 N=1,即 2 倍) + var _n = 1; + var _len = 2; + while (true){ + var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 300 + _len); + if (_tljlist.length == 0){ + break; + } + _n = _len; + _len = _len + 1; + } + return 2 * _n; + }, + + //冲关计算关数 + get_chongguan: function(mainflower, cards){ + var re = {}; + re.count = 0; //关数 + re.wang = 0; //王牌数量 + re.cards = []; //王牌和冲关的牌 + if (cards.length < 28){ + return re; + } + + //拷贝数组 + var _cards = cards.concat(); + //将牌从大到小排序 + _cards = cls_youle_erqiwang_arith.order_cards(mainflower, _cards); + + var _wlist = []; //王 + var _2list = []; //2 + var _7list = []; //7 + var _incards = []; //是否添加得到了re.cards标志 + for (var i = 0; i < _cards.length; i++) { + var _number = cls_youle_erqiwang_arith.id_to_number(_cards[i]); + if (_number >= 53){ + _wlist.push(i); + re.wang = re.wang + 1; + re.cards.push(_cards[i]); + } else if (_number == 2){ + _2list.push(i); + } else if (_number == 7){ + _7list.push(i); + } + _incards.push(0); + } + + //添加到re.cards + var do_addlisttocards = function(_list){ + for (var i = 0; i < _list.length; i++){ + if (!_incards[_list[i]]){ + re.cards.push(_cards[_list[i]]); + _incards[_list[i]] = 1; + } + } + } + + //三王四王冲关判断 + if (_wlist.length >= 3){ + re.count = 1; + var _code = 9553; //当前的牌编码 + var i = 3; //后面查找的起始位置 + if (_wlist.length == 4){ + re.count = 3; + i = 4; + } + //往后查找,看是否还有冲关 + while (i < _cards.length - 1){ + //牌编码,两张牌两张牌一取 + var code_i = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[i]); + var code_j = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[i + 1]); + if (code_i == code_j){ + if (cls_youle_erqiwang_arith.is_continuous(_code, code_i)){ + re.count = re.count + 1; + re.cards.push(_cards[i]); + re.cards.push(_cards[i + 1]); + _incards[i] = 1; + _incards[i + 1] = 1; + _code = code_i; + } + i = i + 2; + } else { + i++; + } + } + } + + //六2六7判断(design §8.1:6/7/8 个 7 → 1/2/3 奖,6/7/8 个 2 同理) + if (_7list.length >= 6){ + re.count = re.count + _7list.length - 5; + do_addlisttocards(_7list); + } + if (_2list.length >= 6){ + re.count = re.count + _2list.length - 5; + do_addlisttocards(_2list); + } + + //注:固定主牌 ≥10 张只触发亮牌(§8.2)、不算奖,故此处不再加奖 + + re.cards = cls_youle_erqiwang_arith.order_cards(mainflower, re.cards); + return re; + }, + + //计算判定的子数倍率(带符号):大光 +3 / 小光 +2 / 过庄 +1 / 升N级 -N(design §7.2.0) + //q: 小光/过庄分界与升级级距(见 get_qvalue:常规40,爬坡分段) + //先判是否达标(升级),再判大光/小光/过庄,顺序不可颠倒 + get_upgrade: function(call, grade, q){ + if (grade >= call){ + //闲家捡分达到叫分:闲赢升级,级数 = 1 + floor((grade-call)/q) + return -1 * (parseInt((grade - call) / q) + 1); + } + if (grade == 0){ + return 3; //大光 + } + if (grade < q){ + return 2; //小光 + } + return 1; //过庄 + } +} + +//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)。min_ary_include/min_ary_deduct 为平台全局,两运行时按全局名引用 +if (typeof module !== "undefined"){ + module.exports = cls_youle_erqiwang_arith; +} + diff --git a/server/games/erqiwang/class.config.js b/server/games/erqiwang/class.config.js new file mode 100644 index 0000000..e59612b --- /dev/null +++ b/server/games/erqiwang/class.config.js @@ -0,0 +1,77 @@ +/////////////////////////////////////////////////// +////// cls_youle_erqiwang_config: 房间配置解析 ////// +/////////////////////////////////////////////////// +// +// roomtype 采用定长数字位串(字符串),每一位对应一个开关,'0'/'1',方便前端逐项勾选: +// 位0 局数: '0'→6局 '1'→12局 +// 位1 扣卡方式: '0'→房主扣卡 '1'→AA每人扣卡 +// 位2 傍王: '0'→关 '1'→开 +// 位3 爬坡: '0'→常规算子 '1'→爬坡 +// 位4 查牌: '0'→可查牌 '1'→不查牌 +// 示例 "10100" = 12局 / 房主扣卡 / 傍王开 / 常规算子 / 可查牌 +// 缺省 "00000" = 6局 / 房主扣卡 / 无傍王 / 常规算子 / 可查牌(对齐 design 缺省) +// +// 【位长协议 · 2026-09-01 升级 · 第 2 版】 +// 字串格式 = 「5 位协议位」+「N 位扩展位(默认 N=6,当前全 '0')」 +// 例:'11011' + '000000' = '11011000000' +// 协议位的位索引(IDX_* = 0..4)永远对应字串前 5 位 charAt(0..4), +// 在所有版本中不变——这是服务端与子游戏的 SSOT(沿用未升级前的约定)。 +// 扩展位在协议位之后(字串末尾),用于将来加新协议项时向后追加, +// 旧客户端/旧服务端都按 charAt(0..4) 读协议位(向后兼容)。 +// +// 兼容策略:parse() / bit() 接受任意长度: +// - 长度 ≥ 5:bit(idx) 读 charAt(idx),charAt 越界返回 false(等同缺省) +// - 长度 < 5 / 非字符串:parse() 整体按 '00000' 兜底 +// 不再校验扩展位内容:当前扩展位全 '0' 是惯例,将来版本可能填非 0, +// 服务端只读协议位 0..4,扩展位由专门读扩展位的代码(如果有)去解释。 +// +// 本模块是 roomtype 位串格式的唯一解析入口(SSOT);游戏逻辑一律经 parse() 读取, +// 不得在别处按下标硬解析。 +// +var cls_youle_erqiwang_config = cls_youle_erqiwang_config || { + + //位索引常量 + IDX_ASET: 0, //局数 + IDX_DEDUCT: 1, //扣卡方式 + IDX_BANGWANG:2, //傍王 + IDX_CLIMB: 3, //爬坡 + IDX_NOCHECK: 4, //查牌 + + //预留扩展位长度(字串末尾,当前全 '0')。 + //将来加新协议项时,扩展位长不变、协议位索引不变(永远 0..4), + //扩展位长可以动态增加(任何位置 [5, 5+N) 都行),与旧版兼容。 + //本常量仅作 SSOT 标记与未来断言使用,不影响 parse() / bit() 行为。 + RESERVE_LEN: 6, + + //取第 idx 位是否为 '1';roomtype 缺失/过短一律按 '0'(缺省)处理 + bit: function(roomtype, idx){ + if (!roomtype || typeof roomtype != "string"){ + return false; + } + if (idx < 0 || idx >= roomtype.length){ + return false; + } + return roomtype.charAt(idx) == "1"; + }, + + //解析 roomtype 位串为具名配置对象 + //协议位 charAt(0..4) = IDX_ASET..IDX_NOCHECK;扩展位(如果有)由调用方另读 + parse: function(roomtype){ + // 防御式:非字符串 / 过短 → 整体按全 '0' 处理 + if (!roomtype || typeof roomtype != "string" || roomtype.length < 5){ + roomtype = "00000"; + } + var cfg = {}; + cfg.aset12 = cls_youle_erqiwang_config.bit(roomtype, cls_youle_erqiwang_config.IDX_ASET); //true=12局 false=6局 + cfg.deductAA = cls_youle_erqiwang_config.bit(roomtype, cls_youle_erqiwang_config.IDX_DEDUCT); //true=AA每人 false=房主 + cfg.bangwang = cls_youle_erqiwang_config.bit(roomtype, cls_youle_erqiwang_config.IDX_BANGWANG);//true=开傍王 + cfg.climb = cls_youle_erqiwang_config.bit(roomtype, cls_youle_erqiwang_config.IDX_CLIMB); //true=常规算子 + cfg.nocheck = cls_youle_erqiwang_config.bit(roomtype, cls_youle_erqiwang_config.IDX_NOCHECK); //true=不查牌 + return cfg; + } +} + +//Node(本地/单元测试)导出;友乐/浏览器无 module 时跳过,按全局名引用(dev-guide 01 §1) +if (typeof module !== "undefined"){ + module.exports = cls_youle_erqiwang_config; +} diff --git a/server/games/erqiwang/class.desk.js b/server/games/erqiwang/class.desk.js new file mode 100644 index 0000000..17b8ec4 --- /dev/null +++ b/server/games/erqiwang/class.desk.js @@ -0,0 +1,217 @@ +/////////////////////////////////////////////////// +////////// cls_youle_erqiwang_desk: 牌桌 ////////// +/////////////////////////////////////////////////// +//跨模块依赖:Node 走 require 守卫;友乐/浏览器由 min_loadJsFile 加载为同名全局(dev-guide 01 §1) +if (typeof require !== "undefined"){ + var cls_youle_erqiwang_paiju = require("./class.paiju.js"); +} + +var cls_youle_erqiwang_desk = cls_youle_erqiwang_desk || { + + countdown_jiaofen: 15, //叫分倒计时 + countdown_xuanzhu: 20, //选主倒计时 + countdown_maipai: 25, //埋牌倒计时 + countdown_chupai: 30, //出牌倒计时 + + //创建牌桌对象 + new: function(o_room){ + + var desk = {}; + + desk.o_room = o_room; //桌所属的房间对象 + //玩家得分。每个元素:[累积总分, [每局总分...], 基础分累计, 冲关分累计, 傍王分累计] + //后三位是大局结算面板要分项展示的累计(design §7 捡分子数 / §8.1 冲关 / §8.3 傍王), + //三者之和恒等于累积总分——下发时组装成对象(见 get_desk_account),数组下标只在内部用 + desk.seatlist = []; + desk.seatlist.push([0, [], 0, 0, 0]); + desk.seatlist.push([0, [], 0, 0, 0]); + desk.seatlist.push([0, [], 0, 0, 0]); + desk.prepare = [0,0,0]; //玩家准备状态 + desk.paiju_list = []; //牌局列表 + + //方法 + desk.method = {}; + + //当前牌局 + desk.method.curr_paiju = function(){ + return desk.paiju_list[desk.paiju_list.length - 1]; + } + + //新牌局 + desk.method.do_new_paiju = function(firstseat){ + cls_youle_erqiwang_desk.do_new_paiju(desk, firstseat); + } + + //叫分倒计时 + desk.method.get_countdown_jiaofen = function(){ + return cls_youle_erqiwang_desk.countdown_jiaofen; + } + + //叫主倒计时 + desk.method.get_countdown_xuanzhu = function(){ + return cls_youle_erqiwang_desk.countdown_xuanzhu; + } + + //埋牌倒计时 + desk.method.get_countdown_maipai = function(){ + return cls_youle_erqiwang_desk.countdown_maipai; + } + + //出牌倒计时 + desk.method.get_countdown_chupai = function(){ + return cls_youle_erqiwang_desk.countdown_chupai; + } + + //准备 + desk.method.do_prepare = function(seat){ + return cls_youle_erqiwang_desk.do_prepare(desk, seat); + } + + //获取大局结算包 + desk.method.get_desk_account = function(msg){ + return cls_youle_erqiwang_desk.get_desk_account(desk, msg); + } + + return desk; + }, + + //新开一局 + do_new_paiju: function(o_desk, firstseat){ + //恢复玩家准备状态 + o_desk.prepare = [0,0,0]; + + if (o_desk.paiju_list.length > 0){ + //上一局的结算快照三件套一并清除(新局开始后重连不该再看到上一局的结算面板) + delete o_desk.paiju_list[o_desk.paiju_list.length - 1].tmp_jiesuan_aset; + delete o_desk.paiju_list[o_desk.paiju_list.length - 1].tmp_jiesuan_bottom; + delete o_desk.paiju_list[o_desk.paiju_list.length - 1].tmp_jiesuan_account; + } + + //新开一局 + cls_youle_erqiwang_paiju.new(o_desk, firstseat); + + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.rpc = "fapai"; + msg.data = {}; + msg.data.success = true; + msg.data.asetidx = o_desk.paiju_list.length; + msg.data.asetcount = o_desk.o_room.asetcount; + //阶段:发完牌即 step1(叫分)。由服务端权威给出、不让前端按 rpc 推—— + //没有包承载的状态变化等于逼前端去猜(server 03 §1.4 / 红线「状态机唯一在服务端」) + msg.data.step = o_desk.method.curr_paiju().step; + //控制权:当前等待叫分者。与重连包 CallRun.seat 同一个 get_callgrade_seat()(SSOT) + msg.data.seat = o_desk.method.curr_paiju().method.get_callgrade_seat(); + msg.data.countdown = o_desk.method.get_countdown_jiaofen(); + for (var i = 0; i < o_desk.o_room.seatlist.length; i++) { + msg.conmode = o_desk.o_room.seatlist[i].conmode; + msg.fromid = o_desk.o_room.seatlist[i].fromid; + msg.data.cards = o_desk.method.curr_paiju().method.get_seat_cards(i); + youle_erqiwang.app.SendPack(msg); + } + }, + + //准备 + do_prepare: function(o_desk, seat){ + o_desk.prepare[seat] = 1; + if (o_desk.prepare[0] && o_desk.prepare[1] && o_desk.prepare[2]){ + //新开一局 + var paiju = o_desk.method.curr_paiju(); + var firstseat = paiju.banker; + if (paiju.result == 1 || paiju.result == 2){ + firstseat = (firstseat + 1) % 3; + } + cls_youle_erqiwang_desk.do_new_paiju(o_desk, firstseat); + } else { + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.rpc = "zhunbei"; + msg.data = {}; + msg.data.success = true; + msg.data.seat = seat; + o_desk.o_room.method.sendpack_toother(msg, -1); + } + }, + + //大局结算包 + get_desk_account: function(o_desk, msg){ + //保存战绩 + var do_save_grade = function(){ + var o_gameinfo1 = {}; + o_gameinfo1.roomcode = o_desk.o_room.roomcode; + o_gameinfo1.asetcount = o_desk.paiju_list.length; + o_gameinfo1.createtime = o_desk.o_room.createtime; + o_gameinfo1.makewartime = o_desk.o_room.makewartime; + o_gameinfo1.players = []; + for (var i = 0; i < o_desk.o_room.seatlist.length; i++){ + var _player = {}; + _player.seat = i; + _player.playerid = o_desk.o_room.seatlist[i].playerid; + _player.name = o_desk.o_room.seatlist[i].nickname; + _player.avatar = o_desk.o_room.seatlist[i].avatar; + _player.score = o_desk.seatlist[i][0]; + o_gameinfo1.players.push(_player); + } + + var o_gameinfo2 = []; + for (var i = 0; i < o_desk.paiju_list.length; i++) { + var _paiju = {}; + _paiju.starttime = o_desk.paiju_list[i].starttime; + _paiju.endtime = o_desk.paiju_list[i].endtime; + _paiju.seatlist = [0, 0, 0]; + _paiju.seatlist[0] = o_desk.seatlist[0][1][i]; + _paiju.seatlist[1] = o_desk.seatlist[1][1][i]; + _paiju.seatlist[2] = o_desk.seatlist[2][1][i]; + _paiju.callproc = o_desk.paiju_list[i].callproc; + _paiju.banker = o_desk.paiju_list[i].banker; + _paiju.call = o_desk.paiju_list[i].call; + _paiju.flower = o_desk.paiju_list[i].flower; + _paiju.result = o_desk.paiju_list[i].result; + //数据量太多,json转数组后进行存储 + _paiju.cards = []; + for (var j = 0; j < o_desk.paiju_list[i].cards.length; j++) { + var _pai = o_desk.paiju_list[i].cards[j]; + var _card = []; + _card.push(_pai.id); + _card.push(_pai.flower); + _card.push(_pai.number); + _card.push(_pai.score); + _card.push(_pai.dealowner); + _card.push(_pai.playround); + _card.push(_pai.playindex); + _card.push(_pai.playowner); + _paiju.cards.push(_card); + } + o_gameinfo2.push(_paiju); + } + + //累积游戏得分 + for (var i = 0; i < o_desk.o_room.seatlist.length; i++) { + o_desk.o_room.seatlist[i].gameinfo.grade = o_desk.seatlist[i][0]; + } + //保存战绩 + youle_erqiwang.import.save_grade(o_desk.o_room, o_gameinfo1, o_gameinfo2, 1); + } + min_ontimeout(do_save_grade, 1000); + + //大局结算下发:组装成对象数组,字段自解释(内部 o_desk.seatlist 是数组,下标语义不清,不直接外抛) + msg.data.account = []; + for (var s = 0; s < o_desk.seatlist.length; s++){ + var _a = {}; + _a.score = o_desk.seatlist[s][0]; //累积总分 + _a.grades = o_desk.seatlist[s][1]; //每局总分列表 + _a.grade_jf_total = o_desk.seatlist[s][2]; //基础分(捡分子数得分)累计 + _a.grade_cg_total = o_desk.seatlist[s][3]; //冲关分累计(design §8.1,不含傍王) + _a.grade_bw_total = o_desk.seatlist[s][4]; //傍王分累计(design §8.3) + msg.data.account.push(_a); + } + return msg; + } +} + +//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)。SendPack/youle_erqiwang.* 为平台全局 +if (typeof module !== "undefined"){ + module.exports = cls_youle_erqiwang_desk; +} diff --git a/server/games/erqiwang/class.export.js b/server/games/erqiwang/class.export.js new file mode 100644 index 0000000..918c5ed --- /dev/null +++ b/server/games/erqiwang/class.export.js @@ -0,0 +1,279 @@ +/////////////////////////////////////////////////// +/////// cls_youle_erqiwang_export: 输出接口 /////// +/////////////////////////////////////////////////// +//跨模块依赖:Node 走 require 守卫;友乐/浏览器由 min_loadJsFile 加载为同名全局(dev-guide 01 §1) +if (typeof require !== "undefined"){ + var cls_youle_erqiwang_config = require("./class.config.js"); + var cls_youle_erqiwang_arith = require("./class.arith.js"); + var cls_youle_erqiwang_desk = require("./class.desk.js"); + var cls_youle_erqiwang_paiju = require("./class.paiju.js"); +} + +var cls_youle_erqiwang_export = cls_youle_erqiwang_export || { + + new: function() { + + var exp = {}; + + //解析roomtype获取开房所需房卡(design §10.1) + exp.get_needroomcard = function(roomtype){ + var cfg = cls_youle_erqiwang_config.parse(roomtype); + if (cfg.deductAA){ + //AA每人扣卡:6局每人1张 / 12局每人2张 + return cfg.aset12 ? 2 : 1; + } + //房主扣卡:6局2张 / 12局4张 + return cfg.aset12 ? 4 : 2; + } + + //解析roomtype获取总局数(design §10.1:6局 / 12局) + exp.get_asetcount = function(roomtype){ + var cfg = cls_youle_erqiwang_config.parse(roomtype); + return cfg.aset12 ? 12 : 6; + } + + //加入房间时根据roomtype获取需要的房卡数量 + exp.get_needroomcard_joinroom = function(roomtype){ + var cfg = cls_youle_erqiwang_config.parse(roomtype); + if (cfg.deductAA){ + //AA每人扣卡:加入者按自己那份扣(6局1张 / 12局2张) + return cfg.aset12 ? 2 : 1; + } + //房主扣卡:加入者不扣 + return 0; + } + + //开战 + exp.makewar = function(o_room){ + //牌桌对象 + o_room.o_desk = cls_youle_erqiwang_desk.new(o_room); + for (var i = 0; i < o_room.seatlist.length; i++) { + o_room.seatlist[i].gameinfo.isbet = 1; + } + + //开始第一局 + var do_newpaiju = function(){ + o_room.o_desk.method.do_new_paiju(0); + } + min_ontimeout(do_newpaiju, 1000); + } + + //获取某个玩家的牌桌整桌当前信息 + exp.get_deskinfo = function(o_room, seat){ + if (!o_room.o_desk){ + return null; + } + var paiju = o_room.o_desk.method.curr_paiju(); + //本局房间可选规则:爬坡改变「叫分 → 基础子数」的取值(design §7.3.1), + //各阶段下发的 multiple 必须与结算包 aset.multiple 用同一套规则 + var cfg = cls_youle_erqiwang_config.parse(o_room.roomtype); + var deskinfo = {}; + deskinfo.count = o_room.asetcount; //总局数 + deskinfo.idx = paiju.idx; //当前局数 + deskinfo.PlayerInfo = []; //玩家当前累积得分 + for (var i = 0; i < o_room.o_desk.seatlist.length; i++) { + deskinfo.PlayerInfo.push(o_room.o_desk.seatlist[i][0]); + }; + deskinfo.step = paiju.step; //牌局状态 + deskinfo.MyCards = paiju.method.get_seat_cards(seat); //我手上的牌 + switch (paiju.step){ + case 1: //发完牌叫分 + deskinfo.CallRun = {}; + //当前叫分位置 + deskinfo.CallRun.seat = paiju.method.get_callgrade_seat(); + //叫分倒计时 + deskinfo.CallRun.countdown = o_room.o_desk.method.get_countdown_jiaofen(); + //当前叫分 + deskinfo.CallRun.nowcall = paiju.method.get_callgrade_value(); + //当前叫分倍数 + deskinfo.CallRun.multiple = cls_youle_erqiwang_arith.get_base_bycall(deskinfo.CallRun.nowcall, cfg.climb); + //所有玩家的叫分情况 + deskinfo.CallRun.call = [null, null, null]; + deskinfo.CallRun.call[0] = paiju.method.get_seat_callgrade(0); + deskinfo.CallRun.call[1] = paiju.method.get_seat_callgrade(1); + deskinfo.CallRun.call[2] = paiju.method.get_seat_callgrade(2); + break; + case 2: //选主 + deskinfo.ChooseMain = {}; + //控制权:选主/投降都只由庄家做(mod.xuanzhu / mod.touxiang 的 SEAT 校验以 banker 为准)。 + //与 shangzhuang 推送的 nextseat 同源同值——两条路径都由服务端算好给出, + //前端不再自己套「选主 = 庄家」这条规则(字段名与 CallRun.seat 一致:分组内的 seat = 轮到谁) + deskinfo.ChooseMain.seat = paiju.banker; + //庄家 + deskinfo.ChooseMain.banker = paiju.banker; + //庄家叫分 + deskinfo.ChooseMain.call = paiju.call; + //庄家叫分倍数 + deskinfo.ChooseMain.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb); + //选主倒计时 + deskinfo.ChooseMain.countdown = o_room.o_desk.method.get_countdown_xuanzhu(); + //底牌(发牌时没发给玩家、扣在桌面的 8 张):只有庄家可见(70分的3秒亮牌是上庄时的一次性事件,重连不重放) + if (seat == paiju.banker){ + deskinfo.ChooseMain.bottomcards = paiju.method.get_bottomcards(); + } + //是否可投降 + if (paiju.call != 70){ + deskinfo.ChooseMain.touxiang = 0; + } else { + deskinfo.ChooseMain.touxiang = 1; + } + //当前抓分倍数(design §7.2.0):与 shangzhuang 推送同源同值。 + //选主阶段一张牌都还没出,捡分恒为 0 → 必为 +3(大光); + //漏发会让重连后顶部「抓分」角标掉回 0,与增量路径不一致 + deskinfo.ChooseMain.curmultiple = paiju.method.get_curmultiple(); + break; + case 3: //埋牌 + deskinfo.BuryCards = {}; + //控制权:埋牌只由庄家做(mod.maipai 的 SEAT 校验以 banker 为准)。 + //与 xuanzhu 推送的 nextseat 同源同值 + deskinfo.BuryCards.seat = paiju.banker; + //庄家 + deskinfo.BuryCards.banker = paiju.banker; + //庄家叫分 + deskinfo.BuryCards.call = paiju.call; + //庄家叫分倍数 + deskinfo.BuryCards.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb); + //埋牌倒计时 + deskinfo.BuryCards.countdown = o_room.o_desk.method.get_countdown_maipai(); + //底牌(发牌留桌的 8 张):只有庄家可见 + if (seat == paiju.banker){ + deskinfo.BuryCards.bottomcards = paiju.method.get_bottomcards(); + } + //主牌花色 + deskinfo.BuryCards.flower = paiju.flower; + //当前抓分倍数(design §7.2.0):埋牌阶段同样一张牌未出,捡分恒为 0 → 必为 +3 + deskinfo.BuryCards.curmultiple = paiju.method.get_curmultiple(); + //埋牌阶段无投降(投降是选主阶段与选主互斥的选择) + break; + case 5: //出牌 + deskinfo.PushCards = {}; + //庄家 + deskinfo.PushCards.banker = paiju.banker; + //庄家叫分 + deskinfo.PushCards.call = paiju.call; + //庄家叫分倍数 + deskinfo.PushCards.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb); + //出牌倒计时 + deskinfo.PushCards.countdown = o_room.o_desk.method.get_countdown_chupai(); + //主牌花色 + deskinfo.PushCards.flower = paiju.flower; + //捡分 + var jian_grade = paiju.method.get_jian_grade(); + if (seat == paiju.banker){ + //埋牌底牌(庄家埋下的 8 张)。与「底牌」(发牌留桌 8 张,见 ChooseMain/BuryCards.bottomcards) + //是两批不同的牌,字段名刻意区分,勿混用 + deskinfo.PushCards.burycards = paiju.method.get_burycard(); + } else { + //闲家当前的捡分分牌 + deskinfo.PushCards.gradecards = jian_grade.cards; + } + //闲家当前的捡分分数 + deskinfo.PushCards.grade = jian_grade.grade; + //当前抓分倍数(design §7.2.0):与 chupai 包同源同值,重连后角标立即正确 + deskinfo.PushCards.curmultiple = paiju.method.get_curmultiple(); + //当前的出牌情况:与 chupai1/2/3 共用同一处快照函数(结构一致、且是深拷贝, + //不把活对象挂进下发包,见 class.paiju.js get_playproc) + deskinfo.PushCards.playproc = cls_youle_erqiwang_paiju.get_playproc(paiju); + //本轮跟牌的必出牌(design §5.2):只在【轮到本座位】且本轮已有人出牌时才有, + //算的也只是本座位自己的手牌,不涉及他家(server 红线:按可见性下发) + if (paiju.playproc && paiju.playproc.currseat == seat){ + var _must = paiju.method.get_mustcard(seat); + if (_must){ + deskinfo.PushCards.mustcard = _must; + } + } + //报无主:与 chupai1/2/3 的 baozhu 同源同门控(mod.chupai)。 + //【必须带】它是「余主公示 + 明牌按钮」的开关(design §9,前端 + //Sprites_Table.js 的明牌按钮据此显隐)。只在 chupai 包里给、重连包不给, + //等于重连后按钮凭空消失——状态变更没有包承载(server 红线:发全下发面)。 + //不查牌模式恒 0(与 chupai 一致,协议 §11–§13 baozhu 条目) + deskinfo.PushCards.baozhu = (!cfg.nocheck && paiju.method.have_baofu()) ? 1 : 0; + //座位列表(报副统计)与亮牌:仅可查牌模式下展示(§9/§8.2) + if (!cfg.nocheck){ + //【深拷贝】与 maipai / chupai1/2/3 共用同一处快照函数,且不把活表挂进下发包 + //(paiju.seatlist 是 do_playcard 每手就地改写的活对象,见 class.paiju.js get_seatlist) + deskinfo.PushCards.seatlist = cls_youle_erqiwang_paiju.get_seatlist(paiju); + if (seat != paiju.banker){ + var _lp = paiju.method.get_liangpai(); + if (_lp){ + deskinfo.PushCards.liangpai = _lp; + } + } + } + //出牌历史:往轮打出、已被收走的牌属于「查牌」范畴,仅可查牌模式下发(design §9)。 + //不查牌模式下不带此属性——当前这一轮桌面上的牌由上面的 playproc 恢复,两种模式都有, + //否则后出的人无从跟牌(design §9 末尾的边界说明) + if (!cfg.nocheck){ + //【直读归档、不重建】pushlist 的唯一来源是 do_playcard 出牌当场写下的 + //paiju.playhistory(见 class.paiju.js)。旧实现是从 paiju.cards 的 playround/dealowner + //反查再自己排一次序——那是同一份数据「这一手打出的牌」的第二个写入处: + //一手多张(甩牌/对子/拖拉机)时它与 chupai1/2/3 的 data.cards 会给出不同顺序, + //前端增量回放与重连重建因此对不上(同编码的一对牌尤其:排序无法区分它们)。 + deskinfo.PushCards.pushlist = paiju.method.get_pushlist(); + } + break; + case 6: //结算 + deskinfo.Balance = {}; + //玩家的准备状态 + //【返回副本】o_room.o_desk.prepare 是一张全程就地改写的活数组 + //(do_prepare 每次准备就地把某一格置 1,do_new_paiju 又就地重建), + //把它本体挂进下发包会让「已发出的包」随后续准备操作被回改。 + //与 get_playproc / get_seatlist / get_pushlist / get_bottomcards 同一条标准: + //平台真实链路虽然立刻序列化看不出来,但这是隐患,不留。 + deskinfo.Balance.readystate = o_room.o_desk.prepare.concat(); + if (o_room.o_desk.prepare[seat] == 0){ + //结算面板的三件套:与 jiesuan 推送【同源同值】(都取 get_paiju_account 末尾 + //冻结的那份快照)。只给 aset 会让「结算面板还开着时断线重连/硬刷新」丢掉 + //抠底明细与末局大结算——增量路径有、重连路径空白,正是「同一份数据两条路径 + //给的不一样」(server 红线:发全下发面,漏发修在服务端发包处)。 + //结算情况 + deskinfo.Balance.aset = paiju.tmp_jiesuan_aset; + //抠底明细:仅正常出牌结算存在(投降/解散没有抠底这一步),与 jiesuan.bottom 同一份 + if (paiju.tmp_jiesuan_bottom){ + deskinfo.Balance.bottom = paiju.tmp_jiesuan_bottom; + } + //大局结算:仅末局或解散时存在,与 jiesuan.account 同一份。 + //「有就带、没有就不带」与 jiesuan 推送的取舍完全一致,前端两条路径同一套解析 + if (paiju.tmp_jiesuan_account){ + deskinfo.Balance.account = paiju.tmp_jiesuan_account; + } + } + break; + } + + return deskinfo; + } + exp.createroom_needprepare = function(roomtype, o_game_config) { + + return 1; // 1 = 需要准备 + }; + //解散房间 + //平台在 battlestate==1 时即会调用(rpc.js 开战处 makewar 后立刻置 1),而本游戏的首个牌局 + //是 makewar 里 min_ontimeout(...,1000) 延迟创建的——这段窗口内 o_desk 已存在但 paiju_list 为空。 + //此时返回 null(平台对 null 的 _deskfree 有分支:照常发解散包、不带 deskfree), + //而不是让 curr_paiju() 的 undefined 解引用抛异常打断整条解散链路。 + exp.get_disbandRoom = function(o_room){ + if (!o_room.o_desk){ + return null; + } + var paiju = o_room.o_desk.method.curr_paiju(); + if (!paiju){ + return null; + } + var msg = {}; + return paiju.method.get_paiju_account(2, msg); + } + + return exp; + } +} + +//对内输出接口(友乐/浏览器:youle_erqiwang 由 mod.js 先建;Node 单测无此全局时跳过) +if (typeof youle_erqiwang !== "undefined"){ + youle_erqiwang.export = cls_youle_erqiwang_export.new(); +} + +//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1) +if (typeof module !== "undefined"){ + module.exports = cls_youle_erqiwang_export; +} \ No newline at end of file diff --git a/server/games/erqiwang/class.import.js b/server/games/erqiwang/class.import.js new file mode 100644 index 0000000..418cce6 --- /dev/null +++ b/server/games/erqiwang/class.import.js @@ -0,0 +1,37 @@ +/////////////////////////////////////////////////// +/////// cls_youle_platform_import: 输入接口 /////// +/////////////////////////////////////////////////// +var cls_youle_erqiwang_import = cls_youle_erqiwang_import || { + + new: function() { + + var imp = {}; + + //检查玩家 + imp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid){ + return youle_erqiwang.app.youle_room.export.check_player(agentid, gameid, roomcode, seat, playerid, conmode, fromid); + } + + //第一局结算扣除房卡 + imp.deduct_roomcard = function(o_room){ + return youle_erqiwang.app.youle_room.export.deduct_roomcard(o_room); + } + + //保存战绩 + imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag){ + youle_erqiwang.app.youle_room.export.save_grade(o_room, o_gameinfo1, o_gameinfo2, freeroomflag); + } + + return imp; + } +} + +// 输入接口(友乐/浏览器:youle_erqiwang 由 mod.js 先建;Node 单测无此全局时跳过) +if (typeof youle_erqiwang !== "undefined"){ + youle_erqiwang.import = cls_youle_erqiwang_import.new(); +} + +//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)。youle_erqiwang.app 为平台全局 +if (typeof module !== "undefined"){ + module.exports = cls_youle_erqiwang_import; +} \ No newline at end of file diff --git a/server/games/erqiwang/class.pai.js b/server/games/erqiwang/class.pai.js new file mode 100644 index 0000000..0a7320a --- /dev/null +++ b/server/games/erqiwang/class.pai.js @@ -0,0 +1,66 @@ +/////////////////////////////////////////////////// +////////// cls_youle_erqiwang_pai: 牌 ////////// +/////////////////////////////////////////////////// +var cls_youle_erqiwang_pai = cls_youle_erqiwang_pai || { + + //创建单张牌对象 + new: function(id, flower, number, score, dealowner){ + /* + 参数说明: + id 绝对id + flower 牌面花色 + number 牌面数值 + code 牌编码 + + 扑克牌的统一编码规则: + 1,牌面花色的定义 5:王 4:黑桃 3:红心 2:梅花 1:方块 + 2,牌面数值的定义 01:A 02:2 03:3 ... 09:9 10:10 11:J 12:Q 13:K 53:小王 54:大王 + 3,牌编码的定义 + 3.1 常主的牌编码定义: + 未叫主之前: + 2的牌编码定义 黑桃2:2402 红心2:2302 梅花2:2202 方块2:2102 + 7的牌编码定义 黑桃7:7407 红心2:7307 梅花7:7207 方块7:7107 + 王的牌编码定义 小王:9553 大王:9554 + 叫主之后: + 主2牌编码定义 原有牌编码的基础上+1000,例如红心为主牌,则红心2的牌编码变为3302 + 主7牌编码定义 原有牌编码的基础上+1000,例如红心为主牌,则红心7的牌编码变为8307 + 3.2 普通牌的牌编码定义 + A的定义:14 + 未叫主之前: + 3位数字 第一位为牌面花色 第二三位为牌面数值 + 叫主之后: + 主牌花色 4位数字,即在原有牌编码的基础上+1000,表示是主牌 + 非主牌花色 3位数字,即原有牌编码不变 + 3.3 综上所述 + 牌编码是3位的,即小于1000的都是非主牌 + 牌编码是4位的,即大于1000的都是主牌 + 可根据牌编码进行牌大小的比较和排序 + 4 牌型定义 + >100: 单张, 101一张单张,102两张单张,103三张单张 ... + >200: 对子, 201一对,202两对,203三对 ... + >300: 拖拉机,302两连对拖拉机,303三连对拖拉机 ... + + */ + var pai = {}; + pai.id = id; //绝对id,即数组下标,从0开始计数 + pai.flower = flower; //牌面花色 + pai.number = number; //牌面数值 + pai.score = score; //牌在游戏中的分值 + pai.dealowner = dealowner; //发牌状态 + //-1:规则去除的牌 + // 0:未发的牌,即底牌 + //>0:发牌发到谁手上,从1开始计数,即位置编号+1,位置编号是从0开始计数的 + pai.playround = -1; //出牌状态 + //-1:未出的牌 + // 0:埋牌 + //>0:牌是第几轮出出去的,从1开始计数 + pai.playindex = -1; //本轮中的出牌顺序,从1开始计数 + pai.playowner = -1; //出牌后被谁得到,与位置编号对应 + return pai; + } +} + +//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1) +if (typeof module !== "undefined"){ + module.exports = cls_youle_erqiwang_pai; +} diff --git a/server/games/erqiwang/class.paiju.js b/server/games/erqiwang/class.paiju.js new file mode 100644 index 0000000..da8eda3 --- /dev/null +++ b/server/games/erqiwang/class.paiju.js @@ -0,0 +1,1223 @@ +/////////////////////////////////////////////////// +////////// cls_youle_erqiwang_paiju: 牌局 ///////// +/////////////////////////////////////////////////// +//跨模块依赖:Node 走 require 守卫;友乐/浏览器无 require,由 min_loadJsFile 加载为同名全局(dev-guide 01 §1) +if (typeof require !== "undefined"){ + var cls_youle_erqiwang_arith = require("./class.arith.js"); + var cls_youle_erqiwang_config = require("./class.config.js"); + var cls_youle_erqiwang_pai = require("./class.pai.js"); +} + +var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || { + + new: function(o_desk, firstseat) { + var paiju = {}; + o_desk.paiju_list.push(paiju); + + // ================= 实例的私有属性 ================= // + //发牌过程中待发的位置列表 + var tmpdeal = []; + //三家需要各发28张牌 + for (var i = 0; i < 28; i++){ + tmpdeal.push(0); //0表示第一个玩家的位置 + tmpdeal.push(1); //1表示第二个玩家的位置 + tmpdeal.push(2); //2表示第三个玩家的位置 + } + //8张底牌 + for (var i = 0; i < 8; i++){ + tmpdeal.push(-1); //-1 表示是底牌 + } + + // ================= 实例的私有方法 ================= // + + //初始化座位列表 + var init_seatlist = function(){ + //第一层是玩家列表,与位置序号对应 + //第二层前4个数组是花色列表,与花色-1对应 + //第三层的第一位是无该花色的牌标志 + //第三层的第二位是该花色无对标志 + //第二层第5个数组是报副情况 + //第三层的第一位是剩余主牌数量 + //第三层的第二位是剩余主牌对子 + //0号位 + paiju.seatlist.push([[0,0],[0,0],[0,0],[0,0],[-1,-1]]); + //1号位 + paiju.seatlist.push([[0,0],[0,0],[0,0],[0,0],[-1,-1]]); + //2号位 + paiju.seatlist.push([[0,0],[0,0],[0,0],[0,0],[-1,-1]]); + } + + //初始化牌列表并发牌 + var init_cards = function(){ + //两副牌 + for (var i = 1; i <= 2; i++){ + //四种花色 + for (var j = 1; j <= 4; j++){ + for (var k = 1; k <= 13; k++){ + //牌id + var id = (i - 1) * 54 + (j - 1) * 13 + k - 1; + //分值 + var score = 0; + //发牌状态 + var dealowner = 0; + //特殊处理 + switch (k) + { + case 5: + score = 5; + break; + case 10: + score = 10; + break; + case 13: + score = 10; + break; + case 3: + dealowner = -1; + break; + case 4: + dealowner = -1; + break; + } + var pai = cls_youle_erqiwang_pai.new(id, j, k, score, dealowner); + paiju.cards.push(pai); + //发牌 + if (k != 3 && k != 4){ + do_dealpai(pai); + } + } + } + + //小王 + var pai = cls_youle_erqiwang_pai.new((i - 1) * 54 + 53 - 1, 5, 53, 0, 0); + paiju.cards.push(pai); + do_dealpai(pai); + + //大王 + var pai = cls_youle_erqiwang_pai.new((i - 1) * 54 + 54 - 1, 5, 54, 0, 0); + paiju.cards.push(pai); + do_dealpai(pai); + } + } + + //将单张牌随机发到玩家手中或成为底牌 + var do_dealpai = function(o_pai){ + var idx = min_random(0, tmpdeal.length - 1); + if (tmpdeal[idx] == -1) { //底牌 + o_pai.dealowner = 0; + } else { + o_pai.dealowner = tmpdeal[idx] + 1; + } + //将数组的最后一位至该位置 + if (idx < tmpdeal.length - 1) { + tmpdeal[idx] = tmpdeal[tmpdeal.length - 1]; + }; + //数组长度减一 + tmpdeal.length = tmpdeal.length - 1; + } + + //冻结底牌快照(发牌留桌的 8 张) + //——内容在发牌结束时就已确定(dealowner==0 之后不再变),【显示顺序也必须一并冻结】。 + //理由(SSOT):排序用的 order_cards 依赖主牌花色,而底牌是在【选主之前】的上庄时刻 + //就翻给庄家看的——那时 paiju.flower 还是 -1,根本还没有主牌。若每次调用都按「当时的 + //flower」重排,上庄推送(flower=-1)与重连 BuryCards(step3,flower 已定)会对同一份数据 + //给出两种顺序(实测:主花色的那张 K 被排到队首),前端增量路径与重连路径因此对不上。 + //故在发牌结束时按【翻底那一刻的口径】算一次并冻结,之后所有下发点只取这份快照的副本。 + var init_bottomcards = function(){ + var ary = []; + for (var i = 0; i < paiju.cards.length; i++){ + if (paiju.cards[i].dealowner == 0){ + ary.push(paiju.cards[i].id); + } + } + //此处 paiju.flower 必为 -1(选主在 step2 才发生),即「无主牌」序 = 翻底时的顺序 + paiju.bottomcards = cls_youle_erqiwang_arith.order_cards(paiju.flower, ary); + } + + //进入叫分阶段 + var start_call = function(){ + paiju.step = 1; + var o_call = {}; + o_call.seat = paiju.firstseat; + o_call.call = null; //null:未叫分 0:不叫分 >0:叫分分数 + paiju.callproc.push(o_call); + } + + // ================= 实例的公有属性 ================= // + paiju.o_desk = o_desk; + //牌局号 + paiju.idx = o_desk.paiju_list.length; + //第一个叫分者的位置 + paiju.firstseat = firstseat; + //开始时间 + paiju.starttime = min_now(); + //结束时间 + paiju.endtime = null; + //座位列表 + paiju.seatlist = []; + //牌列表 + paiju.cards = []; + //当前阶段 + paiju.step = -1; //1发完牌叫分 2选主/投降 3埋牌 5出牌 6结算(无独立投降阶段,投降在step2) + //叫分过程 + paiju.callproc = []; + //庄家位置 + paiju.banker = -1; + //庄家叫分 + paiju.call = -1; + //主牌花色 + paiju.flower = -1; + //底牌快照(发牌留桌的 8 张,含显示顺序):发牌结束时冻结,见 init_bottomcards + paiju.bottomcards = []; + //当前出牌情况 + paiju.playproc = {}; + //出牌历史归档:外层下标 = 轮次-1,内层恒 3 个数组、下标 = 座位;每一项就是那一手 + //【实际打出、且已归一成权威顺序】的牌。唯一写入处是 do_playcard 的 doplay()。 + //【为什么要存】重连包 PushCards.pushlist 需要往轮的出牌历史,而牌一旦落地就只剩 + //「第几轮 + 属于谁」(cards[i].playround/dealowner),一手之内的顺序无从恢复—— + //从那里反查就必须自己再排一次序,那是同一份数据的第二个写入处(SSOT 违反), + //且遇到同编码的两张牌(一对)时两处必然分岔。改为出牌当场归档、下游只读。 + paiju.playhistory = []; + //输赢结果 0:庄赢 1:闲赢 2:庄投降 3:中途解散 + paiju.result = null; + //结算包快照,供断线重连时使用,新开一局时清除。三件套与 jiesuan 推送【同源同值】: + // tmp_jiesuan_aset 单局结算(三种结算来源恒有) + // tmp_jiesuan_bottom 抠底明细(仅正常出牌结算有;投降/解散为 null) + // tmp_jiesuan_account 大局结算(仅末局或解散有;其余为 null) + //只存 aset 会让「结算面板还开着时重连/硬刷新」丢掉抠底明细与大局结算(增量路径有、 + //重连路径没有),即同一份数据两条路径给的不一样。唯一写入处是 get_paiju_account 末尾。 + paiju.tmp_jiesuan_aset = null; + paiju.tmp_jiesuan_bottom = null; + paiju.tmp_jiesuan_account = null; + + //初始化座位列表 + init_seatlist(); + //初始化牌列表并发牌 + init_cards(); + //冻结底牌快照(必须在发牌之后、选主之前) + init_bottomcards(); + //进入叫分阶段 + start_call(); + + // ================= 实例的公有方法 ================= // + paiju.method = {}; + + //获取某个位置玩家手上的牌 + paiju.method.get_seat_cards = function(seat){ + return cls_youle_erqiwang_paiju.get_seat_cards(paiju, seat); + } + + //获取当前的叫分位置 + paiju.method.get_callgrade_seat = function(){ + return paiju.callproc[paiju.callproc.length - 1].seat; + } + + //获取当前的叫分分数 + paiju.method.get_callgrade_value = function(){ + return cls_youle_erqiwang_paiju.get_callgrade_value(paiju); + } + + //获取某个玩家的当前叫分情况 + paiju.method.get_seat_callgrade = function(seat){ + return cls_youle_erqiwang_paiju.get_seat_callgrade(paiju, seat); + } + + //叫分或不叫 + paiju.method.do_callgrade = function(call){ + cls_youle_erqiwang_paiju.do_callgrade(paiju, call); + } + + //获取底牌 + paiju.method.get_bottomcards = function(){ + return cls_youle_erqiwang_paiju.get_bottomcards(paiju); + } + + //选主 + paiju.method.do_choiceflower = function(flower){ + cls_youle_erqiwang_paiju.do_choiceflower(paiju, flower); + } + + //校验客户端提交的牌id列表本身是否合法(数组/整数/范围/无重复) + paiju.method.check_cards_valid = function(cards){ + return cls_youle_erqiwang_paiju.check_cards_valid(paiju, cards); + } + + //判断牌是否在玩家手上 + paiju.method.check_cards_inhand = function(cards, seat){ + return cls_youle_erqiwang_paiju.check_cards_inhand(paiju, cards, seat); + } + + //埋牌 + paiju.method.do_burycard = function(cards){ + cls_youle_erqiwang_paiju.do_burycard(paiju, cards); + } + + //检查选中的牌是否可出,并出牌 + paiju.method.do_playcard = function(cards){ + return cls_youle_erqiwang_paiju.do_playcard(paiju, cards); + } + + //是否有人报副 + paiju.method.have_baofu = function(){ + return (paiju.seatlist[0][4][0] == 0 || paiju.seatlist[1][4][0] == 0 || paiju.seatlist[2][4][0] == 0); + } + + //在牌列表中获取分牌和分值 + paiju.method.get_grade_incard = function(cards){ + return cls_youle_erqiwang_paiju.get_grade_incard(paiju, cards); + } + + //获取埋牌 + paiju.method.get_burycard= function(){ + return cls_youle_erqiwang_paiju.get_burycard(paiju); + } + + //获取抠底包 + paiju.method.get_bottom_account = function(msg){ + return cls_youle_erqiwang_paiju.get_bottom_account(paiju, msg); + } + + //小局结算,type 0正常结算 1投降结算 2解散结算 + paiju.method.get_paiju_account = function(type, msg){ + return cls_youle_erqiwang_paiju.get_paiju_account(paiju, type, msg); + } + + //获取闲家当前的捡分分数和分牌 + paiju.method.get_jian_grade = function(){ + return cls_youle_erqiwang_paiju.get_jian_grade(paiju); + } + + //获取庄家亮牌信息(design §8.2) + paiju.method.get_liangpai = function(){ + return cls_youle_erqiwang_paiju.get_liangpai(paiju); + } + + //获取当前抓分倍数(design §7.2.0,实时预览) + paiju.method.get_curmultiple = function(){ + return cls_youle_erqiwang_paiju.get_curmultiple(paiju); + } + + //获取出牌历史快照(重连包 PushCards.pushlist) + paiju.method.get_pushlist = function(){ + return cls_youle_erqiwang_paiju.get_pushlist(paiju); + } + + //获取三家座位牌况快照(maipai / chupai1/2/3 / 重连包 PushCards.seatlist) + paiju.method.get_seatlist = function(){ + return cls_youle_erqiwang_paiju.get_seatlist(paiju); + } + + //获取某座位本轮跟牌的必出牌(design §5.2) + paiju.method.get_mustcard = function(seat){ + return cls_youle_erqiwang_paiju.get_mustcard(paiju, seat); + } + + return paiju; + }, + + //下一个位置 + get_nextseat: function(seat){ + return (seat + 1) % 3; + }, + + //获取某个位置玩家手上的牌 + get_seat_cards: function(o_paiju, seat){ + var aryCardIDs = []; + for (var i = 0; i < o_paiju.cards.length; i++){ + var o_card = o_paiju.cards[i]; + if (o_card.playround == -1){ //未出的牌 + if ((o_card.dealowner == seat + 1) || //发到手上 + (o_card.dealowner == 0 && seat == o_paiju.banker)){ //庄家包括底牌 + aryCardIDs.push(o_paiju.cards[i].id); + } + } + } + return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs); + }, + + //获取某个位置玩家发到手上的牌 + get_seat_cards_deal: function(o_paiju, seat){ + var aryCardIDs = []; + for (var i = 0; i < o_paiju.cards.length; i++){ + var o_card = o_paiju.cards[i]; + if (o_card.dealowner == seat + 1){ + aryCardIDs.push(o_paiju.cards[i].id); + } + } + return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs); + }, + + //获取某个位置玩家发到手上的牌(庄家包含底牌) + get_seat_cards_owner: function(o_paiju, seat){ + var aryCardIDs = []; + for (var i = 0; i < o_paiju.cards.length; i++){ + var o_card = o_paiju.cards[i]; + if ((o_card.dealowner == seat + 1) || //发到手上 + (o_card.dealowner == 0 && seat == o_paiju.banker)){ //庄家包括底牌 + aryCardIDs.push(o_paiju.cards[i].id); + } + } + return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs); + }, + + //获取算奖用的静态初始手牌快照(design §8) + //庄家:埋牌后保留的28张(发牌+底,排除已埋 playround==0);闲家:发牌后的28张 + get_seat_cards_award: function(o_paiju, seat){ + var aryCardIDs = []; + for (var i = 0; i < o_paiju.cards.length; i++){ + var o_card = o_paiju.cards[i]; + if (seat == o_paiju.banker){ + if ((o_card.dealowner == seat + 1 || o_card.dealowner == 0) && o_card.playround != 0){ + aryCardIDs.push(o_card.id); + } + } else { + if (o_card.dealowner == seat + 1){ + aryCardIDs.push(o_card.id); + } + } + } + return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs); + }, + + //获取某个位置玩家手上的主牌 + get_seat_zhucards: function(o_paiju, seat){ + var aryCardIDs = []; + for (var i = 0; i < o_paiju.cards.length; i++){ + var o_card = o_paiju.cards[i]; + if (o_card.playround == -1){ //未出的牌 + if ((o_card.dealowner == seat + 1) || //发到手上 + (o_card.dealowner == 0 && seat == o_paiju.banker)){ //庄家包括底牌 + if (o_card.flower == o_paiju.flower || o_card.flower == 5 || o_card.number == 2 || o_card.number == 7){ + aryCardIDs.push(o_paiju.cards[i].id); + } + } + } + } + return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs); + }, + + //获取当前的叫分分数 + get_callgrade_value: function(o_paiju){ + var value = 0; + for (var i = o_paiju.callproc.length - 2; i >= 0; i--) { + if (o_paiju.callproc[i].call) { + value = o_paiju.callproc[i].call; + break; + } + } + return value; + }, + + //获取某个位置的叫分 返回值:null未叫 0不叫 >0当前叫分 + get_seat_callgrade: function(o_paiju, seat){ + for (var i = o_paiju.callproc.length - 2; i >= 0; i--){ + if (o_paiju.callproc[i].seat == seat){ + return o_paiju.callproc[i].call; + } + } + return null; + }, + + //叫分 + do_callgrade: function(o_paiju, call){ + var o_call = o_paiju.callproc[o_paiju.callproc.length - 1]; + o_call.call = call; + + //等待下一个叫分者叫分 + var do_wait_nextseat_call = function(seat){ + var o_call = {}; + o_call.seat = seat; + o_call.call = null; + o_paiju.callproc.push(o_call); + } + + //上庄 + var do_up_banker = function(seat, call){ + //庄家位置 + o_paiju.banker = seat; + //庄家叫分 + o_paiju.call = call; + //进入选主阶段 + o_paiju.step = 2; + } + + //叫了5分直接上庄 + if (call == 5){ + do_up_banker(o_call.seat, call); + return; + } + //叫了分 + if (call > 0){ + //下一个位置 + var nextseat = cls_youle_erqiwang_paiju.get_nextseat(o_call.seat); + //下一个位置的叫分 + var nextcall = cls_youle_erqiwang_paiju.get_seat_callgrade(o_paiju, nextseat); + //下一个位置不叫分 + if (nextcall == 0){ + //再下一个位置 + nextseat = cls_youle_erqiwang_paiju.get_nextseat(nextseat); + } + //等待下一个叫分者叫分 + do_wait_nextseat_call(nextseat); + return; + } + + //不叫 + if (call == 0){ + //下一个位置 + var nextseat_1 = cls_youle_erqiwang_paiju.get_nextseat(o_call.seat); + //下一个位置的叫分 + var nextcall_1 = cls_youle_erqiwang_paiju.get_seat_callgrade(o_paiju, nextseat_1); + //再下一个位置 + var nextseat_2 = cls_youle_erqiwang_paiju.get_nextseat(nextseat_1); + //再下一个位置的叫分 + var nextcall_2 = cls_youle_erqiwang_paiju.get_seat_callgrade(o_paiju, nextseat_2); + + if (nextcall_1 != 0 && nextcall_2 != 0){ + //另外两家都没有不叫分 + do_wait_nextseat_call(nextseat_1); + return; + } + if (nextcall_1 != 0 && nextcall_2 == 0){ + //nextseat_1上庄 + do_up_banker(nextseat_1, nextcall_1); + return; + } + if (nextcall_1 == 0 && nextcall_2 != 0){ + //nextseat_2上庄 + do_up_banker(nextseat_2, nextcall_2); + return; + } + } + }, + + //获取底牌(发牌留桌的 8 张):唯一来源是发牌时冻结的快照 o_paiju.bottomcards(见 init_bottomcards)。 + //【返回副本】——order_cards 是原地排序,把快照本体交出去会被调用方就地重排,冻结即失效。 + get_bottomcards: function(o_paiju){ + return o_paiju.bottomcards.concat(); + }, + + //获取【供下发的】当前出牌情况快照:deskinfo.PushCards 与 chupai1/2/3 共用这一处, + //两条路径因此结构完全一致(前端增量回放与重连重建可复用同一份解析)。 + //必须【深拷贝】:playproc 是一个全程复用的活对象(new_playround 就地重置同一个对象, + //do_playcard 也就地改 cards/maxseat/maxcard),直接把它挂进 msg 会让「已发出的包」 + //随后续出牌被回改;平台真实链路虽然立刻序列化看不出来,但这是隐患,不留。 + //structure:round/start/currseat/startcount/startflower/starttype/maxseat/maxcard/ + // cards(定长3、下标=座位)/shuai_demand + //获取【供下发的】出牌历史快照(重连包 PushCards.pushlist)。 + //唯一来源是 do_playcard 当场归档的 playhistory,这里只拷贝、不重算也不重排: + //它与 chupai1/2/3 的 data.cards、playproc.cards 是同一次 order_playcards 的产物, + //因此两条路径逐元素相等(前端增量回放与重连重建必定一致)。 + //【返回深拷贝】不把活数组挂进下发包(同 get_playproc)。 + get_pushlist: function(o_paiju){ + var re = []; + for (var i = 0; i < o_paiju.playhistory.length; i++){ + var _round = o_paiju.playhistory[i]; + var _one = []; + for (var s = 0; s < 3; s++){ + _one.push(_round[s].concat()); + } + re.push(_one); + } + return re; + }, + + //获取【供下发的】三家座位牌况快照(maipai / chupai1/2/3 / 重连包 PushCards.seatlist)。 + //结构:外层定长 3(下标=座位),每家 5 个二元数组——前 4 个是花色1~4 的 [无该花色, 无对], + //第 5 个是报副 [剩余主牌数, 剩余主对数]。 + //【返回深拷贝】o_paiju.seatlist 是一张全程就地改写的活表(do_playcard 里按缺门/无对/报无主 + //逐格赋值),把它本体挂进下发包会让「已发出的包」随后续出牌被回改;平台真实链路虽然立刻 + //序列化看不出来,但这是隐患,与 get_playproc / get_pushlist / get_bottomcards 同一条标准。 + get_seatlist: function(o_paiju){ + var re = []; + for (var s = 0; s < o_paiju.seatlist.length; s++){ + var _one = []; + for (var f = 0; f < o_paiju.seatlist[s].length; f++){ + _one.push(o_paiju.seatlist[s][f].concat()); + } + re.push(_one); + } + return re; + }, + + get_playproc: function(o_paiju){ + var _p = o_paiju.playproc; + if (!_p){ + return null; + } + var re = {}; + re.round = _p.round; + re.start = _p.start; + re.currseat = _p.currseat; + re.startcount = _p.startcount; + re.startflower = _p.startflower; + re.starttype = _p.starttype; + re.maxseat = _p.maxseat; + re.maxcard = _p.maxcard; + //本轮各位置出的牌:定长 3、下标 = 座位序号;尚未出牌的位置留空(序列化后为 null) + re.cards = []; + re.cards.length = 3; + if (_p.cards){ + for (var i = 0; i < 3; i++){ + if (_p.cards[i]){ + re.cards[i] = _p.cards[i].concat(); + } + } + } + //首家甩牌的分量需求(§5.4.4);tractors 是数组,同样要拷贝,非甩牌为 null + if (_p.shuai_demand){ + var _sd = {}; + _sd.tractors = _p.shuai_demand.tractors ? _p.shuai_demand.tractors.concat() : []; + _sd.pairs = _p.shuai_demand.pairs; + _sd.singles = _p.shuai_demand.singles; + re.shuai_demand = _sd; + } else { + re.shuai_demand = null; + } + return re; + }, + + //选主 + do_choiceflower: function(o_paiju, flower){ + o_paiju.flower = flower; + //主牌的牌编码在 arith.id_to_code 里按 mainflower 动态计算,选主时只需记录花色 + //进入埋牌阶段 + o_paiju.step = 3; + }, + + //校验客户端提交的牌id列表本身是否合法:必须是非空数组、元素为本局牌表内的整数牌id、且互不重复 + //(客户端入参不可信:重复id 会被 can_playcard 当成对子/拖拉机,也会让埋牌少埋;越界id 会取到 + // undefined 而在后续解引用时抛异常中断 DoPack。此处显式失败,不做隐式兜底) + check_cards_valid: function(o_paiju, cards){ + if (Object.prototype.toString.call(cards) != "[object Array]"){ + return false; + } + if (cards.length <= 0){ + return false; + } + var _seen = {}; + for (var i = 0; i < cards.length; i++){ + var _id = cards[i]; + //必须是落在本局牌表范围内的整数牌id + if (typeof _id != "number" || _id != parseInt(_id) || _id < 0 || _id >= o_paiju.cards.length){ + return false; + } + //同一张牌不得重复提交 + if (_seen[_id]){ + return false; + } + _seen[_id] = 1; + } + return true; + }, + + //判断牌是否在玩家手上(入参合法性由 check_cards_valid 一并把关,本函数是唯一的持牌校验入口) + check_cards_inhand: function(o_paiju, cards, seat){ + if (!cls_youle_erqiwang_paiju.check_cards_valid(o_paiju, cards)){ + return false; + } + for (var i = 0; i < cards.length; i++) { + var o_card = o_paiju.cards[cards[i]]; + if (o_card.playround != -1){ + //已埋下或已打出的牌 + return false; + } + if (seat == o_paiju.banker){ + //是庄家 + if (o_card.dealowner != seat + 1 && o_card.dealowner != 0){ + return false; + } + } else { + //是闲家 + if (o_card.dealowner != seat + 1){ + return false; + } + } + } + return true; + }, + + //埋牌 + do_burycard: function(o_paiju, cards){ + for (var i = 0; i < cards.length; i++) { + o_paiju.cards[cards[i]].playround = 0; + }; + //埋牌后直接进入出牌阶段,由庄家首出(投降已在选主阶段 step2 决定,无独立投降阶段) + o_paiju.step = 5; + cls_youle_erqiwang_paiju.new_playround(o_paiju, 1, o_paiju.banker); + }, + + //新一轮出牌 + new_playround: function(o_paiju, round, startseat){ + o_paiju.playproc.round = round; //第几轮 + o_paiju.playproc.start = startseat; //本轮的起始出牌位置 + o_paiju.playproc.currseat = startseat; //本轮的当前的出牌位置 + o_paiju.playproc.startcount = -1; //本轮第一个出牌的张数 + o_paiju.playproc.startflower = -1; //本轮第一个出牌的花色 + o_paiju.playproc.starttype = -1; //本轮第一个出牌的牌型 + o_paiju.playproc.maxseat = -1; //谁的牌最大,位置 + o_paiju.playproc.maxcard = -1; //满足第一个玩家出牌的牌型最大牌 + o_paiju.playproc.cards = []; //本轮各位置出的牌,数组与位置序号对应 + o_paiju.playproc.cards.length= 3; + o_paiju.playproc.shuai_demand= null; //首家甩牌的分量需求(供跟牌逐分量匹配),非甩牌为null + }, + + //【一手出牌的顺序:唯一口径】把一手牌规整成本局的权威展示顺序 + //(按主牌花色从大到小)。只在 do_playcard 落牌那一刻调一次,之后全体下游只读: + // · playproc.cards[座位] → chupai1/2/3 与重连包的 playproc + // · do_playcard 返回的 re.cards → chupai1/2/3 的 data.cards + // · playhistory[轮次][座位] → 重连包的 PushCards.pushlist + //三者同源同数组内容,所以前端“增量回放”与“重连重建”必定逐元素相等。 + // + //为什么不能用客户端提交的原序:那是玩家的点击顺序,同一手牌每次都可能不同, + //服务端不得依赖、也不回显(server 04 §8:前端不是数据源)。 + //【返回新数组】——order_cards 是原地排序,直接排入参会就地改写调用方(含 pack.data.cards)。 + order_playcards: function(o_paiju, cards){ + if (!cards){ + return cards; + } + return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, cards.concat()); + }, + + //在牌列表中获取分牌和分值 + get_grade_incard: function(o_paiju, cards){ + var re = {}; + re.grade = 0; + re.cards = []; + for (var i = 0; i < cards.length; i++) { + if (o_paiju.cards[cards[i]].score > 0){ + re.grade = re.grade + o_paiju.cards[cards[i]].score; + re.cards.push(cards[i]); + } + } + return re; + }, + + //检查选中的牌是否可出,并出牌 + do_playcard: function(o_paiju, cards){ + //【顺序归一:唯一写入处】入参是客户端提交的点击顺序,服务端不采信、不下发。 + //在这里一次性转成权威顺序(order_playcards),此后 playproc.cards / 返回给下发包的 + //re.cards / 重连包 pushlist 全都是同一个口径,不会再有第二处排序(SSOT)。 + //顺带隔离了 can_playcard 的【原地排序】——否则它会就地改写 pack.data.cards。 + cards = cls_youle_erqiwang_paiju.order_playcards(o_paiju, cards); + + //出牌 + var doplay = function(playindex){ + for (var i = 0; i < cards.length; i++) { + o_paiju.cards[cards[i]].playround = o_paiju.playproc.round; + o_paiju.cards[cards[i]].playindex = playindex; + }; + o_paiju.playproc.cards[o_paiju.playproc.currseat] = cards; + //归档这一手,供重连包 PushCards.pushlist 直读(唯一写入处,见 paiju.playhistory) + while (o_paiju.playhistory.length < o_paiju.playproc.round){ + o_paiju.playhistory.push([[], [], []]); + } + o_paiju.playhistory[o_paiju.playproc.round - 1][o_paiju.playproc.currseat] = cards.concat(); + + //进入出牌阶段 + o_paiju.step = 5; + + //报无主处理(design §9) + //先看出牌者自己的主牌是否已出空(出空即"报无主"); + //一旦全场有人报无主,立即为【全体三人】刷新剩余主牌数/主对子数——design §9 要求 + //"一旦有玩家的主牌全部打空,系统就为全体三人显示另外两家各自的主牌数量与对子数", + //而不是等另外两家各自轮到出牌时才逐个补上。 + var _self = o_paiju.playproc.currseat; + if (cls_youle_erqiwang_paiju.get_seat_zhucards(o_paiju, _self).length == 0){ + o_paiju.seatlist[_self][4][0] = 0; + o_paiju.seatlist[_self][4][1] = 0; + } + if (o_paiju.method.have_baofu()){ + for (var _s = 0; _s < 3; _s++){ + var _zhu = cls_youle_erqiwang_paiju.get_seat_zhucards(o_paiju, _s); + var _zhupair = cls_youle_erqiwang_arith.get_pairlist(o_paiju.flower, _zhu); + o_paiju.seatlist[_s][4][0] = _zhu.length; + o_paiju.seatlist[_s][4][1] = _zhupair.length; + + if (_zhu.length == 0){ + //主牌出空,等同于主花色也没有了、也没有主花色的对子 + o_paiju.seatlist[_s][o_paiju.flower - 1][0] = 1; + o_paiju.seatlist[_s][o_paiju.flower - 1][1] = 1; + } else if (_zhupair.length == 0){ + o_paiju.seatlist[_s][o_paiju.flower - 1][1] = 1; + } + } + } + } + + //本轮中的第二个出牌位置 + var nextseat_1 = cls_youle_erqiwang_paiju.get_nextseat(o_paiju.playproc.start); + //本轮中的第三个出牌位置 + var nextseat_2 = cls_youle_erqiwang_paiju.get_nextseat(nextseat_1); + + if (o_paiju.playproc.currseat == o_paiju.playproc.start){ //是本轮的第一个出牌 + //两名对手未出的主牌,供甩牌最大性判定(§5.4.2) + var opp_zhulist = []; + for (var os = 0; os < 3; os++){ + if (os != o_paiju.playproc.currseat){ + opp_zhulist.push(cls_youle_erqiwang_paiju.get_seat_zhucards(o_paiju, os)); + } + } + var can = cls_youle_erqiwang_arith.can_playcard(o_paiju.flower, cards, o_paiju.playproc.currseat, o_paiju.seatlist, opp_zhulist); + var _shuaicuo = false; + if (!can.result){ + if (can.shuaicuo){ + //甩错惩罚(§5.4.5):收回甩牌,本轮只强制打出最小一张 + _shuaicuo = true; + cards = cls_youle_erqiwang_paiju.order_playcards(o_paiju, [can.smallest]); + can = cls_youle_erqiwang_arith.can_playcard(o_paiju.flower, cards, o_paiju.playproc.currseat, o_paiju.seatlist, opp_zhulist); + if (!can.result){ + var reErr = {}; + reErr.result = false; + return reErr; + } + } else { + var re = {}; + re.result = false; + return re; + } + } + //出牌(cards 在甩错时已被替换为仅最小一张) + doplay(1); + //记录下牌型、花色、牌型最大牌 + o_paiju.playproc.startcount = cards.length; + o_paiju.playproc.startflower = can.flower; + o_paiju.playproc.starttype = can.cardtype; + o_paiju.playproc.maxseat = o_paiju.playproc.currseat; + o_paiju.playproc.maxcard = can.cardvalue; + //合法甩牌的分量需求,供跟牌逐分量强制匹配(§5.4.4);甩错退化为单张则为 null + o_paiju.playproc.shuai_demand = can.shuai_demand ? can.shuai_demand : null; + o_paiju.playproc.currseat = nextseat_1; + //返回结果 + var re1 = {}; + re1.result = true; + re1.idx = 1; + re1.count = cards.length; + re1.flower = can.flower; + re1.cardtype = can.cardtype; + //实际打出的牌(甩错时为单张),已是权威顺序。 + //【给副本】cards 本体已挂在 playproc.cards[座位] 上,直接交出去会让「已发出的包」 + //随后续改动被回改(同 get_playproc 的深拷贝理由) + re1.cards = cards.concat(); + re1.shuaicuo = _shuaicuo; + //合法甩牌的分量构成:cardtype 只能表达单一牌型,甩牌(单张/对子/拖拉机自由混搭, + //design §5.4.3)会被压平成 1xx/2xx,客户端无从据此还原分组,故另行下发分量需求 + if (o_paiju.playproc.shuai_demand){ + re1.shuai = o_paiju.playproc.shuai_demand; + } + return re1; + } + + //不是本轮中的第一个出牌,是跟牌 + var cardsinhand = cls_youle_erqiwang_paiju.get_seat_cards(o_paiju, o_paiju.playproc.currseat); + var can = cls_youle_erqiwang_arith.can_followcard(o_paiju.flower, cardsinhand, cards, o_paiju.playproc.startcount, o_paiju.playproc.startflower, o_paiju.playproc.starttype); + if (!can.result){ + var re = {}; + re.result = false; + return re; + } + //首家是甩牌时,追加"拖拉机→对子→单张"的强制分量匹配校验(§5.4.4) + if (o_paiju.playproc.shuai_demand){ + if (!cls_youle_erqiwang_arith.flush_follow_ok(o_paiju.flower, cardsinhand, cards, o_paiju.playproc.shuai_demand)){ + var reF = {}; + reF.result = false; + return reF; + } + } + + //与之前的出牌比较大小 + if (can.cardvalue > o_paiju.playproc.maxcard){ + o_paiju.playproc.maxseat = o_paiju.playproc.currseat; + o_paiju.playproc.maxcard = can.cardvalue; + } + + //无花色、无对子处理 + if (can.nopair){ + o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.playproc.startflower - 1][1] = 1; + } + if (can.noflower){ + o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.playproc.startflower - 1][0] = 1; + o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.playproc.startflower - 1][1] = 1; + } + + //是本轮中的第二个出牌 + if (o_paiju.playproc.currseat == nextseat_1){ + //出牌 + doplay(2); + //下一个出牌者 + o_paiju.playproc.currseat = nextseat_2; + //返回结果 + var re2 = {}; + re2.idx = 2; + re2.result = true; + re2.cards = cards.concat(); //实际打出的牌,权威顺序(理由同 re1.cards) + return re2; + } + + //是本轮中的第三个出牌 + //出牌 + doplay(3); + //本轮出牌结束 + for (var i = 0; i < o_paiju.playproc.cards.length; i++) { + for (var j = 0; j < o_paiju.playproc.cards[i].length; j++) { + o_paiju.cards[o_paiju.playproc.cards[i][j]].playowner = o_paiju.playproc.maxseat; + } + } + + //返回结果 + var re3 = {}; + re3.result = true; + re3.idx = 3; + re3.cards = cards.concat(); //实际打出的牌,权威顺序(理由同 re1.cards) + re3.maxseat = o_paiju.playproc.maxseat; + if (o_paiju.playproc.maxseat != o_paiju.banker){ + re3.grade = 0; + // re3.gradecards = []; + for (var i = 0; i < o_paiju.playproc.cards.length; i++) { + var grade_incards = cls_youle_erqiwang_paiju.get_grade_incard(o_paiju, o_paiju.playproc.cards[i]); + re3.grade = re3.grade + grade_incards.grade; + // re3.gradecards = re3.gradecards.concat(grade_incards.cards); + } + } + + //判断是否还有牌在玩家手上未出 + var havecard_inhand = function(){ + for (var i = 0; i < o_paiju.cards.length; i++) { + if (o_paiju.cards[i].playround == -1 && o_paiju.cards[i].number != 3 && o_paiju.cards[i].number != 4){ + return true; + } + } + return false; + } + // if (!havecard_inhand() || o_paiju.playproc.round == 2){ //测试用 + if (!havecard_inhand()){ + //进入结算阶段 + o_paiju.step = 6; + } else { + //新一轮出牌 + cls_youle_erqiwang_paiju.new_playround(o_paiju, o_paiju.playproc.round + 1, o_paiju.playproc.maxseat); + } + return re3; + }, + + //获取埋牌 + get_burycard: function(o_paiju){ + var aryCardIDs = []; + for (var i = 0; i < o_paiju.cards.length; i++){ + if (o_paiju.cards[i].playround == 0){ + aryCardIDs.push(o_paiju.cards[i].id); + } + } + return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs); + }, + + //获取庄家亮牌信息(design §8.2):基于庄家埋牌后 28 张手牌,达标才亮,只亮数量/结构 + //返回 null 表示无需亮牌;否则返回 {zhu,zhupair,zhutuo?, wang?, qi?, er?} + //手牌来源必须是 get_seat_cards_award 的【静态快照】(埋牌后 28 张,含之后已打出的), + //不能用 get_seat_cards(只取未出的牌):出牌开始亮出的这份统计全局固定、不随出牌缩水, + //否则闲家断线重连后 get_deskinfo 会按庄家当前剩余手牌重算,亮牌信息缩水甚至变成 null + //亮牌(design §8.2):庄家埋牌后、出牌前,若手中【固定主牌】达到门槛, + //就向两个闲家亮出【自己全部固定主牌的具体牌面】。 + // 固定主牌 = 双王 + 全部花色的 2 + 全部花色的 7,【不含】主花色的普通牌 A/K/Q/J/10/9/8/6/5 + // 门槛(任一满足即亮):固定主牌总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6 + // 不限叫分;是否真的发给闲家由调用方按查牌模式门控(design §9) + //返回 { cards: [牌id...] }(按本局主牌序从大到小),不达标返回 null。 + //注意:亮出的内容是固定的一份,不随"被哪一条门槛触发"而增减。 + get_liangpai: function(o_paiju){ + if (o_paiju.banker < 0){ + return null; + } + //静态初始手牌快照(与 8.1 算奖同源):庄家埋牌后的 28 张 + var cards = cls_youle_erqiwang_paiju.get_seat_cards_award(o_paiju, o_paiju.banker); + var wang = 0, qi = 0, er = 0; + var gudingcards = []; //固定主牌 + for (var i = 0; i < cards.length; i++){ + var num = cls_youle_erqiwang_arith.id_to_number(cards[i]); + if (num >= 53){ //大王 54 / 小王 53 + wang = wang + 1; + gudingcards.push(cards[i]); + } else if (num == 7){ //任意花色的 7 + qi = qi + 1; + gudingcards.push(cards[i]); + } else if (num == 2){ //任意花色的 2 + er = er + 1; + gudingcards.push(cards[i]); + } + } + //门槛判定(design §8.2) + if (gudingcards.length < 10 && wang < 3 && qi < 6 && er < 6){ + return null; + } + var re = {}; + re.cards = cls_youle_erqiwang_arith.order_cards(o_paiju.flower, gudingcards); + return re; + }, + //获取抠底包 + get_bottom_account: function(o_paiju, msg){ + msg.data.bottom = {}; + msg.data.bottom.cards = o_paiju.method.get_burycard(); + if (o_paiju.playproc.maxseat != o_paiju.banker){ //闲家抠底 + var multiple = cls_youle_erqiwang_arith.get_bottom_multiple(o_paiju.flower, o_paiju.playproc.cards[o_paiju.playproc.maxseat]); + if (multiple > 0){ + msg.data.bottom.multiple = multiple; + var get = o_paiju.method.get_grade_incard(msg.data.bottom.cards); + msg.data.bottom.grade1 = get.grade; + msg.data.bottom.grade2 = multiple * msg.data.bottom.grade1; + } + } + return msg; + }, + + //获取闲家捡分(不包含抠底) + get_jian_grade: function(o_paiju){ + var re = {}; + re.grade = 0; + re.cards = []; + for (var i = 0; i < o_paiju.cards.length; i++){ + if (o_paiju.cards[i].playowner > -1 && o_paiju.cards[i].playowner != o_paiju.banker){ + if (o_paiju.cards[i].score > 0){ + re.grade = re.grade + o_paiju.cards[i].score; + re.cards.push(i); + } + } + } + return re; + }, + + //当前抓分倍数(design §7.2.0):按【当前累计捡分】实时算出的判定倍率,【带符号】。 + //供客户端顶部「抓分」列角标显示「若此刻结束是几倍」,以及对局中的判定动画(D-8)。 + //与结算包 aset.upgrade 同源同算法(get_qvalue + get_upgrade),差别只在: + //这里的捡分【不含扣底】——扣底要到最后一轮打完才产生,出牌过程中尚不存在。 + //叫分未定(call<=0)时返回 0,前端不显示角标。 + get_curmultiple: function(o_paiju){ + var _call = o_paiju.call; + if (!_call || _call <= 0){ + return 0; + } + var _cfg = cls_youle_erqiwang_config.parse(o_paiju.o_desk.o_room.roomtype); + var _grade = cls_youle_erqiwang_paiju.get_jian_grade(o_paiju).grade; + var _q = cls_youle_erqiwang_arith.get_qvalue(_call, _cfg.climb); + //【带符号】返回,与结算包 aset.upgrade 完全同口径: + // 正数 3/2/1 = 庄家 大光/小光/过庄;负数 -N = 闲家升 N 级。 + //符号就是「谁赢」的区分,不能取绝对值——否则 3(大光) 与 -3(升3级)、 + //2(小光) 与 -2(升2级)、1(过庄) 与 -1(升1级) 三对会完全撞在一起, + //客户端的判定动画将无从分辨该播哪个(前端清单 S-8)。 + //只关心倍数大小的用途(如顶部「抓分」角标)自行取绝对值即可。 + return cls_youle_erqiwang_arith.get_upgrade(_call, _grade, _q); + }, + + //某座位本轮跟牌的「必出牌」(design §5.2),供客户端自动选中。 + //与服务端出牌校验用的是同一个 get_followcard,故建议与校验结果天然一致。 + //返回 null(不下发)的情形: + // 1. 非出牌阶段、或该座位就是本轮首家(首家自由出牌,无必出); + // 2. 本轮首家【甩牌】——甩牌跟牌走 flush_follow_ok 的逐分量匹配(design §5.4.4), + // 与 get_followcard 不是同一条路径,此时给建议会给错,宁可不发; + // 3. 算出来的必出牌为空(玩家可自由选)。 + get_mustcard: function(o_paiju, seat){ + if (o_paiju.step != 5){ + return null; + } + var _proc = o_paiju.playproc; + //本轮还没人出牌 / 该座位是首家 → 无必出 + if (!_proc || _proc.start == seat || _proc.startcount == null || _proc.startcount <= 0){ + return null; + } + //甩牌不走这条路径,见上 + if (_proc.shuai_demand){ + return null; + } + var _inhand = cls_youle_erqiwang_paiju.get_seat_cards(o_paiju, seat); + if (!_inhand || _inhand.length <= 0){ + return null; + } + var _get = cls_youle_erqiwang_arith.get_followcard( + o_paiju.flower, _inhand, _proc.startcount, _proc.startflower, _proc.starttype); + if (!_get || !_get.mustcard || _get.mustcard.length <= 0){ + return null; + } + return _get.mustcard; + }, + + //获取小局结算包,type 0正常结算 1投降结算 2解散结算 + get_paiju_account: function(o_paiju, type, msg){ + o_paiju.endtime = min_now(); + //第一局结算扣除房卡 + if (o_paiju.idx == 1 && type != 2){ + var do_deduct_roomcard = function(){ + youle_erqiwang.import.deduct_roomcard(o_paiju.o_desk.o_room); + } + min_ontimeout(do_deduct_roomcard, 1000); + } + + if (msg.rpc != "jiesuan"){ + msg.rpc = "jiesuan"; + } + if (!msg.data){ + msg.data = {}; + } + //主动推送的 data 必须自带成败标志(dev-guide server 03 §4:成败唯一是 data.success) + msg.data.success = true; + + //【阶段唯一写入处】结算包一出,本局即进入 step6: + // 正常出牌结算 do_playcard 已置 6、投降结算 mod.touxiang 已置 6,此处是幂等重申; + // 解散结算此前没有任何地方推进过 step(可能停在 1/2/3/5),由这里落定。 + //随后显式下发(见下方 msg.data.step):阶段变更必须有包承载,不让前端按 rpc 猜(server 03 §1.4) + o_paiju.step = 6; + msg.data.step = o_paiju.step; + + //读取房间可选规则(缺省:常规算子、不傍王;见 class.config.js 位串约定) + var o_room = o_paiju.o_desk.o_room; + var _cfg = cls_youle_erqiwang_config.parse(o_room.roomtype); + var _climb = _cfg.climb; //爬坡 + var _bangwang = _cfg.bangwang; //傍王 + + //闲家捡分(含抠底) + var _call = o_paiju.call; + var _jian = cls_youle_erqiwang_paiju.get_jian_grade(o_paiju); + var _grade = _jian.grade; + if (msg.data.bottom && msg.data.bottom.grade2){ + _grade = _grade + msg.data.bottom.grade2; + } + + //算子:_base 基础子数、_upgrade 判定倍率(带符号,送客户端)、_X 每对子子数、_win 胜负(1庄赢 -1闲赢 0无) + var _base, _upgrade, _X, _win; + switch (type){ + case 0: + _base = cls_youle_erqiwang_arith.get_base_bycall(_call, _climb); + var _q = cls_youle_erqiwang_arith.get_qvalue(_call, _climb); + _upgrade = cls_youle_erqiwang_arith.get_upgrade(_call, _grade, _q); + _win = (_upgrade > 0) ? 1 : -1; + _X = ((_upgrade < 0) ? (0 - _upgrade) : _upgrade) * _base; + o_paiju.result = (_upgrade > 0) ? 0 : 1; + break; + case 1: //投降:70分坐庄,基础子数1,庄家直接输1个子 + _base = 1; + _upgrade = -99; //送客户端的投降标识 + _win = -1; + _X = 1; + o_paiju.result = 2; + break; + case 2: //解散:本局不结算子数与算奖 + _base = 0; + _upgrade = 0; + _win = 0; + _X = 0; + o_paiju.result = 3; + break; + } + + msg.data.aset = {}; + msg.data.aset.banker = o_paiju.banker; + msg.data.aset.call = _call; + msg.data.aset.multiple = _base; + msg.data.aset.flower = o_paiju.flower; + msg.data.aset.grade = _grade; + msg.data.aset.upgrade = _upgrade; + msg.data.aset.bangwang = _bangwang ? 1 : 0; + msg.data.aset.climb = _climb ? 1 : 0; + + //算奖:各家奖数 N =(庄家?常规算奖 : 0)+(傍王?王数 : 0) + //静态初始手牌快照:庄家取埋牌后28张、闲家取发牌后28张 + var _c0 = cls_youle_erqiwang_paiju.get_seat_cards_award(o_paiju, 0); + var _c1 = cls_youle_erqiwang_paiju.get_seat_cards_award(o_paiju, 1); + var _c2 = cls_youle_erqiwang_paiju.get_seat_cards_award(o_paiju, 2); + var _cglist = [ + cls_youle_erqiwang_arith.get_chongguan(o_paiju.flower, _c0), + cls_youle_erqiwang_arith.get_chongguan(o_paiju.flower, _c1), + cls_youle_erqiwang_arith.get_chongguan(o_paiju.flower, _c2) + ]; + + var award_count = function(cgobj, seat){ + var n = 0; + if (seat == o_paiju.banker){ + n = n + cgobj.count; //常规算奖只计庄家(design §8.1) + } + if (_bangwang){ + n = n + cgobj.wang; //傍王勾选时庄闲都算,每张王1奖(design §8.3) + } + return n; + } + var _nlist = [ + award_count(_cglist[0], 0), + award_count(_cglist[1], 1), + award_count(_cglist[2], 2) + ]; + + //算奖并入结算(design §8.4):玩家i从另两家各收 X×Ni、各付 X×Nj,净得 X×(2Ni−Nj−Nk) + var award_pay = function(n){ + return [ + _X * (2 * n[0] - n[1] - n[2]), + _X * (2 * n[1] - n[0] - n[2]), + _X * (2 * n[2] - n[0] - n[1]) + ]; + } + var _aw = award_pay(_nlist); + + //冲关与傍王【分别】再各算一次,供大局结算分项展示(S-6)。 + //必须在这里拆——N = 冲关 + 傍王 求和后才代入公式,事后无法从 grade_aw 反推各自占比。 + //公式对 N 是线性的,故 _awcg[i] + _awbw[i] 恒等于 _aw[i](下方 _p 赋值处有断言性注释)。 + var _ncg = [0, 0, 0]; //冲关分量:只计庄家(design §8.1) + var _nbw = [0, 0, 0]; //傍王分量:勾选时庄闲都算(design §8.3) + for (var q = 0; q < 3; q++){ + if (q == o_paiju.banker){ _ncg[q] = _cglist[q].count; } + if (_bangwang){ _nbw[q] = _cglist[q].wang; } + } + var _awcg = award_pay(_ncg); + var _awbw = award_pay(_nbw); + + //捡分子数:_win=1庄赢→庄+X×2/闲−X;_win=-1闲赢→庄−X×2/闲+X + var _jf = [0, 0, 0]; + if (o_paiju.banker >= 0){ + for (var s = 0; s < 3; s++){ + if (s == o_paiju.banker){ + _jf[s] = _win * _X * 2; + } else { + _jf[s] = _win * _X * -1; + } + } + } + + msg.data.aset.seatlist = []; + for (var i = 0; i < 3; i++){ + var _p = {}; + _p.cards = _cglist[i].cards; //算奖相关牌(王/冲关牌) + _p.chongguan = _cglist[i].count; //常规算奖奖数(仅庄家计入 N) + _p.wang = _cglist[i].wang; //王数 + _p.naward = _nlist[i]; //该家总奖数 N + _p.grade_aw = _aw[i]; //算奖得分 X×(2Ni−Nj−Nk),= grade_cg + grade_bw + _p.grade_cg = _awcg[i]; //其中的冲关分量(design §8.1,不含傍王) + _p.grade_bw = _awbw[i]; //其中的傍王分量(design §8.3) + _p.grade_jf = _jf[i]; //捡分子数得分(基础分) + _p.grade = _p.grade_aw + _p.grade_jf; //本局总得分 + //大局累积:总分 + 三项分量(基础/冲关/傍王),供大局结算面板分项展示 + o_paiju.o_desk.seatlist[i][0] = o_paiju.o_desk.seatlist[i][0] + _p.grade; + _p.score = o_paiju.o_desk.seatlist[i][0]; + o_paiju.o_desk.seatlist[i][2] = o_paiju.o_desk.seatlist[i][2] + _p.grade_jf; + o_paiju.o_desk.seatlist[i][3] = o_paiju.o_desk.seatlist[i][3] + _p.grade_cg; + o_paiju.o_desk.seatlist[i][4] = o_paiju.o_desk.seatlist[i][4] + _p.grade_bw; + //保存每局得分 + o_paiju.o_desk.seatlist[i][1].push(_p.grade); + msg.data.aset.seatlist.push(_p); + } + + if (o_paiju.idx >= o_paiju.o_desk.o_room.asetcount || type == 2){ + msg = o_paiju.o_desk.method.get_desk_account(msg); + } + + //保存结算包,供断线重连时使用(get_deskinfo 的 case 6 直读这三份,不重算)。 + //必须与本次下发的 jiesuan 推送【同源】——取的就是刚组好的同一份 msg.data.*, + //这样「增量收到 jiesuan」与「结算面板开着时重连」得到的结算数据逐字段相等。 + //bottom 只有正常出牌结算才有(get_bottom_account 组的)、account 只有末局/解散才有, + //没有的情形显式置 null(不是漏发,是本来就不存在;下发侧据此决定带不带该分组) + o_paiju.tmp_jiesuan_aset = msg.data.aset; + o_paiju.tmp_jiesuan_bottom = msg.data.bottom ? msg.data.bottom : null; + o_paiju.tmp_jiesuan_account = msg.data.account ? msg.data.account : null; + + return msg; + } +} + +//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)。min_random/min_now/min_ontimeout、youle_erqiwang.import 为平台全局 +if (typeof module !== "undefined"){ + module.exports = cls_youle_erqiwang_paiju; +} diff --git a/server/games/erqiwang/docs/compliance/00-已知不一致-初步清单.md b/server/games/erqiwang/docs/compliance/00-已知不一致-初步清单.md new file mode 100644 index 0000000..ec5cb02 --- /dev/null +++ b/server/games/erqiwang/docs/compliance/00-已知不一致-初步清单.md @@ -0,0 +1,53 @@ +# 二七王 服务端实现 vs 设计文档:初步不一致清单 + +> 本文是首轮通读 `server/games/erqiwang/*.js` 与 `docs/design/design.md` 后记录的**初步**疑点清单,作为逐条深入核对的线索。完整、逐节的核对结论见同目录 [`01-design合规逐节核对.md`](./01-design合规逐节核对.md)。 +> +> 方向约定(与根 `CLAUDE.md` 一致):`design.md` 是玩法规则的唯一权威,代码与其冲突时**应改代码去符合规则**(除非规则标「待确认」);本清单只做记录,不代表已修复。 + +--- + +## 1. 算子(基础子数)与设计表格完全对不上 + +- 设计 §7.1 常规算子基础子数:65→2、60→3、55→4、50 及以下→6;70 投降→1、70 打牌→2。 +- 代码 `arith.get_multiple_bycall` 只返回 1/2/4/0(`call>60→1`、`call>40→2`、`call>0→4`),与设计四档数值、结构均不符。 + +## 2. 爬坡 / 傍王 房间选项未做成开关 + +- `arith.get_upgrade` 用 `halfcall = ceil(call/2)` 作小光/过庄分界与升级级距;设计常规算子该分界**固定 40**、级距**固定 40**。 +- 代码里**没有任何读取房间「爬坡 / 傍王」勾选的分支**:`grade_w`(傍王)无条件计算、`halfcall` 恒用。设计 §7.3 / §8.3 / §10.3 中这两项都是可选规则,需要开关。 + +## 3. 扣底倍数不符 + +- 设计 §6.3:单张主 ×1、主对子 ×2、两连对 ×4、三连对 ×6、N 连对 ×2N(线性)。 +- 代码 `arith.get_bottom_multiple`:单张 →2、对子 →4、两连对 →8、三连对 →16、四连对 →32、五连对 →64(近似指数),且单张即翻倍;六连对落空返回 `undefined`。 + +## 4. 「固定主牌 ≥10 张」给了算奖 + +- 代码 `arith.get_chongguan` 对 `王+2+7≥10` 追加 `total−9` 奖(「10 个老主判断」)。 +- 设计 §8.2 明确举例:固定主牌 ≥10 张只触发**亮牌**、**不触发任何算奖**。 + +## 5. 算奖归属模型与结算方式不同 + +- 设计 §8.1:常规算奖**只计庄家**;§8.3 傍王才庄闲都算(且需勾选);§8.4:算奖额外支付额 = `X × N`(`X` 为第 7 节算出的基础输赢子数)。 +- 代码:对**三家**都算「冲关」并两两结算,且叠加「冲关必叫 / 四王必踢」的**作废**规则(设计中无此机制);`grade_cg`、`grade_w` 是**扁平奖数差**(每奖记 1 子),**未乘 `X`**。 + +## 6. 叫分缺上限校验 + +- 设计 §4:叫分范围 5 ~ 70。 +- 代码 `mod.jiaofen` 只校验 `call%5==0` 与「后叫必须更低」,**缺 `call<=70` 上限**,首家可叫出 75、100 等非法分值。 + +## 7. 投降资格范围过宽 + +- 设计 §4.4 / §7.1:投降是 **70 分坐庄专属**分支。 +- 代码 `mod.touxiang` 用 `call < 65 → 拒绝`,即 **65 分也能投降**。 + +## 8. 70 分暗牌亮 3 秒等交互细节未实现 + +- 设计 §4.4 / §7.1:70 分坐庄,8 张暗牌需向两个闲家亮 3 秒(投降 / 打牌都要)。 +- 代码未见相关下发逻辑。 + +--- + +以上为初步清单。经逐节精查后,另发现若干**运行期代码缺陷**(如 `can_followcard` 引用未定义变量 `tuolaji_list` 会抛异常、`can_playcard` 的 14 张上限 `do_returnfalse` 漏写括号导致失效等)、以及**甩牌合法性 / 强制跟牌 / 局数扣卡 / 查牌模式**等更多不一致,详见 [`01-design合规逐节核对.md`](./01-design合规逐节核对.md)。 + +> **本文与 01 的前两轮都只沿 design.md 逐节核对玩法规则**,因此平台接入、客户端入参校验、下发面一致性、协议红线这几类问题整类不在核对范围内。第三轮补上这些维度后又查出 1 项阻断级(子游戏从未在 `server/youle/app.js` 注册,运行时完全不可达)、3 项严重(重复牌id 可伪造牌型、入参无类型校验、下发面 `multiple` 忽略爬坡开关)与协议红线缺失(全仓无 `data.success`),见 [`01`「第三轮核对」](./01-design合规逐节核对.md#第三轮核对平台接入--入参--下发面--协议红线)。 diff --git a/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md b/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md new file mode 100644 index 0000000..8935fcf --- /dev/null +++ b/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md @@ -0,0 +1,531 @@ +# 二七王 服务端实现 vs `design.md` 逐节合规核对 + +> 权威方向(同根 `CLAUDE.md`):`design.md` 是玩法规则的**唯一权威**。凡代码与规则冲突,除非规则项标注「待确认」,一律视为**代码缺陷**,应改代码去符合规则,**不得改规则迁就代码**。 +> +> 核对对象:`server/games/erqiwang/` 下 `mod.js`、`class.pai.js`、`class.paiju.js`、`class.arith.js`、`class.desk.js`、`class.export.js`、`class.import.js`。 +> +> 严重度标记: +> - 🟥 **严重**:影响发牌 / 计分 / 输赢 / 核心玩法正确性。 +> - 🟧 **中等**:可选规则缺失、边界校验缺失、局部规则偏差。 +> - 🟨 **轻微**:交互提示 / 展示细节 / 死代码。 +> - 🐞 **代码缺陷**:运行期异常或明显写错(无论是否直接违背规则)。 +> - ✅ **符合**。 + +--- + +## 整改进度 + +> 本轮已按 design.md 修正的项(均已用 Node 脚本对 design 判定表逐格验证,见"验证"列)。下方"结论摘要"与各节详评仍保留**原始不一致**描述以留痕;本节记录当前状态。 + +> **二次全量复核结论(限定范围)**:对整改后代码做了一次独立对抗性复核(§7/§8 算子算奖、§5 出牌甩牌、§4 开局投降暗牌阶段机、§2/§3/§9/§10 编码查牌房间,逐条比对 + Node 实测)。结果:**就 design.md 的玩法规则维度而言,服务端符合**,且未发现整改引入的符号/下标/边界错、无 step4 死代码残留、投降 36 张快照与 X×N 算奖均正确。 +> - 复核时发现的唯一未落地项 **§5.4.4 强制跟牌分量拆解已于本轮修复**:合法甩牌在 `can_playcard` 记录分量需求 `shuai_demand`(拖拉机长度/对子/单张),存入 `playproc`;跟牌时 `do_playcard` 调 `arith.flush_follow_ok` 在"同花色凑张数、主牌最大化"之上追加"拖拉机→对子→单张"的强制分量匹配(能凑同长主拖必凑、能凑主对必凑,不得拆散去垫)。已用 Node 正反面/边界用例验证。 +> +> ⚠️ **该结论只覆盖"玩法规则是否符合 design.md"这一个维度**。第三轮核对(见下方「第三轮核对」)在**平台接入、客户端入参校验、下发面一致性、协议红线**四个维度上又发现 1 项阻断级、3 项严重、若干中等缺陷——它们不属于 design.md 的玩法条款,所以逐节核对没有覆盖到,但同样会让服务端跑不起来或被利用。**"符合 design.md" ≠ "实现完整正确"**,后续复核不要再把前者当成后者。 + +| 项 | 位置 | 整改内容 | 验证 | +| --- | --- | --- | --- | +| D1 崩溃 | `class.arith.js` `can_followcard` | 用 `get_pairlist`+`get_tuolaji_list(_followcards)` 校验拖拉机牌型,消除未定义 `tuolaji_list` | 语法通过 | +| D3 | `can_playcard` | `do_returnfalse;` → `do_returnfalse()`,恢复 14 张上限 | 语法通过 | +| D4 | `do_choiceflower` | 删除空循环死代码 | 语法通过 | +| §6.3 | `get_bottom_multiple` | 改为线性:单张 1、主对 2、N 连对 2N | ✅ 10 用例 | +| §7 算子 | `get_base_bycall`/`get_qvalue`/`get_upgrade`(新)+ `get_paiju_account` | 实现常规算子(大光×3、Q=40) 与爬坡(§7.3 分段) 两套,按爬坡位(roomtype 位3)切换;`X=multiple×|upgrade|` | ✅ 74 格逐档 | +| §8.1 | `get_chongguan` + `get_paiju_account` | 常规算奖只计庄家;连对链修复(传实际主花色);移除 ≥10 老主奖;去掉设计外的"冲关必叫/四王必踢"作废 | ✅ get_chongguan 用例 | +| §8 快照 | `get_seat_cards_award`(新) | 庄家用埋牌后 28 张(排除已埋)、闲家用发牌后 28 张 | 语法通过 | +| §8.3 傍王 | `get_paiju_account` | 改为按傍王位(roomtype 位2)开关,庄闲每王 1 奖,去作废 | ✅ 公式用例 | +| §8.4 | `get_paiju_account` | 算奖并入结算改为 `X×(2Ni−Nj−Nk)`(乘 X) | ✅ §8.4 举例 | +| §4.2 | `mod.jiaofen` | 增加叫分上限 `call<=70` | — | +| §4.4 投降 | `mod.touxiang` + `do_burycard` + `get_deskinfo` | 投降仅 70 分、在**选主阶段 step2** 与选主互斥、不选主不埋牌;结算基础 1、庄输 1 子/闲;算奖用庄家 **36 张**(发牌+暗牌,无主花色→无连对链);埋牌后 step 直接进 5、删除已废弃的投降 step4 与重连 Surrender 视图 | — | +| §10.1 | `get_asetcount`/`get_needroomcard` | 局数 6/12;房主扣卡 2/4 | — | +| §5.4 甩牌 | `arith`(新增 `trump_rank`/`decompose_trump`/`opp_can_beat_flush`/`flush_follow_ok`/`remove_pairs`)+ `can_playcard` + `do_playcard` + `mod.chupai` | 副牌绝对禁甩;最大性改为**按对手全部主牌**逐分量判定;甩错惩罚(收回、只打最小一张、下发 `shuaicuo`);**§5.4.4 强制跟牌分量拆解**(拖拉机→对子→单张优先级,`shuai_demand`+`flush_follow_ok`) | ✅ 分解/最大性/各路径/跟牌分量用例 | +| roomtype | `class.config.js`(新)+ `export`/`paiju` | 数组下标改为**位串**(`"01011"`),`class.config.js` 唯一解析(SSOT) | ✅ parse/扣卡/局数用例 | +| §4 暗牌亮牌 | `shangzhuang` + `get_deskinfo` | 70 分坐庄把 8 张暗牌下发给所有玩家 + `ancard3s=1`(供摸牌前亮 3 秒);修正**非 70 分暗牌泄露给闲家**(现暗牌只发庄家,符合 §4"只有庄家可见");重连的暗牌一律仅庄家可见 | — | +| §8.2 亮牌 | `get_liangpai`(新)+ `mod.maipai` + `get_deskinfo` | 庄家埋牌后 28 张按阈值(固定主≥10/王≥3/7≥6/2≥6)统计,出牌开始向闲家亮(只亮数量/结构);可查牌模式才下发 | ✅ 阈值/统计用例 | +| §9 查牌 | `class.config.js`(nocheck) + `mod.chupai` + `get_deskinfo` + `mod.mingpai`(新) | 报无主的座位统计 `info`/`baozhu`/`PushCards.seatlist` 与亮牌均按 `cfg.nocheck` 门控;新增 `mingpai` RPC(可查牌+已报无主时查看他家全部主牌) | — | + +**尚未整改(多需与客户端/协议协同,或为较大的独立子系统)**: + +- ✅ **§5.4 甩牌(已全部完成)**:副牌绝对禁甩、最大性按对手全部主牌逐分量判定、甩错惩罚、以及 §5.4.4 强制跟牌分量拆解(`flush_follow_ok`:拖拉机→对子→单张优先级)均已实现并用 Node 验证。 +- 🟩 **§9 查牌 / §8.2 亮牌(服务端已完成)**:亮牌 `get_liangpai`(庄家埋牌后 28 张按阈值统计)在 `maipai`/重连下发给闲家;报无主的座位统计(`info`/`baozhu`/`PushCards.seatlist`)与亮牌均按查牌位 `cfg.nocheck` 门控(不查牌一律不下发);新增 `mingpai` RPC(可查牌 + 已报无主时查看他家全部主牌)。剩客户端展示(面板/明牌按钮)。 +- 🟩 **§4.4 70 分暗牌亮 3 秒(服务端已就绪)**:`shangzhuang` 对 70 分坐庄把 8 张暗牌下发给所有玩家并带 `ancard3s=1`,供客户端在庄家摸暗牌前亮 3 秒;顺带修正了**非 70 分把暗牌泄露给闲家**的问题(现非 70 分暗牌只发庄家,符合 §4"只有庄家可见")。剩下的 3 秒动画由客户端完成。 +- ✅ **§11 闲家 3 提示(踩/没分/有分)**:`mod.tishi` 服务端转发给对家已实现;剩客户端展示。 +- 🟧 **客户端建房配置**:`roomtype` 已改为位串(见下表:位0局数/位1扣卡/位2傍王/位3爬坡/位4查牌),需在 erqiwang 客户端建房界面按位勾选拼串;服务端已按该格式读取(`class.config.js`)。 + +### roomtype 位串约定(服务端已按此读取;解析入口 `class.config.js`) + +`roomtype` 为**定长数字位串(字符串)**,每一位(`charAt`)一个开关 `'0'/'1'`,便于前端逐项勾选。缺省 `"00000"` = 6局/房主扣卡/无傍王/常规算子/可查牌。解析对缺失/过短字符串按 `'0'` 兜底。 + +| 位 | 含义 | `'0'` | `'1'` | +| --- | --- | --- | --- | +| 0 | 局数 | 6局(缺省)| 12局 | +| 1 | 扣卡方式 | 房主扣卡(缺省)| AA每人扣卡 | +| 2 | 傍王 | 关(缺省)| 开 | +| 3 | 爬坡 | 常规算子(缺省)| 爬坡 | +| 4 | 查牌模式 | 可查牌(缺省)| 不查牌 | + +示例 `"10100"` = 12局 / 房主扣卡 / 傍王开 / 常规算子 / 可查牌。 + +--- + +## 第三轮核对(平台接入 / 入参 / 下发面 / 协议红线) + +> 前两轮只沿 design.md 逐节核对玩法规则,因此**整类问题在核对范围之外**。本轮补上这些维度,结论如下(均已修复并有单测回归)。 + +| # | 严重度 | 位置 | 问题 | 处置 | +| --- | --- | --- | --- | --- | +| A1 | 🟥 阻断 | `server/youle/app.js` | 只加载 `games2/jinxianmahjong/mod.js`,**从未加载 `games/erqiwang/mod.js`**,全仓平台侧对 erqiwang 零引用 → `youle_erqiwang` 模块从不创建、`pack.route="erqiwang"` 无法命中三层路由,**整个子游戏运行时不可达** | 已在 app.js 补注册(server dev-guide README 允许触碰的唯一平台文件) | +| B1 | 🟥 严重 | `class.paiju.js` `check_cards_inhand` | 不查重复牌id:`do_playcard([方块K,方块K])` 实测 `result=true, cardtype=201`——同一张牌被判成一对,可伪造拖拉机/甩牌分量,且该分牌 score 被重复累加;`maipai` 传 8 个相同 id 实测**只埋下 1 张**,庄家带 35 张进入出牌阶段 | 新增 `check_cards_valid`(非空数组 / 0~107 整数 / 无重复)作为唯一入参校验入口 | +| B2 | 🟥 严重 | `mod.js` `maipai`/`chupai` | 越界 id 或非数组入参在解引用处抛异常,中断 `DoPack`;且 `cards.length` 读在校验之前 | 校验前置于 length 判断之前 | +| B3 | 🟥 严重 | `arith.get_multiple_bycall` + 6 个调用点 | 写死 `climb=false`:**爬坡房对局全程界面显示常规算子子数,与结算 `aset.multiple` 不符**(叫 45 分:界面 6、结算 7) | 删除该兼容包装,全部改调 `get_base_bycall(call, cfg.climb)`,与结算同源 | +| C1 | 🟥 红线 | 全部下发包 + 所有失败分支 | **全仓无 `data.success`**;失败一律裸 `return`、不回任何包,前端只能干等倒计时。违反 server dev-guide「成败唯一是 data.success、主动推送必须自带」与 client dev-guide「发包只请求、收包才表现」 | 成功包统一补 `success:true`;新增 `ERR` 码表 + `do_sendfail`,每条失败分支回 `success:false + errcode` 给请求者 | +| D1 | 🟧 中等 | `arith.is_continuous` | `is_8`/`is_6` 只判 `code % 100`、不校验花色:`is_continuous(♥8=308, ♣6=206)` 实测为 `true`。`get_chongguan` 是唯一扫描整手牌(含各花色副牌)的调用点,连对链走到主 8 后会被任意花色的一对 6 错误接续、多算奖(design §8.1 要求 A~5 段必须是主花色) | 追加「编码相差 2」约束(同花色 8/6 恒差 2) | +| D2 | 🟧 中等 | `export.get_deskinfo` pushlist | ① 内层循环上界误用轮次数当座位数,实测 5 轮时内层被撑成长度 5 且含 `undefined`;② 排序传的是上个循环泄漏的 `pai.flower`(=最后一张牌大王的花色 5),等于按「无主牌」排序 | 内层固定 3、改用 `paiju.flower`,并对缺失轮次加守卫 | +| D3 | ✅ 规则如此 | `class.desk.js` | 下发了 `countdown`,但服务端无任何定时器做超时托管,玩家不操作则牌局停住等待 | **规则设计者确认:维持现状、不做超时动作**。已写入 design §11「倒计时只作展示,不触发任何自动操作」并注明不是缺口;牌局停住由玩家走房间解散流程收场(按当前累计分结算,design §12.2)。server dev-guide「服务端自动操作复用真人链路」约束的是「若要做代打就必须复用真人链路」,本局不做代打,故不适用 | +| D4 | 🟨 轻微 | `paiju.do_playcard` | 报无主后只更新出牌者自己那一格,B/C 要等各自轮到出牌才补上,界面最多滞后两次出牌(design §9 要求「为全体三人显示另外两家」) | 改为一旦 `have_baofu()` 成立即按实际手牌刷新三个座位 | +| D5 | 🟨 轻微 | `can_playcard` / `chupai1` | `cardtype` 只能表达单一牌型,甩牌被压平成 1xx/2xx(「主K对+主5」→103),客户端连"这是一次甩牌"都看不出来 | `chupai1` 增发 `shuai` 分量构成(与 `shuai_demand` 同源) | +| D6 | 🟧 中等 | `export.get_deskinfo` | 不查牌模式下 `pushlist`(已打出牌历史)仍照发。规则设计者已确认:**不可查牌模式必须屏蔽出牌历史,只有可查牌模式才能查看** | 已按确认修复:`pushlist` 按 `cfg.nocheck` 门控;design §9 同步改写为无歧义表述(四项功能整体开关 + 「当前轮桌面牌不属于查牌、两种模式都可见」的边界) | +| D7 | 🟨 轻微 | `check_cards_inhand` | 牌已出/已埋时 `return;`(undefined)而非 `false` | 已改为 `return false` | + +### 尚未处置 + +**无。** 本轮发现的 11 项已全部收口: + +- A1/B1/B2/B3/C1/D1/D2/D4/D5/D7 共 10 项为缺陷,已改代码 + 补单测; +- D6(不查牌是否屏蔽出牌历史)原为规则歧义,已由规则设计者拍板「屏蔽」,改 design §9 + 代码 + 单测; +- D3(超时托管)原列为待外部输入,已由规则设计者拍板 **维持现状、不做超时动作**,属**规则如此、并非缺陷**,已写入 design §11 固化,避免后续核对反复把它当缺口重提。 + +--- + +## 第四轮核对(全量重核 design.md:先流程、后规则细节) + +> 从 design.md §1~§12 重新逐条对照当前代码(含流程阶段机与全部规则细则),**流程层无缺环**(发牌→叫分→摸暗牌/70分亮3秒→选主/投降互斥→先选主后埋牌→出牌→末轮扣底→算子+算奖→轮庄→大局/解散结算,`step` 迁移 1→2→3→5→6 闭合)。规则层新发现 3 项不一致,已全部修复并补回归单测。 + +| # | 严重度 | 位置 | 问题 | 处置 | +| --- | --- | --- | --- | --- | +| E1 | 🟧 中等 | `class.paiju.js` `get_liangpai` | 亮牌统计取 `get_seat_cards`(只含未出的牌),而 §8.2 的依据是**庄家埋牌后的手牌**。埋牌那一刻算对,但 `get_deskinfo` 在 step5 重连时会按庄家**当前剩余手牌**重算:实测「4王+6个7」的庄家打掉 2 大王 + 1 对♦7 后,重连再取从 `{zhu:10,zhupair:5,zhutuo:1,wang:4,qi:6}` 变成 `null`,闲家重连即丢失亮牌 | 改用已有的静态快照 `get_seat_cards_award(o_paiju, banker)`(埋牌后 28 张、含之后已打出的),并在函数注释里固化"不得用 get_seat_cards"的原因;补 3 条回归用例(快照 / 打出后重连不缩水 / 已埋牌不计入) | +| E2 | 🟨 轻微 | `mod.js` `chupai` | 第三轮 D4 只修了服务端状态(`do_playcard` 里已按 `have_baofu()` 同时刷新三个座位),**下发面没跟上**:出牌包仍只带 `info = seatlist[seat]` 一家,另两家的主牌数/对子数要等它们各自出牌才送达,仍最多滞后两次出牌,与 §9「一旦有人报无主,立即为全体三人显示另外两家」不符 | 出牌包改发整表 `seatlist`(三家),字段名/结构与重连包 `PushCards.seatlist` 统一;协议文档三处 `info` 条目同步改写;补「整表带三家 + 含非出牌者统计」用例 | +| E3 | 🟨 轻微 | `class.arith.js` `can_playcard` | 开头 `if (cards.length > 14)` 是"规则外的约定"硬上限,与 §5.4.3「甩牌无组合数量限制」冲突:手持 15 张以上顶级主牌时的合法甩牌会被整个拒掉(如 4王+8个7+正2对+副2对 共 16 张) | 上限改为结构性上界 28(出牌阶段单人最多持 28 张,且 28 张内牌型编码不溢出 1xx/2xx/3xx 区间),不再设规则性限制;补 16 张合法甩牌用例 | + +**非缺陷的观察(本轮确认,勿再重提)**: + +- 70 分「暗牌亮 3 秒」:服务端在上庄包里一次性下发 8 张暗牌 + `ancard3s=1`,"摸入前亮 3 秒"的先后由前端表现;重连不重放该一次性事件。design 未要求服务端表达先后,**按现状即合规**。 +- §4/§11「选主按钮显示各花色对子数」:design 标注为**前端表现**,服务端已把庄家全部手牌下发,前端可自算,**不是下发面缺口**。 + +### 尚未处置 + +**无。** E1/E2/E3 已全部改代码 + 补单测,协议文档同步。 + +### 第四轮·复验(E1~E3 修复后的独立取证复核) + +对修复后的实现又做了一次**取证式**复核:不再靠通读,而是把 design 的判定语句直接做成可执行探针打在代码上。**未发现新的不一致**,全部符合。取证覆盖: + +| 维度 | 取证方式 | 结果 | +| --- | --- | --- | +| §5.1/§5.2 跟牌 | 层级 2/3/4(放着更长拖拉机不出→拒、拆对出散张→拒、留着对子只出散张→拒)、同花色不足全出+缺口任意、混合出牌牌面判 0、毙牌须完全缺门且牌型对应、首家出主必须跟主 | 15/15 符合 | +| §8.1 算奖 | 三/四王、链从正 7 起、无正 7 链断、正 7-副 7-正 2-副 2-主 A-主 K 链 5、副 K 对(非主花色)不入链、六/七/八个 7 与 2、固定主 ≥10 不给奖 | 13/13 符合 | +| §7 算子 | 常规与爬坡逐档抽查 18 格(含大光/小光/过庄/升 N 级、爬坡 45 独有档、10 分与 5 分无小光档) | 18/18 符合 | +| §5.4 甩牌 | 单一牌型不算甩牌、分量被更大同长拖拉机压→甩错、对手仅持更大散张压不了对子、甩错只打最小一张、"持有即算被压" | 6/6 符合 | +| §6.3 扣底 | 混合甩牌取最高规格倍数、末轮用副牌赢不扣底 | 符合 | +| §8.4 结算 | **闲家也持奖时的三条独立支付线**(此前未覆盖):`N=[4,1,0]`→`aw=[28,-8,-20]`,与捡分子数叠加后零和 | 符合 | +| §12 全流程 | 端到端跑通整局 + 18 局随机模糊(含甩牌轮),校验牌张守恒/捡分一致/结算零和/分配公式/扣底触发条件 | 无异常 | +| §4/§9/§10/§11 | 叫分 6 种序列(含跳过已"不叫"者)、70 分暗牌下发与投降资格、重连各阶段门控(暗牌仅庄家、pushlist/seatlist/亮牌按 `nocheck`、当前轮桌面牌两种模式都有)、爬坡房 `multiple` 三处同源、提示只发对家、明牌返回另两家全部主牌、扣卡 4 组合 | 全部符合 | + +固化产物:新增 `test/test_endgame.js`(L4 端到端,21 项,断言均与具体牌面无关、连跑 10 次无抖动),全套单测 **389 项**全绿。 + +--- + +## 第五轮核对(design 复验 + 首次以 packet_protocol.md 为核对对象) + +> 本轮做两件事:① 再次独立复核 design.md ↔ 代码;② **首次把 `docs/protocol/packet_protocol.md` 本身当作核对对象**(前四轮只在改代码时顺手同步文档,从未反向验证「文档写的 = 客户端实际会收到的」)。 +> +> **design 维度**:§1~§12 逐条重核,含自行重算 §7 边界档(爬坡 10 分/5 分无小光、70 分打牌、常规 40 分无过庄)与 §8.1 连对链(正 7 起链、副 7 跨花色不成对、非主花色断链)——**规则层未发现新的不一致**,前四轮整改均真实落地。 +> +> **新发现 4 项**(1 项严重缺陷 + 2 项协议文档不准确/不完整 + 1 项健壮性),已全部处置。 + +| # | 严重度 | 位置 | 问题 | 处置 | +| --- | --- | --- | --- | --- | +| F1 | 🟥 严重 | `class.arith.js` `can_followcard` 尾段 | 计算 `cardvalue` / `noflower` / `nopair` 时用的是**入参原始数组 `followcards`**(客户端提交顺序),而不是上面已排好序的副本;`get_pairlist` / `get_tuolaji_list` 都按**降序相邻**取对。端到端实测:庄家出红心 KKQQ、闲家用黑桃 KKQQ 主拖拉机毙牌且为末轮——降序提交 `[51,105,50,104]` → `maxseat=1`、闲家捡分 40、扣底 ×4;打乱成 `[51,50,105,104]` → 对子漏判、`cardvalue=0`、`maxseat=0`(庄家赢)、捡分 0(大光)、不扣底。**同一手合法牌,客户端只靠数组顺序就能翻转本轮胜负、捡分归属、扣底与最终结算**;同源问题还能抹掉 `noflower` 缺门标志、污染 §9 下发给全场的牌况表。踩 server 红线「前端不是数据源」「服务器权威」 | 在 `min_ary_deduct` 削减 `_followcards` **之前**另存完整排序快照 `_sortfollow`,尾段 8 处全部改用它(`_followcards` 会被削减、不能复用);函数内注释固化"绝不能用入参 followcards"的原因。补 6 条回归(`test_follow.js` 入参顺序无关性 5 条 + `test_endgame.js` 端到端 1 条),已验证**回退修复后这 6 条全红** | +| F2 | 🟧 中等 | `packet_protocol.md` §14 | 「解散结算」被写成 `route=erqiwang / rpc=jiesuan` 的独立包。实际平台(`server_room/rpc.js`、`server_room/class.room.js`)是把 `get_disbandRoom` 的**返回值整个对象**塞进**房间路由**的解散包:`route=room / rpc=free_room`,`data.deskfree = { rpc:"jiesuan", data:{ success, aset, account } }`——**多一层 `data` 包装**,真实路径是 `data.deskfree.data.aset`。前端照原文档写会取空。另外该包外层 `data.success` 由平台填写,子游戏的 `success` 在 `deskfree.data.success`,与 §0.1 的读法不同 | 新增 §14.1「解散结算的投递方式」:给出真实包结构 JSON、三条取值要点、两层 `success` 的归属,并说明 `deskfree` 可能缺失(见 F4);§14 开头标注原表头只适用于正常出牌结算与投降结算 | +| F3 | 🟨 轻微 | `packet_protocol.md` 全篇 | 文档在 `multiple` / `bangwang` / `climb` / `baozhu` / `seatlist` / `liangpai` / `pushlist` 等多处引用「roomtype 位2 / 位3」,但**全文从未给出 roomtype 位串定义**——它只存在于本 compliance 文档里。前端据协议文档无法拼出建房串 | 新增 §0.5「`roomtype` 房间选项位串」:位表(位0局数/位1扣卡/位2傍王/位3爬坡/位4查牌)+ 缺省与示例 + 缺失兜底规则 + 局数/扣卡 4 组合对照,并指明 `class.config.js` `parse()` 是唯一解析入口(SSOT) | +| F4 | 🟨 轻微 | `class.export.js` `get_disbandRoom` | 未像 `get_deskinfo` 那样判 `o_room.o_desk` / `curr_paiju()` 为空。平台在 `rpc.js` 开战处 `makewar` 后**立刻**置 `battlestate=1`,而本游戏首局是 `makewar` 里 `min_ontimeout(...,1000)` 延迟创建的——这段窗口内解散会在 `curr_paiju()` 的 `undefined` 上解引用抛异常,打断平台整条解散链路 | 两级空守卫,返回 `null`(平台对 `null` 的 `_deskfree` 已有「照常发解散包、不带 deskfree」分支);补 2 条回归用例 | + +**本轮同时复核并确认无误(勿再重提)**: + +- **协议文档字段面**:跑完整局(含 `mingpai`/`tishi`/失败包/投降局)与重连五个阶段,把**实际下发的 `data` 键集**与文档逐包对齐——`fapai / jiaofen / shangzhuang / xuanzhu / maipai / chupai1,2,3 / jiesuan / mingpai / tishi / zhunbei` 及 `deskinfo` 的 `CallRun / ChooseMain / BuryCards / PushCards / Balance`,**字段名、出现条件、按座位裁剪(暗牌仅庄家、`gradecards` 仅闲家、`liangpai` 仅闲家+可查牌、`cardsinhand` 仅出牌者)全部一致**;`errcode` 码表、§0.1 `success` 约定、§0.3「`countdown` 无定时器」说明亦准确。 +- **`can_followcard` 首家/跟家牌面值的比较基准不一致(首家取拖拉机最小对、跟家取最大牌)**:因同花色同长拖拉机不可能共用点数(两副牌已被首家占满),两段区间必然不重叠,比较结果恒正确,**属既有写法差异、非缺陷**。 +- **`get_chongguan` 的 `cards.length < 28` 早退**:属隐式兜底写法,但三个调用点的快照恒为 28(闲家)或 36(70 分投降的庄家),**当前不可触发**。 + +### 尚未处置 + +**无。** F1/F4 已改代码 + 补回归单测(并已用「回退修复→用例转红」反验有效性),F2/F3 已改协议文档。全套单测 **401 项**全绿。 + +### 第五轮·复验(design 维度的穷举/差分取证) + +F1~F4 修复后,对 **design.md 维度**又做了一次独立取证复核。方法上比前几轮更进一步:不再逐条读代码,而是**按 design 原文另写一份参考实现**,再和代码**穷举对拍**——参考实现只依据 design 的文字与表格,不看 `class.arith.js`。**未发现任何不一致。** + +| design 章节 | 取证方式 | 规模 / 结果 | +| --- | --- | --- | +| §2 牌局构成 · §6.1 分牌 | 直接清点 `init_cards` 产物 | 108 张牌表 / 去 3-4 共 16 张 / 发 92 张 = 三家 84 + 暗牌 8 / 11 种点数各 8 张 + 大小王各 2 张 / 分值 5-10-K / 全场总分 200 ✅ | +| §3 主牌顺序 · 对子判定 | 取全部主牌排序后归并成「等级序列」,与 §3 表逐级比对 | 大王-小王-正7-副7-正2-副2-主A…主5 共 15 级完全一致;副牌 A…5 顺序一致;♥7+♣7 / ♥2+♣2 / 大王+小王 均不成对,同花色两副成对 ✅ | +| §5.3 拖拉机相邻 | 15 级相邻链逐段验证 + 反例 | 链全通;7 不与 6/8 相连、6 与 8 同花色相连、♥8+♣6 跨花色不相连、副2 只接主A ✅ | +| §5.1 / §5.2 跟牌强制层级 | **穷举差分**:按 §5.2 原文写参考裁定(缺门/不足/够牌 × 单张/对子/N 连对 + 最长拖拉机优先的档案比较),随机手牌枚举**全部** C 张出牌组合逐个对拍 | 4000 手牌 / **86474 个出牌组合,失配 0** ✅ | +| §5.4.1-3、§5.4.5 甩牌 | **随机差分**:按 §5.4.2 原文写「对手是否持有能压过任一分量的主牌/主对/同长主拖」的参考判定 | **6000 例,失配 0**;副牌禁甩 4 种组合全拒、副牌单一牌型全放行 ✅
(首轮出现的 170 例「最小张不符」经查是**同一等级的两副牌选了不同那张**,非分歧——改按牌面等级比对即归零) | +| §5.4.4 强制跟牌分量拆解 | 按 §5.4.4 五条逐条造正反用例 | 同长主拖必出 / 无同长退化为主对 / 主对不够全出+主单补 / 主牌不够全出+垫副 / 无主牌随意垫 / 多组不同长拖拉机逐组匹配,12 条全符合 ✅ | +| §5.4.5 甩错惩罚落地 | 走 `do_playcard` 真实链路 | 甩错后 `shuaicuo=true`、实际只打出最小一张、其余收回手牌、本轮牌型退化为 101、不再带 `shuai_demand` ✅ | +| §6.2 捡分归属 | 造庄赢/闲赢两轮 | 庄家赢 → 台面分牌作废(闲家捡分 0);闲家赢 → **台面全部分牌(含庄家自己打出的)** 计入闲家 ✅ | +| §6.3 扣底倍数 | §6.3 表逐格 | 单张 1 / 主对 2 / 2-6 连对 4·6·8·10·12;混合甩牌取最高规格;末轮用副牌或混合牌赢 → 0 不扣底 ✅ | +| §7 算子(全部) | **全量对拍**:把 §7.2.1 / §7.3.3 的判定表转写成参考实现,两种算子模式 × 14 个叫分档 × grade 0~260 逐格比 base / Q / 判定 / 最终子数 | **14672 格失配 0**,另抽查 design 表格里写死的最终子数 **113 格失配 0** ✅ | +| §8.1 常规算奖 | 逐组合造手牌 | 三王 1 / 四王 3 / 六·七·八个 7 → 1·2·3 / 六·八个 2 → 1·3;连对链 正7→副7→正2→副2→主A→主K 逐级 2·3·4·5·6·7 奖;链必起于正7(无正7 断链)、缺副7 断于正7、副A 对(非主花色)不入链、无三王则无链奖、固定主 ≥10 不给奖 ✅ | +| §8.2 亮牌 | 四档阈值边界(含刚好差 1) | 王≥3 / 7≥6 / 2≥6 / 固定主≥10 分别触发,各自差 1 即不触发;输出只含数量与结构、无具体牌面 ✅ | +| §8 快照时间点 | 走真实埋牌/出牌/投降链路 | 庄家=埋牌后 28 张(已埋 8 张不在内、出牌后不缩水)、闲家=发牌后 28 张、投降局庄家 36 张且 `flower=-1` ✅ | +| §8.4 算奖并入结算 | §8.4 举例数值 | `X=6, N=[1,2,0]` → `aw=[0,18,-18]`,三条支付线零和;只庄家持奖 → 另两家各付 X ✅ | +| §4.2 叫分 | **穷举全部叫分序列**(DFS,叫分集 {70,50,25,10,5}+不叫) | **80 条终局路径**:受理判定、待叫者永不落在已「不叫」者身上、上庄者恒为最低叫分者、上庄条件恒为「叫 5」或「另两家都不叫」,**异常 0**;首家不能不叫、75/71/67/3/-5 全按 PARAM 拒 ✅ | +| §4.3 暗牌可见性 · §4.4 投降 | 非 70 分 / 70 分两条链路 + 重连 | 非 70 分暗牌只给庄家(闲家无 `bottomcards`、无 `ancard3s`、无 `cards`);70 分三家都收到 8 张 + `ancard3s=1`;重连一律只给庄家(3 秒亮牌不重放);`touxiang` 仅 70 分为 1;投降 base=1、庄 -2 闲各 +1 ✅ | +| §4.6 轮庄 | 三种 result | 庄赢连庄 / 闲赢顺延下家 / 投降顺延下家;首局暂定庄家为 0 号座位 ✅ | +| §9 查牌模式 · §10 房间设置 · §11 提示 | 可查牌与不查牌两套房间跑同一段对局 | 可查牌下 `chupai.seatlist` 整表三家、`baozhu` 有值、重连带 `pushlist`/`seatlist`;不查牌下三者全无、`baozhu` 恒 0、明牌回 RULE;**当前轮桌面牌(`playproc`)两种模式都下发**;局数 6/12、房主 2/4、AA 1/2 与加入者扣卡 4 组合正确;提示只转发给对家、庄家发 → RULE、非法 tip → PARAM;不操作则 `step`/`playproc` 不变(无超时托管)✅ | + +> 一句话结论:**就 design.md 而言,服务端实现完整且准确**。本轮两次「疑似失配」(甩错最小张 170 例、闲家赢轮捡分 15 vs 10)经查**都是探针写错**,代码是对的;已在上表注明,后续核对不要再把它们当缺陷重提。 +> +> **已固化两组差分测试入库**(其余取证为一次性探针):`test/test_calc.js`(§7 全量对拍,7308 格 + design 表 141 格)与 `test/test_followdiff.js`(§5.2 穷举差分,约 11 万组),全套单测由 401 项增至 **462 项**,总耗时仍 < 1.5s。 +> +> 两组都做过**变异检验**:关掉 `follow_tractor_cover_ok` → `test_followdiff` 三种拖拉机首出全红;大光倍率 3→4 / 常规 55 档 base 4→5 / 爬坡 40·35 档 Q 20→40 → `test_calc` 精确指出档位与 grade。 +> +> ⚠️ 固化过程中的一个教训(改这两个文件时别踩回去):**差分测试的强度取决于造牌器,不取决于对拍逻辑**。第一版用纯随机抽牌,跑 7.8 万组全绿,但关掉 `follow_tractor_cover_ok` **依然全绿**——因为「同花色三组互不相邻的两连对」(12 张)这种唯一能区分"只出零散对子"与"先凑最长拖拉机"的结构,随机抽根本抽不出来(少于 3 组时任取 3 对必含相邻对,测不出差别)。补上确定性的 `structHands()` 后才转红。同理,掺入的其他花色一度把这手撑到 15 张、超过枚举上限被**静默跳过**,表面照常全绿。**"跑了很多组且全绿"不等于"测到了",新增差分测试必须做变异检验。** + +--- + +## 第六轮核对(多局大局 + 整局带牌型模糊) + +> 前五轮的端到端**都只跑单局、且只出单张**。本轮补两个从未被驱动过的维度:① `design §12.1 步骤10 → §12.2` 的**多局大局**(跨局轮庄、累计分、大局结算、战绩);② **带牌型的整局链路**(对子/拖拉机/甩牌/甩错/毙牌/扣底与 `do_playcard`→`mod.chupai`→结算的集成)。**未发现任何不一致。** + +| 维度 | 取证方式 | 结果 | +| --- | --- | --- | +| §12.1 步骤10 · §4.6 跨局轮庄 | 真实驱动 6 局(RPC 全链路,含每局 `zhunbei` 开新局) | 每局 `firstseat` 与「上局 banker + result」严格对应;另单独构造出 `result=0` 的**庄赢局**验证**连庄**(此前只有 `do_prepare` 的桩单测,端到端从未走过)✅ | +| §7 累计分 | 6 局 | 每局零和;`desk.seatlist[i][0]` == 逐局累加;`aset.seatlist[i].score` == 累计分 ✅ | +| §12.2 大局结算 | 6 局房与 12 局房各跑满 | `account` **只在末局**出现(6 局房第 6 局、12 局房第 12 局);打满后再 `zhunbei` 回 RULE 拒 ✅ | +| §10.1 扣房卡 | 6 局 | 全程只扣一次(第一局结算),`save_grade` 一次 ✅ | +| 战绩 | `save_grade` 载荷 | `gameinfo1.asetcount`/`players.score` 与累计分一致;`gameinfo2` 每局 `seatlist`/`banker`/`call`/`flower`/`result` 齐全 ✅ | +| §12.2 中途解散 | 打完 2 局后在第 3 局解散 | 解散局 `multiple/upgrade` 均 0、三家得分 `[0,0,0]`、`result=3`;`account` 累计分 == 解散前累计分("按当前累计分结算")✅ | +| 牌局隔离 | 每局开新 paiju | 新局重新发满 92 张;上一局的 `tmp_jiesuan_aset` 已清理 ✅ | +| §5/§6/§7/§8 整局带牌型 | 160 局 / 约 3800 墩(4 个种子 × 40 局),**每一墩**用独立参考实现重算胜者与服务端比对 | 多张首出、合法甩牌、甩错、毙牌墩、扣底均大量出现;牌张守恒 84/8/16、捡分守恒、结算零和、扣底触发条件、算奖分配公式**全部相符,0 异常** ✅ | + +**本轮两次"疑似缺陷"经查都不是**(勿再重提): + +- 某局结算 `[0,0,0]`:是 `jf=[4,-8,4]` 与 `aw=[-4,8,-4]` 恰好抵消,不是漏算。 +- 首轮 40 次尝试跑不出庄赢局:是**用例设计反了**——叫 5 分是闲家最容易达标的档(`grade ≥ 5` 即赢),应叫 70 分才易守。改后立刻出现。 + +**固化产物**:新增 `test/test_fuzz.js`(整局模糊 + 每墩胜者差分,20 局约 480 墩,8 项聚合断言,0.3s)。多局大局部分为一次性探针未入库(跑满 12 局较慢,且其不变量已由 `test_desk`/`test_endgame`/本轮探针共同覆盖)。全套单测 **511 → 519 项**全绿。 + +--- + +## 第七轮核对(反方向:代码 → design) + +> 前六轮都是「design → 代码」方向:拿规则去找代码有没有实现。本轮换**反方向**——从代码出发,把服务端每一个规则决策与常量拉出来,逐个追问「design 里有没有明文依据」。找的是两类此前查不到的东西:**代码做了 design 没授权的事**,以及 **design 有歧义时代码替规则做的选择**。 +> +> **结论:没有发现与 design 冲突的行为。** 但列出 6 项「design 未明文规定、由代码自行决定」的项——它们不是缺陷,但也从未被规则设计者确认过,建议逐条拍板后写回 design。 + +| # | 代码的决定 | design 依据 | 性质 | +| --- | --- | --- | --- | +| R1 | 四个阶段的倒计时秒数 **15 / 20 / 25 / 30**(`class.desk.js`) | §11 只说「四个阶段都会给出一个倒计时秒数」,**未规定具体值** | 实现选择,需确认 | +| R2 | **§5.4.4 拖拉机分量的降级阶梯不要求「最长优先」**:甩牌里有 3 连对分量而闲家只有 2 连对时,代码允许出任意 3 对,不强制先凑出那个 2 连对 | §5.4.4 第 1 条明文写「没有同等连对数的拖拉机,退而求其次用**同等张数的主对子**顶替」——支持代码;但该节开头又说「沿用 §5.2 强制层级同一原则」,而 §5.2 层级 2 要求「能凑多长就必须先凑多长」。**两处措辞可作两种解读** | 歧义,代码按显式的第 1 条实现 | +| R3 | 服务端把提交的一组主牌按「**连续对子最大化合并成拖拉机**」分解(`decompose_trump`) | §5.4.3 只说甩牌可自由混搭,**未规定服务端如何分解**。该选择同时影响最大性判定、跟牌分量需求、扣底倍数(合并对甩牌方更有利:`AAKKQQ` 记 3 连对 ×6 而非 2 连对 ×4) | 实现选择,需确认 | +| R4 | 两个**同级**的副 7 对(♥7对 + ♣7对)**不构成连对** | §5.3 的链只列一次「副7」,同级即非相邻——代码按此。但 design 未直说 | 解读,合理 | +| R5 | `call` 用 `parseInt` 宽松解析:`"65"`(字符串)与 `65.5`(小数截断为 65)都会被接受;而 `cards` 是严格校验(字符串数字/小数一律拒) | design 不管入参类型;协议 §0.2 只对 `cards` 定了严格约束。**两者标准不一致**,但截断后语义正确、不改变规则结果 | 一致性瑕疵,非规则违反 | +| R6 | `get_chongguan` 对 `cards.length < 28` 静默返回 0 奖 | 隐式兜底(违反工程总则「显式失败优于隐式兜底」)。三个调用点的快照恒为 28 或 36,**当前不可触发** | 代码风格,非缺陷 | + +### 本轮新增取证:下发面泄露审计 + +design §4(暗牌只有庄家可见 / 70 分亮 3 秒)、§9(查牌模式)、§11(结束亮底牌)与 server 红线「**发全 ≠ 发多,按可见性下发**」此前**没有任何测试**——各处门控是逐条手工核对的,从未系统验证过「有没有哪个包把不该看的牌送到了某个座位」。 + +本轮把它做成可执行审计(`test/test_leak.js`):跑完整局(可查牌 / 不查牌 × 叫 65 / 70 / 5 共 6 局),对每一个「服务器 → 某座位」的下发面(含 6 个 RPC 的逐座位包 + 各阶段重连快照 + 明牌应答)做深度扫描,取出其中出现的**所有牌 id**,逐个判定该座位此刻是否有权知道这张牌。 + +**结果:1814 个下发面 / 18706 次「座位×牌」可见性判定,0 泄露。** + +变异检验(注入真实泄露确认审计有效): + +| 注入的泄露 | 结果 | +| --- | --- | +| 非 70 分也把 8 张暗牌发给闲家(这正是第三轮修过的历史缺陷) | ✅ 抓住 | +| 重连时把庄家埋的底牌发给闲家 | ✅ 抓住 | +| 出牌包把出牌者剩余手牌发给所有人 | ✅ 抓住 | + +--- + +## 第八轮核对(按 流程 / 牌型 / 玩法 / 算分 四个维度复核) + +> 规则方复述了一条叫分规则:「开局庄家先叫分,不允许不叫分;第一局在没有庄家的情况下,默认座位号 0 玩家开始叫分」。据此按四个维度重新取证,**未发现不一致**。 + +**新增取证(此前从未做过):阶段机迁移矩阵。** 前几轮每个 handler 只测了 1~2 条代表性失败路径,从没系统验证过「某个请求在**别的**阶段会不会被误放行、会不会静默丢弃」。本轮把 8 个 RPC × 5 个阶段做成 **40 格矩阵**逐格驱动: + +| | step1 叫分 | step2 选主/投降 | step3 埋牌 | step5 出牌 | step6 结算 | +| --- | --- | --- | --- | --- | --- | +| jiaofen | ✓ 受理 | × STEP | × STEP | × STEP | × STEP | +| xuanzhu | × STEP | ✓ 受理 | × STEP | × STEP | × STEP | +| touxiang | × STEP | ✓ 受理 | × STEP | × STEP | × STEP | +| maipai | × STEP | × STEP | ✓ 受理 | × STEP | × STEP | +| chupai | × STEP | × STEP | × STEP | ✓ 受理 | × STEP | +| mingpai | × STEP | × STEP | × STEP | ✓ 受理 | × STEP | +| tishi | × STEP | × STEP | × STEP | ✓ 受理 | × STEP | +| zhunbei | × STEP | × STEP | × STEP | × STEP | ✓ 受理 | + +**40 格全部符合**,无误放行、无静默丢弃。(`mingpai` 在 step5 还需「已有人报无主」这一前置,首轮矩阵曾把它标红——是期望表漏了前置条件,不是缺陷。) + +**叫分起始者规则逐条取证**(`design §4.2 + §4.6`,与规则方复述一致): + +| 规则 | 取证 | 结果 | +| --- | --- | --- | +| 第一局默认座位 0 开始叫分 | `makewar → do_new_paiju(0)` → `firstseat=0`、待叫者=0 | ✅ | +| 之后每局由**暂定庄家**先叫 | 连打 6 局,每局 `firstseat` 与「上局 banker + result」逐局比对 | ✅ | +| 庄赢 → **连庄**(起始仍是该庄家) | 随机对局几乎打不出庄赢,用「叫 70 分 + 首家出最大牌」专门构造出 `result=0` 局 | ✅ | +| 庄输 / 投降 → 起始顺延到下家 | 6 局中逐局验证 | ✅ | +| **首家不允许"不叫"** | 第一局与其后每一局(含连庄局)的首家发 `call=0`,一律回 `RULE` 且叫分过程不变 | ✅ | + +四个维度的现有取证汇总: + +| 维度 | 取证手段 | 规模 | +| --- | --- | --- | +| **流程** | 阶段机 40 格矩阵、叫分序列 DFS 80 条终局、多局大局(6 局/12 局)、轮庄含连庄、中途解散、端到端整局 | 本轮新增矩阵 | +| **牌型** | §3 等级序列逐级、§5.3 相邻链(副牌同花色 / 主牌不限花色 / 同档位不相邻)、§5.2 跟牌穷举差分 11 万组 | `test_arith`/`test_followdiff` | +| **玩法** | §5.4 甩牌最大性 6000 例差分、甩错惩罚、§5.4.4 分量拆解、整局带牌型模糊每墩胜者差分、§9 查牌门控、下发面泄露审计 1.8 万次判定 | `test_fuzz`/`test_leak` | +| **算分** | §7 算子全量对拍 7308 格 + design 表 141 格、§6.3 扣底、§8.1 算奖、§8.4 分配公式、结算零和 | `test_calc`/`test_paiju` | + +**固化产物**:新增 `test/test_flow.js`(7 项聚合断言,0.3s)。变异检验:去掉 `chupai`/`maipai` 的阶段校验、把 `jiaofen` 的阶段校验改成静默丢弃——三条全部转红。全套单测 **540 → 547 项**全绿。 + +--- + +## 结论摘要 + +> **注**:下表是**首轮核对**的初始发现快照(保留以记录起点)。其中 §4.4、§5.4、§6.3、§7、§8.1-8.4、§9、§10、§11 等标 🟥/🟧/🟨 的项**后续均已修复并验证**——当前逐节状态以本文档后面的分节详情及全套单测(270 项全绿)为准。 + +| 设计章节 | 主题 | 结论(首轮快照,非当前状态) | +| --- | --- | --- | +| §2 | 牌局构成(92 张、去 3/4) | ✅ 符合 | +| §3 | 主牌顺序(牌编码) | ✅ 符合(副 7/副 2 跨花色成对存疑,见下) | +| §4.1-3 | 发牌 / 叫分 / 埋牌流程 | 🟧 缺叫分上限、投降资格过宽 | +| §4.4 | 70 分投降 / 打牌分支 | 🟥 投降资格 65 分即可、暗牌亮 3 秒未实现、投降结算数值错 | +| §4.6 | 坐庄轮换 | ✅ 符合 | +| §5.1-5.3 | 跟牌 / 毙牌 / 垫牌 / 拖拉机 | ✅ 基本符合(1 处运行期崩溃缺陷) | +| §5.4 | 甩牌 | 🟥 合法性判定模型、副牌禁甩、强制跟牌拆解、甩错惩罚均不符 | +| §6.1-6.2 | 分牌 / 捡分 | ✅ 符合 | +| §6.3 | 扣底倍数 | 🟥 倍数表完全不同 | +| §7 | 算子(基础子数 / 大光小光过庄 / 升级 / 爬坡) | 🟥 系统性不符,且无爬坡开关 | +| §8.1 | 常规算奖(只庄家 + 连对链) | 🟥 三家都算、含额外作废规则、连对链因传参失效、≥10 老主误给奖、庄家快照含底/埋牌牌 | +| §8.2 | 亮牌 | 🟧 未实现 | +| §8.3 | 傍王 | 🟥 无开关(恒开)、含设计外作废、且未乘 X | +| §8.4 | 算奖并入结算 | 🟥 扁平奖数(每奖 1 子),未按 `X × N` | +| §9 | 查牌 / 不查牌模式 | 🟧 无模式开关、明牌功能无服务端支持 | +| §10.1 | 扣卡 / 局数 | 🟥 局数 2/4(应 6/12)、房主扣卡数不符 | +| §10.2-10.3 | 查牌 / 傍王 / 爬坡选项 | 🟥 均无开关 | +| §11 | 交互提示 | 🟨 亮底牌 ✅;甩牌见 §5.4;闲家 3 提示无服务端支持 | + +--- + +## §2 牌局构成 — ✅ 符合 + +`class.paiju.js` `init_cards`:两副牌,点数 3、4 置 `dealowner=-1` 且不发(`if (k != 3 && k != 4) do_dealpai`);`tmpdeal` 共 `28×3 + 8 = 92` 个槽位。发出 92 张(11 种点数 ×4 ×2 + 4 王),去掉 16 张 3/4,与设计一致。分值 `5→5、10→10、K(13)→10` 亦一致(§6.1)。 + +--- + +## §3 主牌顺序 — ✅ 符合(1 处存疑) + +`arith.id_to_code` 以 `flower*100+number` 为底,`A(+13)`、`2(+2000)`、`7(+7000)`、主花色 `(+1000)`、王固定 `9553/9554`,得到 code 降序即设计主牌顺序: + +大王 9554 > 小王 9553 > 正 7(8xxx)> 副 7(7xxx)> 正 2(3xxx)> 副 2(2xxx)> 主花色普通牌 A…5(1xxx)> 副牌 A…5(<1000)。 + +`is_continuous` 正确实现全部相邻段(含 `小王↔正7`、`副2↔主A`、`8↔6`,以及「7 不与 6/8 连」——7 恒为主、不在普通链内)。 + +**存疑(待规则确认)**:不同花色的副 7(code 7107/7207/7307)、副 2(2107/2207/2307)编码互不相等。因此: + +- 排序上三家副 7 被强行分出大小;比大小时 `can_followcard` 又把落在 7xxx/2xxx 段的牌**归一为 7000/2000**(视作同级),二者存在不对称,但因跟牌只能跟同花色、毙牌只能用更大主牌,实测不产生错误赢家。 +- 成对判定 `get_pairlist` 要求 `code 相等`,故「副 7 对 / 副 2 对」只认**同花色两张**,跨花色的两张副 7 不算一对。设计 §3 把副 7 列为同一等级,但未明确「两张不同花色副 7 是否成对」——**需与规则设计者确认**。若规则要求跨花色副牌可成对,则此处需改。 + +--- + +## §4 开局与坐庄 + +### §4.1 发牌(每人 28 + 8 暗牌)— ✅ 符合 + +### §4.2 叫分 — 🟧 缺上限校验 + +- ✅ 步进 5:`mod.jiaofen` 校验 `call % 5 != 0` 拒绝。 +- ✅ 暂定庄家必须叫:首家 `currcall==0 && call==0` 被拒。 +- ✅ 后叫更低:`curr_call != 0 && call >= curr_call` 被拒。 +- ✅ 叫 5 立即上庄、两家不叫则上庄:`paiju.do_callgrade` 实现。 +- 🟧 **缺 `call <= 70` 上限**:`mod.jiaofen` 未限制上界,首家可叫 75、100 等(设计 §4 范围 5~70)。 + +### §4.3 埋牌 — ✅ 符合(选主/埋牌顺序已确认解决) + +庄家得暗牌(`get_seat_cards` 对庄家含 `dealowner==0`),`mod.maipai` 校验埋 8 张且都在庄家手上,`do_burycard` 置 `playround=0` 后由庄家开首轮。 + +✅ **顺序问题已解决**:复查时曾发现 design §4 列举顺序是「埋牌→选主」、而代码是「选主(step2)→埋牌(step3)」,二者相反。经与规则设计者确认,正确顺序为**先选主、后埋牌**(庄家知道主副后才好决定埋哪 8 张)——即**代码本就正确**,是 design §4 的列举笔误。已修正 design §4 的步骤顺序,并新增 design §12「完整牌局游玩流程」明确整局先后。代码无需改动。 + +### §4.4 70 分投降 / 打牌 — 🟥 多处不符 + +- 🟥 **投降资格过宽**:设计仅 **70 分**可投降;代码 `mod.touxiang` 判 `o_paiju.call < 65` 才拒绝,即 **65 分也能投降**。应改为「仅 `call == 70` 允许」。同样地 `shangzhuang.touxiang`、`get_deskinfo` 各阶段的 `touxiang` 字段都用 `call < 65` 判定,需一并改。 +- 🟥 **投降结算数值错**:设计投降为「基础子数 1、庄家每闲家输 1 子」。代码 `get_paiju_account(type=1)`:`upgrade=-99→-2`、`multiple=get_multiple_bycall(70)=1`,庄家 `grade_jf = -2 × 1 × 2 = -4`(每闲家 2 子)。金额翻倍且模型不对。 +- 🟨 **暗牌亮 3 秒未实现**:设计要求 70 分坐庄把 8 张暗牌向两闲家亮 3 秒(投降 / 打牌都要),代码无此下发。 + +### §4.6 坐庄轮换 — ✅ 符合 + +`class.export.js` `makewar` → `do_new_paiju(0)`:首局暂定庄家为 0 号座位(设计 §4.6)。`class.desk.js` `do_prepare`:`result==1(闲赢)||result==2(投降)` 时下一局 `firstseat=(banker+1)%3`(下家),否则连庄。两点均与设计一致。 + +--- + +## §5 出牌规则 + +### §5.1 / §5.2 跟牌 / 毙牌 / 垫牌 — ✅ 基本符合(含 1 处崩溃缺陷) + +- 每轮由上轮最大者先出、每局首轮庄家先出:`new_playround(maxseat)` / `do_burycard→new_playround(1, banker)`。✅ +- 跟同花色、缺门可毙(主牌压过)或垫(任意副牌不争):`arith.get_followcard`/`can_followcard` 按「同花色是否够 / 有无对子 / 有无拖拉机」给必出牌与可出牌,毙牌能否赢由 `cardvalue`(副 7/副 2 归一、跨花色垫牌记 0、主牌对/拖拉机才 >0)裁定。✅ 与 §5.2「毙牌必须用对应牌型」一致。 +- 🐞 **`can_followcard` 引用未定义变量 `tuolaji_list` 会抛异常**(`class.arith.js` 约 801 行,`get.cantype` 为拖拉机型 3xx 分支内 `if (tuolaji_list.length == 0)`,该变量在此作用域未赋值)。当跟牌方手中存在**多个**符合长度的候选拖拉机(`get_followcard` 返回 `cantype=startcardtype`)时命中此分支,`DoPack` 抛错、该次出牌链中断。需修复。 + +### §5.3 拖拉机定义 — ✅ 符合(见 §3 的 `is_continuous`) + +### §5.4 甩牌 — 🟥 原合法性 / 禁令 / 跟牌 / 惩罚多处不符(🟩 第 1/2/4 点已整改,见「整改进度」) + +> **整改状态**:下述第 1(副牌禁甩)、第 2(最大性按全手牌判定)、第 3(强制跟牌分量优先级,`flush_follow_ok`)、第 4(甩错惩罚)**均已实现并验证**。以下保留原始不一致描述留痕。 + +1. 🟥 **副牌禁甩未落实**:设计 §5.4.1「所有副牌完全禁止甩牌,无论外面是否剩余该花色副牌」。代码 `can_playcard` 对副牌甩牌只要求「其他两家没有主牌 **且** 没有该副花色(`other_noflower`)」即**放行**(约 415-419、427-438 行),等于「对手缺门时允许甩副牌」,与绝对禁令冲突。 +2. 🟥 **甩牌合法性判定模型不符**:设计 §5.4.2 要求服务端**按全部手牌**判断「对手是否持有能压过甩牌任一分量的更大主牌 / 主对 / 更长主拖拉机」。代码改用出牌过程累积的 `seatlist[i][flower-1][0/1]`、`[4]` 等**报副标志**(`other_noflower`/`other_noflowerpair`),语义是「对手是否已被记录为该花色 / 主牌 / 对子出空」,既非「按全部手牌」也非「更大」而是「有没有」,与设计判定完全不同。 +3. 🟥 **强制跟牌拆解未按分量**:设计 §5.4.4 要求闲家把甩牌拆成各分量(拖拉机优先跟拖拉机、对子优先跟对子…)逐个匹配。代码把甩牌压成单一 `cardtype`(如 `102 两张单张甩`、`203 三对甩`),`get_followcard` 对甩牌型只要求「同花色、张数相等」,不校验对子 / 拖拉机结构(如多对甩分支 `cantype=100+startcount` 只按单张计数)。 +4. 🟥 **甩错惩罚未实现**:设计 §5.4.5 规定甩错要「收回甩牌、本轮只强制打出最小一张、失去甩牌资格」。代码把非法甩牌当作无效输入(`can_playcard` 返回 `result=false`→`mod.chupai` 直接 `return`),无任何惩罚流程。 +5. ✅ **报无主**:`do_playcard` 出牌后自动检测主牌出空并置 `seatlist[..][4][0]=0`,属自动判定(设计允许由服务端精确计算)。 + +--- + +## §6 捡分与扣底 + +### §6.1 分牌 / §6.2 捡分 — ✅ 符合 + +`do_playcard` 仅在 `maxseat != banker` 时累计本轮分;`get_jian_grade` 汇总 `playowner != banker` 的分牌。庄家赢的轮分牌作废、不计入。与设计一致。 + +### §6.3 扣底 — 🟥 倍数表完全不同 + +- ✅ 触发条件「闲家用主牌赢下最后一轮」:`get_bottom_account` 仅在 `maxseat != banker` 时调 `get_bottom_multiple`,后者首末张非主(code<1000)即返回 0(不扣底),与「非主牌赢末轮不算扣底」一致。 +- 🟥 **倍数不符**: + + | 闲家扣底牌型 | 设计倍数 | 代码 `get_bottom_multiple` | + | --- | --- | --- | + | 单张主 | 1 | 2 | + | 主对子 | 2 | 4 | + | 两连对 | 4 | 8 | + | 三连对 | 6 | 16 | + | 四连对 | 8 | 32 | + | 五连对 | 10 | 64 | + | 六连对 | 12 | `undefined`(🐞 落空返回未定义) | + + 代码为近似 `2^k`、且单张即翻倍;设计为线性 `2N`(单张不翻倍)。 + +--- + +## §7 结算:子数与升级 — 🟥 系统性不符 + +设计的四层模型(§7.0):`基础子数(由叫分定) × 判定倍率(大光×3/小光×2/过庄×1/升N级×N) = X`(每「庄–闲」对子的基础金额)。常规算子分界 `Q` 固定 40、升级级距固定 40;勾选爬坡才改用 §7.3 的梯度子数与分段 `Q`。 + +代码实际实现(`get_paiju_account` + `get_multiple_bycall` + `get_upgrade`): + +- `multiple = get_multiple_bycall(call)`:`call>60→1`、`>40→2`、`>0→4`、`≤0→0`。 +- `upgrade = get_upgrade(call, grade)`:大光→4、小光→2、过庄→1、升级→`-(floor((grade-call)/halfcall)+1)`;分界与级距用 `halfcall = ceil(call/2)`。 +- 每对子金额 = `multiple × upgrade`,庄家收两家(×2)、闲家各付(×-1)。 + +**问题**: + +1. 🟥 **基础子数不符**:代码 `multiple∈{1,2,4}` 与设计常规算子 `{65:2, 60:3, 55:4, 50↓:6}` 数值、档位都不同。 +2. 🟥 **大光倍率错**:代码大光 `=4`,设计大光 `×3`。(小光 ×2、过庄 ×1 与设计一致。) +3. 🟥 **小光 / 过庄分界与升级级距错**:代码用 `halfcall=ceil(call/2)`;设计常规算子固定 40。例如叫 65:代码分界 35(35~64 记过庄),设计分界 40(40~64 才过庄,35~39 仍是小光)。 +4. 🟥 **无爬坡开关**:设计 §7.3 是「勾选爬坡才生效」的可选梯度;代码无任何房间选项分支,只有一套公式。巧合的是 `halfcall` 对低档(40/35→20、30/25→15、20/15→10、10/5→5)恰等于爬坡的 `Q`,但对高档(65→35≠40、60→30≠40、50→25≠40)不等,且基础子数 `{1,2,4}` 与常规 `{2,3,4,6}`、爬坡 `{2,3,4,6,7,8,…}` 都不符——即代码既不是常规算子、也不是爬坡,而是第三套数值。 + +**常规算子每对子最终子数对照(代码 vs 设计)**: + +| 叫分 | 判定 | 设计 | 代码 | +| --- | --- | --- | --- | +| 65 | 大光 / 小光 / 过庄 / 升1 / 升2 | 6 / 4 / 2 / 2 / 4 | 4 / 2 / 1 / 1 / 2 | +| 60 | 同上 | 9 / 6 / 3 / 3 / 6 | 8 / 4 / 2 / 2 / 4 | +| 55 | 同上 | 12 / 8 / 4 / 4 / 8 | 8 / 4 / 2 / 2 / 4 | +| 50 | 同上 | 18 / 12 / 6 / 6 / 12 | 8 / 4 / 2 / 2 / 4 | +| 40 | 大光 / 小光 /(无过庄)/ 升1 / 升2 | 18 / 12 / — / 6 / 12 | 16 / 8 /(误记过庄 4)/ 4 / 8 | + +(叫 40 一行还暴露分界问题:设计 `call≤40` 无过庄档,代码却在 grade 20~39 记出「过庄」。代码升级子数 = `multiple(=4) × 级数`,故升 1 级 4 子、升 2 级 8 子。) + +> **70 分打牌**同样落在本套公式里且同样不符:代码 `multiple = get_multiple_bycall(70) = 1`(设计打牌基础子数应为 2),`halfcall = 35`(设计 `Q = 40`);大光 4×1=4、小光 2、过庄 1,而设计应为大光 6、小光 4、过庄 2。 + +--- + +## §8 算奖规则 — 🟥 归属、开关、数值均不符 + +### §8.1 常规算奖(只庄家) — 🟥 + +- ✅ 基础组合计数正确:`get_chongguan` 中 `3 王→1、4 王→3`、`6/7/8 个 7→1/2/3`、`6/7/8 个 2→1/2/3`。 +- 🟥 **只应计庄家,代码却算三家**:`get_paiju_account` 对 0/1/2 号位都调 `get_chongguan` 并两两结算(`grade_cg = cg×2 − 另两家`)。设计 §8.1「常规算奖只认庄家身份,不看闲家手牌」。 +- 🟥 **设计外的作废规则**:`do_obsolete_with_call` 引入「冲关必叫」「四王必踢(`call>60`)」,设计中不存在。 +- 🟥 **连对链算奖失效(传参 bug)**:设计 §8.1「三 / 四王在有正 7 及以后连续对子时每对 +1 奖」。但 `get_chongguan` 在正常 / 投降结算被以 `mainflower = -1` 调用(`get_paiju_account` 中 `if (type != 2) _flower = -1;`)。此时 `id_to_code` 不加 `+1000`,正 7 与副 7 都落到 7xxx、主花色普通对子不被识别为主,`is_continuous(小王, 正7)` 因 `is_zheng7` 只认 8xxx 而返回假——连对链一对也加不上。即该奖项**实质未生效**。 +- 🟥 **≥10 老主误给奖**:`if (_total >= 10) re.count += _total - 9`(「10 个老主判断」)。设计 §8.2 明示固定主牌 ≥10 只亮牌、**不算奖**。 +- 🟥 **庄家算奖手牌快照错(含底牌与已埋牌)**:设计 §8「算奖依据静态初始手牌——庄家是**埋牌完成后的那 28 张**,闲家是发牌后的 28 张」。代码 `get_paiju_account` 用 `get_seat_cards_owner(seat)` 取牌喂给 `get_chongguan`,而该函数按 `dealowner ∈ {seat+1, 0}` 取牌、**不排除 `playround==0`(已埋)**:庄家因此拿到「28 张发牌 + 8 张底牌 = 36 张」,把**已埋进底牌的 8 张也计入算奖**,闲家则是正确的 28 张。庄家的常规算奖与傍王都因此可能虚高(例如把埋掉的王/7/2 仍算进去)。应改为庄家取「埋牌后保留的 28 张」(排除 `playround==0`)。 + +### §8.2 亮牌 — 🟧 未实现 + +设计 §8.2 的四档亮牌门槛(固定主 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6,向闲家亮统计信息,受「不查牌」模式抑制)在服务端无对应计算与下发。 + +### §8.3 傍王(可选) — 🟥 + +- 🟥 **无开关(恒开)**:`grade_w`(傍王得分)在 `get_paiju_account` 中无条件计算。设计 §8.3 / §10.3 傍王需勾选才生效。 +- 🟥 **含设计外作废**:`grade_w` 复用 `obsolete` 置 0 逻辑;设计傍王「每张王算一奖、庄闲都算」,无作废概念。 + +### §8.4 算奖并入结算 — 🟥 未乘 X + +- 设计 §8.4:某玩家 `N` 奖,则另外两人各额外付 `X × N`(`X` 为 §7 第三层的每对子子数)。 +- 代码:`grade_cg`、`grade_w` 是**扁平奖数两两差**(`cg×2 − 另两家`),每奖等价 1 子,**未乘 `X`**。举例设计(X=6、庄家 1 奖)应从每闲家收 6 子;代码只收 1 子/家。 +- ✅ 结构上「所有两两之间(含闲–闲)都结算」这一点与设计一致,仅倍率错。 + +--- + +## §9 牌局查看模式 — 🟧 + +- 报无主后,服务端确有记录他家剩余主牌数 / 对子数(`seatlist[..][4]` 与花色标志),并通过 `chupai*`/`PushCards` 的 `info`、`seatlist`、`baozhu` 下发,供客户端展示——方向正确。 +- 🟧 **无「可查牌 / 不查牌」模式开关**:`baozhu` 恒发、`seatlist` 恒带,未按房间设置抑制(设计 §9 / §10.2 的不查牌模式应屏蔽这些信息)。 +- 🟧 **「明牌」(查看他家具体主牌)无服务端支持**:服务端不会向闲家下发他家的具体主牌牌面。 + +--- + +## §10 房间设置选项 — 🟥 + +### §10.1 扣卡方式 / 局数 — ✅ 已整改(下述为原始不一致留痕) + +> **现状(已改)**:`get_asetcount` 按位串返回 6/12;`get_needroomcard` 房主 6局2张/12局4张、AA 6局1张/12局2张,均符合 §10.1。以下为原始(数组下标版)不一致记录。 + +- 🟥(原始)**局数不符**:旧 `export.get_asetcount` 返回 `roomtype[0]==1→2`、`==2→4`。设计 §10.1 为 **6 局 / 12 局**。 +- 🟥(原始)**房主扣卡数不符**:旧 `get_needroomcard` 房主档返回 1/2;设计房主扣卡为 6 局 2 张 / 12 局 4 张。 + +### §10.2 查牌模式 — 🟥 无开关(见 §9) + +### §10.3 附加规则(傍王 / 爬坡,可同时勾选)— 🟥 均无开关(见 §7.4、§8.3) + +--- + +## §11 牌局交互提示 — 🟩 + +- ✅ 牌局结束亮底牌:`get_bottom_account` 结算恒带 `bottom.cards`。 +- ✅ 选主对子数 / 70 分投降按钮门控:随 `shangzhuang`/`xuanzhu` 阶段包下发(见 §4)。 +- ✅ 甩牌相关提示:见 §5.4(甩牌实现已核对一致)。 +- ✅ **闲家 3 提示(踩 / 没分 / 有分)**:`mod.tishi`(design §11)——闲家在出牌阶段(step5)发 踩/没分/有分,服务端不校验真实性、只 `SendPack` 转发给对家(另一闲家 `3-banker-seat`);庄家不参与。协议见 packet_protocol §13.7/13.8,测试见 `test_rpc`。 + +--- + +## 附录:运行期代码缺陷汇总(与规则合规相对独立,但都需修) + +| # | 位置 | 现象 | 影响 | +| --- | --- | --- | --- | +| D1 | `class.arith.js` `can_followcard`(约 801 行) | 引用未赋值的 `tuolaji_list.length` | 跟牌方有多个候选拖拉机时抛异常、出牌中断 🐞 | +| D2 | `class.arith.js` `get_bottom_multiple`(六连对分支) | 六连对时无返回值 | 返回 `undefined`,扣底倍数计算异常 🐞 | +| D3 | `class.arith.js` `can_playcard`(约 276 行) | `do_returnfalse;` 漏写 `()`,语句空转 | 「一次最多 14 张」上限失效,超量出牌被当作合法 🐞 | +| D4 | `class.paiju.js` `do_choiceflower`(约 417-421 行) | `for` 循环体为空、取出 `o_card` 后无操作 | 死代码(主牌重编码实际在 `id_to_code` 动态完成),无害但应清理 🟨 | + +> D1~D3 均为真实缺陷;虽多数属「代码写错」而非「规则理解错」,但会导致对应规则(跟拖拉机校验、扣底、出牌张数上限)无法正确执行,需按缺陷修复处理。 + +--- + +## 总体判断 + +- **流程骨架**(发牌、叫分推进、选主埋牌、逐轮出牌收束、断线重连快照、战绩存储)实现完整且大体正确。 +- **牌型与大小体系**(编码、排序、相邻、跟牌 / 毙牌 / 垫牌的必出可出计算)设计良好、基本符合规则,主要缺陷是 D1 崩溃与甩牌子系统。 +- **不符合集中在「算分 / 算奖 / 房间选项」三块**:§6.3 扣底倍数、§7 算子全套、§8 算奖归属与并入方式、§10 局数 / 扣卡 / 各类开关——这几处需按 `design.md` 重做,是后续整改的重点。 +- 结合数值特征(`multiple` 只有 1/2/4、局数 2/4、扣底 2^k、含「冲关必叫 / 四王必踢」等设计外机制),**服务端很可能是在早期或另一套规则版本上实现的,与当前 `design.md` 已系统性脱节**,而非个别笔误。 diff --git a/server/games/erqiwang/docs/compliance/02-测试计划.md b/server/games/erqiwang/docs/compliance/02-测试计划.md new file mode 100644 index 0000000..96fb258 --- /dev/null +++ b/server/games/erqiwang/docs/compliance/02-测试计划.md @@ -0,0 +1,236 @@ +# 二七王服务端 · 合规测试计划 + +> 目标:用 Node 单测/集成测**系统覆盖 `design.md` 的全部服务端可验证规则**,做到"规则 → 用例"可追溯,作为"代码是否符合手册"的**自动化证据**(与 `01-design合规逐节核对.md` 的人工审计互补)。 +> +> 运行:`node server/games/erqiwang/test/run.js`。测试原理与双运行时见 `test/README.md`。测试纪律(正/反/边界、不软化断言、失败先裁根因)见 `docs/server/development-guide/04` §10。 + +--- + +## 0. 分层与测试类型 + +| 层 | 含义 | 依赖 | 现状 | +| --- | --- | --- | --- | +| **L1 单元** | 纯函数直接调用(arith/config/paiju 静态计算) | 仅 `_shim` 的平台工具 | 已有 `test_arith`/`test_config`/`test_paiju`(部分) | +| **L2 局内集成** | `cls_youle_erqiwang_paiju.new(o_desk,firstseat)` 造牌局,驱动 `do_callgrade→do_choiceflower→do_burycard→do_playcard…→get_paiju_account` 验证状态/结算 | mock `o_desk`/`o_room` + shim;**不经 mod.js** | 仅 `get_paiju_account` 造了极简对象,**无完整一局驱动** | +| **L3 RPC/广播** | `mod.js` 收包入口 → 校验 → 广播 | mock `check_player`/`SendPack`/`sendpack_toother`,捕获下发包 | **完全未覆盖** | + +**发牌随机性处理**:`do_dealpai` 用 `min_random`。L2 需要**可控发牌**——测试 `_shim` 提供可注入的 `min_random`(预置序列或指定发牌结果),使一局可复现。这是 L2 的前置基建。 + +--- + +## 1. 覆盖矩阵(按 design 章节) + +状态:✅ 已覆盖 · 🟡 部分 · ❌ 待补。用例列标注 正/反/边界 三类(`docs/server/development-guide/04` §10 要求三类齐全)。 + +### §2 牌局构成 +| 规则 | 被测 | 用例(正/反/边界) | 层 | 状态 | +| --- | --- | --- | --- | --- | +| 92 张、去两副 3/4 | `init_cards`/`do_dealpai` | 正:发完 3 家各 28 + 底 8 = 92;反:牌堆不含 number 3/4;边界:两副各花色计数 | L2 | ❌ | +| 分值 5→5、10→10、K→10、余 0 | `init_cards` | 正:各面值 score;边界:非分牌 score=0 | L1/L2 | 🟡(扣底/结算间接) | + +### §3 主牌顺序 / 编码 +| 规则 | 被测 | 用例 | 层 | 状态 | +| --- | --- | --- | --- | --- | +| 主牌 code 降序=设计顺序(大王>小王>正7>副7>正2>副2>主A…主5>副牌) | `id_to_code`/`order_cards` | 正:排序结果逐位;边界:正/副 7、正/副 2 的相对大小;主A vs 副2 | L1 | ❌(间接用到,无专测) | +| 相邻链 + 6/8 可连、7 不与 6/8 连 | `is_continuous` | 正:全相邻段逐对;反:7-6/8-7/9-7 不连;边界:8-6 连、大王-小王连 | L1 | 🟡(甩牌/算奖间接) | +| 副7/副2 跨花色比大小归一 | `trump_rank`/`can_followcard` | 正:副7 vs 副7 不可压;反:正7 压副7 | L1 | 🟡(甩牌间接) | + +### §4 开局与坐庄 +| 规则 | 被测 | 用例 | 层 | 状态 | +| --- | --- | --- | --- | --- | +| 叫分 5~70、步进5、暂定庄必叫、后叫更低/不叫 | `mod.jiaofen` 校验 + `do_callgrade` | 正:合法叫分推进;反:>70/非5倍/首家0/叫≥当前;边界:叫70、叫5 | L3(校验)+L1(do_callgrade) | ❌ | +| 叫5立即上庄、两家不叫上庄 | `do_callgrade` | 正:叫5→banker;正:一家叫另两家不叫→banker;边界:三家叫分序列 | L1 | ❌ | +| 坐庄轮换:庄赢连庄、庄输/投降下家、首局庄=0号 | `desk.do_prepare`/`makewar` | 正:result=0 连庄;正:result=1/2 顺延;边界:首局 firstseat=0 | L2/L3 | ❌ | +| 阶段机 1→2→3→5→6,无 step4 | `do_up_banker`/`do_choiceflower`/`do_burycard` | 正:各 handler 后 step 值;反:错误 step 调用被拒 | L2/L3 | ❌ | +| 投降仅70分/step2/庄家/不埋牌 | `mod.touxiang` | 正:70分 step2 投降→结算;反:≠70/step≠2/非庄;边界:投降后 step=6 | L3 | ❌ | +| 70分暗牌向所有玩家亮3秒(+ancard3s)、非70只发庄 | `mod.jiaofen`(shangzhuang) | 正:70→闲家有 bottomcards+ancard3s;反:非70 闲家无 bottomcards | L3 | ❌ | +| 选主后先选主后埋牌 | `do_choiceflower`/`do_burycard` | 正:flower 记录、step 推进;边界:埋牌须8张且在手 | L2 | 🟡(check_cards_inhand 未测) | + +### §5 出牌规则 +| 规则 | 被测 | 用例 | 层 | 状态 | +| --- | --- | --- | --- | --- | +| **§5.1/5.2 跟牌/毙牌/垫牌/混合出牌/拖拉机覆盖度** | `get_followcard`/`can_followcard` | 正:同花色跟、有对必出对、有拖必出拖、两对不连必出、尽量长拖优先;反:缺牌型被拒、放长拖只出散对被拒;毙牌(须完全缺门):副单→主单、副对→主对、副N连→主N连;垫牌/混合出牌:cardvalue=0 不争权 | L1 | ✅(`test_follow` 50 例,含 else 欠约束修复回归) | +| §5.3 拖拉机相邻 | `is_continuous`/`get_tuolaji_list` | 见 §3 | L1 | 🟡 | +| §5.4 甩牌:副禁甩/最大性/甩错 | `can_playcard`/`opp_can_beat_flush`/`decompose_trump` | 已覆盖(合法/甩错含smallest/副禁甩/单张/对子/拖拉机) | L1 | ✅ | +| §5.4.4 跟甩牌强制分量拆解 | `flush_follow_ok` | 已覆盖(有对必打对/有拖必打拖/退化/主牌最大化/无主全垫) | L1 | ✅ | + +### §6 捡分与扣底 +| 规则 | 被测 | 用例 | 层 | 状态 | +| --- | --- | --- | --- | --- | +| §6.1 分值 | `init_cards` | 见 §2 | L1 | 🟡 | +| §6.2 闲家赢归闲、庄赢作废、两闲谁赢都算闲 | `do_playcard`/`get_jian_grade` | 正:闲赢累计分;反:庄赢不计;边界:两闲各赢 | L2 | 🟡(1 集成例) | +| §6.3 扣底倍数 单1/对2/N连2N、全主牌守卫、取最高规格 | `get_bottom_multiple` | 已覆盖 | L1 | ✅ | +| §6.3 触发:闲家用主牌赢末轮才扣底 | `get_bottom_account` | 正:闲家主牌赢末轮翻倍;反:非主牌赢不扣、庄家赢不扣 | L2 | ❌ | + +### §7 结算:子数与升级 +| 规则 | 被测 | 用例 | 层 | 状态 | +| --- | --- | --- | --- | --- | +| 常规算子 基础子数 + 大光×3/小光×2/过庄×1/升N级、Q=40 | `get_base_bycall`/`get_qvalue`/`get_upgrade` | 已覆盖各档代表值 | L1 | ✅(可补每档过庄/小光分界与升3级) | +| 爬坡 基础子数梯度 + 分段Q | 同上(climb=true) | 已覆盖各档 | L1 | ✅ | +| X=基础×倍率、庄赢/闲赢符号、按房间爬坡切换 | `get_paiju_account` | 正:大光/过庄/升级/爬坡局 seatlist;边界:banker=-1 | L2 | 🟡(仅65小光+投降) | + +### §8 算奖 +| 规则 | 被测 | 用例 | 层 | 状态 | +| --- | --- | --- | --- | --- | +| §8.1 常规算奖只庄家、三/四王+连对链、6-8个7/2、无≥10老主/无作废 | `get_chongguan` | 已覆盖 | L1 | ✅ | +| §8.2 亮牌阈值/统计、只亮结构 | `get_liangpai` | 已覆盖 4 例 | L1 | 🟡(补 王≥3/7≥6 组合、边界9/10) | +| §8.3 傍王按位开关、庄闲每王1奖 | `get_paiju_account`(bangwang) | 正:傍王局 grade_aw 含王奖;反:未勾选不计 | L2 | ❌ | +| §8.4 算奖并入 X×(2Ni−Nj−Nk)、含闲-闲、投降X=1 | `get_paiju_account` | 正:庄1奖/闲傍王2奖的三家净额;边界:三家同时有奖 | L2 | ❌(公式单测有,集成未测) | +| §8 快照:庄埋后28/闲发后28;投降庄36无主 | `get_seat_cards_award` | 正:庄排除已埋;正:投降含底36;边界:无主花色无连对链 | L2 | 🟡(投降 count 间接) | + +### §9 查牌 / 明牌 +| 规则 | 被测 | 用例 | 层 | 状态 | +| --- | --- | --- | --- | --- | +| 报无主统计 info/baozhu 门控 | `mod.chupai` | 正:可查牌下发 info/baozhu;反:不查牌 info 缺失、baozhu=0 | L3 | ❌ | +| 亮牌/PushCards.seatlist 门控 | `mod.maipai`/`get_deskinfo` | 正:可查牌闲家有 liangpai;反:不查牌无 | L3 | ❌ | +| 明牌 mingpai:可查牌+已报无主+出牌阶段 | `mod.mingpai` | 正:条件满足返回他家主牌;反:不查牌/未报无主/非出牌阶段被拒 | L3 | ❌ | + +### §10 房间设置 +| 规则 | 被测 | 用例 | 层 | 状态 | +| --- | --- | --- | --- | --- | +| 位串解析、局数6/12、扣卡房主2·4/AA1·2 | `config.parse`/`export.get_asetcount`/`get_needroomcard` | 已覆盖(config 复刻映射) | L1 | ✅(可改为直接调 export 函数) | +| 傍王/爬坡/查牌三开关生效 | `get_paiju_account`/`mod.*` | 傍王/爬坡见 §7/§8;查牌见 §9 | L2/L3 | 🟡 | + +### §12 完整流程 +| 规则 | 被测 | 用例 | 层 | 状态 | +| --- | --- | --- | --- | --- | +| 一局端到端:发牌→叫分坐庄→选主→埋牌→逐轮出牌→末轮扣底→结算→轮庄 | 全链路 | 正:一局可控发牌打到结算,各家总分正确;含连庄/下家轮转 | L2 | ❌ | + +> §11 牌局交互提示中,选主对子数/亮底牌等随对应阶段包下发;闲家三提示(踩/没分/有分)为服务端转发项(`mod.tishi`,见 P1)。 + +--- + +## 2. 待补用例清单(按优先级) + +### P0 · 规则/金额关键 —— ✅ 已完成 +1. ✅ **§5.1/5.2 正常跟牌/毙牌/垫牌/混合出牌/拖拉机覆盖度**(`can_followcard`,`test_follow.js` 50 例)——单张/对子/拖拉机的必出、毙牌数量对应(副单→主单/副对→主对/副N连→主N连、且毙牌须完全缺门)、垫牌与"混合出牌"(有该花色不够+补主牌→`cardvalue=0` 不争权)、缺门/同花色不足/对子不够各分支、多候选拖拉机(D1 崩溃回归)、**降级递归 else 欠约束回归**(三组等长 2 连对时出孤立对→拒),正反边界齐全。 +2. ✅ **§7/§8 结算集成扩充**(`get_paiju_account`,`test_paiju.js`)——大光/过庄/升2级、爬坡局、傍王局(庄3王,N=4)、不傍王对照、投降,逐档验证 seatlist 各家 grade。 +3. ✅ **§4.2 叫分坐庄**(`do_callgrade`,`test_callgrade.js` 6 例)——叫5立即上庄、两家不叫上庄、后叫更低→最低者上庄、不同首家。 +4. ✅ **§6.3 扣底触发**(`get_bottom_account`,`test_paiju.js`)——闲家主对/两连对赢末轮才扣底(×2/×4);副牌赢、庄家赢均不扣。 + +> 注:§4.2 中 `mod.jiaofen` 的**入参校验**(>70/非5倍/首家必叫/后叫更低)属 L3,随投降/暗牌/查牌门控一起在 P1 补。 + +### P1 · 规则关键、需 RPC 脚手架 —— ✅ 已完成(`_rpc.js` + `test_rpc.js` / `test_desk.js`) +5. ✅ **§9 查牌门控 + mingpai**——chupai 的 info/baozhu 按 `nocheck` 门控(可查有/不查无);mingpai 合法下发他家主牌,不查牌/未报无主/非出牌阶段均拒。 +6. ✅ **§4 投降 + 暗牌亮牌**——touxiang 仅 70分/step2/庄家;真实 jiaofen 驱动到 70分上庄,验证 shangzhuang 闲家有 bottomcards+ancard3s、叫5上庄闲家无。 +7. ✅ **§4.2 `mod.jiaofen` 入参校验**——>70/非5倍/首家必叫/后叫更低 全部拒,合法接受。 +8. ✅ **§4.6 坐庄轮换 `do_prepare`**——庄赢连庄、闲赢/投降下家。 + +### P2 · 补强既有 —— ✅ 已完成 +9. ✅ §2 牌局构成专测(`test_deal`)、§3 编码/排序专测(`test_arith`:order_cards + is_continuous 链)。 +10. ✅ §9 `mod.maipai` 亮牌门控 / `get_deskinfo` PushCards 门控(`test_rpc` 端到端驱动后验证)。 +11. ✅ §8.2 亮牌更多阈值(6个7/固定主9-10边界)、§7 算子每档分界+升3级(`test_arith`)。 +12. ✅ §12 端到端(`test_rpc` 真实驱动到出牌就绪)。 +13. ✅ §10 直接调 `export.get_asetcount`/`get_needroomcard`/`get_needroomcard_joinroom`(`test_config`)。 +14. ✅ **§11 闲家三提示 `mod.tishi`**——正:闲家 step5 发 踩/没分/有分,只转发给对家(`3-banker-seat`,验证 fromid/seat/tip);反:庄家发/非出牌阶段/非法 tip/缺 tip 均拒(`test_rpc` 11 例)。 + +--- + +## 3. 集成脚手架需求(实现 L2/L3 的前置) + +- **L2 局内驱动器 `test/_harness.js`**: + - 可注入发牌的 `min_random`(或直接构造 `o_paiju.cards` 指定各家手牌),使牌局可复现。 + - mock `o_desk`(seatlist 累积、`get_desk_account`、`o_room.roomtype/asetcount`)与 `o_room`。 + - 提供"驱动一局到某阶段"的辅助:叫分序列 → 选主 → 埋牌 → 出牌序列。 +- **L3 RPC 捕获器**:mock `youle_erqiwang.import.check_player`(返回构造的 `o_room`)、`o_room.method.sendpack_toother`/`youle_erqiwang.app.SendPack`(把下发包收集进数组),断言包字段(含 §9 门控、暗牌、投降、chupai1/2/3、jiesuan)。 + +--- + +## 4. 完成定义(DoD)—— ✅ 已达成 + +- ✅ 覆盖矩阵中所有 ❌/🟡 项补到 ✅(详见 §5 现状),每条规则至少含正/反/边界; +- `node test/run.js` 全绿、退出码 0; +- 新增用例遵守测试纪律:不改正式代码去迁就测试、不软化断言、失败先用证据裁根因; +- 本计划与 `01-design合规逐节核对.md` 的结论一致(自动化测试成为人工审计的可回归证据)。 + +--- + +## 5. 当前状态小结 —— ✅ DoD 达成,P0/P1/P2 全部完成 + +**共 511 项断言全绿**(`test_arith` 122 / `test_calc` 49 / `test_callgrade` 6 / `test_config` 19 / `test_deal` 12 / `test_desk` 6 / `test_endgame` 25 / `test_follow` 58 / `test_followdiff` 12 / `test_input` 34 / `test_paiju` 50 / `test_rpc` 89 / `test_success` 29)。其中 `test_calc`/`test_followdiff` 见 §7、G1~G9 见 §8。 + +design 全部服务端可验证章节均有正/反/边界单测: +- **§2 构成**:`test_deal` 92张/去3-4/三家28+底8/分值。 +- **§3 编码/排序**:`test_arith` order_cards 降序=设计顺序、is_continuous 相邻链逐段+反例。 +- **§4**:叫分坐庄 do_callgrade、`mod.jiaofen` 入参校验、投降 touxiang 条件、70分上庄暗牌亮牌下发、坐庄轮换 do_prepare。 +- **§5**:**正常跟牌全面覆盖(§5.1/5.2)**——按跟牌方手牌各分支穷举:主牌首出与副牌首出、单张/对子/拖拉机各牌型、有同花必出对应牌型、同花不足则出仅有的+补、缺门毙牌(数量对应)与垫牌(不压)、拖拉机的对子不够/同花无对/**多候选拖拉机(D1崩溃回归)**/3连对/富余对子必出拖、手牌数==首出张数全部必出;**甩牌全面覆盖(§5.4)**——合法(纯单/纯对/单+拖/单+对+拖三混)、甩错(单张/主对/拖拉机分量各自被压、双对之一被压)、副牌禁甩(对手缺门与非缺门)、甩错惩罚**实际执行**(do_playcard 只打最小一张、其余留手);**跟甩牌全面覆盖(§5.4.4)**——对子必出、拖拉机必出/退化对子/退化到单张、多组拖拉机、混合demand逐分量对位、主牌最大化、无主全垫,以及"首出甩牌→跟牌违规→do_playcard 拒"的集成。 +- **§6**:捡分(结算集成间接)、扣底倍数+触发。 +- **§7**:算子常规+爬坡逐档、每档分界+升3级、结算集成(大光/过庄/升级/爬坡/傍王/投降)。 +- **§8**:算奖 get_chongguan、亮牌 get_liangpai(含 6个7/固定主9-10边界)、算奖并入 X×N(傍王对照)。 +- **§9**:查牌门控(chupai info/baozhu、maipai liangpai、deskinfo PushCards seatlist)、明牌 mingpai。 +- **§10**:位串解析、局数/扣卡(直接调 export 接口)。 +- **§12**:端到端(叫分→上庄→选主→埋牌→出牌ready,真实 RPC 驱动)。 + +> 说明:§12 覆盖到"出牌准备就绪"(各阶段串接正确);完整 28 墩逐墩对打需一个合法出牌选择器,属后续增强,非规则合规缺口(逐墩规则已由 §5/§6 单测覆盖)。 + +--- + +## 6. 第三轮补充:design 之外的可验证维度 + +> 前两轮的覆盖矩阵完全按 design 章节组织,因此**平台接入、客户端入参、下发面一致性、协议红线**这几类根本不在矩阵里——270 项全绿却漏掉了 1 项阻断级与 3 项严重缺陷(见 `01-design合规逐节核对.md`「第三轮核对」)。本节把这些维度补进测试计划,后续复核必须一并跑。 + +| 维度 | 被测 | 用例(正/反/边界) | 文件 | 状态 | +| --- | --- | --- | --- | --- | +| 客户端入参合法性 | `check_cards_valid` / `check_cards_inhand` | 正:单张/多张/边界 id 0 与 107;反:非数组(undefined/null/字符串/伪数组)、空数组、重复 id、越界、负数、小数、数字字符串;反:不在手上/已出/已埋 | `test_input` | ✅ 21 | +| 重复 id 不得伪造牌型 | `mod.maipai` / `mod.chupai` | 反:8 个重复 id 埋牌被拒且一张未埋;反:重复 id 出牌被拒;正:8 张不同牌埋牌成功且实埋 8 张;反:越界/非数组入参不抛异常且不产生出牌 | `test_input` | ✅ 7 | +| §9 报无主即时刷新 | `do_playcard` | 正:打空瞬间三家统计与主花色标志同时刷新;边界:报无主前三家均为 `[-1,-1]` | `test_input` | ✅ 6 | +| 红线 `data.success`(成功包) | `class.desk`/`mod.js`/`get_paiju_account` | 正:fapai/zhunbei/jiaofen/shangzhuang/xuanzhu/maipai/chupai1/mingpai/tishi 均带 `success:true`;正:jiesuan 的正常/投降/解散三种 type 都带 | `test_success` | ✅ 12 | +| 红线 `data.success`(失败回包) | 各 handler 失败分支 | 反:每个 handler 取代表性失败路径,断言"恰好回 1 包 + `success:false` + errcode 正确 + 回到请求者 fromid",覆盖 STEP/SEAT/PARAM/RULE/PLAYER 五类 | `test_success` | ✅ 17 | +| §7.3 下发面 multiple 与结算同源 | `mod.jiaofen` / `get_deskinfo` / `get_paiju_account` | 正:叫 45 分在常规房/爬坡房分别得 6/7;正:上庄包与重连 ChooseMain 一致;边界:与结算包 `aset.multiple` 逐一对齐 | `test_rpc` | ✅ 7 | +| §5.4 甩牌下发面 | `chupai1` | 正:合法甩牌带 `shuai` 分量;反:非甩牌不带;反:甩错只打最小一张且不带 `shuai` | `test_rpc` | ✅ 8 | +| 重连 pushlist 结构 | `get_deskinfo` | 正:轮数正确、内层恒 3 个座位、无 undefined;正:按本局主牌花色排序(主5 排在副A 前) | `test_rpc` | ✅ 4 | +| §9 出牌历史按查牌模式门控 | `get_deskinfo` | 反:不查牌房无 `pushlist`;正:可查牌房有 `pushlist`;边界:两种模式下当前轮桌面牌 `playproc.cards` 都必须恢复 | `test_rpc` | ✅ 4 | +| §3 相邻链花色 | `is_continuous` | 正:同花色 8-6 连;反:三组跨花色 8-6 不连;反:差 2 但非 8/6 不连 | `test_arith` | ✅ 5 | + +**关于超时**:`countdown` 只作展示、服务端不做任何超时动作,这是 design §11 确认的规则(并非缺口),因此**没有也不需要「超时自动操作」的用例**;反过来,若将来有人给服务端加了超时代打,那才是违反 design §11——现已由 `test_rpc` 的**守卫用例**拦下(见 §8 G9)。 + +--- + +## 7. 第五轮:按 design 原文另写参考实现的差分测试 + +> 前六节的用例都是**手写的场景枚举**——覆盖的是"我想到的情况"。第五轮补两组**差分测试**:把 design 的规则整条转写成参考实现,与代码大面积对拍,覆盖手写用例想不到的组合。 + +| 文件 | 覆盖 | 规模 | +| --- | --- | --- | +| `test_calc.js` | §7 算子全量对拍:参考实现逐字转写自 §7.1/§7.2/§7.3 判定表,比 base / Q / 判定倍率 / 最终子数 | 2 模式 × 14 档 × grade 0~260 = **7308 格**,另按 design 的 17 张表逐格抽查 **141 个写死的最终子数** | +| `test_followdiff.js` | §5.2 跟牌强制层级穷举差分:参考裁定按原文写,对每手牌**枚举全部 C 张出牌组合**逐个对拍 | 约 **11 万组** | + +**差分测试的强度取决于造牌器,不取决于对拍逻辑。** 第一版 `test_followdiff` 用纯随机抽牌跑 7.8 万组全绿,但把 `follow_tractor_cover_ok` 整个关掉**依然全绿**——因为唯一能区分"只出零散对子"与"先凑最长拖拉机"的形状是「同花色三组互不相邻的两连对」(12 张),随机抽根本抽不出来。故: + +- 随机源必须是**固定种子 PRNG**(不用 `Math.random`),保证用例集可复现; +- 关键结构必须用**确定性手牌**钉死(`structHands()`),不能指望随机覆盖到; +- 注意别让掺入的其他花色把手牌撑过枚举上限而被**静默跳过**(踩过一次)。 + +--- + +## 8. 第五轮:逐规则的正/反/边界补全 + +> 对 design §1~§12 重新做了一次"每条规则是否三类用例齐全"的审计。覆盖面本来就广,但仍有 9 处 design 明文规则缺用例(多是**规则本身**而非入参校验,前几轮的矩阵按"被测函数"组织,容易漏掉这类"跨函数才体现"的规则)。 + +| # | design | 规则 | 补的用例(正/反/边界) | 文件 | +| --- | --- | --- | --- | --- | +| G1 | §4.2 | 暂定庄家必须叫、其后才可"更低或不叫" | 边界:下限 5 接受;正:非首家 `call=0` 接受;反:负数 / 缺 `call`(NaN) / 步进外小值 1 / 与当前叫分相同 均拒 | `test_rpc` | +| G2 | §4.2 | **"不叫"即退出本局叫分,轮次跳过他** | 正:首家叫 65 后轮到 seat1;正:seat1 不叫后跳到 seat2;反:seat1 反悔再叫 → SEAT 拒、`callproc` 不变、仍轮到 seat2、未产生庄家 | `test_rpc` | +| G3 | §4.5 | 埋牌仅庄家、仅埋牌阶段、恰 8 张 | 反:非庄家 → SEAT;反:step2/step5 埋 → STEP;边界:7 张 / 9 张 / 空数组 → PARAM | `test_rpc` | +| G4 | §5.1 | **每轮由上一轮牌面最大的一方先出** | 正:闲 1 赢 → 下一轮 `start/currseat` 都是 1;边界:闲 2 赢 → 改由闲 2 先出 | `test_paiju` | +| G5 | §6.2 | 闲家赢 → 台面**全部**分牌计入;庄家赢 → 分牌作废 | 反:庄赢 → 下发面无 `grade`、闲家捡分 0;正:闲 1 赢 → 15 分(**含庄家自己打出的 ♥5**);边界:闲 2 赢结果与闲 1 赢一致("两个闲家谁大不影响") | `test_paiju` | +| G6 | §8 | 算奖快照的时间点与静态性 | 正:庄家 = 埋牌后 28 张、已埋 8 张不在内;正:闲家 = 发牌后 28 张;边界:投降局庄家 = 发牌+暗牌 36 张;边界:之后打出 10 张,快照仍是同样的 28 张 | `test_paiju` | +| G7 | §3 | **对子只认同花色同点数的两副** | 正:同花色两副 ♥7/♥2/大王成对;反:♥7+♣7、♥2+♣2、正7+副7、正2+副2、大王+小王 均不成对;边界:四张跨花色 7/2 组不出拖拉机,而正7对+副7对可以 | `test_arith` | +| G8 | §6.3 | 赢末轮的牌不全是主牌就不扣底 | 反:副牌两连对赢 → 0;反:混合出牌(主K+副K)→ 0;边界:主副提交顺序颠倒仍判 0 | `test_arith` | +| G9 | §11 | **无超时托管**(守卫) | 守卫:叫分→选主→埋牌→出牌全程 `min_ontimeout` 调用次数为 0。将来若有人加"超时自动出牌/代打",必然要注册定时器,这条即转红 | `test_rpc` | + +**每条都做了变异检验**(在副本上注入违反该规则的改动,确认对应断言转红): + +| 注入的缺陷 | 转红的断言 | +| --- | --- | +| 去掉 `mod.jiaofen` 的"非当前叫分位"校验 | G2 三条 | +| 埋牌张数 `!= 8` 放宽为 `< 1` | G3 两条边界 | +| §6.2 改成庄家赢也累计闲家捡分 | G5 一条 | +| 下一轮改为固定庄家先出 | G4 两条 | +| 算奖快照不排除已埋的牌 | G6 两条 + 亮牌快照一条 | +| `get_pairlist` 放宽为"只看点数" | G7 五条 | +| 在 `mod.chupai` 里注册一个定时器 | G9 一条 | + +> 过程中的一个教训:M2(埋牌张数)第一次跑出来是**整个文件崩溃**而不是干净的 FAIL——因为 `mkBury` 桩缺 `do_burycard`,校验一被改松就走到成功路径抛异常,后面的断言全不执行。**桩要完整到能走完成功路径**,否则"校验被改松"这类变异只会表现为崩溃,定位与信号都变差。已修。 + +> 原列在此处的两项「待外部输入」均已由规则设计者拍板:「不查牌模式是否屏蔽出牌历史」确认屏蔽,已改 design §9 + 代码 + 用例(见上表);「超时托管默认动作」确认维持现状、不做超时动作,已写入 design §11。至此测试计划无待外部输入项。 diff --git a/server/games/erqiwang/docs/design/design.md b/server/games/erqiwang/docs/design/design.md new file mode 100644 index 0000000..89f2774 --- /dev/null +++ b/server/games/erqiwang/docs/design/design.md @@ -0,0 +1,788 @@ +# 二七王 玩法设计文档 + +> 本文档是二七王玩法的唯一权威规则说明。内容随讨论持续与规则设计者逐项确认、修订;文中标注"待确认"的地方是当前仍未有定论的开放问题,其余内容均已确认生效。 + +## 目录 + +1. [术语约定](#1-术语约定) +2. [牌局构成](#2-牌局构成) +3. [主牌顺序](#3-主牌顺序) +4. [开局与坐庄流程](#4-开局与坐庄流程) +5. [出牌规则](#5-出牌规则) + - [5.1 跟牌基本原则](#51-跟牌基本原则) + - [5.2 按牌型跟牌](#52-按牌型跟牌) + - [5.3 拖拉机定义与相邻关系](#53-拖拉机定义与相邻关系) + - [5.4 甩牌详细规则](#54-甩牌详细规则) +6. [捡分与扣底](#6-捡分与扣底) + - [6.1 分牌](#61-分牌) + - [6.2 捡分规则](#62-捡分规则) + - [6.3 扣底](#63-扣底) +7. [结算:子数与升级](#7-结算子数与升级) + - [7.0 四层关系总览 / 查表索引](#70-四层关系总览) + - [7.1 常规算子:叫分 → 基础子数](#71-常规算子叫分-→-基础子数未勾选爬坡时的默认规则) + - [7.2 常规算子的判定表](#72-捡分-vs-叫分庄家的过庄-小光-大光闲家的升级常规算子) + - [7.3 爬坡](#73-爬坡叫分-→-基础子数与保小光分段可选规则开房时勾选爬坡生效) +8. [算奖规则](#8-算奖规则) + - [8.1 冲关](#81-冲关) + - [8.2 亮牌规则](#82-亮牌规则) + - [8.3 傍王(可选规则)](#83-傍王可选规则) + - [8.4 算奖如何影响最终结算](#84-算奖如何影响最终结算) +9. [牌局查看模式](#9-牌局查看模式) +10. [房间设置选项](#10-房间设置选项) +11. [牌局交互提示](#11-牌局交互提示) +12. [完整牌局游玩流程](#12-完整牌局游玩流程) + +--- + +## 1. 术语约定 + +| 术语 | 含义 | +| --- | --- | +| 主牌 / 副牌 | 庄家选定的花色为"主牌",其余三个花色为"副牌";主牌大于副牌 | +| 固定主牌 | 不论选择哪个花色为主,大王、小王、以及所有花色的"2""7"恒为主牌 | +| 正2 / 正7 | 选定花色的"2""7",大于其余花色的"2""7"(口语里也叫"主2""主7",本文档统一用"正2""正7")| +| 副2 / 副7 | 非选定花色的"2""7" | +| 底牌 | 发牌时**没有发给玩家**、扣在桌面的 8 张牌;庄家确认后可摸起查看,摸起后就不再扣在桌面,而是并入庄家手牌 | +| 埋牌底牌 | 庄家"埋牌"时从手牌中选出重新扣下的 8 张牌;"底牌"与"埋牌底牌"是两批不同的牌,不通用、不互换 | +| 埋牌 | 庄家看到底牌、摸起查看后,从手牌中选出 8 张重新扣下,作为"埋牌底牌" | +| 捡分 | 出牌中赢下的分牌(5、10、K)计入闲家总得分的过程 | +| 扣底 | 出完所有手牌后的最后一轮,若被闲家用主牌牌型压过庄家,则埋下的 8 张埋牌底牌也计入闲家捡分,并按牌型翻倍 | +| 亮牌 | 庄家埋牌后、出牌前,若手中固定主牌达到门槛(总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6 任一),向两个闲家**亮出自己全部固定主牌的具体牌面**(见第 8.2 节)。不限叫分,仅可查牌模式 | +| 余主公示 | 任一玩家主牌出空(报无主)后,为**全体三人**展示另外两家各自的**剩余主牌数与剩余主对数**(见第 9 节)。只给数量,不给具体牌面 | +| 明牌 | 可查牌模式下,查看**另外两家**手中全部未出主牌的具体牌面(见第 9 节)。与"亮牌"同样给牌面,区别在:亮牌是**庄家公开自己的**,明牌是**去看别人的** | +| 摸底 | 庄家坐定后把那 8 张**底牌**摸起查看并并入手牌(见第 4 节)。**非 70 分坐庄时只有庄家看得到这 8 张** | +| 开底 | **70 分坐庄**时,在庄家摸底**之前**,先把这 8 张底牌向**所有玩家**翻开 3 秒,之后才由庄家摸入手牌(见第 4 节)。给的是具体牌面 | +| 查底牌 | 对局中随时回看那 8 张底牌的功能(前端底栏按钮)。庄家全程可用;闲家仅在**开底**过的局(即 70 分坐庄)可用 | +| 拖拉机 | 同一花色(含主牌)里点数相邻的连续对子,如 K K Q Q 为"两托"(两连对) | +| 甩牌 | 首家出牌时,一次性打出多组主牌组合(单张 / 对子 / 拖拉机自由搭配);仅主牌可以甩牌,副牌禁止甩牌,只能分单张 / 对子 / 拖拉机依次打出(见第 5.4 节) | +| 毙牌 | 跟牌时手中没有本轮花色(缺门),改用主牌打出、且牌面大过本轮目前所有人的牌(见第 5.1 / 5.2 节) | +| 垫牌 | 跟牌时手中没有本轮花色(缺门)、且不用主牌毙牌,随意出一张其他副牌,不参与、不争夺本轮的出牌权(见第 5.1 节) | +| 混合出牌 | 首家出副牌,你手中有该花色副牌但张数不够凑齐首家总张数,又没有(或不用)别的副牌补差额,只能用主牌补足缺口——打出的是"该花色副牌 + 主牌"的混合牌。这既不是垫牌(不是缺门、不是纯副牌),也不是毙牌(不是纯主牌牌型、压不过首家),服务端判定其牌面为 0,**与垫牌等效:不参与、不争夺本轮出牌权**(见第 5.1 / 5.2 节) | +| 子 | 结算时用于计算输赢筹码的单位,与"倍数"是两个独立概念(见第 7 节) | +| 冲关 | 局末对固定组合(三王/四王、六 2/七 2/八 2、六 7/七 7/八 7、连续对子链等)的额外奖励结算,**只计庄家**(见第 8.1 节)。旧称"常规算奖",现统一叫**冲关**;服务端字段名 `chongguan` 与此同源 | +| 算奖 | **上位概念** = 冲关(8.1,只计庄家)+ 傍王(8.3,可选、庄闲都算)。两个分量求和后才是某玩家的总奖数 `N`,据此结算(见第 8.4 节)。**说"冲关"指的是庄家那一份,说"算奖"指的是合计** | +| 傍王 | 可选房间规则:勾选后每张王额外算一奖,庄家、闲家都要算;这是在 8.1 冲关之外**额外叠加**的奖励,与冲关互不冲突(见第 8.3 节) | +| 爬坡 | 可选房间规则:改变叫分对应的基础子数与保小光分数线,未勾选时按"常规算子"结算(见第 7 节) | + +> **术语变更记录(2026-08-25)**:原「**暗牌**」(发牌留桌的 8 张)改称「**底牌**」;原「**底牌**」(庄家埋下的 8 张)改称「**埋牌底牌**」。 +> +> 改名原因:前端界面上「底牌」按钮查看的正是发牌留桌那 8 张,旧命名与界面倒挂、极易写反。 +> +> **本文已全文改用新术语。** 但 `docs/protocol/packet_protocol.md` 与服务端代码里的 `bottomcards` 字段名**尚未拆分**——两批牌目前仍共用这一个字段名,只靠「出现在哪个包」区分语义:`shangzhuang` / `ChooseMain` / `BuryCards` 里的是**底牌**,`maipai` / `PushCards` / 结算 `bottom` 分组里的是**埋牌底牌**。拆分计划见 `docs_dev/二七王-UI资源与精灵清单.md` §7.5 的 S-4。 + +> **易混术语对照(都带"亮/明"字,但方向与粒度各不相同,务必分清)**: +> +> | 术语 | 谁公开给谁 | 给的是什么 | 触发条件 | 协议字段 | +> | --- | --- | --- | --- | --- | +> | **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后手牌达门槛(§8.2) | `liangpai` | +> | **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(§9) | `seatlist[seat][4]` + `baozhu` | +> | **明牌** | 他家 → 请求者 | **具体牌面**(他家全部未出主牌) | 可查牌 + 报无主,主动点击(§9) | `mingpai.others[].zhucards` | +> | **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(§4) | `ancard3s` + `bottomcards` | +> +> 一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。 +> 另有一组「底」字族专管那 8 张底牌:**摸底**(庄家摸起看)、**开底**(70 分时全场看 3 秒)、**查底牌**(随时回看)、**扣底**(末轮翻开埋牌底牌计分)。 +> 另注:**「亮主」不是本文档的术语**——它是前端选主界面的标题文案(庄家选主牌花色那一步),与上表四项无关。 + +--- + +## 2. 牌局构成 + +两副扑克牌共 108 张,去掉两副牌中的"3"和"4"(2 种点数 × 4 花色 × 2 副 = 16 张),剩余 **92 张**: + +- 点数 5、6、7、8、9、10、J、Q、K、A、2:共 11 种点数,每种 8 张(2 副 × 4 花色) +- 大王、小王:各 2 张(每副各 1 对) + +花色为黑桃 ♠、红桃 ♥、梅花 ♣、方块 ♦。 + +--- + +## 3. 主牌顺序 + +每局开局前庄家选择一个花色作为本局主牌,其余三个花色为副牌。 + +固定主牌(大王、小王、所有花色的 2 和 7)不论选择哪个花色为主,恒定属于主牌;选定花色里的其余普通牌也算主牌。 + +主牌从大到小排列: + +| 顺序 | 牌 | +| --- | --- | +| 1 | 大王 | +| 2 | 小王 | +| 3 | 正 7(选定花色的 7) | +| 4 | 副 7(其余花色的 7) | +| 5 | 正 2(选定花色的 2) | +| 6 | 副 2(其余花色的 2) | +| 7 及以下 | 选定花色的其余普通牌:A、K、Q、J、10、9、8、6、5 | + +副牌(非选定花色,且非 2、7、王)从大到小:A、K、Q、J、10、9、8、6、5。 + +> **关于"对子"的判定**:一副牌里同一张牌有两副(deck1 / deck2),"对子"指**同一张具体牌的两副**(如两张♦7、两张大王)。正 7、副 7、正 2、副 2 虽在上表中各占一个**大小等级**(用于排序与比大小时,同级的三张副 7 视为等大),但**成对只认同一花色同点数的两副**——即两张**不同花色**的副 7(如♠7 + ♥7)**不构成一对**,两张不同花色的副 2 同理。拖拉机的连对判定也以此为基础(每一节连对里的每个对子都须是同花色同点数两副)。 + +--- + +## 4. 开局与坐庄流程 + +1. 每人摸 28 张牌,剩余 8 张扣在桌面作为"底牌"。 +2. 由暂定庄家开始,按逆时针方向依次叫分: + - 叫分范围 5 ~ 70 分,步进 5 分。 + - 叫分数值代表"庄家承诺闲家捡分总数将严格低于该数值"(即闲家捡分只要达到或超过这个数值,庄家就算没达成承诺,见第 7 节);数值越低代表叫分者越有信心,因此后叫的人必须叫出比当前更低的分数才能压过前者,不能叫相同或更高的分数。 + - **暂定庄家必须叫分,不能"不叫"**:叫分从暂定庄家开始,他必须报出一个 5 ~ 70 之间的具体分数,不存在"暂定庄家弃权、三家都不叫"这种情况;起始叫分确定之后,后面的玩家才可以选择"叫出更低的分数"或"不叫"。 + - 若某玩家直接叫出 5 分,则无人能再压过,立即确定为庄家。 + - 若一家叫分后,另外两家都选择"不叫",则该叫分玩家成为本局庄家。 +3. **摸底**:庄家确定后把 8 张底牌摸入手牌(摸完后庄家共 36 张): + - **非 70 分坐庄**:桌面 8 张底牌**只有庄家可见**,庄家查看后摸入手牌。 + - **70 分坐庄**(触发**开底**):在庄家**摸底**之前,先把这 8 张底牌**向所有玩家(含两个闲家)翻开 3 秒**,然后才由庄家把它们摸入手牌。 +4. **选主 / 投降(同一决策点,二选一,互斥)**: + - **选主**:庄家在 4 个花色中选定一个作为本局主牌(见第 3 节)→ 进入下一步埋牌、随后出牌。 + - **投降**:仅当**叫分为 70 分**时,此决策点才额外提供"投降"选项;选择投降表示放弃本局,**直接结束、不再选主、不埋牌、不出牌**,按 7.1 节"70 分坐庄 · 投降"的固定结果结算(基础子数 1 个,庄家直接输;算奖仍照常,见 8.4 节)。选主与投降互斥——**选了花色就等于放弃投降、正常打牌;选了投降就不再选主**。 + - 叫分 < 70 分时没有投降选项,只能选主。 + - (**开底**——70 分坐庄的 8 张底牌向所有玩家翻开 3 秒,发生在上一步"庄家摸底之前",见步骤 3。) + - **前端表现**:此阶段界面显示 4 个花色选主按钮,且**每个花色按钮上要显示该花色在庄家手中有多少对**(供庄家判断选哪门为主);70 分坐庄时并列再显示一个投降按钮。 +5. **埋牌**(仅"选主/打牌"路径):庄家从 36 张里选出 8 张牌重新扣下("埋牌"),作为"埋牌底牌",供最后判断"扣底"使用;埋牌后庄家保留 28 张。(**本局先选主、后埋牌**——庄家先定主牌花色,知道主副后再决定埋哪 8 张。) +6. 坐庄轮换规则(决定下一局的"暂定庄家"): + - 第一局的暂定庄家为 **0 号座位**的玩家(此前没有打过任何牌局、还不存在"上一局的庄家",故取固定座位)。 + - 若庄家本局获胜(庄赢),下一局暂定庄家仍为该玩家(连庄)。 + - 若庄家本局失败(闲家捡分达标 / 庄家投降),下一局暂定庄家变为该玩家的下家(按逆时针顺延)。 + +--- + +## 5. 出牌规则 + +### 5.1 跟牌基本原则 + +- 每一轮由上一轮牌面最大的一方先出牌;**每一局(每一手牌)的第一轮固定由庄家先出**——此时还没有"上一轮"可以比较,只能由庄家开局。 +- 首家出什么花色,其余两家必须跟出同花色的牌;若手中没有该花色的牌或数量不够(缺门),可以任选以下一种方式补齐: + - **毙牌**:改用主牌打出,且牌面要大过本轮目前所有人的牌,即可抢下本轮的出牌权; + - **垫牌**:随意出一张其他副牌,不参与、不争夺本轮出牌权。 +- 首家若出主牌,其余两家必须跟出主牌,规则同上(此时缺门只能垫副牌,不存在再用主牌毙牌的情况)。 + +### 5.2 按牌型跟牌 + +跟牌的总原则:**在本轮首家花色内,尽量凑出与首家相同的牌型结构;能凑出的高规格部分必须优先打出,凑不齐的部分才用低规格牌补足。** 服务端按此层级精确计算每名跟牌者的"必出牌"与"可选牌",玩家无法跳过更高规格的强制部分。 + +| 首家牌型 | 跟牌要求 | +| --- | --- | +| 单张 | 跟同花色任意单张;同花色缺门则任意牌补齐 | +| 对子 | 有同花色对子必须打出对子;没有对子则打两张同花色单牌;同花色牌不足两张时,现有的同花色牌必须打出、不足部分任意补齐 | +| 拖拉机(N 连对) | 见下方「拖拉机跟牌的强制层级」 | + +#### 拖拉机跟牌的强制层级(同花色牌足够时) + +首家出 N 连对拖拉机,跟牌者手中该花色牌数量 ≥ 首家张数时,按以下优先级**从高到低**确定必出部分,高一级能凑出就必须先凑,不允许跳过: + +1. **有同长(N 连对)的同花色拖拉机** → 必须打出该拖拉机;若手中有多组等长拖拉机,可任选其一打出。 +2. **没有 N 连对,但持有更短的同花色拖拉机(仅 3 连对及以上首家才会走到这一步)** → 必须先拆出手中能凑到的**最长**拖拉机作为强制部分,再用次长拖拉机 / 对子 / 单张继续补足到相同张数——即"能凑多长的拖拉机,就必须先凑多长",不允许放着更长的拖拉机不出而只出零散对子。 +3. **没有任何同花色拖拉机,但对子数量足够(对子数 × 2 ≥ 首家张数)** → 用现有对子凑够张数打出(多余的对子可自选保留)。 +4. **对子数量不够(对子数 × 2 < 首家张数)** → 手中现有的同花色对子**全部**必须打出,剩余缺口用同花色单张补齐。 +5. **完全没有同花色对子** → 用同花色单张随意补齐张数。 + +**同花色牌数量不足**首家张数时(手中还有该花色的牌,但张数不够——无论首家出的是单张、对子还是 N 连对拖拉机):手中该花色的牌**全部必须打出**(不论它们是否成对、是否成拖拉机),不足的缺口用**任意其他花色的牌**补齐——补齐的牌**不要求成对、不要求成拖拉机、也不要求是主牌**,随意垫即可。特别地,**即便首家出的是拖拉机,缺口部分也不强制补成对子或拖拉机**(服务端只校验总张数,不校验缺口部分的结构)。若缺口只能用主牌补(手中没有别的副牌可垫),打出的就是"该花色副牌 + 主牌"的**混合出牌**:它不是缺门、不是毙牌,牌面判为 0,**与垫牌等效,不争夺本轮出牌权**(见术语表「混合出牌」)。 + +若**完全缺门**(手中没有该花色的任何一张牌):整手都可任意出,可选择垫牌或用主牌毙牌(见下)。 + +毙牌的前提是**完全缺门**——手中**没有本轮首家花色的任何一张牌**。只要手里还剩该花色的牌(哪怕凑不成对子、凑不成拖拉机),都必须优先打出这些同花色牌(按上文 5.2 层级),**不允许留着该花色的牌改用主牌毙**。缺门后用主牌毙牌时,牌型与张数须与首家一一对应,才算"压过"、抢下出牌权: + +- 若首家出副牌单张,其余玩家**完全缺门**该副牌花色,可以任意出牌(含主牌);但要压过首家,出的主牌也必须是单张(数量对应)。 +- 若首家出副牌对子,其余玩家**完全缺门**该副牌花色,要压过首家,出的主牌必须也是对子(不能用任意两张主牌顶替)。 +- 若首家出副牌拖拉机(N 连对),其余玩家**完全缺门**该副牌花色,要压过首家,出的主牌必须也是同样连对组数(N 连对)的主拖拉机,总张数完全对应;单张主牌或零散的主对子都不能顶替拖拉机。(注意:只要手里还有该副牌花色的牌,哪怕凑不成同长度拖拉机,也必须先出这些同花色牌,不能改用主牌毙。) + +### 5.3 拖拉机定义与相邻关系 + +**拖拉机 = 在各自的大小序列里「位置相邻」的两个(或以上)对子**,如 K K Q Q 为两连对(两托),K K Q Q J J 为三连对(三托)。副牌与主牌各有自己的序列,相邻规则不同,分开说: + +#### 副牌拖拉机(非主花色) + +**必须同一花色**,点数在下面这条序列里相邻: + +``` +A - K - Q - J - 10 - 9 - 8 - 6 - 5 +``` + +序列里没有 7 和 2(它们是固定主牌,见第 3 节),所以 **7 不与 6、8 相连,而 6 与 8 相连**(中间隔的 7 已被抽走);同理 4、3 不在牌堆里(见第 2 节),5 是副牌序列的最小一档。**跨花色不构成连对**——♥K 对 + ♣Q 对不是拖拉机。 + +#### 主牌拖拉机(主花色 + 固定主牌) + +主牌只有**一条**完整的大小序列(即第 3 节的主牌顺序): + +``` +大王 - 小王 - 正7 - 副7 - 正2 - 副2 - 主A - 主K - 主Q - 主J - 主10 - 主9 - 主8 - 主6 - 主5 +``` + +只要在这条序列里**位置相邻**的对子,就构成拖拉机,**不限花色**。因此「正7 对 + 副7 对」「副7 对 + 正2 对」「副2 对 + 主A 对」「大王对 + 小王对」都是合法的两连对,尽管两个对子的花色不同。序列末段(主A 到 主5)都是**选定花色**的普通牌,其中同样是 6 与 8 相连、7 不入此段。 + +> **同一档位的两个对子不相邻**:「副7」在序列里只占**一个**位置,所以 ♥7 对 + ♣7 对是**两个平级的对子、不是拖拉机**;副2 同理。这与第 3 节「成对只认同花色同点数的两副」是同一条原则的两面——先按同花色同点数成对,再看这些对子在序列里的位置是否相邻。 + +### 5.4 甩牌详细规则 + +#### 5.4.1 基础定义 + +**甩牌**:当你是本轮首家出牌人时,一次性打出多组主牌混合牌型(单张、对子、拖拉机自由组合),一轮打完多张主牌,无需分多轮依次打出。 + +两条硬性基础禁令: + +1. **仅主牌允许甩牌,所有副牌完全禁止甩牌**:无论外面是否剩余该花色副牌,副牌只能分开单出、出对子、出拖拉机,不能一次性打包甩出。 +2. **只有本轮先手才能执行甩牌**:若他人先出牌,你只能跟牌、垫牌、用主牌毙牌,不具备甩牌资格。 + +#### 5.4.2 生效条件(最大性原则)与报无主 + +想要成功甩牌,必须同时满足:**剩余两名对手手中,不存在任何一张、一对、一组拖拉机能压制你甩出的整套主牌组合**。简单说,全场所有更大的主牌、更大的主对子、更长的主拖拉机必须全部在你手中,两家闲家没有任何牌型能盖过你的甩牌组合。 + +**合法性由服务端在甩牌提交时自动判定**:服务端掌握全部玩家的手牌,甩牌一旦提交,立即检查另外两名玩家手中是否"持有"(不要求对方真正打出)能压过这套组合任意一部分的主牌;只要有人持有,就直接判定为甩错(见 5.4.5),不需要、也不会等到对方实际出牌反制才判定。 + +"报无主"是给玩家参考的辅助信息,**不是合法性判定的依据**(合法性判定始终由服务端按全部手牌精确计算):对局中非最后一轮,任意玩家手里没有主牌了,必须口头报无主,方便其他玩家在决定要不要甩牌前,大致判断风险: + +- **另外两家都报了无主**:全场只剩你自己持有主牌,甩牌必然合法,无需记牌。 +- **另外两家只有一家报了无主、还有一家没报**:那一家仍持有主牌,你若甩牌需要自行记牌估算风险;但即使判断失误,最终是否算甩错仍由服务端精确判定,不取决于你的记牌是否正确。 + +#### 5.4.3 甩牌可包含的牌型组合 + +甩牌仅限本局主牌(固定主牌 + 选定主花色的普通牌),内部可自由混搭任意数量、任意种类牌型: + +- **纯单张**:大王、小王、正 7、副 7、正 2、副 2、主花色单牌混合; +- **纯对子**:大王对 / 小王对(各自成对,大王与小王点数不同,不能混成一对)、正七对、副七对、正二对、主花色数字对子; +- **主拖拉机**:大小王拖拉机、七七连对、二二连对、主花色数字连对; +- **混合搭配**:单张 + 对子 + 多连拖拉机一同甩出,无组合数量限制。 + +#### 5.4.4 其余两家应对甩牌的强制跟牌规则 + +甩牌一旦被判定合法(见 5.4.2:服务端已确认两名闲家都不持有能盖压这套组合任何一部分的主牌),闲家就不可能再用主牌把它压下去——"能不能盖压"在甩牌提交的那一刻就已经由服务端裁定完毕,不会走到"闲家出牌盖压"这一步。 + +甩牌本身可能是单张、对子、多个不同长度拖拉机自由混搭的复杂组合(见 5.4.3)。闲家跟牌沿用 5.2「拖拉机跟牌的强制层级」同一原则(高规格能凑就必须先凑:拖拉机 → 对子 → 单张),只是这里要把整套甩牌**拆解成一个个独立的分量,逐个分量分别匹配**,而不是把整套甩牌当成一个笼统的整体来对待: + +1. **每一组拖拉机分量**:闲家手中若有同等连对数的主拖拉机,必须优先拆出来跟这一组;没有同等连对数的拖拉机,退而求其次用同等张数的主对子顶替;主对子也不够,才能拆主单张顶替。甩牌里若有多组不同长度的拖拉机,逐组分别按此规则处理。 +2. **每一组单独的对子分量**:闲家手中若有主对子,必须优先拆出来跟这一组;没有主对子,才能拆主单张顶替。 +3. **每一组单独的单张分量**:用主单张顶替即可。 +4. **闲家手中的主牌拆解完仍不够覆盖甩牌剩余的分量**:不够的那部分只能随意垫副牌。 +5. **闲家完全没有主牌**:只能随意垫任意副牌,本轮无法压制你的甩牌,本轮出牌权归你。 + +无论怎么拆解跟牌,都不影响这套甩牌本身的合法性——合法性在提交时已经由服务端按 5.4.2 判定完毕,闲家的跟牌动作只是"凑够同等张数、同等结构"的手牌垫上去,不可能真正反超压制。 + +#### 5.4.5 甩错判定与惩罚规则 + +**甩错判定**:甩牌提交时,服务端按全部玩家的真实手牌自动检查——只要两名对手任意一人**持有**(不要求已经打出)能盖压你甩牌内任意一组牌型的主牌,本次甩牌立即判定为甩错,对方是否愿意打出来压制不影响判定结果。 + +**统一惩罚标准**: + +- 本次甩牌全部收回手牌; +- 本轮仅强制打出你甩出组合里最小的那一张牌; +- 本轮失去再次甩牌、一次性多出牌的资格,剩余手牌只能分轮正常打出; +- 线下娱乐、线上棋牌平台均统一执行该惩罚,不额外扣分。 + +甩错判罚后,本轮实际只算打出了那一张最小的单张主牌,其余两家按 5.2 节的"单张"跟牌规则正常应对即可,不再涉及甩牌相关规则。 + +--- + +## 6. 捡分与扣底 + +### 6.1 分牌 + +牌面 5 记 5 分,牌面 10 记 10 分,牌面 K 记 10 分,其余牌不计分。 + +### 6.2 捡分规则 + +每一轮出完牌后比较牌面大小: + +- 若闲家最大,则本轮牌面上的分牌全部计入闲家捡分(两个闲家谁大不影响,都算闲家捡到)。 +- 若庄家最大,则本轮牌面上的分牌作废——不计入闲家捡分总数,也不属于庄家的个人分数(庄家本身没有"捡分"这个概念,庄家的输赢只看闲家捡分总数是否达到叫分,见第 7 节)。 + +### 6.3 扣底 + +所有手牌出完后,若最后一轮闲家**用主牌**(不论单张、对子、拖拉机)**牌面压过庄家**、赢下最后一轮,才视为"扣底"(若闲家赢了最后一轮但用的不是主牌,则不算扣底)。触发后,庄家埋下的 8 张埋牌底牌翻开,若有分则计入闲家捡分。 + +埋牌底牌的分要不要翻倍、翻几倍,取决于闲家扣底所用的那手牌(赢下最后一轮的牌)本身是什么牌型: + +| 闲家扣底所用牌型 | 倍数 | +| --- | --- | +| 单张主牌 | 不翻倍,按原始分值计入 | +| 主对子(一对主牌) | 2 倍 | +| 两连对拖拉机 | 4 倍 | +| 三连对拖拉机 | 6 倍 | +| N 连对拖拉机 | 2 × N 倍,以此类推递增 | + +若最后一轮闲家是用第 5.4 节的**甩牌**(单张 + 对子 + 拖拉机混合的组合)扣底,取这套组合里**最高规格牌型**对应的倍数计算,其余较低规格的部分不额外叠加。例如甩出"单张 + 一对 + 两连对拖拉机"并以此扣底,最高规格是两连对拖拉机,整手按 4 倍计算。 + +> **重要区分**:扣底翻的是**闲家的捡分得分(grade)**本身,属于"算分"阶段;这与第 7 节里"大光 ×3""小光 ×2""升 N 级 ×N"这些子数倍率是完全不同的两件事——那些乘的是**子数**,属于"算子"阶段。两者的先后顺序是:扣底先把埋牌底牌的分(可能翻倍)计入闲家的捡分总数,这个最终捡分总数再拿去跟庄家叫分比较,判定过庄 / 小光 / 大光 / 闲家赢(升级),最后才由判定结果决定子数乘几倍。 + +--- + +## 7. 结算:子数与升级 + +叫分与结算涉及两套并存的数值:**倍数**(用于计算"捡分得分" grade)与**子数**(用于计算最终筹码输赢),两者是独立概念,不能混用。 + +> **重要区分**:本节里提到的倍率(大光 ×3、小光 ×2、过庄 ×1、升 N 级 ×N),乘的都是**子数**(最终结算给玩家的筹码单位),不是对局过程中闲家捡到的分数(捡分得分)本身。捡分得分只用来判定庄家的过庄 / 小光 / 大光,或闲家的升级级数,一旦等级判定完成,实际相乘的对象是这一局最终要结算的子数。 + +> **算子模式的选择**:7.1 + 7.2 描述的"常规算子"是默认结算方式;开房时勾选"爬坡"后,才改用 7.3 的子数梯度和保小光分段;未勾选"爬坡"时,一律按常规算子(7.1 + 7.2)结算,两套规则不会同时生效。 + +### 7.0 四层关系总览 + +结算分四层,前三层是"算子"(本节),第四层是"算奖"(第 8 节),一层的输出是下一层的输入: + +1. **叫分 → 基础子数**:叫分档位本身决定这一局的基础子数(见 7.1 / 7.3 的对照表),叫分越低(庄家承诺越苛刻),基础子数越高。 +2. **捡分 vs 叫分 → 判定结果**:局末闲家的捡分总数(含扣底,见 6.3 节)与庄家叫分作比较,只会落入互斥的两个分支之一: + - **闲家没达到叫分**(庄家赢)→ 结果记在**庄家**名下,分三级:**过庄 / 小光 / 大光**。 + - **闲家达到或超过叫分**(闲家赢,庄家倒庄)→ 结果记在**闲家**名下,只有一个概念:**升级**(第几级)。 + - **大光 / 小光和升级是两套完全独立的命名体系,分别只属于庄家一侧和闲家一侧,不能混用或类比**:庄家赢的时候没有"升级",闲家赢的时候也没有"大光 / 小光"。 +3. **判定结果 → 子数倍率**:过庄 / 小光 / 大光 / 升级第几级,各自对应一个子数倍率,乘上第 1 层的基础子数,得到这一局的**子数结算结果 `X`**(前三层,即"算子",到此为止)。`X` 是庄家与**每一个**闲家之间"一对一"结算的基础金额:庄家赢(过庄 / 小光 / 大光)时,两个闲家各输给庄家 `X` 子;闲家赢(升级)时,庄家各输给两个闲家 `X` 子。 +4. **算奖 → 结算倍率**:第 8 节算出的"奖数"(8.1 冲关 + 8.3 傍王,各玩家分别求和)**是"持有奖数的这个人"从其余两名玩家那里各自多收一笔钱的独立结算线,作用对象是这个人本身,不是整场结算的统一系数**——某玩家 P 这一局总共有 `N_P` 奖,则**另外两名玩家(不分庄闲)都要各自额外付给 P:`X × N_P` 子**。三名玩家可能同时都持有各自的奖数,会产生最多 3 条互相独立的额外支付线,与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。算奖的计算过程本身不受叫分、过庄 / 小光 / 大光、升级影响(只看手牌结构),详见 8.3 节的完整举例。 + +> **例外**:70 分坐庄选择投降时不走第 1~3 层模型——直接固定基础子数 1 个、庄家输掉,不比较捡分、不判定过庄 / 小光 / 大光,见 7.1 末尾说明;但第 4 层算奖仍然照常叠加(投降是选主阶段的选择、未选主也未埋牌,按庄家"发牌 + 底牌"共 36 张、无主牌花色计算,见 8.4 节)。 + +**查表索引**:本节按叫分档位给出了完整判定表,位置如下: + +| 叫分 | 常规算子(未勾选"爬坡") | 爬坡(勾选"爬坡") | +| --- | --- | --- | +| 70 分 | 7.2.1 末尾"70 分坐庄" | 与常规算子一致,见 7.3.3 末尾说明 | +| 65 / 60 / 55 / 50 / 45 分 | 7.2.1 对应表格 | 与常规算子数值一致(45 分子数不同),见 7.3.3 末尾说明 | +| 40 / 35 / 30 / 25 / 20 / 15 / 10 分 | 7.2.1"40 分及以下坐庄"通用规则 | 7.3.3 对应表格(子数、`Q` 值均与常规算子不同) | +| 5 分 | 7.2.1"40 分及以下坐庄"内的"5 分坐庄没有小光"说明 | 7.3.3"5 分坐庄"(没有小光) | + +### 7.1 常规算子:叫分 → 基础子数(未勾选"爬坡"时的默认规则) + +叫分范围是 5 ~ 70 分(见第 4 节),常规算子下完整的"叫分 → 基础子数"对照: + +| 叫分 | 65 分 | 60 分 | 55 分 | 50 分及以下(50 / 45 / 40 / 35 / 30 / 25 / 20 / 15 / 10 / 5) | +| --- | --- | --- | --- | --- | +| 基础子数 | 2 个子 | 3 个子 | 4 个子 | 统一 6 个子,不再随叫分继续增加 | + +即叫分从 65 分往下每降 5 分子数依次是 2 → 3 → 4,但**到 50 分封顶**:50 分及以下所有档位(50、45、40、35、30、25、20、15、10、5)基础子数都固定是 6 个子,不会像爬坡那样继续往上加(爬坡的区别见 7.3)。 + +70 分叫庄是特例,基础子数不接着上面的阶梯往下走,而是在"投降"和"打牌"两条路径下分别取值: + +- **选择投降**:基础子数按 **1 个**算,庄家直接输 1 个子(此时局面还没有真正出牌捡分,不经过 7.2 的大光 / 小光 / 升级判定,是一次性的固定结算)。 +- **选择打牌**:基础子数按 **2 个**算(与 65 分相同),之后**照样按 7.2 节的方法,用捡分总数判定大光 / 小光 / 过庄 / 升级**,不是不细分——完整判定表见 7.2.1。 + +70 分叫庄时,发牌时桌面留下的那 8 张底牌(见第 4 节;不是埋牌时埋下的那 8 张「埋牌底牌」)需要**在庄家把它们摸入手牌之前、向所有玩家亮出 3 秒**,之后庄家才摸入手中;投降和打牌两种情况都适用(因为亮牌发生在选主 / 投降决策之前)。 + +### 7.2 捡分 vs 叫分:庄家的过庄 / 小光 / 大光,闲家的升级(常规算子) + +#### 7.2.0 判定方法(对任意叫分档位通用) + +**大光的判定条件是固定的、不随叫分或算子模式变化**:闲家捡分 `grade == 0`(一分未捡到)就是大光,常规算子和爬坡完全一样。会随叫分变化的,是**小光 / 过庄**之间的分界线——常规算子下这条分界线**固定为 40 分**,不随叫分变化(这是与爬坡最核心的区别,爬坡的分界线 `Q` 会随叫分变化,见 7.3.2)。设叫分为 `call`,闲家捡分为 `grade`,按以下顺序依次判断(**必须先判断第 1 步,即是否达标,再判断大光 / 小光 / 过庄**,顺序不能颠倒): + +1. **先判断是否达标**:若 `grade ≥ call`,闲家赢,判为**升级**——先升 1 级,此后每再多捡 40 分再升 1 级;子数倍率就是当前的级数本身:升 1 级 ×1、升 2 级 ×2、升 3 级 ×3……逐级 **+1**,不是逐级翻倍。 +2. **未达标(`grade < call`)时,再看捡分具体落在哪个区间**: + - `grade == 0`(一分未捡到)→ **庄家:大光**,子数倍率 ×3。 + - `0 < grade < 40`(捡了一点,但没满 40 分)→ **庄家:小光**,子数倍率 ×2。 + - `40 ≤ grade < call`(捡满 40 分,但仍没达标)→ **庄家:过庄**,子数倍率 ×1(即基础子数,不翻倍)。 + +之所以要先判断"是否达标":当叫分本身 ≤ 40 时(比如叫 35 分坐庄),闲家只要捡到 35 分就已经达标升级了,根本不可能出现"捡到 40 分却还没达标"这种情况——"过庄"这一档这时候是空的,直接从"小光"跳到"升级"(完整例子见 7.2.1"40 分及以下坐庄")。 + +**大光 / 小光只描述庄家(闲家没达标时,庄家赢得有多彻底);升级只描述闲家(闲家达标之后,赢得有多彻底),两套命名不通用、不能互换。** + +#### 7.2.1 常规算子完整判定表 + +按上面的方法,叫分 > 40 分的档位(65 / 60 / 55 / 50 / 45)"过庄"区间是存在的;叫分 ≤ 40 分的档位(40 / 35 / 30 / 25 / 20 / 15 / 10 / 5)"过庄"区间不存在,闲家捡分只要达到叫分就直接升级。 + +**65 分坐庄(基础 2 个子)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 6 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 4 个子 | +| 40 ~ 64 分 | 庄家:过庄 | ×1 | 2 个子 | +| 65 ~ 104 分 | 闲家:升 1 级 | ×1 | 2 个子 | +| 105 ~ 144 分 | 闲家:升 2 级 | ×2 | 4 个子 | +| 145 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +2 个子 | + +**60 分坐庄(基础 3 个子)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 9 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 6 个子 | +| 40 ~ 59 分 | 庄家:过庄 | ×1 | 3 个子 | +| 60 ~ 99 分 | 闲家:升 1 级 | ×1 | 3 个子 | +| 100 ~ 139 分 | 闲家:升 2 级 | ×2 | 6 个子 | +| 140 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +3 个子 | + +**55 分坐庄(基础 4 个子)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 12 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 8 个子 | +| 40 ~ 54 分 | 庄家:过庄 | ×1 | 4 个子 | +| 55 ~ 94 分 | 闲家:升 1 级 | ×1 | 4 个子 | +| 95 ~ 134 分 | 闲家:升 2 级 | ×2 | 8 个子 | +| 135 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +4 个子 | + +**50 分坐庄(基础 6 个子)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 18 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 12 个子 | +| 40 ~ 49 分 | 庄家:过庄 | ×1 | 6 个子 | +| 50 ~ 89 分 | 闲家:升 1 级 | ×1 | 6 个子 | +| 90 ~ 129 分 | 闲家:升 2 级 | ×2 | 12 个子 | +| 130 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 | + +**45 分坐庄(基础 6 个子——从这一档开始进入"50 分及以下统一 6 个子"的范围)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 18 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 12 个子 | +| 40 ~ 44 分 | 庄家:过庄 | ×1 | 6 个子 | +| 45 ~ 84 分 | 闲家:升 1 级 | ×1 | 6 个子 | +| 85 ~ 124 分 | 闲家:升 2 级 | ×2 | 12 个子 | +| 125 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 | + +**40 分及以下坐庄(40 / 35 / 30 / 25 / 20 / 15 / 10 / 5,基础都是 6 个子;此时 `call ≤ 40`,没有"过庄"这一档)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 18 个子 | +| 1 分 ~(叫分 − 1)分 | 庄家:小光 | ×2 | 12 个子 | +| 达到叫分 | 闲家:升 1 级 | ×1 | 6 个子 | +| 达到叫分 + 40 分 | 闲家:升 2 级 | ×2 | 12 个子 | +| 此后每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 | + +例如叫 10 分坐庄(基础 6 个子):0 分为大光(18 子);1 ~ 9 分为小光(12 子);捡到 10 分起升 1 级(6 子);捡到 50 分起升 2 级(12 子);之后每再多 40 分再升 1 级(每级再 +6 子)。 + +> **5 分坐庄没有"小光"**:分牌只有 5 分、10 分两种面值(见 6.1 节),闲家捡分永远是 5 的倍数。上面"1 分 ~(叫分 − 1)分为小光"这一行,在叫分为 5 分时对应区间是 `1 ~ 4 分`,里面不存在任何 5 的倍数,是空区间。所以叫 5 分坐庄时,闲家捡分只有 0(大光)和达到 5 分及以上(直接升 1 级,之后每再多 40 分再升 1 级)两种结果,没有小光这一档;这是"叫分 ≤ 40 分"这组档位里唯一的特例,其余档位(40/35/30/25/20/15/10)小光区间都至少包含一个 5 的倍数,正常存在。 + +**70 分坐庄** + +- **投降**:基础子数按 1 个算,庄家直接输 1 个子,不经过大光 / 小光 / 过庄 / 升级判定(还没出牌捡分,直接放弃)。 +- **打牌**:基础子数按 2 个算,之后按 7.2.0 的方法正常判定,`Q` 同样固定为 40 分——判定表和 65 分坐庄完全一致,只是把"基础 2 个子"原样代入(数值恰好和 65 分相同): + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 6 个子 | +| 1 ~ 39 分 | 庄家:小光 | ×2 | 4 个子 | +| 40 ~ 69 分 | 庄家:过庄 | ×1 | 2 个子 | +| 70 ~ 109 分 | 闲家:升 1 级 | ×1 | 2 个子 | +| 110 ~ 149 分 | 闲家:升 2 级 | ×2 | 4 个子 | +| 150 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +2 个子 | + +### 7.3 爬坡:叫分 → 基础子数与保小光分段(可选规则,开房时勾选"爬坡"生效) + +#### 7.3.1 基础子数:叫分 → 子数的完整对照 + +叫分从 65 分(2 个子)开始,每叫低 5 分加 1 个子,直到 50 分(6 个子)——这一段与常规算子的四档数值完全相同;50 分以下,继续按每叫低 5 分加 1 个子延伸,一直到叫分范围的下限 5 分: + +| 叫分 | 65 | 60 | 55 | 50 | 45 | 40 | 35 | 30 | 25 | 20 | 15 | 10 | 5 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 基础子数 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | + +#### 7.3.2 保小光分段:叫分 → 小光 / 过庄分界线(Q 值) + +判定方法与 7.2.0 完全相同,只是把固定的 `Q = 40` 换成按叫分区间取值的 `Q`;**大光的判定条件不受 `Q` 影响,永远是 `grade == 0`**,常规算子和爬坡完全一样——`Q` 只决定"小光"和"过庄"之间的分界线: + +| 叫分区间 | 小光 / 过庄的分界线(Q) | +| --- | --- | +| ≥ 45 分(含 65 / 60 / 55 / 50 / 45) | 40 分 | +| 40 / 35 分 | 20 分 | +| 30 / 25 分 | 15 分 | +| 20 / 15 分 | 10 分 | +| 10 分 | 5 分 | +| 5 分 | 5 分(升级级距仍是 5,只是"小光"这一档因为区间为空而不存在,见下方说明) | + +`≥ 45 分`这一档的 `Q = 40`,与 7.2 节常规算子沿用的分界线数值相同,只是子数改按 7.3.1 的公式取值(65/60/55/50 与常规算子数值一致,45 分是爬坡独有的新增档位)。 + +**5 分档没有"小光"**:分牌只有牌面 5(5 分)和牌面 10 / K(10 分)两种面值(见 6.1 节),闲家的捡分总数(`grade`)因此永远是 5 的倍数(0、5、10、15…),不可能出现 1 ~ 4 这样的中间值。当叫分是 5 分时,"小光"原本对应的区间是 `0 < grade < 5`,这个开区间里不存在任何 5 的倍数,也就是说这个区间**必然是空的**——闲家捡分要么是 0(大光),要么一旦捡到任何分牌(最少 5 分)就已经达到叫分、直接升级,中间不存在"捡到了一点、但没达标"的"小光"状态。所以 5 分档只有**大光**和**升级**两种结果,没有"小光"这一档;但 `Q` 值本身仍然是 5(与 10 分档一样),只是在"小光 / 过庄分界线"这个用途上区间恰好收窄为空,`Q = 5` 仍然继续作为"升级级距"生效(见 7.3.3 的 5 分坐庄判定表)。 + +**升级级距的通用规则**:不管哪个叫分档位,闲家赢了之后,"超过叫分多少分算升一级",用的分数差**就是该档位的 `Q` 值**——即上表里每一档的 `Q` 值,既是"小光 / 过庄"的分界线,也是"升级"的级距单位,两者共用同一个数字。例如叫分 40 分时 `Q = 20`:闲家捡分达到 40 分先升 1 级,之后每再多捡 20 分(60、80、100…)就再升一级。 + +#### 7.3.3 各分段的完整判定表(按 7.2.0 的方法代入对应 `call`、`Q`、基础子数) + +**40 分坐庄(基础 8 个子,Q = 20)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 24 个子 | +| 1 ~ 19 分 | 庄家:小光 | ×2 | 16 个子 | +| 20 ~ 39 分 | 庄家:过庄 | ×1 | 8 个子 | +| 40 ~ 59 分 | 闲家:升 1 级 | ×1 | 8 个子 | +| 60 ~ 79 分 | 闲家:升 2 级 | ×2 | 16 个子 | +| 80 分起,每再多 20 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +8 个子 | + +**35 分坐庄(基础 9 个子,Q = 20)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 27 个子 | +| 1 ~ 19 分 | 庄家:小光 | ×2 | 18 个子 | +| 20 ~ 34 分 | 庄家:过庄 | ×1 | 9 个子 | +| 35 ~ 54 分 | 闲家:升 1 级 | ×1 | 9 个子 | +| 55 ~ 74 分 | 闲家:升 2 级 | ×2 | 18 个子 | +| 75 分起,每再多 20 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +9 个子 | + +**30 分坐庄(基础 10 个子,Q = 15)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 30 个子 | +| 1 ~ 14 分 | 庄家:小光 | ×2 | 20 个子 | +| 15 ~ 29 分 | 庄家:过庄 | ×1 | 10 个子 | +| 30 ~ 44 分 | 闲家:升 1 级 | ×1 | 10 个子 | +| 45 ~ 59 分 | 闲家:升 2 级 | ×2 | 20 个子 | +| 60 分起,每再多 15 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +10 个子 | + +**25 分坐庄(基础 11 个子,Q = 15)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 33 个子 | +| 1 ~ 14 分 | 庄家:小光 | ×2 | 22 个子 | +| 15 ~ 24 分 | 庄家:过庄 | ×1 | 11 个子 | +| 25 ~ 39 分 | 闲家:升 1 级 | ×1 | 11 个子 | +| 40 ~ 54 分 | 闲家:升 2 级 | ×2 | 22 个子 | +| 55 分起,每再多 15 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +11 个子 | + +**20 分坐庄(基础 12 个子,Q = 10)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 36 个子 | +| 1 ~ 9 分 | 庄家:小光 | ×2 | 24 个子 | +| 10 ~ 19 分 | 庄家:过庄 | ×1 | 12 个子 | +| 20 ~ 29 分 | 闲家:升 1 级 | ×1 | 12 个子 | +| 30 ~ 39 分 | 闲家:升 2 级 | ×2 | 24 个子 | +| 40 分起,每再多 10 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +12 个子 | + +**15 分坐庄(基础 13 个子,Q = 10)** + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 39 个子 | +| 1 ~ 9 分 | 庄家:小光 | ×2 | 26 个子 | +| 10 ~ 14 分 | 庄家:过庄 | ×1 | 13 个子 | +| 15 ~ 24 分 | 闲家:升 1 级 | ×1 | 13 个子 | +| 25 ~ 34 分 | 闲家:升 2 级 | ×2 | 26 个子 | +| 35 分起,每再多 10 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +13 个子 | + +**10 分坐庄(基础 14 个子,Q = 5——这一档同样没有"小光")** + +分牌只有 5 分、10 分两种面值,捡分永远是 5 的倍数。`Q = 5` 时,"小光"对应的区间 `0 < grade < 5` 里不存在任何 5 的倍数,同样是空区间:闲家捡分不可能停在 1 ~ 4 分,只要捡到分就已经是 5 分(直接落入"过庄"区间)。 + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 42 个子 | +| 5 ~ 9 分 | 庄家:过庄(没有小光这一档) | ×1 | 14 个子 | +| 10 ~ 14 分 | 闲家:升 1 级 | ×1 | 14 个子 | +| 15 ~ 19 分 | 闲家:升 2 级 | ×2 | 28 个子 | +| 20 分起,每再多 5 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +14 个子 | + +**5 分坐庄(基础 15 个子,没有"小光",见 7.3.2 的说明)** + +5 分是叫分范围的下限,捡分只能是 5 的倍数:一旦捡到任何分牌就已经是 5 分,等于直接达标(`grade ≥ call = 5`)。所以"小光"和"过庄"这两个区间都不存在,闲家捡分只有 0(大光)和达到 5 分及以上(直接升 1 级)两种结果。 + +| 闲家捡分 | 判定 | 倍率 | 最终子数 | +| --- | --- | --- | --- | +| 0 分 | 庄家:大光 | ×3 | 45 个子 | +| 5 ~ 9 分 | 闲家:升 1 级(没有小光、过庄这两档) | ×1 | 15 个子 | +| 10 ~ 14 分 | 闲家:升 2 级 | ×2 | 30 个子 | +| 15 分起,每再多 5 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +15 个子 | + +`≥ 45` 分档(65 / 60 / 55 / 50 / 45)判定逻辑与 7.2.1 完全相同(Q = 40),只是 45 分档基础子数按 7.3.1 取 7 个子,其余数值套用 7.2.1 对应叫分的表即可,此处不重复列出。 + +70 分叫庄时与常规算子规则一致(见 7.2.1 末尾"70 分坐庄":投降固定基础子数 1 个,打牌基础子数 2 个并正常套用大光 / 小光 / 过庄 / 升级判定),不适用本节的保小光分段。 + +--- + +## 8. 算奖规则 + +算奖在**每个小局结算时**计算(与第 7 节的子数结算属于同一次小局结算,不是等大局所有局数打完才统一算,也不是在出牌过程中途算)。8.1 冲关只计入庄家;8.3 傍王是额外叠加的独立奖励,庄家、闲家都要算——两者的计入范围不同,注意区分。 + +**8.1 冲关只认庄家的身份,不看闲家的手牌是否达标**:即使某位闲家的手牌结构凑巧也满足 8.1 表里的条件(比如手里也有 3 张王),这份冲关也**不会**计入这位闲家——冲关从规则上就只针对庄家一人。闲家想要获得算奖,只能靠 8.3 傍王(且必须房间勾选了这条可选规则)。 + +**算奖依据的手牌快照时间点**:不管是 8.1(只看庄家)还是 8.3 傍王(庄闲都算),都按**静态初始手牌**计算——庄家是埋牌完成后的那一刻手牌(28 张),闲家是发牌完成后的那 28 张手牌;这个快照全程固定,不随之后的出牌、被吃、被打出而改变。**例外**:70 分投降局庄家未选主也未埋牌,其快照为"发牌 + 底牌"共 36 张(无主牌花色),见 8.4 节。 + +**算奖(8.1 / 8.3)与亮牌(8.2)是两个互不干涉的独立概念**:算奖是结算阶段"该给多少额外结算分"的规则;亮牌只是出牌开始前"要不要向对手公开一部分手牌"的展示规则。两者各自有自己的触发条件,只是恰好都以庄家埋牌后的手牌结构为依据,因此部分门槛数值相同——但达成亮牌的条件不代表一定触发算奖,反之亦然,具体对照见 8.2 末尾的说明。 + +### 8.1 冲关 + +> **旧称「常规算奖」**,现统一叫**冲关**(与服务端字段 `chongguan`、界面「冲关分」「冲关牌型」同口径)。 +> 它只是「算奖」的**其中一个分量**——另一个是 8.3 傍王;两者求和才是某玩家的总奖数 `N`(见 8.4)。 + + +| 组合 | 奖数 | +| --- | --- | +| 三个王 | 1 奖 | +| 四个王 | 3 奖 | +| 六个 7 | 1 奖 | +| 七个 7 | 2 奖 | +| 八个 7 | 3 奖 | +| 六个 2 | 1 奖 | +| 七个 2 | 2 奖 | +| 八个 2 | 3 奖 | + +三王 / 四王在拥有"正 7"及以后连续对子的情况下,每多一对连续对子再加 1 奖,连续顺序为: + +``` +正7 - 副7 - 正2 - 副2 - A - K - Q - J - 10 - 9 - 8 - 6 - 5 +``` + +其中从 A 到 5(即 A、K、Q、J、10、9、8、6、5 这一段普通点数)都必须是选定花色的主牌,才能计入连续对子——非主花色的普通对子不算数,链条到这里就断了。 + +### 8.2 亮牌规则 + +**亮牌与算奖(8.1 / 8.3)是两个独立概念,互不干涉**:亮牌只是"要不要向对手公开一部分手牌"的展示规则,本身不产生任何奖数、不影响结算;算奖该怎么算、算多少,完全按 8.1 / 8.3 的规则来,不因为触发了亮牌而增加或减少。 + +**亮牌要求**(仅针对庄家):庄家**埋牌后**、**出牌开始前**,若手中剩余的**固定主牌**满足下表任一条件,就要向两个闲家**亮出自己全部固定主牌的具体牌面**。 + +| 触发条件(任一满足即亮牌) | +| --- | +| 固定主牌(双王 + 全部 2 + 全部 7)总数 ≥ 10 张 | +| 王 ≥ 3 张 | +| 7 ≥ 6 张 | +| 2 ≥ 6 张 | + +**亮出的内容是固定的一份**——**庄家手中全部固定主牌的具体牌面**(双王 + 全部 2 + 全部 7;**不含**主花色的普通牌 A/K/Q/J/10/9/8/6/5)。不论是被上表哪一条触发、还是同时满足多条,亮出的都是这同一份牌,不因触发条件不同而增减。 + +- **不限叫分**:任何叫分档位都适用,不是 70 分坐庄专有。 +- **仅可查牌模式**:房间勾选了"不查牌"时,闲家看不到亮牌(见第 9 节)。 +- **只亮庄家的**:闲家不亮牌。 +- 亮牌依据的是**埋牌后**的手牌,与 8.1 冲关用的是同一份静态快照(见第 8 节开头),全局固定、不随之后出牌缩水。 + +> **亮牌给的是牌面,不是统计**。这一点与「余主公示」(只给剩余主牌数与对子数,见第 9 节)不同;与「明牌」(查看**他家**全部未出主牌的牌面,见第 9 节)也不同——亮牌是**庄家主动公开自己的**,明牌是**闲家去看别人的**。 + +**亮牌门槛与 8.1 冲关起点的数值巧合,不代表两者是同一条规则**:王 ≥ 3 张 / 7 ≥ 6 张 / 2 ≥ 6 张这三个门槛,数值上恰好分别与 8.1 表里"三个王""六个 7""六个 2"的冲关起点相同,达到时会同时触发"亮牌"和"冲关"这两件独立的事——但它们是各自独立判定后凑巧同时发生,不是"亮牌导致冲关"或者"冲关导致亮牌"。这一点在"固定主牌 ≥ 10 张"这一档表现得最清楚:它只在亮牌这条规则里有定义、会触发亮牌,但 8.1 的冲关表里根本没有为这一档定义奖数,所以**不产生任何冲关奖**——如果亮牌和冲关是同一回事,这里就该矛盾;现在不矛盾,正说明两者互不干涉。 + +### 8.3 傍王(可选规则) + +开房时勾选"傍王"后,每张王额外算一奖:这个奖是**庄家、闲家都要各自计算**(每个人按自己手里的王算,不限庄家)。 + +**傍王奖是在 8.1 冲关之外额外叠加的一份奖励,与冲关互不冲突、不互相替代**:庄家照常按 8.1 的规则算冲关(只计庄家),同时如果勾选了傍王,庄家、闲家还要**各自另外**再按自己手里的王数算一份傍王奖;两份奖励分开计算,最后相加。 + +### 8.4 算奖如何影响最终结算 + +**算奖是"持有奖数的那个人"从其余两名玩家那里各自多收一笔钱的独立结算线,不是把第 7 节算出的输赢结果整体重新乘一遍**:设第 7 节前三层算出的结算子数为 `X`(庄家与每个闲家之间"一对一"结算的基础金额,见 7.0 第 3 层),玩家 P 这一局总共积累了 `N_P` 奖(8.1 冲关 + 8.3 傍王,各自求和后相加;只有庄家可能有 8.1 的分量,8.3 傍王则庄闲都可能有): + +- 若 `N_P > 0`,**另外两名玩家(不分是庄家还是闲家)都要各自额外付给 P:`X × N_P` 子**; +- 若 `N_P = 0`,P 没有额外收入。 + +三名玩家可能同时都持有各自的奖数,这时会产生最多 3 条互相独立、方向不同的额外支付线,互不冲突、按各自的 `N` 值同时生效,最后与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。 + +**举例**(`X = 6` 个子,庄家为 A,闲家为 B、C): + +- 若庄家 A 本局有 1 奖(`N_A = 1`):B、C 各自额外付给 A `6 × 1 = 6` 子。 +- 若闲家 B 同时勾选了"傍王"、本局有 2 奖(`N_B = 2`):A、C 各自额外付给 B `6 × 2 = 12` 子。 +- 这两条支付线同时成立、互不影响:A 从 B、C 各多收 6 子,B 从 A、C 各多收 12 子——包括"闲家 C 要付给闲家 B"这样的支付,即使 B、C 之间在第 1~3 层的基础输赢结算里并不直接结算(基础结算只发生在"庄家 vs 单个闲家"之间),算奖这一层是单独叠加在所有玩家两两之间的。 + +以下三点已确认: + +- **庄家倒庄(闲家达标升级)时,8.1 冲关仍然生效**:冲关只看庄家埋牌后的手牌结构,与本局谁输谁赢无关。 +- **70 分投降时,仍叠加算奖倍率**:投降只是跳过了第 1~3 层的叫分 / 捡分判定(此时 `X = 1`)。投降是选主阶段的选择,庄家**未选主、未埋牌**,故算奖按庄家"发牌 + 底牌"共 36 张手牌计算——因没有选定主牌花色,**没有连对链**,只有三 / 四王、六~八个 7、六~八个 2 这些组合,以及(勾选傍王时)按王数的傍王奖计入;8.1(及勾选傍王时的 8.3)照常计算并叠加。 +- **算奖对象的身份限制会带进 `N` 的计算**:8.1 只算庄家、8.3 傍王勾选后才不分庄闲(见第 8 节开头说明),把这名玩家实际适用的分量求和,才是他自己的 `N`;"其余两人向他支付 `X × N`"这条支付规则本身不因身份而改变,但闲家的 `N` 从一开始就不可能包含 8.1 的分量。 + +--- + +## 9. 牌局查看模式 + +"查牌"指的是**回看那些不该随时可见的信息**:已经打过去的牌、他家手里还剩多少主牌、他家主牌的具体牌面。房间的"查牌模式"开关决定下列四项功能**整体**是否提供。 + +- **可查牌模式**(四项功能全部提供): + 1. **出牌历史**:当前牌局中,所有玩家已经打出的牌(含之前每一轮打出、已被收走的牌),任何时候都可以回看。 + 2. **余主公示**:一旦有玩家的主牌全部打空(即 5.4.2 节所说的"报无主"状态;"主牌"指大王、小王、所有花色的 2、所有花色的 7,以及主花色的其余普通牌,见第 3 节),系统就为**全体三人**(含刚刚报无主的这位玩家自己)显示另外两家各自的主牌数量、以及这些主牌里有多少对子。**只给数量、不给具体牌面**——要看具体牌面是下一条的"明牌"。 + 3. **明牌**:同时**为这三个人都**出现一个"明牌"按钮——判定依据是「**场上有人**报无主」,不是「**自己**报无主」,所以报无主的那位和另外两位一样都能点。点击后可以查看另外两家手中全部主牌的**具体牌面**(不只是数量 / 对子结构),再点一次取消查看、恢复隐藏;**不限次数,整个出牌阶段随时可查**。 + 4. 庄家若触发了 8.2 节的"亮牌要求",两个闲家可以看到**庄家全部固定主牌的具体牌面**(见 8.2 节)。 + +- **不可查牌模式**:以上四项**一项都不提供**—— + - **不提供出牌历史**:之前各轮打出过什么牌一律不能回看,只能靠玩家自己记牌; + - **不提供余主公示**:即使有玩家报无主,也不会显示任何人的主牌数量、对子结构,也没有"明牌"按钮; + - 庄家即使触发了 8.2 节的亮牌条件,闲家也看不到那些牌(见 8.2 节)。 + +> **"查牌"与"当前这一轮桌面上的牌"是两回事**:本轮已经出过牌的玩家、打在桌面上的这几张牌,**两种模式下都必须对全体可见**——否则后出的人无从跟牌、也无从判断本轮谁最大。查牌模式管的是**已经收走的往轮牌**与**他家手牌信息**能不能回看,不影响当前这一轮的正常出牌展示(断线重连回到牌桌时同理:当前轮桌面上的牌照常恢复,往轮历史则按本节的模式开关决定给不给)。 + +--- + +## 10. 房间设置选项 + +创建页依次展示:**局数、扣卡方式、查牌模式、傍王、爬坡**。前三项各自是独立的单选必选组,每组必须且只能保留一个有效选项;傍王和爬坡各自独立可选,可以均不选、任选其一或同时勾选。 + +### 10.1 局数与牌局扣卡方式 + +局数为 6 局 / 12 局,单选必选;扣卡方式为房主扣卡 / AA 制,单选必选。两组分别选择,费用按下表联动展示。 + +| 扣卡方式 | 6 局 | 12 局 | +| --- | --- | --- | +| AA 制(每人扣卡) | 每人 1 张 | 每人 2 张 | +| 房主扣卡 | 2 张 | 4 张 | + +### 10.2 查牌模式 + +可查牌 / 不查牌,单选必选(见第 9 节)。 + +### 10.3 附加规则 + +傍王、爬坡:均可不选,也可以同时勾选,互不冲突。未选傍王不计算傍王奖励;未选爬坡使用常规算子。 + +### 10.4 创建参数与当前实现差异 + +默认选择:6 局、房主扣卡、可查牌,傍王和爬坡均不勾选。前三项已有有效默认选择,用户无需重复点击才能提交。 + +规则设计者要求 `roomtype` 使用 11 位字符串,前 5 位依次为局数、扣卡方式、查牌模式、傍王、爬坡,后 6 位预留。 + +**2026-09-07 服务端核验:长度约定已支持,位序尚不一致。** 当前 `class.config.js` 按局数、扣卡方式、傍王、爬坡、查牌模式解析前 5 位;尾部 6 位当前约定为 `000000`。默认完整值为 `00000000000`。设计要求的位序不能直接当作当前服务器已经支持的编码,否则会把查牌、傍王、爬坡解释成另一项规则。 + +实际传输编码、示例和兼容行为统一见 [协议 §0.5](../protocol/packet_protocol.md#05-roomtype-房间选项位串)。界面顺序可以独立于传输位序;当前前端接入应通过规则编码层匹配现有服务端。若要求传输位序也改为设计顺序,需要另行明确服务端契约变更,本次文档核验未修改服务器实现。 + +--- + +## 11. 牌局交互提示 + +- 选主阶段:4 个花色选主按钮上各显示"该花色在庄家手中的对子数"(供庄家判断选哪门为主);70 分坐庄时并列再显示一个投降按钮——选主与投降是同一决策点的互斥选择(见第 4 节)。 +- 首家出主牌时可以甩牌,甩牌的生效条件、跟牌规则、甩错惩罚详见 5.4 节。 +- 牌局结束需要亮出埋牌底牌。 +- 闲家在出牌时需要有 3 个提示选项: + 1. **踩**:提示对家"我能大过庄家"。 + 2. **没分**:提示对家"我手上没分了"。 + 3. **有分**:提示对家"我手上有分"。 +- **倒计时只作展示,不触发任何自动操作**:叫分 / 选主 / 埋牌 / 出牌四个阶段都会给出一个倒计时秒数,用于界面提醒当前该谁操作。**倒计时归零后不做任何代打**——不自动叫分、不自动选主、不自动埋牌、不自动出牌,也不判负、不跳过该玩家;轮到谁而谁不操作,牌局就停在这一步一直等。 + +> **"无超时托管"是确认过的规则,不是待实现的缺口**:本局不设服务端代打/AI 托管,也不因超时改变任何对局状态。若玩家长时间不操作导致牌局停住,由玩家走房间的**解散**流程收场,按当前累计分结算(见 12.2)。**后续核对不要把"没有超时动作"当成缺陷去补"超时托管/自动出牌"。** + +--- + +## 12. 完整牌局游玩流程 + +本节把前面各节的规则串成一局(一个"小局")从头到尾的完整流程,供整体理解与实现参照。这里只给出**步骤与先后顺序**,每一步的具体细则以其对应章节为准(避免与细则重复而产生分歧)。 + +### 12.1 一个小局的完整流程 + +1. **开局准备**:房间按第 10 节的设置(局数、扣卡方式、傍王、爬坡、查牌)建好后开战。第一局的暂定庄家为 0 号座位;之后每局按 4.6 的轮庄规则确定暂定庄家。 + +2. **发牌**(第 2 节):92 张牌洗匀,三家各摸 28 张,剩余 8 张扣在桌面作为"底牌"。 + +3. **叫分坐庄**(4.2):由暂定庄家起、按逆时针依次叫分。暂定庄家必叫(5~70 分、步进 5,不能"不叫");其后每家只能叫比当前更低的分,或选择"不叫";有人叫出 5 分即立即坐庄,或一家叫分后另外两家都"不叫"也即坐庄。叫分越低表示庄家对"压住闲家捡分"越有信心,对应的基础子数越高(第 7 节)。 + +4. **摸底**:庄家坐定后把 8 张底牌摸入手牌(摸完共 36 张)。非 70 分:这 8 张仅庄家可见。**70 分坐庄先触发「开底」:庄家摸底之前,先把这 8 张向所有玩家翻开 3 秒,再摸入手牌**(4.4 / 第 4 节)。 + +5. **选主 / 投降(同一决策点,二选一,互斥)**(第 4 节): + - **选主**:庄家选定一门花色为本局主牌(大王、小王、所有花色的 2、7 恒为主牌)→ 进入下一步埋牌。前端此阶段每个花色按钮上要显示该花色在庄家手中的对子数;70 分坐庄时并列显示投降按钮。 + - **投降**:仅 70 分坐庄才有此选项;选投降即放弃本局,**不选主、不埋牌、不出牌**,直接按 7.1"70 分 · 投降"结算(算奖仍按下方 8 计,但用庄家 36 张、无主牌花色)。选花色就等于放弃投降、正常打牌。 + - (**开底**——70 分的 8 张底牌翻开 3 秒,发生在步骤 4"摸底之前"。) + +6. **埋牌**(第 4 节,仅"选主/打牌"路径):庄家从 36 张里选 8 张重新扣下作为"埋牌底牌"(供最后"扣底"判断),保留 28 张。(**先选主、后埋牌**。) + +7. **出牌对局**(第 5、6 节): + - 第一轮固定由庄家先出;此后每轮由上一轮牌面最大的一方先出。 + - 首家出什么花色 / 牌型,其余两家按 5.1~5.3 跟牌(同花色不够时可用主牌"毙牌"抢权,或"垫牌"不争)。 + - 首家出主牌时可"甩牌"(5.4):一次性打出多组主牌组合;服务端按全场手牌判定"最大性",甩错则按 5.4.5 收回、只强制打出最小一张。副牌一律不能甩。 + - 每轮比大小定出胜者:闲家赢下的那一轮,牌面上的分牌(5→5 分、10→10 分、K→10 分)计入闲家捡分;庄家赢的轮分牌作废(6.1~6.2)。 + - 过程中:有玩家主牌出空需"报无主",可查牌模式下触发**余主公示**(为全体展示他家主牌数量 / 对子结构,第 9 节);庄家手牌达到 8.2 的门槛时触发**亮牌**。 + +8. **末轮扣底**(6.3):所有手牌出完后,若最后一轮由闲家**用主牌**赢下,则庄家埋的 8 张埋牌底牌翻开,其中的分按闲家赢牌的牌型翻倍(单张 ×1、主对 ×2、N 连对 ×2N)后计入闲家捡分。 + +9. **小局结算**(第 7、8 节,两部分同时结算): + - **算子**:闲家总捡分(含扣底)与庄家叫分比较 → 判定庄家的**过庄 / 小光 / 大光**,或闲家的**升级第几级** → 结合叫分档的基础子数,得到庄家与每个闲家"一对一"的结算子数 `X`(勾选爬坡则改用 7.3 的梯度与分段)。 + - **算奖**:以静态初始手牌(正常局:庄家埋牌后 28 张、闲家发牌后 28 张;**70 分投降局**:庄家用发牌+底牌共 36 张、无主牌花色)计算奖数 `N` —— 冲关(8.1)**只计庄家**;勾选傍王(8.3)则庄闲每张王各算一奖。持有 `N` 奖的玩家从另外两人各多收 `X × N`(8.4)。 + - 以上两部分加总即各家本局盈亏;牌局结束需亮出埋牌底牌(第 11 节)。 + +10. **进入下一局**(4.6):庄家本局赢则连庄(暂定庄家仍是他);庄家输(闲家捡分达标 / 庄家投降)则暂定庄家顺延到其下家。回到第 2 步开新的一小局。 + +### 12.2 大局与结束 + +按房间设定的局数(6 局或 12 局,见第 10 节)打满全部小局后,进行大局结算(累计各家总分并记录战绩);若中途解散,则按当前累计分结算。 + +### 12.3 阶段速览 + +``` +建房(§10) → 发牌(§2) → 叫分坐庄(§4.2) → 摸底[70分先开底] → 选主(§3) → 埋牌(§4) + → [70分:投降/打牌决策(§4.4)] → 出牌对局(§5,§6.1-6.2) → 末轮扣底(§6.3) + → 小局结算=算子(§7)+算奖(§8) → 轮庄(§4.6) → 下一局 …… 打满局数 → 大局结算 +``` + diff --git a/server/games/erqiwang/docs/protocol/packet_protocol.md b/server/games/erqiwang/docs/protocol/packet_protocol.md new file mode 100644 index 0000000..f58d5ce --- /dev/null +++ b/server/games/erqiwang/docs/protocol/packet_protocol.md @@ -0,0 +1,722 @@ +# 二七王协议包列表 + +说明:`data` 均为发送方与接收方之间约定的 JSON 内容;成败判定只看推送包里的 `data.success`。 + +--- + +## 0. 通用约定(所有包适用,下文各表不再逐一重复) + +### 0.0 术语:底牌 vs 埋牌底牌(先读这一条) + +本项目于 2026-08-25 修订了这两个术语(`design.md` §1 已同步,本文亦已改用新称): + +| 术语 | 指哪 8 张 | 旧称 | +| --- | --- | --- | +| **底牌** | 发牌时**没有发给玩家**、扣在桌面的 8 张 | ~~暗牌~~ | +| **埋牌底牌** | 庄家**埋牌**时从手里扣下的 8 张 | ~~底牌~~ | + +**两批牌的字段名已拆分**(2026-08-25,原先共用 `bottomcards`): + +| 字段名 | 含义 | 出现在 | +| --- | --- | --- | +| **`bottomcards`** | **底牌** | `shangzhuang`、`deskinfo.ChooseMain`、`deskinfo.BuryCards` | +| **`burycards`** | **埋牌底牌** | `maipai`、`deskinfo.PushCards` | +| `bottom.cards` | **埋牌底牌** | 结算包的 `bottom` 分组(分组名已表明是抠底相关,字段未改名) | + +同一个包里不会同时出现这两个字段。**下发面**:`burycards` 与 `bottomcards`(非 70 分时)都**只发给庄家**,闲家两者皆无——已由 `test/test_leak.js` 的泄露审计覆盖。 + +### 0.0b 四类「公开信息」术语(都带亮/明字,别读混) + +| 术语 | 谁公开给谁 | 给的是什么 | 触发 | 本文相关字段 | +| --- | --- | --- | --- | --- | +| **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后固定主牌达门槛(design §8.2) | `liangpai.cards` | +| **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(design §9) | `seatlist[seat][4]`、`baozhu` | +| **明牌** | 他家 → 请求者 | **具体牌面**(他家全部未出主牌) | 可查牌 + **任一**玩家报无主 → **三家均可点、随时可查**(design §9) | `mingpai.others[].zhucards` | +| **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(design §4) | `ancard3s` + `bottomcards` | + +一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。 + +另有一组「底」字族专管发牌留桌的那 8 张:**摸底**(庄家摸起看,仅庄家)、**开底**(70 分时全场看 3 秒)、**查底牌**(对局中随时回看)、**扣底**(末轮翻开埋牌底牌计分)。 +(「亮主」不是术语,是前端选主界面的标题文案,与上表无关。) + + +### 0.0c 算奖 = 冲关 + 傍王 + +| 术语 | 范围 | 相关字段 | +| --- | --- | --- | +| **冲关** | design §8.1,**只计庄家**。旧称"常规算奖",现统一叫冲关 | `chongguan`(奖数)、`cards`(冲关牌型) | +| **傍王** | design §8.3,可选规则,勾选后**庄闲都算**,每张王 1 奖 | `wang`(王数)、`bangwang`(开关) | +| **算奖** | **上位概念** = 冲关 + 傍王 | `naward`(总奖数 N)、`grade_aw`(算奖得分) | + +说「**冲关**」指庄家那一份,说「**算奖**」指两者合计。界面上小局结算显示「冲关分」、按钮叫「冲关牌型」。 + +> ⚠️ `grade_aw` 是按合计后的 `N` 算出来的,**冲关与傍王的贡献事后无法拆分**。大局结算若要分别显示「冲关分」「傍王分」,须服务端在算钱时就分开代入公式——见前端清单 §7.5 **S-6**。 + +### 0.1 成败标志 `data.success` + +**每一个「服务器 → 客户端」的包,`data` 都必带 `success`**(布尔): + +- `success: true` —— 操作成功 / 正常推送。下文各包的字段表描述的都是这种情况,表里不再重复列 `success` 这一行。 +- `success: false` —— 操作失败,见 0.2。 + +前端一律 `if (!data.success)` 判成败,**不看 `status` / `code`,也不写 `status` 兼容兜底**。 + +### 0.2 失败回包 + +服务端受理请求时,任一校验不通过都会**回一个失败包**(此前是静默丢弃、前端只能干等倒计时): + +- **rpc 与请求包同名**(如 `chupai` 失败回 `chupai`,而不是 `chupai1/2/3`); +- **只回发给请求者**(`conmode`/`fromid` 取自请求包),其他两家收不到; +- `data` 只含 `success: false` 与 `errcode`,无业务字段。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| success | 布尔 | 恒为 `false` | +| errcode | 整数 | 失败原因,见下表 | + +`errcode` 取值(服务端 `youle_erqiwang.ERR`,`mod.js`): + +| 值 | 名称 | 含义 | +| --- | --- | --- | +| 1 | PLAYER | 玩家/房间/座位校验不通过(平台 `check_player` 返回 null) | +| 2 | NODESK | 牌桌或牌局不存在 | +| 3 | STEP | 当前阶段不允许该操作(如非出牌阶段发 `chupai`) | +| 4 | SEAT | 位置不符,或还没轮到该玩家操作 | +| 5 | PARAM | 参数非法:类型/范围/张数不对、牌不在手上、牌id 重复 | +| 6 | RULE | 规则不允许:叫分未更低、出牌不合法、投降条件不满足、房间模式禁止等 | + +> **牌id 列表的入参约束**(`cards` 字段,`maipai`/`chupai`):必须是**非空数组**,元素必须是 `0 ~ 107` 的**整数**牌id,且**互不重复**。字符串形式的数字(如 `"5"`)、小数、越界值、重复值一律按 `PARAM` 拒绝,服务端不做类型兜底转换。 + +> **例外:`tishi` 成功时不回执**。该包是闲家给对家的主观提示、不含任何对局状态,服务端只转发给对家(design §11),发送者收不到成功包;只有失败才回给发送者。 + +### 0.3 `countdown` 只是展示用的秒数 + +多个包带 `countdown`(叫分 / 选主 / 埋牌 / 出牌倒计时,取自 `class.desk.js` 的四个常量)。它**只供客户端显示提醒,服务端不据此做任何事**: + +- 服务端**没有**对应的定时器,倒计时归零后**不会**自动叫分 / 选主 / 埋牌 / 出牌,也不判负、不跳过该玩家; +- 轮到谁而谁不操作,牌局就停在该阶段一直等,`step` 与 `playproc` 都不变; +- 这是 design §11 确认过的规则(无超时托管),不是未实现的功能。牌局因此停住时,由玩家走平台的**房间解散**流程收场(平台回调子游戏 `get_disbandRoom` → 解散结算,见 §14)。 + +因此**客户端不要在倒计时归零时做任何乐观的界面推进**(不要自行跳阶段、不要清控制权),一切仍以收到服务端推送为准。 + +### 0.4 断线重连包不在此约定内 + +`deskinfo` 由平台组装在 `pack.data.deskinfo` 下(见文末「断线重连」),`data.success` 由**平台**填写,子游戏不注入。 + +### 0.5 `roomtype` 房间选项位串 + +**核对日期:2026-09-07。** 本节描述当前服务端实际编码,依据 [class.config.js](../../class.config.js) 的 `IDX_*`、`RESERVE_LEN`、`parse()`,以及 [test_config.js](../../test/test_config.js)。局数与扣卡由 [class.export.js](../../class.export.js) 的对应接口读取解析结果。 + +当前新前端发送约定为 **11 位字符串 = 前 5 位选项 + 后 6 位预留扩展位**,后 6 位当前填 `000000`。默认完整值为 `00000000000`。索引从 0 开始,对应 `charAt(0..4)`;前端不得把字符串转换为数字、数组或按界面排列顺序直接拼接。 + +| 当前传输位索引 | 含义 | '0' | '1' | 创建页要求 | +| --- | --- | --- | --- | --- | +| 0 | 局数 | 6 局(默认) | 12 局 | 单选必选 | +| 1 | 扣卡方式 | 房主扣卡(默认) | AA 每人扣卡 | 单选必选 | +| 2 | 傍王 | 关(默认) | 开 | 独立可选 | +| 3 | 爬坡 | 常规算子(默认) | 爬坡 | 独立可选 | +| 4 | 查牌模式 | 可查牌(默认) | 不查牌 | 单选必选 | +| 5~10 | 预留扩展位 | 当前发送填 0 | 当前无玩法定义 | 无控件 | + +**设计与实现差异:** 规则设计者指定的前 5 位顺序为“局数、扣卡方式、查牌模式、傍王、爬坡”,而当前源码与测试仍按上表解析。创建页按设计顺序展示,编码层按已核验的服务器位索引构造参数;这不表示设计要求的传输位序已在服务器落地。若后续变更服务端位序,需要同步核验协议和兼容方案,不能仅更新文档就声称契约已改变。 + +局数、扣卡、查牌分别单选必选;傍王与爬坡允许均不勾选、任选其一或同时勾选,未选仍写对应位的 '0'。编码示例(均为当前服务端顺序): + +- `00000000000`:6 局、房主扣卡、可查牌、不傍王、不爬坡。 +- `10100000000`:12 局、房主扣卡、可查牌、傍王、不爬坡。 +- `00001000000`:6 局、房主扣卡、不查牌、不傍王、不爬坡。 +- `11011000000`:12 局、AA 扣卡、不查牌、不傍王、爬坡。 + +**实际解析兼容行为:** 服务端 `parse()` 接受至少 5 位的字符串,仅读取前 5 位;旧 5 位串及更长扩展串均可解析,不强制长度为 11。缺失、非字符串或不足 5 位时整体使用 `00000` 的语义;每个选项位只有字符 '1' 判为开启,其余字符判为关闭。预留位不校验内容,也不影响当前五项配置。此处记录的是来源实现,前端仍应在编码入口生成约定的合法 11 位串,不能依赖服务器容错掩盖错误。 + +`class.config.js` 是玩法配置的唯一解析入口;下游使用具名配置,不重新按下标解释。`multiple` / `bangwang` / `climb` / `baozhu` / `seatlist` / `liangpai` / `pushlist` 按相应配置取值或门控。源码 `cfg.climb` 行的行末注释写着“true=常规算子”,与位定义及算法分支不符;实际 true 表示启用爬坡,以执行逻辑为准。 + +对应的房卡与局数(`export.get_asetcount` / `get_needroomcard` / `get_needroomcard_joinroom`,design §10.1): + +| 扣卡方式(位1) | 局数(位0) | 房主开房扣 | 加入者扣 | +| --- | --- | --- | --- | +| 房主扣卡 `'0'` | 6 局 | 2 张 | 0 | +| 房主扣卡 `'0'` | 12 局 | 4 张 | 0 | +| AA 每人 `'1'` | 6 局 | 1 张 | 1 张 | +| AA 每人 `'1'` | 12 局 | 2 张 | 2 张 | + +### 0.6 「一手出牌」的顺序口径 + +同一份业务数据「某一手打出的牌」会出现在**三个下发位置**: + +| 位置 | 字段 | +|---|---| +| 出牌推送 | `chupai1` / `chupai2` / `chupai3` 的 `data.cards` | +| 本轮进行态 | `playproc.cards[座位]`(`chupai*` 与重连包 `PushCards` 都带)| +| 重连出牌历史 | `PushCards.pushlist[轮次-1][座位]` | + +**这三处的数组内容逐元素完全相等**——顺序统一为「按**本局主牌花色**从大到小」的**权威顺序**。 +服务端只在落牌那一刻归一化一次(`class.paiju.js` 的 `order_playcards`),随后: +出牌推送读它、`playproc.cards` 存它、出牌历史 `playhistory` 归档它,下游一律**只读、不重排**。 + +因此前端**「增量回放」与「重连重建」得到的出牌历史必定一致**,两条路径可以复用同一份解析。 + +> ⚠️ **请求包里的 `cards` 顺序无意义**:那是玩家的点击顺序,同一手牌每次都可能不同。 +> 服务端不采信、不回显、也不据它判定牌型(server dev-guide 04 §8「前端不是数据源」)。 +> 一手单张时看不出差别;**一手多张(甩牌 / 对子 / 拖拉机)时才会显形**,尤其是同编码的一对牌 +> ——排序本身区分不了它们,只有「出牌当场归档」这一个写入处才能保证两条路径给出同一个数组。 + +--- + +## 1. 发牌(fapai) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:fapai + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| asetidx | 整数 | 当前局数 | +| asetcount | 整数 | 总局数 | +| cards | 数组 | 自己得到的牌id列表 | +| step | 整数 | **本局阶段**(发完牌恒为 `1` 叫分;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 | +| seat | 整数 | **控制权**:当前等待叫分者的位置。与重连包 `CallRun.seat` 同源(`get_callgrade_seat()`)| +| countdown | 整数 | 叫分倒计时 | + +--- + +## 2. 叫分或不叫(jiaofen,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:jiaofen + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 叫分者的位置序号 | +| call | 整数 | 分数,0表示不叫 | + +--- + +## 3. 叫分或不叫(jiaofen,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiaofen + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 叫分者的位置序号 | +| call | 整数 | 分数,0表示不叫 | +| currcall | 整数 | 当前叫到的分数 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| step | 整数 | **本局阶段**(叫分尚未结束,恒为 `1`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 | +| nextseat | 整数 | **控制权**:下一个叫分者的位置序号,即本包之后「轮到谁」。与重连包 `CallRun.seat` 同源(`get_callgrade_seat()`)| +| countdown | 整数 | 叫分倒计时 | + +--- + +## 4. 上庄(shangzhuang) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:shangzhuang + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 叫分者的位置序号 | +| call | 整数 | 分数,0表示不叫 | +| banker | 整数 | 庄家的位置序号 | +| grade | 整数 | 庄家的叫分 | +| step | 整数 | **本局阶段**(叫分结束、进入选主/投降,恒为 `2`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 | +| nextseat | 整数 | **控制权**:本包之后「轮到谁」——恒为 `banker`,因为选主与投降都只由庄家做(服务端 `mod.xuanzhu`/`mod.touxiang` 的座位校验同样以 `banker` 为准)。
⚠️ **别和本包的 `seat` 混用**:`seat` 是**最后一个叫分者**,不是控制权。与重连包 `ChooseMain.seat` 同源同值 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| bottomcards | 数组 | 8 张**底牌**(发牌时没发给玩家、扣在桌面的 8 张)。**庄家恒有**;**闲家仅当 70 分坐庄时才有**(供 3 秒亮牌用),非 70 分时闲家没有该属性(底牌只有庄家可见,design §4)。
**顺序在发牌结束时即冻结**(服务端 `paiju.bottomcards`,按「还没有主牌」的口径排一次):底牌是在**选主之前**翻给庄家看的,那时主牌花色尚不存在,故不按主牌花色排。本包与重连包 `ChooseMain.bottomcards` / `BuryCards.bottomcards` 三处**同序**,客户端存一次即可全程复用(含「查底牌」回看)| +| ancard3s | 整数 | **开底**标志。仅 70 分坐庄时出现且为 `1`:表示庄家**摸底**之前,需将 `bottomcards` 这 8 张**底牌**向所有玩家翻开 3 秒(design §4/§7.1);非 70 分无此属性 | +| cards | 数组 | 拿了底牌后手上的牌(36 张,含底牌),庄家才有此属性,闲家没有该属性 | +| countdown | 整数 | 选主倒计时 | +| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同为选主阶段(step2)的决策,见 touxiang 包与 design §4 | +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):按此刻累计捡分实时算出的判定倍率,**带符号**,与结算包 `aset.upgrade` 同口径——`3`/`2`/`1` = 庄家 大光/小光/过庄,`-N` = 闲家升 N 级,`0` = 叫分未定。上庄时捡分恒为 0,故必为 `3`。三家同值(捡分本就公开)。**不含扣底**——扣底要到末轮才产生。
顶部「抓分」角标只关心倍数大小,取 `Math.abs` 即可;**符号供客户端的判定动画分辨该播哪个**(不能取绝对值,否则 3 大光与 -3 升3级会撞在一起)| + +--- + +## 5. 投降(touxiang,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:touxiang + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 庄家的位置序号。投降是**选主阶段(step 2)与选主互斥**的选择:`mod.touxiang` 仅在 `step==2`、`banker==seat`、`call==70` 时受理;点投降即直接结算,不选主、不埋牌、不出牌(design §4) | + +--- + +## 6. 选主(xuanzhu,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:xuanzhu + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 选主者的位置序号 | +| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 | + +--- + +## 7. 选主(xuanzhu,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:xuanzhu + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| banker | 整数 | 庄家的位置序号 | +| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 | +| step | 整数 | **本局阶段**(选主完成、进入埋牌,恒为 `3`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 | +| nextseat | 整数 | **控制权**:本包之后「轮到谁」——恒为 `banker`,埋牌只由庄家做(服务端 `mod.maipai` 的座位校验以 `banker` 为准)。与重连包 `BuryCards.seat` 同源同值 | +| countdown | 整数 | 埋牌倒计时 | +| cards | 数组 | 选主后自己手上的牌id列表 | + +--- + +## 8. 埋牌(maipai,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:maipai + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 埋牌者的位置序号 | +| cards | 数组 | 埋牌的id列表 | + +--- + +## 9. 埋牌(maipai,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:maipai + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| cards | 数组 | 埋牌后手上的牌,去掉了埋牌,庄家才有此属性,闲家没有该属性 | +| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),庄家才有此属性,闲家没有该属性。注意与 `shangzhuang.bottomcards`(**底牌**,发牌留桌 8 张)是两批不同的牌,见 §0.0。
**取服务端权威快照 `get_burycard()`,不是客户端请求包里 `cards` 的原序**:按本局主牌花色从大到小排好,与重连包 `PushCards.burycards` **同源同序** | +| seatlist | 数组 | 三家座位牌况,**仅可查牌模式下发**(不查牌无此属性,design §9),结构与门控同 `chupai1/2/3.seatlist` 与重连包 `PushCards.seatlist`。
埋牌完成时它是刚初始化的**空表**(每家 `[[0,0],[0,0],[0,0],[0,0],[-1,-1]]`)——下发它是为了让「埋牌完成 → 庄家首出」这段窗口内,增量路径与重连路径拿到同一张表,客户端无需为这段窗口特判 | +| playproc | json | **本轮进行态**(design §5.1),**恒有、三家同值**,结构与门控同 [`chupai1.playproc`](#11-第一个玩家出牌chupai1)/重连包 `PushCards.playproc`(同一个快照函数 `get_playproc()`)。`do_burycard` 内部已 `new_playround` 就地初始化好 round-1 的进行态,此包带的正是这份初值:`round=1`、`start`/`currseat` 均为即将首出的庄家、`cards` 全空、`shuai_demand` 为 `null`。
**可见性**:全部字段由桌面公开信息推出(此刻尚无一张牌打出),三家整体下发、不逐座位裁剪 | +| step | 整数 | **本局阶段**(埋牌完成、进入出牌,恒为 `5`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 | +| seat | 整数 | **控制权**:出牌者的位置序号(即将首出的庄家)。服务端取自权威的 `playproc.currseat`,与重连包 `PushCards.playproc.currseat` 同源同值 | +| countdown | 整数 | 出牌倒计时 | +| liangpai | json | **亮牌**(design §8.2)。**只有闲家、且可查牌模式、且庄家达门槛时才有**;不达标或不查牌则无此属性。结构:`{ cards: [牌id...] }`——**庄家手中全部固定主牌的具体牌面**,按本局主牌序从大到小排好。
**固定主牌 = 双王 + 全部花色的 2 + 全部花色的 7**,**不含**主花色的普通牌 A/K/Q/J/10/9/8/6/5。
**门槛**(任一满足即下发):固定主牌总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6。**不限叫分**。
亮出的是**固定的一份**,不随被哪条门槛触发而增减;也**不给数量统计**——数量前端自己数 `cards.length` 即可。
口径是庄家**埋牌后的静态快照**(排除已埋的 8 张,但包含之后已打出的牌),全局固定不随出牌缩水,故重连包里取值一致 | + +--- + +## 10. 出牌(chupai,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:chupai + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 出牌者的位置序号 | +| cards | 数组 | 出牌的id列表 | + +--- + +## 11. 第一个玩家出牌(chupai1) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai1 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 出牌者的位置序号 | +| cards | 数组 | 实际打出的牌id列表;**甩错时**(见 `shuaicuo`)为被强制打出的那一张最小主牌单张。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序——请求包里的 `cards` 顺序服务端不采信、不回显(见 [§0.6](#06-一手出牌的顺序口径))| +| shuaicuo | 整数 | 甩错标志,仅甩错时出现且为 `1`(design §5.4.5:甩牌未通过最大性判定,整套甩牌收回,本轮只强制打出最小一张、失去本轮甩牌资格);正常出牌无此属性 | +| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` | +| count | 整数 | 出牌数量(甩错时为 1) | +| flower | 整数 | 出牌花色 | +| cardtype | 整数 | 出牌牌型:`>100` 单张(101 一张、102 两张…)、`>200` 对子(201 一对、202 两对…)、`>300` 拖拉机(302 两连对、303 三连对…),见 `class.pai.js` 顶部注释。**注意:cardtype 只能表达单一牌型**,而甩牌是单张/对子/拖拉机自由混搭(design §5.4.3),会被牌型推导压平成 1xx 或 2xx(例如「主K对 + 主5」得 103、「两连对 + 一散对」得 203)。**甩牌的真实结构请读 `shuai`,不要据 cardtype 反推** | +| shuai | json | 甩牌分量构成,**仅本次出牌是合法甩牌时才有**(非甩牌、以及甩错退化为单张时都没有该属性)。结构 `{ tractors: [连对数...], pairs: 独立对子数, singles: 单张数 }`,例如「主K对 + 主5」为 `{tractors:[],pairs:1,singles:1}`。与服务端跟牌时逐分量强制匹配用的 `shuai_demand` 同源(design §5.4.3/§5.4.4)| +| nextseat | 整数 | 下一个出牌者的位置序号 | +| countdown | 整数 | 下一个出牌者的出牌倒计时 | +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)| +| cardsinhand | 数组 | 出牌者出牌后手上剩下的牌id列表,只有出牌者才有此属性 | +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与结算包 `aset.upgrade` 同口径(`3`/`2`/`1` 庄家大光/小光/过庄,`-N` 闲家升 N 级,`0` 叫分未定)。同源同算法,差别仅在此处**不含扣底**。三家同值,随本包下发、不另开推送。符号是「谁赢」的区分,客户端判定动画靠它分辨 | +| playproc | json | **本轮进行态**(design §5.1)。**`chupai1/2/3` 三个包恒有,三家同值**;结构与重连包 [`PushCards.playproc`](#断线重连deskinfo) **完全一致**(服务端同一个快照函数 `get_playproc()`,前端增量回放与重连重建复用同一份解析)。
字段:`round` 第几轮、`start` 本轮首出位置、`currseat` 当前该谁出、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮暂时最大者、`maxcard` 其牌编码、`cards` 本轮三家各自出的牌(**定长 3,下标 = 座位序号**,未出的位置为 `null`)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 `null`)。
⚠️ **`chupai3` 带的是【下一轮】的进行态**(`round+1`、`cards` 全空、`currseat == nextseat == maxseat`):本轮第三家一出完,服务端就地开了新一轮。这与「此刻断线重连拿到的 `PushCards.playproc`」完全相同——本轮那三手牌客户端已由 `chupai1/2/3` 各自的 `seat`+`cards` 收到,收牌动画后即清台。**唯一例外**:`chupai3` 打完最后一张牌时本包会转成 `jiesuan`(见 §14),那种情况下**不带** `playproc`。
**可见性**:全部字段都由桌面公开信息推出(`cards` 就是已摊在桌上的牌,`shuai_demand` 与 `chupai1.shuai` 等价且甩出的牌本身已公开),故三家整体下发、不逐座位裁剪 | +| mustcard | 数组 | **下一个出牌者本轮跟牌的必出牌**(design §5.2),供其客户端自动选中。**只发给 `nextseat` 那一家**,其余两家无此属性——它是该玩家自己手牌的子集,整表下发会泄露他家手牌结构。以下情形不下发:`nextseat` 是本轮首家、首家为**甩牌**(甩牌跟牌走逐分量匹配,见 design §5.4.4)、或算出的必出牌为空 | + +--- + +## 12. 第二个玩家出牌(chupai2) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai2 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 出牌者的位置序号 | +| cards | 数组 | 出的牌id列表。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序(见 [§0.6](#06-一手出牌的顺序口径))| +| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` | +| nextseat | 整数 | 下一个出牌者的位置序号 | +| countdown | 整数 | 出牌倒计时 | +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)| +| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 | +| playproc | json | **本轮进行态**,恒有、三家同值,结构见 [§11 `playproc`](#11-第一个玩家出牌chupai1)。此时 `cards` 已含首家与本家两手牌,`currseat` 指向第三家 | +| mustcard | 数组 | **下一个出牌者本轮跟牌的必出牌**(design §5.2),供其客户端自动选中。**只发给 `nextseat` 那一家**,其余两家无此属性——它是该玩家自己手牌的子集,整表下发会泄露他家手牌结构。以下情形不下发:`nextseat` 是本轮首家、首家为**甩牌**、或算出的必出牌为空 | + +--- + +## 13. 第三个玩家出牌(chupai3) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai3 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 出牌者的位置序号 | +| cards | 数组 | 出的牌id列表。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序(见 [§0.6](#06-一手出牌的顺序口径))| +| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` | +| nextseat | 整数 | 下一个出牌者的位置序号 | +| countdown | 整数 | 出牌倒计时 | +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)| +| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 | +| maxseat | 整数 | 本轮出牌谁最大,只有本轮最后一个玩家出牌后才有此属性 | +| grade | 整数 | 本轮闲家得分,只有本轮最后一个玩家出牌后且闲家有得分才有此属性 | +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,口径同 `aset.upgrade`。同源同算法,差别仅在此处**不含扣底**。三家同值,随本包下发、不另开推送 | +| playproc | json | **本轮进行态**,三家同值,结构见 [§11 `playproc`](#11-第一个玩家出牌chupai1)。⚠️ 本包带的是**下一轮**的进行态(`round+1`、`cards` 全空、`currseat == nextseat == maxseat`),与此刻重连拿到的 `PushCards.playproc` 一致。**牌局在本包打完(转为 `jiesuan`)时不带此属性** | + +--- + +## 13.5 明牌(mingpai,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:mingpai + +查看另外两家手中全部主牌的具体牌面(design §9)。服务端仅在**可查牌模式、出牌阶段(step5)、且已有【任一】玩家报无主**(`have_baofu()`)时受理——注意判定的是「场上有人报无主」而非「请求者本人报无主」,报无主那位与另外两位同样有权查看(design §9.3);不满足时按 0.2 回 `mingpai` 失败包(不查牌 / 未报无主 → `RULE`,非出牌阶段 → `STEP`)。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 请求者的位置序号 | + +## 13.6 明牌(mingpai,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:mingpai (只回发给请求者) + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 请求者的位置序号 | +| others | 数组 | 另外两家各自未出的全部主牌,元素 `{ seat: 位置序号, zhucards: [主牌id列表] }` | + +> "再点一次取消查看"是客户端的显示开关,无需再请求服务端。 + +## 13.7 出牌提示(tishi,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:tishi + +闲家在出牌阶段向对家(另一闲家)发"踩/没分/有分"提示(design §11)。服务端仅在**出牌阶段(step5)、且发起者为闲家**(`seat != banker`)、**tip 合法(1/2/3)** 时受理;**不校验提示真实性**(玩家可主观发送);庄家无对家,不受理。不满足时按 0.2 回 `tishi` 失败包给发送者(庄家发 → `RULE`,非出牌阶段 → `STEP`,tip 非法或缺失 → `PARAM`)。**成功时不给发送者回执**,只转发给对家。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 发出提示的闲家位置序号 | +| tip | 整数 | 提示类型:1=踩(我能大过庄家)、2=没分(我手上没分了)、3=有分(我手上有分) | + +## 13.8 出牌提示(tishi,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:tishi (**只转发给对家**,即另一闲家 `3 - banker - seat`) + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 发出提示的闲家位置序号 | +| tip | 整数 | 提示类型:1=踩、2=没分、3=有分 | + +--- + +## 14. 结算(jiesuan) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiesuan + +结算包由以下几个子结构拼装而成。三种结算来源的组成不同: + +- **正常出牌结算**(`mod.chupai` 中最后一张牌出完,`get_paiju_account(0, ...)`):含 `chupai` + `bottom` + `aset`(末局再加 `account`)。 +- **投降结算**(`mod.touxiang`,`get_paiju_account(1, ...)`):只含 `aset`(末局再加 `account`),无 `chupai`、无 `bottom`。 +- **解散结算**(`export.get_disbandRoom`,`get_paiju_account(2, ...)`):只含 `aset` + `account`,无 `chupai`、无 `bottom`。**投递方式与上面两种不同,见 §14.1。** + +上面这张表头(`route: erqiwang / rpc: jiesuan`)**只适用于前两种**——正常出牌结算与投降结算,由子游戏自己 `sendpack_toother` 广播,客户端在 `rpc == "jiesuan"` 的分支里按 `data.aset` 取值。 + +### 14.1 解散结算的投递方式(与前两种不同,客户端需单独处理) + +解散**不是**由子游戏发包,而是平台在解散流程里回调 `youle_erqiwang.export.get_disbandRoom(o_room)`,把**返回值整个对象**塞进**房间路由**的解散包里下发(平台 `server_room/rpc.js`、`server_room/class.room.js`)。因此客户端收到的是: + +```json +{ + "app": "youle", "route": "room", "rpc": "free_room", + "data": { + "seats": [], + "deskfree": { + "rpc": "jiesuan", + "data": { "success": true, "aset": {}, "account": [] } + } + } +} +``` + +要点(照 §14 的表去 `data.aset` 取值会取空): + +- **rpc 是平台的 `free_room`(route 也是 `room`),不是 `erqiwang/jiesuan`**;子游戏返回的 `rpc: "jiesuan"` 只是嵌在 `deskfree` 里的一个普通字段。 +- **多一层 `data` 包装**:真实取值路径是 `data.deskfree.data.aset` 与 `data.deskfree.data.account`。 +- 本包的成败标志有两层:外层 `data.success` 由**平台**填写(解散流程本身成功与否),子游戏结算自带的 `success` 在 `data.deskfree.data.success`。§0.1「每个包 `data` 必带 `success`」说的是子游戏自己发的包;本包外层归平台。 +- `aset` / `account` 的字段结构与下面 §14 的表完全一致(`aset.multiple = 0`、`upgrade = 0`、各家 `grade = 0`,即解散局不结算子数与算奖;`account` 恒有,见 design §12.2「按当前累计分结算」)。 +- 若解散发生在**开战后、首局牌发出前**的窗口内(本游戏首局由 `makewar` 延迟 1 秒创建),`get_disbandRoom` 返回 `null`,平台走「不带 `deskfree`」分支——客户端需容忍 `data.deskfree` 缺失。 + +**顶层字段(三种结算来源恒有)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| success | 布尔 | 恒 `true`(§0.1)| +| step | 整数 | **本局阶段**,结算包恒为 `6`。三种结算来源(正常/投降/解散)统一由 `get_paiju_account` 给出——解散在此之前 `step` 可能还停在 1/2/3/5,由它落定为 6。客户端**不得按 rpc 名硬编码**,一律读此字段 | + +**chupai(出牌包,仅正常出牌结算存在)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 出牌者的位置序号,投降和解散无此属性 | +| cards | 数组 | 出的牌id列表,投降和解散无此属性 | +| maxseat | 整数 | 本轮出牌谁最大,投降和解散无此属性 | +| grade | 整数 | 本轮闲家得分,投降和解散无此属性 | +| gradecards | 数组 | 本轮闲家得分分牌列表,投降和解散无此属性。**注意:当前源码中赋值语句被注释(`class.paiju.js`/`mod.js` 中相关行均被注释掉),该字段实际永远不会出现在下发的包里,属于失效字段** | + +**bottom(抠底包,仅正常出牌结算存在;`cards` 恒有,其余仅闲家抠底时有)** + +> 本分组的 `cards` 是**埋牌底牌**(庄家埋下的 8 张),不是发牌留桌的底牌。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| cards | 数组 | **埋牌底牌**(庄家埋下的 8 张),投降和解散无此属性 | +| multiple | 整数 | 闲家抠底倍数,闲家没抠底无此属性,投降和解散无此属性 | +| grade1 | 整数 | 埋牌底牌的分数,闲家没抠底无此属性,投降和解散无此属性 | +| grade2 | 整数 | 闲家抠底得分,闲家没抠底无此属性,投降和解散无此属性 | + +**aset(单局结算包)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| banker | 整数 | 庄家的位置序号,-1:无庄 | +| call | 整数 | 庄家叫分,-1:无叫分 | +| multiple | 整数 | 基础子数 `get_base_bycall(call, climb)`(design §7.1/§7.3.1):常规算子 65→2、60→3、55→4、50 及以下→6、70打牌→2;投降固定 1、解散 0;无叫分 0 | +| flower | 整数 | 主牌花色,-1:无主牌花色 | +| grade | 整数 | 闲家捡分(含抠底) | +| upgrade | 整数 | 判定倍率(带符号,design §7.2.0):3大光 / 2小光 / 1过庄 / -N升N级(倒庄)/ -99投降 / 0解散或无判定 | +| bangwang | 整数 | 本局是否启用傍王规则 0否 1是(roomtype 位2) | +| climb | 整数 | 本局是否启用爬坡规则 0否 1是(roomtype 位3) | +| seatlist | 数组 | 玩家列表,元素结构见下 | + +`seatlist` 数组元素结构: + +```json +{ + "cards": [], // 冲关牌型:参与冲关的牌(王 + 冲关组合牌)列表,供「冲关牌型」界面展示 + "chongguan": 0, // 冲关奖数(design §8.1,旧称"常规算奖";只有庄家才计入自己的 N) + "wang": 0, // 手牌中的王数(傍王按此计奖) + "naward": 0, // 该家总奖数 N =(庄家?冲关奖数:0)+(傍王?王数:0) + "grade_aw": 0, // 算奖得分 = X×(2Ni−Nj−Nk),X 为每对子子数。= grade_cg + grade_bw + "grade_cg": 0, // 其中的【冲关】分量(design §8.1,不含傍王) + "grade_bw": 0, // 其中的【傍王】分量(design §8.3;未勾傍王时恒 0) + "grade_jf": 0, // 捡分子数得分 + "grade": 0, // 本局总分 = grade_aw + grade_jf + "score": 0 // 累计得分 +} +``` + +> 结算数值模型(design §7~§8):每「庄–闲」对子的基础金额 `X = multiple × |upgrade|`(投降 X=1、解散 X=0)。捡分子数:庄赢时两闲家各付庄家 X、庄家收 2X;闲赢(升级)时庄家各付两闲家 X。算奖:持有 N 奖的玩家从另外两人各多收 `X×N`,三家两两独立叠加(含闲–闲),即 `grade_aw = X×(2Ni−Nj−Nk)`。 +> +> **算奖的两个分量**(供大局结算分项展示):`N = N_冲关 + N_傍王`(冲关只计庄家、傍王勾选后庄闲都算)。 +> 服务端把两个分量**分别代入同一公式**各算一次,得到 `grade_cg` 与 `grade_bw`。 +> 公式对 `N` 是线性的,因此恒有 **`grade_cg + grade_bw == grade_aw`**,且两个分量各自零和。 +> 之所以必须在算钱时就拆,是因为两个 `N` 一旦相加就再也分不开——事后无法从 `grade_aw` 反推各自占比。 + +**account(大局结算包)** + +> 仅当打到最后一局(`o_paiju.idx >= o_room.asetcount`)或房间中途解散(解散结算)时,`jiesuan` 包才会带上此分组;普通的中间局结算包没有 `account` 字段(`class.paiju.js` `get_paiju_account`)。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| account | 数组 | 玩家列表(长度 3,下标 = 座位序号),元素结构见下 | + +```json +[ + { "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 }, + { "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 }, + { "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 } +] +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| score | 整数 | 累积总分(全部小局 `aset.seatlist[i].grade` 之和) | +| grades | 数组 | 每局总分列表,按局序 | +| grade_jf_total | 整数 | **基础分**累计:各局 `grade_jf`(捡分子数得分)之和 | +| grade_cg_total | 整数 | **冲关分**累计:各局 `grade_cg` 之和(design §8.1,**不含**傍王) | +| grade_bw_total | 整数 | **傍王分**累计:各局 `grade_bw` 之和(design §8.3;未勾傍王时恒 0) | + +> 恒等式:`grade_jf_total + grade_cg_total + grade_bw_total == score`,供大局结算面板分项展示(基础分 / 冲关分 / 傍王分 / 总分)。 +> +> ⚠️ **结构变更(2026-08-26)**:原为 `[[累积得分, [每局得分...]], ...]` 的二元数组,现改为**对象数组**并增加三项分解。数组下标语义不清,且前端尚未开工,此时改代价最小。 + +--- + +## 15. 准备(zhunbei,客户端→服务器) + +发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:zhunbei + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| agentid | 字符 | 代理id | +| playerid | 整数 | 玩家id | +| gameid | 字符 | 游戏id | +| roomcode | 整数 | 房间号 | +| seat | 整数 | 位置序号 | + +--- + +## 16. 准备(zhunbei,服务器→客户端) + +发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:zhunbei + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 位置序号 | + +--- + +## 断线重连(deskinfo) + +> 由平台在玩家进入房间/断线重连时回调 `youle_erqiwang.export.get_deskinfo(o_room, seat)` 生成并下发(服务器→客户端);下发的 app/route/rpc 由平台的进房/重连流程决定,不在本子游戏代码内固定。平台把它挂在 `pack.data.deskinfo` 下,**`data.success` 由平台填写、子游戏不注入**(见 0.4)。下列字段即该函数返回的 `deskinfo` 对象结构,按当前 `paiju.step` 只带对应阶段的分组。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| count | 整数 | 总局数 | +| idx | 整数 | 当前局数 | +| PlayerInfo | 数组 | 三个玩家目前的总积分 `[0,0,0]` | +| step | 整数 | 牌桌状态:1发完牌叫分 2选主/投降 3埋牌 5出牌 6结算(无独立投降阶段——投降在 step2 与选主互斥;埋牌后直接进入 step5 出牌) | +| MyCards | 数组 | 自己手上的牌 | + +**CallRun(不在叫分阶段无此属性)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | 当前叫分位置 | +| countdown | 整数 | 叫分倒计时 | +| nowcall | 整数 | 当前叫分 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| call | 数组 | 三家叫分 `[null,0,65]`,null还未叫分,0不叫,>0叫了多少分 | + +**ChooseMain(step 2 选主/投降阶段,不在此阶段无此属性)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | **控制权**:此刻轮到谁操作——恒为 `banker`(选主与投降都只由庄家做)。字段名与 `CallRun.seat` 一致:**各阶段分组里的 `seat` 统一表示「轮到谁」**。与增量推送 `shangzhuang.nextseat` 同源同值;客户端据此设控制权,**不要自己用 `banker` 反推**(那是把「选主=庄家」这条规则搬到前端)| +| banker | 整数 | 庄家 | +| call | 整数 | 叫分 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| countdown | 整数 | 选主倒计时 | +| bottomcards | 数组 | 8 张**底牌**(发牌留桌的 8 张),**只有庄家有此属性**(底牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)。顺序取发牌时冻结的快照,与 `shangzhuang.bottomcards`、`BuryCards.bottomcards` **完全同序**(见 §4)| +| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同一决策点,见 touxiang 包与 design §4 | +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `shangzhuang.curmultiple` 同源同值。选主阶段一张牌都还没出、捡分恒为 0,故**必为 `3`**(大光)。三家同值。有此字段,重连后顶部「抓分」角标才不会掉回 0 | + +> 选主阶段前端需在每个花色按钮上显示"该花色在庄家手中的对子数"(design §4/§11)——庄家的完整手牌由 `MyCards` 提供(庄家为 36 张),对子数由前端据此计算,服务端不额外下发。 + +**BuryCards(step 3 埋牌阶段,不在此阶段无此属性)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| seat | 整数 | **控制权**:此刻轮到谁操作——恒为 `banker`(埋牌只由庄家做)。与增量推送 `xuanzhu.nextseat` 同源同值 | +| banker | 整数 | 庄家 | +| call | 整数 | 叫分 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| flower | 整数 | 主牌花色 | +| countdown | 整数 | 埋牌倒计时 | +| bottomcards | 数组 | 8 张**底牌**(发牌留桌的 8 张),**只有庄家有此属性**(底牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)。顺序取发牌时冻结的快照,与 `shangzhuang.bottomcards`、`ChooseMain.bottomcards` **完全同序**——**不**按本局主牌花色重排(见 §4)| +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `shangzhuang.curmultiple` 同源同值。埋牌阶段同样一张牌未出、捡分恒为 0,故**必为 `3`**(大光)。三家同值 | + +> 埋牌阶段无投降(投降是 step2 与选主互斥的选择,选主后即不可再投降)。 + +**PushCards(step 5 出牌阶段,不在此阶段无此属性)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| banker | 整数 | 庄家 | +| call | 整数 | 叫分 | +| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)| +| flower | 整数 | 主牌花色 | +| countdown | 整数 | 出牌倒计时 | +| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),只有庄家有此属性。注意与 `ChooseMain`/`BuryCards.bottomcards`(**底牌**)是两批不同的牌,见 §0.0。与 `maipai.burycards` **同源同序**(同一个 `get_burycard()`,按本局主牌花色从大到小)| +| grade | 整数 | 当前的捡分分数 | +| gradecards | 数组 | 当前的捡分分牌,只有闲家有此属性 | +| playproc | json | **本轮进行态**,与 `chupai1/2/3.playproc` **同源同结构**(服务端同一个 `get_playproc()` 快照函数,见 [§11 `playproc`](#11-第一个玩家出牌chupai1)):`round` 第几轮、`start` 本轮首出位置、`currseat` 当前出牌位置、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮最大者位置、`maxcard` 最大牌编码、`cards` 本轮三家各自出的牌(**定长 3**,下标=位置序号,未出为 `null`)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 null,供跟牌逐分量强制匹配,design §5.4.4)。**两种查牌模式下恒有此属性**——`cards` 是当前这一轮桌面上的牌,不属于「查牌」,屏蔽了后出的人就无从跟牌(design §9 末尾)| +| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是,**恒有此属性**;与 `chupai1/2/3` 的 `baozhu` **同源同值同门控**(同一个 `have_baofu()` + 同一个查牌位)。它是**余主公示**与“明牌”按钮的开关,重连必须一并恢复,否则重连后按钮凭空消失;**不查牌模式恒为 0**(design §9)| +| seatlist | 数组 | 三家座位牌况,与上文 `maipai` / `chupai1/2/3` 包的 `seatlist` 同名同结构(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`)。**仅可查牌模式下有此属性**(design §9)| +| liangpai | json | **亮牌**,结构同 maipai 包的 `liangpai`(`{ cards: [牌id...] }`);**仅可查牌模式、且请求者为闲家、且庄家达标时有**(供闲家重连后仍能看到,design §8.2)| +| pushlist | 数组 | 出牌历史 `[[[], [], []], [[], [], []], ...]`,外层下标=轮次(从第 1 轮起,含进行中的当前轮),内层**恒为 3 个数组**、按位置序号存该轮各家出的牌id。**每一手与当时那个 `chupai1/2/3` 包的 `cards` 逐元素完全相等**(同一份数据、同一个顺序口径,见 [§0.6](#06-一手出牌的顺序口径)),因此前端“增量回放”与“重连重建”得到的出牌历史必定一致。**仅可查牌模式下有此属性**——往轮打出、已被收走的牌属于「查牌」范畴,不查牌模式一律不下发(design §9)。注意当前这一轮桌面上的牌由 `playproc.cards` 恢复,**两种模式下都有**,否则后出的人无从跟牌 | +| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `chupai1/2/3` 同源同值,供重连后顶部「抓分」角标与判定动画状态立即正确 | +| mustcard | 数组 | **本轮跟牌的必出牌**(design §5.2)。**仅当 `playproc.currseat == 请求者座位` 时才有**,且只算请求者自己的手牌;未轮到本家、本轮首家、甩牌局面、必出牌为空时均无此属性 | + +**Balance(不在结算阶段无此属性)** + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| readystate | 数组 | 所有玩家的准备状态 | +| aset | json | 单局结算包,同上面结算包中的 `aset`,只有当自己的准备状态为0时才有此属性 | +| bottom | json | **抠底包**,结构同 §14 的 `bottom`,与该局 `jiesuan` 推送的 `data.bottom` **同源同值**(服务端在 `get_paiju_account` 末尾冻结的同一份快照)。**仅正常出牌结算才有**(投降/解散没有抠底这一步);同样受「自己的准备状态为 0」门控。
**为什么必须有**:结算面板还开着时断线重连/硬刷新,只恢复 `aset` 会让抠底明细整块空白——同一份数据两条路径给的不一样(server 红线「发全下发面」)。
**可见性**:埋牌底牌在本局结算时已随 `jiesuan` 广播给三家(design §11 结束亮底),此处不构成额外泄露 | +| account | json | **大局结算包**,结构同 §14 的 `account`,与该局 `jiesuan` 推送的 `data.account` **同源同值**。**仅末局或中途解散才有**,与 `jiesuan` 的取舍完全一致(有就带、没有就不带)| + +--- + +## 战绩(大局列表)gameinfo1 + +> 非客户端收发包:大局结束/解散时由 `class.desk.js` `get_desk_account` 组装,经 `import.save_grade(o_room, o_gameinfo1, o_gameinfo2, 1)` 传给平台战绩服务持久化(服务器→平台)。 + +| 参数名 | 类型 | 说明 | +| --- | --- | --- | +| roomcode | 整数 | 房号 | +| asetcount | 整数 | 实际局数 | +| createtime | 字符 | 开房时间 | +| makewartime | 字符 | 开战时间 | +| players | 数组 | 玩家列表,元素结构见下 | + +`players` 数组元素结构: + +```json +{ + "seat": 0, // 座位 + "playerid": 100001, // 玩家ID + "name": "", // 昵称 + "avatar": "", // 头像 + "score": 0 // 成绩 +} +``` + +--- + +## 战绩(大局)gameinfo2 + +> 非客户端收发包:与 gameinfo1 同批,由 `import.save_grade` 的第 3 个参数传给平台战绩服务(服务器→平台)。 + +牌局列表,数组元素结构: + +```json +{ + "starttime": "", // 开始时间 + "endtime": "", // 结束时间 + "seatlist": [6, -6, 0], // 玩家成绩 + "callproc": [], // 叫分过程,详情见代码中的注释 + "banker": 0, // 庄 + "call": 0, // 叫分 + "flower": 0, // 主牌花色 + "result": 0, // 牌局结果 + "cards": [] // 发牌出牌情况,详情见代码中的注释 +} +``` diff --git a/server/games/erqiwang/docs/protocol/二七王接口.xlsx b/server/games/erqiwang/docs/protocol/二七王接口.xlsx new file mode 100644 index 0000000..cda4310 Binary files /dev/null and b/server/games/erqiwang/docs/protocol/二七王接口.xlsx differ diff --git a/server/games/erqiwang/mod.js b/server/games/erqiwang/mod.js new file mode 100644 index 0000000..66e9e9d --- /dev/null +++ b/server/games/erqiwang/mod.js @@ -0,0 +1,791 @@ +////////////////////////////////////////////////////////////////// +/////////////////////////////咻一咻///////////////////////////// +////////////////////////////////////////////////////////////////// + +var youle_erqiwang = youle_erqiwang || cls_mod.new("youle_erqiwang", "erqiwang", youle_app); + +//配置解析类(roomtype 位串解析,SSOT) +min_loadJsFile("games/erqiwang/class.config.js", function(){ +//牌类 +min_loadJsFile("games/erqiwang/class.pai.js", function(){ +//牌局类 +min_loadJsFile("games/erqiwang/class.paiju.js", function(){ +//牌桌类 +min_loadJsFile("games/erqiwang/class.desk.js", function(){ +//前后端共享牌值算法 +min_loadJsFile("games/erqiwang/shared/cards.js", function(){ +//算法类 +min_loadJsFile("games/erqiwang/class.arith.js", function(){ +//对内的输入接口类 +min_loadJsFile("games/erqiwang/class.import.js", function(){ +//对内的输出接口类 +min_loadJsFile("games/erqiwang/class.export.js", function(){ + +}); +}); +}); +}); +}); +}); +}); +}); + +//对内的输入接口类(需要的外部接口) +youle_erqiwang.import = null; +//对内的输出接口类(提供给外部使用的接口) +youle_erqiwang.export = null; + +//失败码(协议层约定,非 design 玩法规则;详见 docs/protocol/packet_protocol.md) +//前端一律只看 data.success 判成败,errcode 仅用于区分失败原因、给出对应提示 +youle_erqiwang.ERR = { + PLAYER: 1, //玩家/房间/座位校验不通过 + NODESK: 2, //牌桌或牌局不存在 + STEP: 3, //当前阶段不允许该操作 + SEAT: 4, //位置不符,或还没轮到该玩家操作 + PARAM: 5, //参数非法(类型/范围/张数/牌不在手上) + RULE: 6 //规则不允许(叫分未更低、出牌不合法、投降条件不满足、房间模式禁止等) +}; + +//失败回包:只回发给请求者,data.success=false +//(dev-guide server 03 §4/§5:成败标志唯一是 data.success,主动推送的 data 必须自带 success) +youle_erqiwang.do_sendfail = function(pack, rpc, errcode){ + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.rpc = rpc; + msg.data = {}; + msg.data.success = false; + msg.data.errcode = errcode; + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + youle_erqiwang.app.SendPack(msg); +} + +//叫分或不叫 +youle_erqiwang.jiaofen = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var seat = parseInt(pack.data.seat); + var call = parseInt(pack.data.call); + //检查玩家的位置是否正确 + var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid); + if (!o_room){ + youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.PLAYER); + return; + } + //牌桌 + var o_desk = o_room.o_desk; + if (!o_desk){ + youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.NODESK); + return; + } + //牌局 + var o_paiju = o_desk.method.curr_paiju(); + if (!o_paiju){ + youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.NODESK); + return; + } + //检查当前的游戏阶段是否是叫分阶段 + if (o_paiju.step != 1){ + youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.STEP); + return; + } + //检查当前的叫分位置 + if (o_paiju.method.get_callgrade_seat() != seat){ + youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.SEAT); + return; + } + //检查叫分:0(不叫)或 5~70 且为 5 的整数倍(design §4) + if (call < 0 || call > 70 || call % 5 != 0){ + youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.PARAM); + return; + } + var curr_call = o_paiju.method.get_callgrade_value(); + //第一家不能不叫分 + if (curr_call == 0 && call == 0){ + youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.RULE); + return; + } + //叫分必须小于之前的叫分 + if (curr_call != 0 && call >= curr_call){ + youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.RULE); + return; + } + + //本局房间可选规则:爬坡改变「叫分 → 基础子数」的取值(design §7.3.1), + //下发面的 multiple 必须与结算包 aset.multiple 用同一套规则,否则界面与结算不符 + var _cfg = cls_youle_erqiwang_config.parse(o_room.roomtype); + + //叫分成功 + o_paiju.method.do_callgrade(call); + + //返回前端 + if (o_paiju.step == 1){ + //等待下一家叫分 + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.rpc = "jiaofen"; + msg.data = {}; + msg.data.success = true; + msg.data.seat = seat; + msg.data.call = call; + msg.data.currcall = o_paiju.method.get_callgrade_value(); + msg.data.multiple = cls_youle_erqiwang_arith.get_base_bycall(msg.data.currcall, _cfg.climb); + //阶段:叫分尚未结束,仍是 step1。阶段唯一由服务端维护、并在包里显式给出—— + //前端不得按 rpc 名反推(server 红线「状态机唯一在服务端 / 任何状态变更都要有包承载」) + msg.data.step = o_paiju.step; + //控制权:下一个叫分者(本包的「轮到谁」就是它,与重连包 CallRun.seat 同一个函数) + msg.data.nextseat = o_paiju.method.get_callgrade_seat(); + msg.data.countdown = o_desk.method.get_countdown_jiaofen(); + o_room.method.sendpack_toother(msg, -1); + return; + } + if (o_paiju.step == 2){ + //等待庄家叫主 + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.rpc = "shangzhuang"; + msg.data = {}; + msg.data.success = true; + msg.data.seat = seat; + msg.data.call = call; + msg.data.banker = o_paiju.banker; + msg.data.grade = o_paiju.call; + msg.data.multiple = cls_youle_erqiwang_arith.get_base_bycall(msg.data.grade, _cfg.climb); + //阶段:叫分结束、进入选主/投降(step2)。与重连包 deskinfo.step 同源同值 + msg.data.step = o_paiju.step; + //控制权:选主与投降都只由庄家做(mod.xuanzhu / mod.touxiang 的 SEAT 校验就以 banker 为准), + //故这里的「轮到谁」= o_paiju.banker —— 权威值取自牌局对象本身,不是前端按 banker 自行推导。 + //本包的 data.seat 是【最后一个叫分者】、不是控制权,别混用(协议 §4) + msg.data.nextseat = o_paiju.banker; + msg.data.countdown = o_desk.method.get_countdown_xuanzhu(); + if (o_paiju.call != 70){ + msg.data.touxiang = 0; + } else { + msg.data.touxiang = 1; + } + //70分坐庄:庄家摸底牌前,8张底牌向所有玩家亮3秒(design §4/§7.1) + if (o_paiju.call == 70){ + msg.data.ancard3s = 1; + } + var _bottomcards = o_paiju.method.get_bottomcards(); + //当前抓分倍数(design §7.2.0):此刻捡分为 0,即大光×3;供客户端顶部「抓分」角标显示 + msg.data.curmultiple = o_paiju.method.get_curmultiple(); + + for (var i = 0; i < o_room.seatlist.length; i++) { + msg.conmode = o_room.seatlist[i].conmode; + msg.fromid = o_room.seatlist[i].fromid; + if (i == o_paiju.banker){ + //庄家:手牌(含摸起的底牌)与底牌本身都可见 + msg.data.cards = o_paiju.method.get_seat_cards(i); + msg.data.bottomcards = _bottomcards; + } else { + delete msg.data.cards; + //闲家:仅70分坐庄时才下发底牌(供亮3秒),否则底牌只有庄家可见 + if (o_paiju.call == 70){ + msg.data.bottomcards = _bottomcards; + } else { + delete msg.data.bottomcards; + } + } + youle_erqiwang.app.SendPack(msg); + } + return; + } +} + +//选主 +youle_erqiwang.xuanzhu = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var seat = parseInt(pack.data.seat); + var flower = parseInt(pack.data.flower); + //检查玩家的位置是否正确 + var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid); + if (!o_room){ + youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.PLAYER); + return; + } + //牌桌 + var o_desk = o_room.o_desk; + if (!o_desk){ + youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.NODESK); + return; + } + //牌局 + var o_paiju = o_desk.method.curr_paiju(); + if (!o_paiju){ + youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.NODESK); + return; + } + //检查当前的游戏阶段是否是选主阶段 + if (o_paiju.step != 2){ + youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.STEP); + return; + } + //检查庄家位置 + if (o_paiju.banker != seat){ + youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.SEAT); + return; + } + //检查花色 + if (flower != 1 && flower != 2 && flower != 3 && flower != 4){ + youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.PARAM); + return; + } + + //选主成功 + o_paiju.method.do_choiceflower(flower); + + //返回前端 + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.rpc = "xuanzhu"; + msg.data = {}; + msg.data.success = true; + msg.data.banker = o_paiju.banker; + msg.data.flower = flower; + //阶段:选主完成即进入埋牌(step3)。与重连包 deskinfo.step 同源同值 + msg.data.step = o_paiju.step; + //控制权:埋牌只由庄家做(mod.maipai 的 SEAT 校验同样以 banker 为准), + //权威值取自牌局对象;前端不再自己套「埋牌 = 庄家」这条规则 + msg.data.nextseat = o_paiju.banker; + msg.data.countdown = o_desk.method.get_countdown_maipai(); + for (var i = 0; i < o_room.seatlist.length; i++) { + msg.conmode = o_room.seatlist[i].conmode; + msg.fromid = o_room.seatlist[i].fromid; + msg.data.cards = o_paiju.method.get_seat_cards(i); + youle_erqiwang.app.SendPack(msg); + } +} + +//埋牌 +youle_erqiwang.maipai = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var seat = parseInt(pack.data.seat); + var cards = pack.data.cards; + //检查玩家的位置是否正确 + var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid); + if (!o_room){ + youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.PLAYER); + return; + } + //牌桌 + var o_desk = o_room.o_desk; + if (!o_desk){ + youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.NODESK); + return; + } + //牌局 + var o_paiju = o_desk.method.curr_paiju(); + if (!o_paiju){ + youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.NODESK); + return; + } + //检查当前的游戏阶段是否是埋牌阶段 + if (o_paiju.step != 3){ + youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.STEP); + return; + } + //检查庄家位置 + if (o_paiju.banker != seat){ + youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.SEAT); + return; + } + //检查埋牌入参本身是否合法(数组/整数牌id/范围/无重复),须先于 length 判断 + if (!o_paiju.method.check_cards_valid(cards)){ + youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.PARAM); + return; + } + //检查埋牌是否是8张 + if (cards.length != 8){ + youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.PARAM); + return; + } + //检查埋牌是否都在庄家手上 + if (!o_paiju.method.check_cards_inhand(cards, seat)){ + youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.PARAM); + return; + } + + //埋牌成功 + o_paiju.method.do_burycard(cards); + + //亮牌(design §8.2):可查牌模式下,出牌开始向两个闲家亮出庄家手牌统计 + var _cfg = cls_youle_erqiwang_config.parse(o_room.roomtype); + var _liangpai = null; + if (!_cfg.nocheck){ + _liangpai = o_paiju.method.get_liangpai(); + } + + //返回前端 + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.rpc = "maipai"; + msg.data = {}; + msg.data.success = true; + //阶段:埋牌完成即进入出牌(step5,无独立投降阶段)。与重连包 deskinfo.step 同源同值 + msg.data.step = o_paiju.step; + //控制权:本包的 seat 就是「轮到谁」——即将首出的庄家,取自权威的 playproc.currseat + //(do_burycard 里 new_playround 刚写好),与重连包 PushCards.playproc.currseat 同源 + msg.data.seat = o_paiju.playproc.currseat; + msg.data.countdown = o_desk.method.get_countdown_chupai(); + if (!_cfg.nocheck){ + //三家座位牌况(缺门/无对 + 报副统计),仅可查牌下发——与 chupai1/2/3、重连包 + //PushCards.seatlist 同一份数据、同一道门控。埋牌完成时它是刚初始化的空表 + //(每家 [[0,0],[0,0],[0,0],[0,0],[-1,-1]]),本身不含任何信息;但「埋牌完成 → + //庄家首出」这段窗口内前端也要有表可画,漏发会让增量路径此刻为空、重连路径却有表。 + //可见性:表内只有「某家缺某花色/某花色无对」(由公开的跟牌行为推出)与报无主后 + //全场公示的余主数量(design §9 明确要求向全体三人展示),故三家整表下发不构成泄露。 + //【深拷贝】seatlist 是全程就地改写的活表,不把活对象挂进下发包(同 playproc/pushlist) + msg.data.seatlist = cls_youle_erqiwang_paiju.get_seatlist(o_paiju); + } + //本轮进行态(design §5.1):do_burycard 内部已调用 new_playround 就地初始化好了 round-1 + //的进行态,与重连包 PushCards.playproc 同一个快照函数、同一结构(三家无差别)。 + //没有它,「埋牌完成 → 庄家首出」这段窗口内增量路径的 table.playproc 是 null, + //重连路径却已有 round-1 初值,两条路径不一致。可见性:round/start/currseat 等全部 + //由桌面公开信息推出,cards 此刻全空、shuai_demand 为 null,不含任何私密信息, + //与 chupai1/2/3.playproc 一样三家整体下发、不逐座位裁剪。 + msg.data.playproc = cls_youle_erqiwang_paiju.get_playproc(o_paiju); + //埋牌底牌(本次埋下的 8 张):取服务端权威快照,【不回显客户端提交的 cards 原序】。 + //get_burycard 与重连包 PushCards.burycards 是同一个函数、同一套按本局主牌花色的排序, + //两条路径因此顺序一致(SSOT);直接回显 cards 会把客户端的选牌顺序当成下发顺序。 + var _burycards = cls_youle_erqiwang_paiju.get_burycard(o_paiju); + for (var i = 0; i < o_room.seatlist.length; i++) { + msg.conmode = o_room.seatlist[i].conmode; + msg.fromid = o_room.seatlist[i].fromid; + if (i == o_paiju.banker){ + msg.data.cards = o_paiju.method.get_seat_cards(i); + //埋牌底牌(本次埋下的 8 张)。与 shangzhuang.bottomcards(底牌,发牌留桌 8 张) + //是两批不同的牌,字段名刻意区分,勿混用 + msg.data.burycards = _burycards; + delete msg.data.liangpai; + } else { + delete msg.data.cards; + delete msg.data.burycards; + //闲家:可查牌且庄家达标时亮牌 + if (_liangpai){ + msg.data.liangpai = _liangpai; + } else { + delete msg.data.liangpai; + } + } + youle_erqiwang.app.SendPack(msg); + } +} + +//投降 +youle_erqiwang.touxiang = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var seat = parseInt(pack.data.seat); + //检查玩家的位置是否正确 + var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid); + if (!o_room){ + youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.PLAYER); + return; + } + //牌桌 + var o_desk = o_room.o_desk; + if (!o_desk){ + youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.NODESK); + return; + } + //牌局 + var o_paiju = o_desk.method.curr_paiju(); + if (!o_paiju){ + youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.NODESK); + return; + } + //投降是选主阶段(step 2)与选主互斥的选择:点投降直接结算,不选主、不埋牌(design §4) + if (o_paiju.step != 2){ + youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.STEP); + return; + } + //检查庄家位置 + if (o_paiju.banker != seat){ + youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.SEAT); + return; + } + //仅 70 分坐庄可投降(design §4 / §7.1) + if (o_paiju.call != 70){ + youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.RULE); + return; + } + + //投降结算 + o_paiju.step = 6; + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.rpc = "jiesuan"; + msg.data = {}; + msg = o_paiju.method.get_paiju_account(1, msg); + o_room.method.sendpack_toother(msg, -1); +} + +//出牌 +youle_erqiwang.chupai = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var seat = parseInt(pack.data.seat); + var cards = pack.data.cards; + //检查玩家的位置是否正确 + var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid); + if (!o_room){ + youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.PLAYER); + return; + } + //牌桌 + var o_desk = o_room.o_desk; + if (!o_desk){ + youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.NODESK); + return; + } + //牌局 + var o_paiju = o_desk.method.curr_paiju(); + if (!o_paiju){ + youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.NODESK); + return; + } + //检查当前的游戏阶段是否是出牌阶段 + if (o_paiju.step != 5){ + youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.STEP); + return; + } + //检查出牌位置 + if (seat != o_paiju.playproc.currseat){ + youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.SEAT); + return; + } + //检查出牌入参本身是否合法(数组/整数牌id/范围/无重复,含非空) + if (!o_paiju.method.check_cards_valid(cards)){ + youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.PARAM); + return; + } + //检查出牌是否都在玩家手上 + if (!o_paiju.method.check_cards_inhand(cards, seat)){ + youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.PARAM); + return; + } + + //检查选中的牌是否可出,并出牌 + var re = o_paiju.method.do_playcard(cards); + if (!re.result){ + youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.RULE); + return; + } + + //查牌模式(design §9):不查牌时不下发任何主牌统计(info/报无主) + var _cfg = cls_youle_erqiwang_config.parse(o_room.roomtype); + + //返回前端 + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.data = {}; + msg.data.success = true; + msg.data.seat = seat; + //实际打出的牌(甩错时为单张),顺序由 do_playcard 的 order_playcards 统一给出—— + //与 playproc.cards、重连包 PushCards.pushlist 同一个口径(SSOT)。 + //【不回落到入参 cards】那是客户端的点击顺序,服务端不采信、更不回显(server 04 §8); + //do_playcard 成功时三个分支都会给出 re.cards,缺了就是它的缺陷,显式暴露、不兜底。 + msg.data.cards = re.cards; + if (!_cfg.nocheck){ + //三家座位牌况(缺门/无对 + 报副统计),仅可查牌下发。 + //必须整表下发、不能只带出牌者一家:design §9 要求「一旦有玩家报无主,就为全体三人 + //显示另外两家各自的主牌数量与对子数」,服务端已在 do_playcard 里同时刷新三个座位 + //(class.paiju.js),只带一家会让另两家的统计滞后到它们各自出牌时才补上。 + //字段名与重连包 PushCards.seatlist 一致,前端两条路径复用同一份解析。 + //【深拷贝】seatlist 是全程就地改写的活表(本函数下方 do_playcard 每手都在改它), + //不把活对象挂进下发包(同 playproc/pushlist 的理由) + msg.data.seatlist = cls_youle_erqiwang_paiju.get_seatlist(o_paiju); + } + msg.data.nextseat = o_paiju.playproc.currseat; + msg.data.countdown = o_desk.method.get_countdown_chupai(); + //当前抓分倍数(design §7.2.0):随本包一起下发、不另开推送;三家同值,grade 本就公开 + msg.data.curmultiple = o_paiju.method.get_curmultiple(); + //本轮进行态(design §5.1):与重连包 PushCards.playproc 同一个快照函数、同一结构。 + //没有它,前端在非重连时就拿不到「本轮谁先手/谁最大/三家各出了什么/第几轮」,只能自己 + //累积推导——那就是把对局推进逻辑搬到前端(前端红线「数据驱动、前端无对局状态机」)。 + //注意 re.idx==3 时 do_playcard 已在内部调用 new_playround 就地开了新一轮, + //故 chupai3 带的是【新一轮】的进行态(与 nextseat 同步),这与此刻重连拿到的完全一致。 + //可见性:round/start/currseat/startcount/startflower/starttype/maxseat/maxcard 都是 + //由桌面公开信息直接推出的;cards 就是本轮已摊在桌上的牌;shuai_demand 是首家甩牌的 + //分量构成,而甩出的牌本身已公开、chupai1 也已把等价的 shuai 下发给三家。全部对三家可见, + //无需逐座位裁剪(重连包 PushCards.playproc 本就对三家无差别下发,两条路径口径一致) + msg.data.playproc = cls_youle_erqiwang_paiju.get_playproc(o_paiju); + if (re.shuaicuo){ + msg.data.shuaicuo = 1; //甩错:本次甩牌被收回,只打出了最小一张(§5.4.5) + } + //报无主:仅可查牌模式下才据此展示他家主牌数量/对子(§9) + if (!_cfg.nocheck && o_paiju.method.have_baofu()){ + msg.data.baozhu = 1; + } else { + msg.data.baozhu = 0; + } + + switch (re.idx) + { + case 1: //第一个出牌 + msg.rpc = "chupai1"; + msg.data.count = re.count; + msg.data.flower = re.flower; + msg.data.cardtype = re.cardtype; + //合法甩牌的分量构成(design §5.4.3),供前端按 拖拉机/对子/单张 分组展示; + //非甩牌(含甩错退化为单张)无此属性 + if (re.shuai){ + msg.data.shuai = re.shuai; + } + break; + case 2: //第二个出牌 + msg.rpc = "chupai2"; + break; + case 3: //第三个出牌 + msg.rpc = "chupai3"; + msg.data.maxseat = re.maxseat; + if (re.grade > 0){ + msg.data.grade = re.grade; + // msg.data.gradecards = re.gradecards; + } + break; + } + + if (o_paiju.step == 5){ + //下一个出牌者本轮跟牌的必出牌(design §5.2),供其客户端自动选中。 + //【只发给 nextseat 一家】——mustcard 是该玩家自己手牌的子集,发给他本人不泄露; + //整表下发三家会把另两家的手牌结构透露出去(server 红线:发全 ≠ 发多,按可见性下发)。 + var _nextseat = o_paiju.playproc.currseat; + var _mustcard = o_paiju.method.get_mustcard(_nextseat); + for (var i = 0; i < o_room.seatlist.length; i++){ + msg.conmode = o_room.seatlist[i].conmode; + msg.fromid = o_room.seatlist[i].fromid; + if (i == seat){ + msg.data.cardsinhand = o_paiju.method.get_seat_cards(i); + } else { + delete msg.data.cardsinhand; + } + if (i == _nextseat && _mustcard){ + msg.data.mustcard = _mustcard; + } else { + delete msg.data.mustcard; + } + youle_erqiwang.app.SendPack(msg); + } + } else if (o_paiju.step == 6){ + //小局结算 + //出牌包的调整 + msg.data.chupai = {}; + msg.data.chupai.seat = msg.data.seat; + msg.data.chupai.cards = msg.data.cards; + msg.data.chupai.maxseat = msg.data.maxseat; + if (msg.data.grade){ + msg.data.chupai.grade = msg.data.grade; + } + if (msg.data.gradecards){ + msg.data.chupai.gradecards = msg.data.gradecards; + } + delete msg.data.seat; + delete msg.data.cards; + delete msg.data.seatlist; + delete msg.data.nextseat; + delete msg.data.countdown; + delete msg.data.baozhu; + //本轮进行态只对「还要继续出牌」有意义;本包已转为 jiesuan,不带 + delete msg.data.playproc; + delete msg.data.cardsinhand; + delete msg.data.maxseat; + delete msg.data.grade; + delete msg.data.gradecards; + + //获取抠底包 + msg = o_paiju.method.get_bottom_account(msg); + //获取小局结算包 + msg = o_paiju.method.get_paiju_account(0, msg); + o_room.method.sendpack_toother(msg, -1); + } +} + +//明牌:查看另外两家手中全部主牌的具体牌面(design §9,仅可查牌模式且已有人报无主) +youle_erqiwang.mingpai = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var seat = parseInt(pack.data.seat); + //检查玩家的位置是否正确 + var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid); + if (!o_room){ + youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.PLAYER); + return; + } + //牌桌 + var o_desk = o_room.o_desk; + if (!o_desk){ + youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.NODESK); + return; + } + //牌局 + var o_paiju = o_desk.method.curr_paiju(); + if (!o_paiju){ + youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.NODESK); + return; + } + //只在出牌阶段 + if (o_paiju.step != 5){ + youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.STEP); + return; + } + //不查牌模式禁止明牌 + var cfg = cls_youle_erqiwang_config.parse(o_room.roomtype); + if (cfg.nocheck){ + youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.RULE); + return; + } + //必须已有玩家报无主才允许明牌 + if (!o_paiju.method.have_baofu()){ + youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.RULE); + return; + } + + //另外两家未出的全部主牌,只回发给请求者 + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.rpc = "mingpai"; + msg.data = {}; + msg.data.success = true; + msg.data.seat = seat; + msg.data.others = []; + for (var i = 0; i < 3; i++){ + if (i != seat){ + var one = {}; + one.seat = i; + one.zhucards = cls_youle_erqiwang_paiju.get_seat_zhucards(o_paiju, i); + msg.data.others.push(one); + } + } + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + youle_erqiwang.app.SendPack(msg); +} + +//闲家出牌提示(design §11):闲家在出牌阶段向对家(另一闲家)发"踩/没分/有分"信号。 +//纯转发、不校验真实性、只发给对家;庄家无对家,不参与。tip:1=踩 2=没分 3=有分。 +youle_erqiwang.tishi = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var seat = parseInt(pack.data.seat); + var tip = parseInt(pack.data.tip); + //检查玩家的位置是否正确 + var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid); + if (!o_room){ + youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.PLAYER); + return; + } + //牌桌 + var o_desk = o_room.o_desk; + if (!o_desk){ + youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.NODESK); + return; + } + //牌局 + var o_paiju = o_desk.method.curr_paiju(); + if (!o_paiju){ + youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.NODESK); + return; + } + //只在出牌阶段(step5)任意时刻可发 + if (o_paiju.step != 5){ + youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.STEP); + return; + } + //只有闲家可发提示,庄家无对家、不参与 + if (seat == o_paiju.banker){ + youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.RULE); + return; + } + //提示类型:1=踩 2=没分 3=有分 + if (tip != 1 && tip != 2 && tip != 3){ + youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.PARAM); + return; + } + + //对家 = 另一个闲家(三座位号之和为 0+1+2=3) + var partner = 3 - o_paiju.banker - seat; + + //只转发给对家 + var msg = {}; + msg.app = "youle"; + msg.route = "erqiwang"; + msg.rpc = "tishi"; + msg.data = {}; + msg.data.success = true; + msg.data.seat = seat; //发出提示的闲家 + msg.data.tip = tip; + msg.conmode = o_room.seatlist[partner].conmode; + msg.fromid = o_room.seatlist[partner].fromid; + youle_erqiwang.app.SendPack(msg); +} + +//准备 +youle_erqiwang.zhunbei = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var seat = parseInt(pack.data.seat); + //检查玩家的位置是否正确 + var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid); + if (!o_room){ + youle_erqiwang.do_sendfail(pack, "zhunbei", youle_erqiwang.ERR.PLAYER); + return; + } + //牌桌 + var o_desk = o_room.o_desk; + if (!o_desk){ + youle_erqiwang.do_sendfail(pack, "zhunbei", youle_erqiwang.ERR.NODESK); + return; + } + //牌局 + var o_paiju = o_desk.method.curr_paiju(); + if (!o_paiju){ + youle_erqiwang.do_sendfail(pack, "zhunbei", youle_erqiwang.ERR.NODESK); + return; + } + //检查当前的游戏阶段是否是结算阶段 + if (o_paiju.step != 6){ + youle_erqiwang.do_sendfail(pack, "zhunbei", youle_erqiwang.ERR.STEP); + return; + } + //检查当前局数小于总局数 + if (o_desk.paiju_list.length >= o_room.asetcount){ + youle_erqiwang.do_sendfail(pack, "zhunbei", youle_erqiwang.ERR.RULE); + return; + } + + //准备成功 + o_desk.method.do_prepare(seat); +} + +//Node(本地/单元测试)导出模块对象;友乐/浏览器无 module 时跳过(dev-guide 01 §1) +if (typeof module !== "undefined"){ + module.exports = youle_erqiwang; +} diff --git a/server/games/erqiwang/shared/cards.js b/server/games/erqiwang/shared/cards.js new file mode 100644 index 0000000..6365636 --- /dev/null +++ b/server/games/erqiwang/shared/cards.js @@ -0,0 +1,304 @@ +/////////////////////////////////////////////////////////////// +////// youle_erqiwang_shared_cards: 前后端共享牌值算法 ////////// +/////////////////////////////////////////////////////////////// +// 【权威源】本文件是前后端共享算法的唯一可修改处。 +// 前端副本 client/js/01_SubGame/codes/shared/cards.js 由仓库根目录的 +// sync_shared.cmd 单向同步生成,为只读;任何改动都必须落在本文件, +// 改完跑一次 sync_shared.cmd,否则 client/tests/test_shared_sync.js 会红。 +// +// 内容:牌值编码与牌型分组的纯函数,不依赖对局状态、不依赖平台 API。 +// 牌编码区间约定(id_to_code 的产物,各处判定据此): +// 9554 大王 / 9553 小王 / 8000–9000 正7 / 7000–8000 副7 +// 3000–4000 正2 / 2000–3000 副2 / 1000–2000 主花色普通牌 / 100–500 副牌普通牌 +// ⇒ 主牌 ⟺ code > 1000 +var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { + + //——— 牌编码区间边界:上面那张区间表的机器可读形式,是【全部区间判定的唯一权威来源】——— + //一律是【开区间】:CODE_ZHENG7_MIN < code < CODE_ZHENG7_MAX ⟺ 正7,其余同理。 + //本文件内部(is_continuous / trump_rank / id_to_code)与前端 core/CardMark.js 都引用这些常量, + //不再各处重抄 3000/4000/8000/9000 之类裸字面量(工程总则 §1 SSOT / §6 配置优先于硬编码)。 + CODE_XIAOWANG: 9553, //小王(单值,非区间) + CODE_DAWANG: 9554, //大王(单值,非区间) + CODE_ZHENG7_MIN: 8000, //正7:8000 < code < 9000 + CODE_ZHENG7_MAX: 9000, + CODE_FU7_MIN: 7000, //副7:7000 < code < 8000 + CODE_FU7_MAX: 8000, + CODE_ZHENG2_MIN: 3000, //正2:3000 < code < 4000 + CODE_ZHENG2_MAX: 4000, + CODE_FU2_MIN: 2000, //副2:2000 < code < 3000 + CODE_FU2_MAX: 3000, + CODE_ZHU_MIN: 1000, //主牌 ⟺ code > CODE_ZHU_MIN;主花色普通牌:1000 < code < 2000 + CODE_ZHU_MAX: 2000, + CODE_A_IN_ZHU: 14, //主A 的 code % 100(A 在 id_to_code 里 +13 提到 14) + + //几对连续对子起算拖拉机(design §5.3)。group_tractor_runs 返回极大连续段, + //段长 >= 本值即拖拉机——服务端分解甩牌与前端手牌标记共用这一个阈值 + TRACTOR_MIN_PAIRS: 2, + + //牌id转花色 + id_to_flower: function(cardid){ + if (cardid == 52 || cardid == 106){ + //小王 + return 5; + } + if (cardid == 53 || cardid == 107){ + //大王 + return 5; + } + return parseInt((cardid % 54) / 13) + 1; + }, + + //牌id转牌面数值 + id_to_number: function(cardid){ + if (cardid == 52 || cardid == 106){ + //小王 + return 53; + } + if (cardid == 53 || cardid == 107){ + //大王 + return 54; + } + return (cardid % 54) % 13 + 1; + }, + + //牌id转牌编码 + id_to_code: function(mainflower, cardid){ + if (cardid == 52 || cardid == 106){ + //小王 + return youle_erqiwang_shared_cards.CODE_XIAOWANG; + } + if (cardid == 53 || cardid == 107){ + //大王 + return youle_erqiwang_shared_cards.CODE_DAWANG; + } + var flower = youle_erqiwang_shared_cards.id_to_flower(cardid); + var number = youle_erqiwang_shared_cards.id_to_number(cardid); + var code = flower * 100 + number; + //特殊处理 + switch (number) + { + case 1: + //A 提到 K 之上(14) + code = code + 13; + break; + case 2: + //2 提到副2 段 + code = code + youle_erqiwang_shared_cards.CODE_FU2_MIN; + break; + case 7: + //7 提到副7 段 + code = code + youle_erqiwang_shared_cards.CODE_FU7_MIN; + break; + } + //主牌花色处理:整体再上移一个主牌段(副2→正2、副7→正7、普通牌→主花色普通牌) + if (flower == mainflower){ + code = code + youle_erqiwang_shared_cards.CODE_ZHU_MIN; + } + return code; + }, + + //从大到小排序(冒泡排序) + order_cards: function(mainflower, aryCardIDs){ + if (aryCardIDs){ + for (var i = 0; i < aryCardIDs.length; i++) { + for (var j = i + 1; j < aryCardIDs.length; j++) { + var i_code = youle_erqiwang_shared_cards.id_to_code(mainflower, aryCardIDs[i]); + var j_code = youle_erqiwang_shared_cards.id_to_code(mainflower, aryCardIDs[j]); + if (i_code < j_code) { + var tmp = aryCardIDs[j]; + aryCardIDs[j] = aryCardIDs[i]; + aryCardIDs[i] = tmp; + } + } + } + } + return aryCardIDs; + }, + + //判断两个牌是否是连续的 + is_continuous: function(bigcode, smallcode){ + var S = youle_erqiwang_shared_cards; + //是小王 + var is_xiaowang = function(_code){ + if (_code == S.CODE_XIAOWANG){ + return true; + } + return false; + } + //是正7 + var is_zheng7 = function(_code){ + if (_code > S.CODE_ZHENG7_MIN && _code < S.CODE_ZHENG7_MAX){ + return true; + } + return false; + } + //是负7 + var is_fu7 = function(_code){ + if (_code > S.CODE_FU7_MIN && _code < S.CODE_FU7_MAX){ + return true; + } + return false; + } + //是正2 + var is_zheng2 = function(_code){ + if (_code > S.CODE_ZHENG2_MIN && _code < S.CODE_ZHENG2_MAX){ + return true; + } + return false; + } + //是负2 + var is_fu2 = function(_code){ + if (_code > S.CODE_FU2_MIN && _code < S.CODE_FU2_MAX){ + return true; + } + return false; + } + //是主A + var is_zhuA = function(_code){ + if (_code > S.CODE_ZHU_MIN && _code < S.CODE_ZHU_MAX && _code % 100 == S.CODE_A_IN_ZHU){ + return true; + } + return false; + } + //是8 + var is_8 = function(_code){ + if (_code % 100 == 8){ + return true; + } + return false; + } + //是6 + var is_6 = function(_code){ + if (_code % 100 == 6){ + return true; + } + return false; + } + + //普通的连牌 + if (bigcode - smallcode == 1){ + return true; + } + //小王、正7 + if (is_xiaowang(bigcode) && is_zheng7(smallcode)){ + return true; + } + //正7、负7 + if (is_zheng7(bigcode) && is_fu7(smallcode)){ + return true; + } + //负7、正2 + if (is_fu7(bigcode) && is_zheng2(smallcode)){ + return true; + } + //正2、负2 + if (is_zheng2(bigcode) && is_fu2(smallcode)){ + return true; + } + //负2、主A + if (is_fu2(bigcode) && is_zhuA(smallcode)){ + return true; + } + //8、6(design §5.3:7 不与 6/8 相连,6 与 8 相连) + //必须是同一花色的 8 与 6:牌编码同花色相差 2(主花色 1408-1406=2、副牌 308-306=2), + //跨花色则相差 100 的倍数。只判 %100 会把「♥8 + ♣6」误判为连对, + //进而在 get_chongguan 扫描整手牌时让副牌的 6 对错误接续主 8、多算奖(design §8.1 连对链) + if (is_8(bigcode) && is_6(smallcode) && (bigcode - smallcode == 2)){ + return true; + } + return false; + }, + + //获取对子列表,cards为同一花色的排过序的牌列表 + get_pairlist: function(mainflower, flowercards){ + var pairlist = []; + var i = 0; + while (i < flowercards.length - 1){ + //牌编码,两张牌两张牌一取 + var code_i = youle_erqiwang_shared_cards.id_to_code(mainflower, flowercards[i]); + var code_j = youle_erqiwang_shared_cards.id_to_code(mainflower, flowercards[i + 1]); + if (code_i == code_j){ + pairlist.push([flowercards[i], flowercards[i + 1]]); + i = i + 2; + } else { + i++; + } + } + return pairlist; + }, + + //把对子列表扫成【极大连续段】。pairlist 为同一牌组内、已按牌编码从大到小排好的对子列表 + //(形如 get_pairlist 的返回值:[[cardid, cardid], ...])。 + //返回 [[pair, ...], ...]:每一段是若干【相邻且 is_continuous】的对子,段与段之间必不连续; + //孤立对子也作为长度 1 的段返回,故 concat 所有段 == 原 pairlist(顺序不变)。 + //段长 >= TRACTOR_MIN_PAIRS 即拖拉机。 + //【唯一实现】服务端 decompose_trump(甩牌分解)与前端 core/CardMark.js(手牌「拖」标记) + //此前各写过一遍同样的扫描,规则内容(什么算连续、几对起算拖拉机)散在两处——现收敛到这里。 + group_tractor_runs: function(mainflower, pairlist){ + var runs = []; + var i = 0; + while (i < pairlist.length){ + var run = [pairlist[i]]; + var k = i; + while (k + 1 < pairlist.length){ + var code_k = youle_erqiwang_shared_cards.id_to_code(mainflower, pairlist[k][0]); + var code_k1 = youle_erqiwang_shared_cards.id_to_code(mainflower, pairlist[k + 1][0]); + if (!youle_erqiwang_shared_cards.is_continuous(code_k, code_k1)){ + break; + } + run.push(pairlist[k + 1]); + k++; + } + runs.push(run); + i = k + 1; + } + return runs; + }, + + //获取拖拉机列表,pairlist为同一花色的排过序的对子列表,cardtype为拖拉机牌型 + get_tuolaji_list: function(mainflower, pairlist, cardtype){ + var tuolaji = []; + var count = cardtype - 300; //拖拉机的对子数量 + var i = 0; + for (var i = 0; i < pairlist.length - count + 1; i++){ + //每次按拖拉机的对子数量取N对 + var tmplist = pairlist.slice(i, i + count); + //判断是否是cardtype类型的拖拉机 + var is_tlj = true; + for (var j = 0; j < tmplist.length - 1; j++) { + var code_j1 = youle_erqiwang_shared_cards.id_to_code(mainflower, tmplist[j][0]); + var code_j2 = youle_erqiwang_shared_cards.id_to_code(mainflower, tmplist[j+1][0]); + if (!youle_erqiwang_shared_cards.is_continuous(code_j1, code_j2)){ + is_tlj = false; + break; + } + } + if (is_tlj){ + var tlj = []; + for (var k = 0; k < tmplist.length; k++){ + tlj = tlj.concat(tmplist[k]); + } + tuolaji.push(tlj); + } + } + return tuolaji; + }, + + //主牌归一化大小(副7 同级取 7000、副2 同级取 2000,其余用牌编码),仅用于甩牌分量比大小 + trump_rank: function(mainflower, cardid){ + var code = youle_erqiwang_shared_cards.id_to_code(mainflower, cardid); + if (code > youle_erqiwang_shared_cards.CODE_FU7_MIN && code < youle_erqiwang_shared_cards.CODE_FU7_MAX){ //副7 + return youle_erqiwang_shared_cards.CODE_FU7_MIN; + } + if (code > youle_erqiwang_shared_cards.CODE_FU2_MIN && code < youle_erqiwang_shared_cards.CODE_FU2_MAX){ //副2 + return youle_erqiwang_shared_cards.CODE_FU2_MIN; + } + return code; + }, + +} + +//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1) +if (typeof module !== "undefined"){ + module.exports = youle_erqiwang_shared_cards; +} diff --git a/server/games/erqiwang/test/README.md b/server/games/erqiwang/test/README.md new file mode 100644 index 0000000..f088095 --- /dev/null +++ b/server/games/erqiwang/test/README.md @@ -0,0 +1,62 @@ +# 二七王服务端单元测试 + +## 运行 + +```bash +node server/games/erqiwang/test/run.js # 跑全部单测 +node server/games/erqiwang/test/test_arith.js # 单独跑某一组 +``` + +退出码 0 表示全过,非 0 表示有失败;`run.js` 逐个以独立进程 spawn 各 `test_*.js` 并汇总。 + +## 原理:一套代码,两个运行时(dev-guide 01 §1) + +正式代码(`class.*.js`)遵循平台的双运行时规范: + +- **友乐 / 浏览器**:无 `require`,由 `mod.js` 用 `min_loadJsFile` 加载为**全局对象**;文件顶部的 `if (typeof require !== 'undefined')` 守卫被跳过,跨模块按**同名全局**引用。 +- **Node(本地 / 单元测试)**:有 `require`,守卫块 `var X = require('./class.X.js')` 取到依赖;文件底部 `if (typeof module !== 'undefined') module.exports = ...` 导出。 + +因此这两处守卫是**纯增量**:友乐运行时行为完全不变,只在 Node 下额外启用 require/exports。 + +## `_shim.js`:平台全局垫片(仅测试用,不进正式代码) + +正式代码按全局名引用少量平台工具(`min_ary_include/min_ary_deduct/min_random/min_now/min_ontimeout`)与模块入口 `youle_erqiwang.import`。友乐由平台提供,Node 单测由 `test/_shim.js` 提供等价实现(数组/随机工具忠实复刻 `server/minhttp.js`,`youle_erqiwang.import` 用 mock)。测试文件顶部先 `require('./_shim')` 再 `require('../class.*.js')`。 + +## 测试计划 + +完整的"规则 → 用例"覆盖矩阵、缺口清单与优先级见 [`../docs/compliance/02-测试计划.md`](../docs/compliance/02-测试计划.md)。 + +## 覆盖 + +- `test_arith.js`:主牌排序/相邻链(§3)、扣底倍数(§6.3)、算子常规+爬坡逐档+分界+升3级(§7)、算奖 get_chongguan(§8.1)、甩牌分解/最大性/合法性(§5.4)、跟甩牌强制分量拆解(§5.4.4)。 +- `test_follow.js`:正常跟牌/毙牌/垫牌,单张/对子/拖拉机的必出与缺门毙牌数量对应(§5.1/5.2)。 +- `test_callgrade.js`:叫分坐庄 do_callgrade(叫5立即/两家不叫/后叫更低)(§4.2)。 +- `test_config.js`:roomtype 位串解析、局数/扣卡映射(§10.1)。 +- `test_paiju.js`:亮牌 get_liangpai(§8.2,含「埋牌后静态快照、出牌后重连不缩水」回归)、结算 get_paiju_account 集成(§7/§8,大光/过庄/升级/爬坡/傍王/投降)、扣底触发 get_bottom_account(§6.3)。 +- `test_deal.js`:发牌构成——92张/去3-4/三家28+底8/分值(§2/§6.1)。 +- `test_desk.js`:坐庄轮换 do_prepare(庄赢连庄/闲赢·投降下家)(§4.6)。 +- `test_endgame.js`(L4 端到端,用真实发牌跑完整一局):叫分→上庄→选主→埋牌→28 轮出牌→小局结算的流程闭合(§12.1),以及与牌面无关的不变量(牌张守恒 84/8/16、捡分=闲家赢得分牌+扣底分、`X` 分配庄±2X/闲∓X、算奖 `X×(2Ni−Nj−Nk)`、结算零和、`result` 与判定一致);另覆盖 §4.4/§7.1 投降局与 §12.2 中途解散两条支线。 +- `test_rpc.js`(L3,用 `_rpc.js` 装配 mod.js + 捕获下发包):明牌 mingpai、出牌 chupai 的 seatlist/baozhu 查牌门控(§9,含「seatlist 整表带三家」)、投降 touxiang 条件、jiaofen 入参校验、70分上庄暗牌亮牌下发(§4)。 + +### 两组差分测试(按 design 原文另写参考实现,与代码对拍) + +下面两组与上面的手写用例互补:它们不枚举"我想到的场景",而是**把 design 的规则整条转写成参考实现**,再和代码大面积比对,专治手写用例覆盖不到的欠约束分支。 + +- `test_calc.js`:**§7 算子全量对拍**。参考实现逐字转写自 §7.1/§7.2/§7.3 的判定表,与 `get_base_bycall` / `get_qvalue` / `get_upgrade` 比对 **2 种算子模式 × 14 个叫分档 × grade 0~260 = 7308 格**(每格比 base/Q/判定倍率/最终子数四项),另按 design 的 17 张表逐格抽查 141 个"写死的最终子数"。断言按(模式,叫分档)与表名分组,失败能直接定位到档位。 +- `test_followdiff.js`:**§5.2 跟牌强制层级穷举差分**。参考裁定按 §5.2 原文写(缺门/不足/够牌 × 单张/对子/N 连对 + 最长拖拉机优先的档案比较),对每手牌**枚举全部 C 张出牌组合**逐个比对"参考允许 vs `can_followcard` 允许",任何一侧多放行或多拒绝都会被抓到(约 11 万组)。 +- `test_flow.js`:**阶段机迁移矩阵 + 叫分起始者**(design §4.2/§4.6/§12.1)。① 8 个 RPC × 5 个阶段 = **40 格**逐格验证「该受理的受理、不该受理的一律回 `STEP` 失败包」——此前每个 handler 只测了 1~2 条代表性失败路径,没系统覆盖「某请求在别的阶段会不会被误放行 / 会不会静默丢弃」;断言里带 `cells === 40`,防止驱动失败导致整行"跳过"而假绿。② 第一局起始叫分者 = 座位 0、之后每局 = 暂定庄家(**庄赢连庄 / 庄输下家**)、每局首家都不能"不叫";连庄分支(`result=0`)随机打不出来,用「叫 70 分 + 首家出最大牌」专门构造。 +- `test_leak.js`:**下发面泄露审计**(server 红线「发全 ≠ 发多,按可见性下发」+ design §4/§9/§11)。跑 6 局(可查牌/不查牌 × 叫 65/70/5),对每一个「服务器 → 某座位」的下发面——含各 RPC 的逐座位包、各阶段重连快照、明牌应答——深度扫描出其中所有牌 id,逐个判定该座位此刻**是否有权知道这张牌**(自己的牌 / 已打出 / 庄家的暗牌与底牌 / 70 分亮 3 秒 / 结束亮底牌 / §9 明牌)。约 1800 个下发面、1.8 万次判定。此前各处门控只有逐条手工核对,没有系统验证。 +- `test_fuzz.js`:**整局模糊 + 每墩胜者差分**。`test_endgame.js` 的驱动器只会出单张,所以对子/拖拉机/甩牌/甩错在「整局链路」里从未跑过——单测逐个覆盖了 `can_playcard`/`can_followcard`,但它们与 `do_playcard`/`mod.chupai`/结算的**集成**是空白。本文件用带牌型的对局补上(20 局约 480 墩,四种房间选项组合轮换),并对**每一墩**用独立参考实现重算"谁最大"与服务端比对(不是抽查,每墩都比),另独立重算闲家捡分、扣底倍数、算奖与捡分子数的分配。提交时还会把牌按**最坏顺序**摆(`[K,K,Q,Q]→[K,Q,K,Q]`,把每个对子拆开),压服务端判定的入参顺序无关性。 + +三组都做过**变异检验**(在副本上注入缺陷确认会转红):关掉 `follow_tractor_cover_ok` → `test_followdiff` 三种拖拉机首出全红;大光倍率改 3→4 / 常规 55 档 base 改 4→5 / 爬坡 40·35 档 Q 改 20→40 → `test_calc` 精确指出档位与 grade;牌的归属写给非胜者、单张毙牌不再压过副牌、算奖分配去掉 `×X`、跟牌牌面值改回未排序入参 → `test_fuzz` 全部转红。 + +改这三个文件时别破坏的写法约定: + +- **随机源必须是固定种子的 PRNG**,不用 `Math.random`——同一份代码每次跑的用例集完全相同。`test_fuzz.js` 连**发牌**也接到同一个种子上:它末尾的覆盖下限(甩牌/毙牌/扣底各出现多少次)是发牌相关的,用随机发牌时稀有事件会在某些运行里一次都不出现而让断言随机转红(实测 60 次里有 1 次 0 次扣底)。随机发牌的整局覆盖由 `test_endgame.js` 承担,两者互补。 +- **关键结构要用确定性手牌钉死**,不能指望随机造牌覆盖到。例如「同花色三组互不相邻的两连对」(12 张)是唯一能区分"只出零散对子"与"先凑最长拖拉机"的形状(少于 3 组时任取 3 对必含相邻对,测不出差别);纯随机造牌造不出它,关掉覆盖度校验也全绿,差分就形同虚设。`structHands()` 就是为此存在的,另注意别让掺入的其他花色把手牌撑过枚举上限而被静默跳过。 +- **不变量不能拿被测字段自证**。`test_fuzz` 里踩过两次:① 闲家捡分若按 `playowner` 累加再与 `aset.grade` 比,`playowner` 写错时两边一起错、断言照样通过——改成按**独立算出的墩胜者**累计;② 扣底倍数若再调 `get_bottom_multiple` 去比,倍数表被改成恒返回 2 也通过——改成按 design §6.3 独立重算。 + +## 约定(dev-guide §10 测试纪律) + +- 测试代码可用现代 JS(不受严格 ES5 限制);**正式代码不得为测试放宽/加后门**。 +- 正反面 + 边界都要覆盖;失败先裁根因(业务缺陷改业务、脚本缺陷改脚本),不软化断言。 diff --git a/server/games/erqiwang/test/_assert.js b/server/games/erqiwang/test/_assert.js new file mode 100644 index 0000000..b4cb800 --- /dev/null +++ b/server/games/erqiwang/test/_assert.js @@ -0,0 +1,16 @@ +// 极简断言助手(测试代码不受严格 ES5 限制,dev-guide 05/04 §10) +module.exports = function () { + let pass = true; + let count = 0; + function eq(name, got, exp) { + const p = JSON.stringify(got) === JSON.stringify(exp); + count++; + if (!p) pass = false; + console.log((p ? 'PASS' : 'FAIL') + ' ' + name + ' got=' + JSON.stringify(got) + ' exp=' + JSON.stringify(exp)); + } + function done(label) { + console.log((pass ? 'ALL PASS' : 'HAS FAILURE') + ' [' + label + '] ' + count + ' checks'); + return pass; + } + return { eq, done }; +}; diff --git a/server/games/erqiwang/test/_rpc.js b/server/games/erqiwang/test/_rpc.js new file mode 100644 index 0000000..94d7cb5 --- /dev/null +++ b/server/games/erqiwang/test/_rpc.js @@ -0,0 +1,56 @@ +// L3 RPC 测试脚手架:装配 mod.js(含 RPC 方法),捕获下发包 +require('./_shim'); +// mod.js 的 RPC handler 按全局名引用这些类,先挂到 global +global.cls_youle_erqiwang_config = require('../class.config.js'); +global.cls_youle_erqiwang_pai = require('../class.pai.js'); +global.cls_youle_erqiwang_arith = require('../class.arith.js'); +global.cls_youle_erqiwang_paiju = require('../class.paiju.js'); +global.cls_youle_erqiwang_desk = require('../class.desk.js'); +// mod.js 顶部依赖:cls_mod.new 造模块对象、min_loadJsFile 置为同步 no-op(类已单独 require) +global.cls_mod = { new: (name, route, app) => ({ modname: name, routename: route, app }) }; +global.youle_app = {}; +global.min_loadJsFile = (p, cb) => { if (cb) cb(); }; + +const mod = require('../mod.js'); // = youle_erqiwang,含 jiaofen/xuanzhu/.../mingpai/zhunbei + +const clone = m => JSON.parse(JSON.stringify(m)); + +// 装配一局:给定 roomtype 与 o_paiju,返回 { sent, o_room },并把 check_player/SendPack/sendpack_toother +// 全部指向同一个 sent 数组(快照每个下发包,因 mod 会跨座位改删字段) +function setup(roomtype, o_paiju, seatlist) { + const sent = []; + const o_desk = { + paiju_list: [o_paiju], + o_room: null, + seatlist: [[0, [], 0, 0, 0], [0, [], 0, 0, 0], [0, [], 0, 0, 0]], + prepare: [0, 0, 0], + method: { + curr_paiju: () => o_paiju, + get_countdown_jiaofen: () => 15, get_countdown_xuanzhu: () => 20, + get_countdown_maipai: () => 25, get_countdown_chupai: () => 30, + do_prepare: () => { }, get_desk_account: m => m + } + }; + const o_room = { + roomtype, asetcount: 6, + seatlist: seatlist || [{ conmode: 0, fromid: 0 }, { conmode: 0, fromid: 1 }, { conmode: 0, fromid: 2 }], + o_desk, + method: { sendpack_toother: m => sent.push(clone(m)) } + }; + o_desk.o_room = o_room; + if (o_paiju) o_paiju.o_desk = o_desk; + mod.import = { check_player: () => o_room, deduct_roomcard: () => { }, save_grade: () => { } }; + mod.app = { SendPack: m => sent.push(clone(m)) }; + return { sent, o_room, o_desk }; +} + +// 构造 108 张牌(带 flower/number/dealowner/playround),供 get_seat_zhucards / get_seat_cards 用 +function make108(deal) { + const A = global.cls_youle_erqiwang_arith; + const cards = []; + for (let i = 0; i < 108; i++) cards[i] = { id: i, dealowner: 1, playround: -1, playowner: -1, score: 0, flower: A.id_to_flower(i), number: A.id_to_number(i) }; + if (deal) for (const [cid, owner] of deal) cards[cid].dealowner = owner; + return cards; +} + +module.exports = { mod, setup, make108, id: (d, f, n) => (d - 1) * 54 + (f - 1) * 13 + (n - 1) }; diff --git a/server/games/erqiwang/test/_shim.js b/server/games/erqiwang/test/_shim.js new file mode 100644 index 0000000..caf490b --- /dev/null +++ b/server/games/erqiwang/test/_shim.js @@ -0,0 +1,26 @@ +// Node 单测环境:提供正式代码按全局名引用的平台工具(不进正式代码,dev-guide 01 §1) +global.min_ary_include = function(aryparent, arychild){ + for (var i = 0; i < arychild.length; i++){ + var found = false; + for (var j = 0; j < aryparent.length; j++){ if (aryparent[j] == arychild[i]){ found = true; break; } } + if (!found){ return false; } + } + return true; +}; +global.min_ary_deduct = function(aryparent, arychild){ + var re = []; + for (var i = 0; i < aryparent.length; i++){ + var found = false; + for (var j = 0; j < arychild.length; j++){ if (aryparent[i] == arychild[j]){ found = true; break; } } + if (!found){ re.push(aryparent[i]); } + } + return re; +}; +global.min_random = function(min, max){ return min + Math.round(Math.random() * (max - min)); }; +global.min_now = function(ts){ return ts ? ts : Date.now(); }; +global.min_ontimeout = function(func, time){ /* 单测不真正异步执行 */ }; +global.youle_erqiwang = global.youle_erqiwang || { + import: { check_player: function(){ return null; }, deduct_roomcard: function(){}, save_grade: function(){} }, + app: { SendPack: function(){} } +}; +module.exports = {}; diff --git a/server/games/erqiwang/test/run.js b/server/games/erqiwang/test/run.js new file mode 100644 index 0000000..6b2820e --- /dev/null +++ b/server/games/erqiwang/test/run.js @@ -0,0 +1,16 @@ +// 二七王服务端单测总运行器:逐个 spawn test_*.js(各自独立进程/全局),汇总结果 +// 用法:node server/games/erqiwang/test/run.js +const fs = require('fs'); +const path = require('path'); +const cp = require('child_process'); + +const dir = __dirname; +const files = fs.readdirSync(dir).filter(f => /^test_.*\.js$/.test(f)).sort(); +let allOk = true; +for (const f of files) { + console.log('\n===== ' + f + ' ====='); + const r = cp.spawnSync(process.execPath, [path.join(dir, f)], { stdio: 'inherit' }); + if (r.status !== 0) allOk = false; +} +console.log('\n' + (allOk ? '===== 全部单测通过 =====' : '===== 存在失败单测 =====')); +process.exit(allOk ? 0 : 1); diff --git a/server/games/erqiwang/test/test_arith.js b/server/games/erqiwang/test/test_arith.js new file mode 100644 index 0000000..01971c2 --- /dev/null +++ b/server/games/erqiwang/test/test_arith.js @@ -0,0 +1,287 @@ +// arith 纯算法单测:扣底(§6.3) / 算子(§7) / 算奖(§8.1) / 甩牌(§5.4) / 跟甩牌(§5.4.4) +require('./_shim'); +const A = require('../class.arith.js'); +const t = require('./_assert')(); + +// 牌id构造:deck d(1-2), flower f(1-4), number n(1-13) +const id = (d, f, n) => (d - 1) * 54 + (f - 1) * 13 + (n - 1); +const big = d => (d - 1) * 54 + 53; // 大王 +const small = d => (d - 1) * 54 + 52; // 小王 +const MF = 1; // 主花色=方块(1) + +// ============ §6.3 扣底倍数 get_bottom_multiple ============ +const mult = cards => A.get_bottom_multiple(MF, cards); +t.eq('扣底 单张主A', mult([id(1, 1, 1)]), 1); +t.eq('扣底 单张正7', mult([id(1, 1, 7)]), 1); +t.eq('扣底 主对K', mult([id(1, 1, 13), id(2, 1, 13)]), 2); +t.eq('扣底 两连对KQ', mult([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)]), 4); +t.eq('扣底 三连对KQJ', mult([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12), id(1, 1, 11), id(2, 1, 11)]), 6); +t.eq('扣底 单张副A(非主)', mult([id(1, 2, 1)]), 0); +t.eq('扣底 小王对', mult([small(1), small(2)]), 2); +t.eq('扣底 8-6连对', mult([id(1, 1, 8), id(2, 1, 8), id(1, 1, 6), id(2, 1, 6)]), 4); +t.eq('扣底 两连对+单(取最高规格)', mult([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12), id(1, 1, 9)]), 4); + +// ============ §7 算子:判定标签 + 每对子子数 X ============ +function judge(call, grade, climb) { + const base = A.get_base_bycall(call, climb); + const q = A.get_qvalue(call, climb); + const upg = A.get_upgrade(call, grade, q); + const X = Math.abs(upg) * base; + let label = upg === 3 ? '大光' : upg === 2 ? '小光' : upg === 1 ? '过庄' : upg < 0 ? '升' + (-upg) + '级' : '?'; + return label + '/' + X; +} +// 常规算子 (design §7.2.1) +t.eq('常规65 大光', judge(65, 0, false), '大光/6'); +t.eq('常规65 小光', judge(65, 20, false), '小光/4'); +t.eq('常规65 过庄', judge(65, 50, false), '过庄/2'); +t.eq('常规65 升1', judge(65, 65, false), '升1级/2'); +t.eq('常规65 升2', judge(65, 105, false), '升2级/4'); +t.eq('常规60 大光', judge(60, 0, false), '大光/9'); +t.eq('常规55 大光', judge(55, 0, false), '大光/12'); +t.eq('常规50 大光', judge(50, 0, false), '大光/18'); +t.eq('常规45 过庄', judge(45, 44, false), '过庄/6'); +t.eq('常规40 grade20 小光(无过庄)', judge(40, 20, false), '小光/12'); +t.eq('常规40 升1', judge(40, 40, false), '升1级/6'); +t.eq('常规10 grade5 小光', judge(10, 5, false), '小光/12'); +t.eq('常规10 升1', judge(10, 10, false), '升1级/6'); +t.eq('常规5 大光', judge(5, 0, false), '大光/18'); +t.eq('常规5 升1(无小光)', judge(5, 5, false), '升1级/6'); +t.eq('常规70打牌 大光', judge(70, 0, false), '大光/6'); +t.eq('常规70打牌 过庄', judge(70, 69, false), '过庄/2'); +// 爬坡 (design §7.3.3) +t.eq('爬坡40 大光', judge(40, 0, true), '大光/24'); +t.eq('爬坡40 小光', judge(40, 19, true), '小光/16'); +t.eq('爬坡40 过庄', judge(40, 20, true), '过庄/8'); +t.eq('爬坡40 升1', judge(40, 40, true), '升1级/8'); +t.eq('爬坡40 升2', judge(40, 60, true), '升2级/16'); +t.eq('爬坡30 过庄', judge(30, 15, true), '过庄/10'); +t.eq('爬坡20 升1', judge(20, 20, true), '升1级/12'); +t.eq('爬坡10 过庄(无小光)', judge(10, 5, true), '过庄/14'); +t.eq('爬坡10 升1', judge(10, 10, true), '升1级/14'); +t.eq('爬坡5 大光', judge(5, 0, true), '大光/45'); +t.eq('爬坡5 升1(无小光过庄)', judge(5, 5, true), '升1级/15'); +t.eq('爬坡45 过庄', judge(45, 44, true), '过庄/7'); + +// ============ §8.1 算奖 get_chongguan ============ +function fill28(req) { + const used = new Set(req), out = req.slice(), NN = [1, 5, 6, 8, 9, 10, 11, 12, 13]; + for (const f of [1, 2, 3, 4]) for (const n of NN) { + if (out.length >= 28) break; + const c = id(1, f, n); + if (!used.has(c)) { used.add(c); out.push(c); } + } + return out; +} +const cg = (req, key) => A.get_chongguan(MF, fill28(req))[key]; +t.eq('算奖 3王 count', cg([big(1), small(1), small(2)], 'count'), 1); +t.eq('算奖 3王 wang', cg([big(1), small(1), small(2)], 'wang'), 3); +t.eq('算奖 4王 count', cg([big(1), big(2), small(1), small(2)], 'count'), 3); +t.eq('算奖 6个7(无王)', cg([id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)], 'count'), 1); +t.eq('算奖 8个2(无王)', cg([id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 3, 2), id(2, 3, 2), id(1, 4, 2), id(2, 4, 2)], 'count'), 3); +t.eq('算奖 3王+正7对+副7对(连对链)', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7)], 'count'), 3); +// ≥10固定主但互不成对 => 只3王(旧代码会误+1) +t.eq('算奖 10固定主无对无链', cg([big(1), small(1), small(2), id(1, 1, 7), id(1, 2, 7), id(1, 3, 7), id(1, 4, 7), id(1, 1, 2), id(1, 2, 2), id(1, 3, 2)], 'count'), 1); +// 连对链延伸到主A / 副花色A不算链 +t.eq('算奖 连对链到主A对', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 1, 1), id(2, 1, 1)], 'count'), 6); +t.eq('算奖 副花色A对不续链', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 2, 1), id(2, 2, 1)], 'count'), 5); + +// ============ shared/cards.js:牌编码区间常量 + 极大连续段扫描 ============ +const S = require('../shared/cards.js'); + +// 常量必须与 id_to_code 的实际产物一致(常量是区间判定的 SSOT,写歪了这里就红) +const inOpen = (code, lo, hi) => code > lo && code < hi; +t.eq('常量 正7 区间', inOpen(S.id_to_code(MF, id(1, 1, 7)), S.CODE_ZHENG7_MIN, S.CODE_ZHENG7_MAX), true); +t.eq('常量 副7 区间', inOpen(S.id_to_code(MF, id(1, 2, 7)), S.CODE_FU7_MIN, S.CODE_FU7_MAX), true); +t.eq('常量 正2 区间', inOpen(S.id_to_code(MF, id(1, 1, 2)), S.CODE_ZHENG2_MIN, S.CODE_ZHENG2_MAX), true); +t.eq('常量 副2 区间', inOpen(S.id_to_code(MF, id(1, 2, 2)), S.CODE_FU2_MIN, S.CODE_FU2_MAX), true); +t.eq('常量 主花色普通牌区间', inOpen(S.id_to_code(MF, id(1, 1, 5)), S.CODE_ZHU_MIN, S.CODE_ZHU_MAX), true); +t.eq('常量 主牌判据 code>CODE_ZHU_MIN 对主花色牌成立', S.id_to_code(MF, id(1, 1, 5)) > S.CODE_ZHU_MIN, true); +t.eq('常量 主牌判据 对副牌不成立', S.id_to_code(MF, id(1, 2, 5)) > S.CODE_ZHU_MIN, false); +t.eq('常量 小王编码', S.id_to_code(MF, small(1)), S.CODE_XIAOWANG); +t.eq('常量 大王编码', S.id_to_code(MF, big(1)), S.CODE_DAWANG); +t.eq('常量 主A 的 code%100', S.id_to_code(MF, id(1, 1, 1)) % 100, S.CODE_A_IN_ZHU); +t.eq('常量 拖拉机起算对数', S.TRACTOR_MIN_PAIRS, 2); + +// group_tractor_runs:极大连续段(服务端 decompose_trump 与前端手牌「拖」标记共用) +const pr = (n) => [id(1, 1, n), id(2, 1, n)]; // 主花色 n 的对子 +const runLens = (pairs) => S.group_tractor_runs(MF, pairs).map(r => r.length); +t.eq('连续段 空表', S.group_tractor_runs(MF, []), []); +t.eq('连续段 单个对子是长度1的段', runLens([pr(13)]), [1]); +t.eq('连续段 KQ 连续合成一段', runLens([pr(13), pr(12)]), [2]); +t.eq('连续段 KQJ 三连对是一段(极大)', runLens([pr(13), pr(12), pr(11)]), [3]); +t.eq('连续段 K 与 9 不连 → 两段', runLens([pr(13), pr(9)]), [1, 1]); +t.eq('连续段 KQ + 9 → 2+1', runLens([pr(13), pr(12), pr(9)]), [2, 1]); +t.eq('连续段 KQ + 98 → 2+2', runLens([pr(13), pr(12), pr(9), pr(8)]), [2, 2]); +// 8-6 相连(design §5.3:7 不与 6/8 相连),且必须同花色 +t.eq('连续段 主8-主6 相连', runLens([pr(8), pr(6)]), [2]); +t.eq('连续段 跨花色 主8+副6 不相连', runLens([pr(8), [id(1, 2, 6), id(2, 2, 6)]]), [1, 1]); +// 段的拼接必须恰好还原入参(不丢对子、不改顺序) +const runsIn = [pr(13), pr(12), pr(9)]; +t.eq('连续段 拼接还原入参', [].concat.apply([], S.group_tractor_runs(MF, runsIn)), runsIn); +// arith 上挂的引用与 shared 同一实现 +t.eq('arith 挂了 group_tractor_runs 引用', A.group_tractor_runs === S.group_tractor_runs, true); + +// ============ §5.4 甩牌 decompose / 最大性 / can_playcard ============ +const types = cards => A.decompose_trump(MF, cards).map(c => c.type + (c.len > 1 ? c.len : '')).sort().join(','); +t.eq('分解 大王对+主K单', types([big(1), big(2), id(1, 1, 13)]), 'pair,single'); +t.eq('分解 主K主Q两连对', types([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)]), 'tractor2'); +t.eq('分解 主K对+主9对(不连)', types([id(1, 1, 13), id(2, 1, 13), id(1, 1, 9), id(2, 1, 9)]), 'pair,pair'); +// 拖拉机段与孤立对子、单张混排:分量顺序与 cards 归属都不变(改走 shared 扫描后的行为一致性) +const mixComp = A.decompose_trump(MF, [id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12), id(1, 1, 9), id(2, 1, 9), id(1, 1, 5)]); +t.eq('分解 KQ拖+9对+5单 的分量序', mixComp.map(c => c.type + '/' + c.len), ['tractor/2', 'pair/1', 'single/1']); +t.eq('分解 拖拉机分量含4张', mixComp[0].cards.length, 4); +t.eq('分解 对子分量含2张', mixComp[1].cards.length, 2); +const play = (cards, opps) => A.can_playcard(MF, cards, 0, null, opps); +const K1 = id(1, 1, 13), K2 = id(2, 1, 13), A1 = id(1, 1, 1), P5 = id(1, 1, 5), P6 = id(1, 1, 6), fK1 = id(1, 2, 13), f9 = id(1, 2, 9), fK2 = id(2, 2, 13), Q1 = id(1, 1, 12), Q2 = id(2, 1, 12); +t.eq('甩牌 合法(大王对+主K单)', play([big(1), big(2), K1], [[P5, P6], []]).result, true); +let r = play([big(1), big(2), K1], [[A1, P5], []]); +t.eq('甩牌 甩错(对手有主A>主K) shuaicuo', r.shuaicuo, true); +t.eq('甩牌 甩错 smallest=主K', r.smallest, K1); +r = play([fK1, f9], [[], []]); +t.eq('甩牌 副牌禁甩 result', r.result, false); +t.eq('甩牌 副牌禁甩 无shuaicuo', r.shuaicuo, undefined); +t.eq('甩牌 主单张(非甩)可出', play([big(1)], [[A1], [K1]]).result, true); +t.eq('甩牌 副对子(非甩)可出', play([fK1, fK2], [[], []]).result, true); +t.eq('甩牌 主拖拉机(单分量不受最大性约束)', play([K1, K2, Q1, Q2], [[big(1), big(2)], []]).result, true); +// —— 甩牌首出补全:各分量被压导致甩错 + 各牌型组合合法(design §5.4.2/§5.4.3)—— +r = play([big(1), K1, K2], [[A1, id(2, 1, 1)], []]); // 大王单+主K对;对手主A对 +t.eq('甩牌 甩错·主对被更大主对压', r.shuaicuo, true); +r = play([big(1), Q1, Q2, id(1, 1, 11), id(2, 1, 11)], [[A1, id(2, 1, 1), K1, K2], []]); // 大王+主QJ两连对;对手主AK两连对 +t.eq('甩牌 甩错·拖拉机被同长更大主拖压', r.shuaicuo, true); +t.eq('甩牌 合法·纯双单张(大王+主K)', play([big(1), K1], [[P5], []]).result, true); +t.eq('甩牌 合法·纯双对不连(主K对+主9对)', play([K1, K2, id(1, 1, 9), id(2, 1, 9)], [[P5, P6], []]).result, true); +r = play([K1, K2, id(1, 1, 9), id(2, 1, 9)], [[id(1, 1, 11), id(2, 1, 11)], []]); // 主9对被主J对压 +t.eq('甩牌 甩错·双对之一被压', r.shuaicuo, true); +t.eq('甩牌 合法·单张+拖拉机', play([big(1), K1, K2, Q1, Q2], [[P5, P6], []]).result, true); +t.eq('甩牌 合法·单+对+拖三混', play([big(1), id(1, 1, 10), id(2, 1, 10), K1, K2, Q1, Q2], [[P5, P6], []]).result, true); +t.eq('甩牌 副禁甩·对手非缺门仍禁', play([fK1, f9], [[fK2, id(1, 2, 10)], []]).result, false); +// design §5.4.3「无组合数量限制」:曾有「一次最多14张」的规则外硬上限,会把 15 张以上的 +// 合法甩牌整个拒掉。16 张全固定主牌(4王 + 8个7 + 正2对 + 副2对)、两对手无主牌 → 必须可甩 +const SHUAI16 = [big(1), big(2), small(1), small(2), + id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7), id(1, 4, 7), id(2, 4, 7), + id(1, 1, 2), id(2, 1, 2), id(1, 4, 2), id(2, 4, 2)]; +t.eq('甩牌 16张(超14张上限)仍可甩', play(SHUAI16, [[], []]).result, true); +t.eq('甩牌 16张 有分量需求下发', !!play(SHUAI16, [[], []]).shuai_demand, true); + +// ============ §5.4.4 跟甩牌强制分量拆解 flush_follow_ok ============ +const J1 = id(1, 1, 11), N9a = id(1, 1, 9), N9b = id(2, 1, 9), P8 = id(1, 1, 8); +const fA = id(1, 2, 1), fB = id(1, 2, 5), fC = id(1, 2, 6), fD = id(1, 2, 8); +const ffo = (hand, follow, demand) => A.flush_follow_ok(MF, hand, follow, demand); +t.eq('跟牌 含主对KK', ffo([K1, K2, J1, N9a], [K1, K2, J1], { tractors: [], pairs: 1, singles: 1 }), true); +t.eq('跟牌 拆散主对(3散)→拒', ffo([K1, K2, J1, N9a], [J1, N9a, K1], { tractors: [], pairs: 1, singles: 1 }), false); +t.eq('跟牌 无主对散张可跟', ffo([J1, N9a, P6], [J1, N9a, P6], { tractors: [], pairs: 1, singles: 1 }), true); +const hand5 = [K1, K2, Q1, Q2, P6, J1, N9a]; +t.eq('跟牌 打2连拖+单', ffo(hand5, [K1, K2, Q1, Q2, P6], { tractors: [2], pairs: 0, singles: 1 }), true); +t.eq('跟牌 有2连拖却打5散→拒', ffo(hand5, [K1, Q1, P6, J1, N9a], { tractors: [2], pairs: 0, singles: 1 }), false); +t.eq('跟牌 有2连拖却只打一对→拒', ffo(hand5, [K1, K2, P6, J1, N9a], { tractors: [2], pairs: 0, singles: 1 }), false); +const hand6 = [K1, K2, N9a, N9b, P6, J1, P8]; +t.eq('跟牌 无同长拖两对退化', ffo(hand6, [K1, K2, N9a, N9b, P6], { tractors: [2], pairs: 0, singles: 1 }), true); +t.eq('跟牌 有两对却打5散→拒', ffo(hand6, [K1, N9a, P6, J1, P8], { tractors: [2], pairs: 0, singles: 1 }), false); +t.eq('跟牌 主牌最大化(有主用尽)', ffo([K1, Q1, fA, fB], [K1, Q1, fA], { tractors: [], pairs: 1, singles: 1 }), true); +t.eq('跟牌 留主牌垫副→拒', ffo([K1, Q1, fA, fB], [K1, fA, fB], { tractors: [], pairs: 1, singles: 1 }), false); +t.eq('跟牌 无主全垫副', ffo([fA, fB, fC, fD], [fA, fB, fC], { tractors: [], pairs: 1, singles: 1 }), true); +// —— 跟甩牌补全:拖分量退化到单张 / 多组拖拉机 / 混合demand(design §5.4.4)—— +t.eq('跟牌 拖退化·1对不够必打+其余单', ffo([K1, K2, N9a, P8, P6, id(1, 1, 5)], [K1, K2, N9a, P8, P6], { tractors: [2], pairs: 0, singles: 1 }), true); +t.eq('跟牌 拖退化·有1对却拆散→拒', ffo([K1, K2, N9a, P8, P6, id(1, 1, 5)], [K1, N9a, P8, P6, id(1, 1, 5)], { tractors: [2], pairs: 0, singles: 1 }), false); +t.eq('跟牌 双拖·两组2连拖必打', ffo([K1, K2, Q1, Q2, id(1, 1, 10), id(2, 1, 10), N9a, N9b], [K1, K2, Q1, Q2, id(1, 1, 10), id(2, 1, 10), N9a, N9b], { tractors: [2, 2], pairs: 0, singles: 0 }), true); +t.eq('跟牌 混合·拖+对+单各对位', ffo([K1, K2, Q1, Q2, N9a, N9b, P8, id(1, 1, 5)], [K1, K2, Q1, Q2, N9a, N9b, P8], { tractors: [2], pairs: 1, singles: 1 }), true); +t.eq('跟牌 混合·有对却拆散→拒', ffo([K1, K2, Q1, Q2, N9a, N9b, P8, id(1, 1, 5)], [K1, K2, Q1, Q2, N9a, P8, id(1, 1, 5)], { tractors: [2], pairs: 1, singles: 1 }), false); + +// ============ §3 主牌顺序 / 相邻链 ============ +// order_cards 结果应为 design §3 从大到小:大王>小王>正7>副7>正2>副2>主A>主K…主5>副牌 +const big1 = big(1), small1 = small(1), Z7 = id(1, 1, 7), F7 = id(1, 2, 7), Z2 = id(1, 1, 2), F2 = id(1, 2, 2), zA = id(1, 1, 1), zK = id(1, 1, 13), z5 = id(1, 1, 5); +t.eq('排序 主牌降序=设计顺序', A.order_cards(MF, [z5, F2, Z2, big1, F7, Z7, small1, zA, zK]), [big1, small1, Z7, F7, Z2, F2, zA, zK, z5]); +// is_continuous 相邻链逐段(design §3/§5.3) +const cont = (a, b) => A.is_continuous(A.id_to_code(MF, a), A.id_to_code(MF, b)); +t.eq('相邻 大王-小王', cont(big(1), small(1)), true); +t.eq('相邻 小王-正7', cont(small(1), Z7), true); +t.eq('相邻 正7-副7', cont(Z7, F7), true); +t.eq('相邻 副7-正2', cont(F7, Z2), true); +t.eq('相邻 正2-副2', cont(Z2, F2), true); +t.eq('相邻 副2-主A', cont(F2, zA), true); +t.eq('相邻 主A-主K', cont(zA, zK), true); +t.eq('相邻 主8-主6(6/8可连)', cont(id(1, 1, 8), id(1, 1, 6)), true); +t.eq('相邻 主6-主5', cont(id(1, 1, 6), z5), true); +t.eq('相邻 正7-主6 不连(7不入普通序)', cont(Z7, id(1, 1, 6)), false); +t.eq('相邻 主8-正7 不连', cont(id(1, 1, 8), Z7), false); +// 8/6 相邻必须同花色:跨花色的 8 与 6 不成连对(否则 get_chongguan 连对链会被副牌 6 对错误接续) +t.eq('相邻 副♥8-副♥6 同花色 连', cont(id(1, 3, 8), id(1, 3, 6)), true); +t.eq('相邻 副♥8-副♣6 跨花色 不连', cont(id(1, 3, 8), id(1, 2, 6)), false); +t.eq('相邻 主8-副♣6 跨花色 不连', cont(id(1, 1, 8), id(1, 2, 6)), false); +t.eq('相邻 副♠8-副♦6 跨花色 不连', cont(id(1, 4, 8), id(1, 1, 6)), false); +// 顺带确认非 8/6 的等差2组合仍不连(防守卫写宽) +t.eq('相邻 主A-主Q 不连(差2但非8/6)', cont(zA, id(1, 1, 12)), false); + +// ============ §3「关于对子的判定」:对子只认同一张具体牌的两副 ============ +// design §3 注:正/副7、正/副2 虽各占一个大小等级,但**成对只认同花色同点数的两副**; +// 两张不同花色的副7(如♠7+♥7)不构成一对,副2 同理;大王与小王点数不同也不能混成一对。 +// 拖拉机的连对判定以此为基础(每节连对里的每个对子都须是同花色同点数两副)。 +const pl = cards => A.get_pairlist(MF, A.order_cards(MF, cards.slice())).length; +t.eq('对子 正 同花色两副♥7 成对', pl([id(1, 3, 7), id(2, 3, 7)]), 1); +t.eq('对子 反 ♥7+♣7 跨花色副7 不成对', pl([id(1, 3, 7), id(1, 2, 7)]), 0); +t.eq('对子 反 正7+副7 不成对', pl([id(1, 1, 7), id(1, 2, 7)]), 0); +t.eq('对子 正 同花色两副♥2 成对', pl([id(1, 3, 2), id(2, 3, 2)]), 1); +t.eq('对子 反 ♥2+♣2 跨花色副2 不成对', pl([id(1, 3, 2), id(1, 2, 2)]), 0); +t.eq('对子 反 正2+副2 不成对', pl([id(1, 1, 2), id(1, 2, 2)]), 0); +t.eq('对子 正 大王两副 成对', pl([big(1), big(2)]), 1); +t.eq('对子 反 大王+小王 不成对', pl([big(1), small(1)]), 0); +// 连对以「同花色同点数两副」为基础:四张跨花色的副7/副2 一对都凑不出,自然也没有拖拉机 +t.eq('对子 边界 ♥7♣7♥2♣2 四张 → 0 对', pl([id(1, 3, 7), id(1, 2, 7), id(1, 3, 2), id(1, 2, 2)]), 0); +t.eq('连对 边界 上述四张组不出拖拉机', + A.get_tuolaji_list(MF, A.get_pairlist(MF, A.order_cards(MF, [id(1, 3, 7), id(1, 2, 7), id(1, 3, 2), id(1, 2, 2)])), 302).length, 0); +// 对照:正7对 + 副7对(各自同花色两副)才是合法两连对 +t.eq('连对 正 正7对+副7对 构成两连对', + A.get_tuolaji_list(MF, A.get_pairlist(MF, A.order_cards(MF, [id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7)])), 302).length, 1); + +// ============ §5.3 拖拉机:副牌须同花色 / 主牌按链相邻·不限花色 ============ +// 这一层此前只测到 is_continuous(两张牌相不相邻),没测到「这几个对子能不能真的组成拖拉机」。 +// design §5.3 改写后明确:副牌**必须同一花色**;主牌只要在主牌序列里**位置相邻**即可、**不限花色**; +// 同一档位的两个对子(如 ♥7对 + ♣7对)平级、不相邻。 +const tljOf = cards => { + const s = A.order_cards(MF, cards.slice()); + const ps = A.get_pairlist(MF, s); + return ps.length >= 2 && A.get_tuolaji_list(MF, ps, 300 + ps.length).length > 0; +}; +// —— 主牌链:跨花色也成连对 —— +t.eq('§5.3 主链 正7对+副7对(跨花色) 成连对', tljOf([id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7)]), true); +t.eq('§5.3 主链 副7对+正2对(跨花色) 成连对', tljOf([id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2)]), true); +t.eq('§5.3 主链 正2对+副2对(跨花色) 成连对', tljOf([id(1, 1, 2), id(2, 1, 2), id(1, 3, 2), id(2, 3, 2)]), true); +t.eq('§5.3 主链 副2对+主A对(跨花色) 成连对', tljOf([id(1, 2, 2), id(2, 2, 2), id(1, 1, 1), id(2, 1, 1)]), true); +t.eq('§5.3 主链 大王对+小王对 成连对', tljOf([big(1), big(2), small(1), small(2)]), true); +t.eq('§5.3 主链 小王对+正7对 成连对', tljOf([small(1), small(2), id(1, 1, 7), id(2, 1, 7)]), true); +t.eq('§5.3 主链 三连(副7对+正2对+副2对) 成立', tljOf([id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 3, 2), id(2, 3, 2)]), true); +t.eq('§5.3 主链 主8对+主6对 成连对(7被抽走)', tljOf([id(1, 1, 8), id(2, 1, 8), id(1, 1, 6), id(2, 1, 6)]), true); +// —— 同一档位的两个对子平级、不相邻 —— +t.eq('§5.3 同档 ♥7对+♣7对 不成连对', tljOf([id(1, 3, 7), id(2, 3, 7), id(1, 2, 7), id(2, 2, 7)]), false); +t.eq('§5.3 同档 ♥2对+♣2对 不成连对', tljOf([id(1, 3, 2), id(2, 3, 2), id(1, 2, 2), id(2, 2, 2)]), false); +// —— 副牌:必须同花色 —— +t.eq('§5.3 副牌 ♥K对+♥Q对 同花色 成连对', tljOf([id(1, 3, 13), id(2, 3, 13), id(1, 3, 12), id(2, 3, 12)]), true); +t.eq('§5.3 副牌 ♥K对+♣Q对 跨花色 不成连对', tljOf([id(1, 3, 13), id(2, 3, 13), id(1, 2, 12), id(2, 2, 12)]), false); +t.eq('§5.3 副牌 ♥8对+♥6对 成连对(7是主牌被抽走)', tljOf([id(1, 3, 8), id(2, 3, 8), id(1, 3, 6), id(2, 3, 6)]), true); +t.eq('§5.3 副牌 ♥8对+♣6对 跨花色 不成连对', tljOf([id(1, 3, 8), id(2, 3, 8), id(1, 2, 6), id(2, 2, 6)]), false); +t.eq('§5.3 副牌 ♥6对+♥5对 成连对(5为最小档)', tljOf([id(1, 3, 6), id(2, 3, 6), id(1, 3, 5), id(2, 3, 5)]), true); +t.eq('§5.3 副牌 ♥9对+♥K对 不相邻 不成连对', tljOf([id(1, 3, 9), id(2, 3, 9), id(1, 3, 13), id(2, 3, 13)]), false); +// —— §2 牌堆里没有 3 和 4,所以副牌序列的最小一档就是 5 —— +// (规则复核时有人把副牌链写成 "…-6-5-3",那需要牌堆保留 3;但 92 = 28×3 + 8 只有在 +// 3、4 都剔除时才成立。这里就地钉住"牌堆里 3 和 4 各 0 张",避免链的末端再被写错。) +const _P = require('../class.paiju.js'); +const _deck = _P.new({ paiju_list: [] }, 0).cards.filter(c => c.dealowner !== -1); +t.eq('§2 参与发牌的牌里 3 和 4 各 0 张', + [3, 4].map(n => _deck.filter(c => c.number === n).length), [0, 0]); +t.eq('§2 参与发牌共 92 张 = 28×3 + 暗牌8', [_deck.length, 28 * 3 + 8], [92, 92]); + +// ============ §6.3 扣底触发的反面:赢末轮的牌不全是主牌就不扣底 ============ +t.eq('扣底 反 副牌两连对赢末轮 → 0', mult([id(1, 2, 13), id(2, 2, 13), id(1, 2, 12), id(2, 2, 12)]), 0); +t.eq('扣底 反 混合出牌(主K+副K)赢末轮 → 0', mult([id(1, 1, 13), id(1, 2, 13)]), 0); +t.eq('扣底 反 主牌在前副牌在后也判 0(与提交顺序无关)', mult([id(1, 2, 13), id(1, 1, 13)]), 0); + +// ============ §7 算子:每档小光/过庄分界 + 升3级(补强)============ +t.eq('常规65 g39小光', judge(65, 39, false), '小光/4'); +t.eq('常规65 g40过庄(分界)', judge(65, 40, false), '过庄/2'); +t.eq('常规65 g64过庄', judge(65, 64, false), '过庄/2'); +t.eq('常规65 g65升1', judge(65, 65, false), '升1级/2'); +t.eq('常规65 g104升1', judge(65, 104, false), '升1级/2'); +t.eq('常规65 g105升2', judge(65, 105, false), '升2级/4'); +t.eq('常规65 g145升3', judge(65, 145, false), '升3级/6'); + +process.exit(t.done('arith') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_calc.js b/server/games/erqiwang/test/test_calc.js new file mode 100644 index 0000000..027b123 --- /dev/null +++ b/server/games/erqiwang/test/test_calc.js @@ -0,0 +1,102 @@ +// §7 算子全量对拍:把 design §7.1/§7.2/§7.3 的判定表**按原文另写一份参考实现**, +// 再与 class.arith 的 get_base_bycall / get_qvalue / get_upgrade 逐格比对。 +// 参考实现只依据 design 的文字与表格,不看 class.arith 的实现,属差分测试而非同义反复。 +// 覆盖:2 种算子模式 × 14 个叫分档 × grade 0~260,比 base / Q / 判定倍率 / 最终子数 X 四项。 +require('./_shim'); +const A = require('../class.arith.js'); +const t = require('./_assert')(); + +// ============ 参考实现(逐字转写自 design)============ +// §7.1 常规算子:65→2 60→3 55→4 50及以下统一6;70 打牌→2 +// §7.3.1 爬坡:65 起每低 5 分加 1 个子,50→6 后继续延伸到 5 分→15;70 仍为 2 +const CLIMB_BASE = { 65: 2, 60: 3, 55: 4, 50: 6, 45: 7, 40: 8, 35: 9, 30: 10, 25: 11, 20: 12, 15: 13, 10: 14, 5: 15 }; +function refBase(climb, call) { + if (call === 70) return 2; // §7.1 末尾:70 分打牌基础子数 2 + if (!climb) return (call === 65) ? 2 : (call === 60) ? 3 : (call === 55) ? 4 : 6; + return CLIMB_BASE[call]; +} +// §7.2.0 常规算子:小光/过庄分界固定 40,升级级距同为 40 +// §7.3.2 爬坡:≥45→40、40/35→20、30/25→15、20/15→10、10→5、5→5;70 分不适用分段,仍 40 +function refQ(climb, call) { + if (!climb) return 40; + if (call === 70 || call >= 45) return 40; + if (call === 40 || call === 35) return 20; + if (call === 30 || call === 25) return 15; + if (call === 20 || call === 15) return 10; + return 5; +} +// §7.2.0 判定顺序:**必须先判是否达标(升级),再判大光/小光/过庄** +// 返回带符号倍率:大光 +3 / 小光 +2 / 过庄 +1 / 升 N 级 −N +function refUpgrade(call, grade, Q) { + if (grade >= call) return -(Math.floor((grade - call) / Q) + 1); + if (grade === 0) return 3; + if (grade < Q) return 2; + return 1; +} + +// ============ 全量对拍 ============ +const CALLS = [70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5]; +let totalCells = 0; +for (const climb of [false, true]) { + const tag = climb ? '爬坡' : '常规'; + for (const call of CALLS) { + const eb = refBase(climb, call), eq = refQ(climb, call); + const gb = A.get_base_bycall(call, climb), gq = A.get_qvalue(call, climb); + const bad = []; + if (gb !== eb) bad.push(['base', gb, eb]); + if (gq !== eq) bad.push(['Q', gq, eq]); + for (let g = 0; g <= 260; g++) { + const exp = refUpgrade(call, g, eq); + const got = A.get_upgrade(call, g, gq); + totalCells++; + if (got !== exp) { bad.push(['判定 grade=' + g, got, exp]); continue; } + // §7.0 第3层:X = 基础子数 × |判定倍率| + if (gb * Math.abs(got) !== eb * Math.abs(exp)) bad.push(['X grade=' + g, gb * Math.abs(got), eb * Math.abs(exp)]); + } + t.eq('§7 全量对拍 ' + tag + ' 叫' + call + ' 分(base/Q/grade0~260)失配', bad.slice(0, 3), []); + } +} +t.eq('§7 全量对拍覆盖格数', totalCells, 2 * CALLS.length * 261); + +// ============ design 判定表里「写死的最终子数」逐格抽查 ============ +// [climb, call, grade, 期望最终子数 X, 期望判定(G大光/X小光/P过庄/Un升n级)] +const TABLES = { + '§7.2.1 常规 65 分坐庄': [[0, 65, 0, 6, 'G'], [0, 65, 1, 4, 'X'], [0, 65, 39, 4, 'X'], [0, 65, 40, 2, 'P'], [0, 65, 64, 2, 'P'], [0, 65, 65, 2, 'U1'], [0, 65, 104, 2, 'U1'], [0, 65, 105, 4, 'U2'], [0, 65, 144, 4, 'U2'], [0, 65, 145, 6, 'U3']], + '§7.2.1 常规 60 分坐庄': [[0, 60, 0, 9, 'G'], [0, 60, 39, 6, 'X'], [0, 60, 40, 3, 'P'], [0, 60, 59, 3, 'P'], [0, 60, 60, 3, 'U1'], [0, 60, 99, 3, 'U1'], [0, 60, 100, 6, 'U2'], [0, 60, 139, 6, 'U2'], [0, 60, 140, 9, 'U3']], + '§7.2.1 常规 55 分坐庄': [[0, 55, 0, 12, 'G'], [0, 55, 39, 8, 'X'], [0, 55, 40, 4, 'P'], [0, 55, 54, 4, 'P'], [0, 55, 55, 4, 'U1'], [0, 55, 94, 4, 'U1'], [0, 55, 95, 8, 'U2'], [0, 55, 135, 12, 'U3']], + '§7.2.1 常规 50 分坐庄': [[0, 50, 0, 18, 'G'], [0, 50, 39, 12, 'X'], [0, 50, 40, 6, 'P'], [0, 50, 49, 6, 'P'], [0, 50, 50, 6, 'U1'], [0, 50, 89, 6, 'U1'], [0, 50, 90, 12, 'U2'], [0, 50, 130, 18, 'U3']], + '§7.2.1 常规 45 分坐庄': [[0, 45, 0, 18, 'G'], [0, 45, 39, 12, 'X'], [0, 45, 40, 6, 'P'], [0, 45, 44, 6, 'P'], [0, 45, 45, 6, 'U1'], [0, 45, 84, 6, 'U1'], [0, 45, 85, 12, 'U2'], [0, 45, 125, 18, 'U3']], + '§7.2.1 常规 40 分及以下(无过庄档)': [[0, 40, 0, 18, 'G'], [0, 40, 39, 12, 'X'], [0, 40, 40, 6, 'U1'], [0, 40, 79, 6, 'U1'], [0, 40, 80, 12, 'U2'], [0, 10, 0, 18, 'G'], [0, 10, 5, 12, 'X'], [0, 10, 9, 12, 'X'], [0, 10, 10, 6, 'U1'], [0, 10, 49, 6, 'U1'], [0, 10, 50, 12, 'U2']], + '§7.2.1 常规 5 分坐庄(无小光)': [[0, 5, 0, 18, 'G'], [0, 5, 5, 6, 'U1'], [0, 5, 44, 6, 'U1'], [0, 5, 45, 12, 'U2'], [0, 5, 85, 18, 'U3']], + '§7.2.1 70 分坐庄·打牌': [[0, 70, 0, 6, 'G'], [0, 70, 39, 4, 'X'], [0, 70, 40, 2, 'P'], [0, 70, 69, 2, 'P'], [0, 70, 70, 2, 'U1'], [0, 70, 109, 2, 'U1'], [0, 70, 110, 4, 'U2'], [0, 70, 150, 6, 'U3'], [1, 70, 0, 6, 'G'], [1, 70, 70, 2, 'U1']], + '§7.3.3 爬坡 40 分坐庄(Q=20)': [[1, 40, 0, 24, 'G'], [1, 40, 1, 16, 'X'], [1, 40, 19, 16, 'X'], [1, 40, 20, 8, 'P'], [1, 40, 39, 8, 'P'], [1, 40, 40, 8, 'U1'], [1, 40, 59, 8, 'U1'], [1, 40, 60, 16, 'U2'], [1, 40, 79, 16, 'U2'], [1, 40, 80, 24, 'U3']], + '§7.3.3 爬坡 35 分坐庄(Q=20)': [[1, 35, 0, 27, 'G'], [1, 35, 19, 18, 'X'], [1, 35, 20, 9, 'P'], [1, 35, 34, 9, 'P'], [1, 35, 35, 9, 'U1'], [1, 35, 54, 9, 'U1'], [1, 35, 55, 18, 'U2'], [1, 35, 75, 27, 'U3']], + '§7.3.3 爬坡 30 分坐庄(Q=15)': [[1, 30, 0, 30, 'G'], [1, 30, 14, 20, 'X'], [1, 30, 15, 10, 'P'], [1, 30, 29, 10, 'P'], [1, 30, 30, 10, 'U1'], [1, 30, 44, 10, 'U1'], [1, 30, 45, 20, 'U2'], [1, 30, 60, 30, 'U3']], + '§7.3.3 爬坡 25 分坐庄(Q=15)': [[1, 25, 0, 33, 'G'], [1, 25, 14, 22, 'X'], [1, 25, 15, 11, 'P'], [1, 25, 24, 11, 'P'], [1, 25, 25, 11, 'U1'], [1, 25, 39, 11, 'U1'], [1, 25, 40, 22, 'U2'], [1, 25, 55, 33, 'U3']], + '§7.3.3 爬坡 20 分坐庄(Q=10)': [[1, 20, 0, 36, 'G'], [1, 20, 9, 24, 'X'], [1, 20, 10, 12, 'P'], [1, 20, 19, 12, 'P'], [1, 20, 20, 12, 'U1'], [1, 20, 29, 12, 'U1'], [1, 20, 30, 24, 'U2'], [1, 20, 40, 36, 'U3']], + '§7.3.3 爬坡 15 分坐庄(Q=10)': [[1, 15, 0, 39, 'G'], [1, 15, 9, 26, 'X'], [1, 15, 10, 13, 'P'], [1, 15, 14, 13, 'P'], [1, 15, 15, 13, 'U1'], [1, 15, 24, 13, 'U1'], [1, 15, 25, 26, 'U2'], [1, 15, 35, 39, 'U3']], + '§7.3.3 爬坡 10 分坐庄(Q=5,无小光)': [[1, 10, 0, 42, 'G'], [1, 10, 5, 14, 'P'], [1, 10, 9, 14, 'P'], [1, 10, 10, 14, 'U1'], [1, 10, 14, 14, 'U1'], [1, 10, 15, 28, 'U2'], [1, 10, 20, 42, 'U3']], + '§7.3.3 爬坡 5 分坐庄(无小光、无过庄)': [[1, 5, 0, 45, 'G'], [1, 5, 5, 15, 'U1'], [1, 5, 9, 15, 'U1'], [1, 5, 10, 30, 'U2'], [1, 5, 15, 45, 'U3']], + '§7.3.1/§7.3.3 爬坡 ≥45 档(45 为爬坡独有)': [[1, 45, 0, 21, 'G'], [1, 45, 39, 14, 'X'], [1, 45, 40, 7, 'P'], [1, 45, 44, 7, 'P'], [1, 45, 45, 7, 'U1'], [1, 45, 85, 14, 'U2'], [1, 65, 0, 6, 'G'], [1, 60, 0, 9, 'G'], [1, 55, 0, 12, 'G'], [1, 50, 0, 18, 'G']] +}; +let cellCount = 0; +for (const name in TABLES) { + const bad = []; + for (const [c, call, grade, expX, expKind] of TABLES[name]) { + cellCount++; + const base = A.get_base_bycall(call, !!c), q = A.get_qvalue(call, !!c); + const up = A.get_upgrade(call, grade, q); + const kind = up === 3 ? 'G' : up === 2 ? 'X' : up === 1 ? 'P' : ('U' + (-up)); + const X = base * Math.abs(up); + if (X !== expX || kind !== expKind) bad.push(['grade=' + grade, X + '/' + kind, expX + '/' + expKind]); + } + t.eq(name + ' 逐格失配', bad, []); +} +t.eq('design 判定表抽查格数', cellCount, 141); + +// ============ 投降 / 解散:不走 7.1~7.3 模型(§7.0 例外、§12.2)============ +// 数值由 get_paiju_account 直接固定(投降 base=1、解散 0),此处只钉住 get_base_bycall 对 70 分的取值 +t.eq('§7.1 70 分打牌基础子数 2(两种模式一致)', [A.get_base_bycall(70, false), A.get_base_bycall(70, true)], [2, 2]); +t.eq('§7.1 无叫分 → 0', [A.get_base_bycall(0, false), A.get_base_bycall(0, true)], [0, 0]); + +process.exit(t.done('calc') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_callgrade.js b/server/games/erqiwang/test/test_callgrade.js new file mode 100644 index 0000000..0124dcb --- /dev/null +++ b/server/games/erqiwang/test/test_callgrade.js @@ -0,0 +1,28 @@ +// §4.2 叫分坐庄 do_callgrade(纯函数,operate on 最小 o_paiju)。期望取自 design §4.2 +require('./_shim'); +const P = require('../class.paiju.js'); +const t = require('./_assert')(); + +const mkCall = fs => ({ callproc: [{ seat: fs, call: null }], banker: -1, call: -1, step: 1 }); +// 依次叫分序列(calls 里每个值 = 当前轮到者的叫分,0=不叫) +function seq(firstseat, calls) { + const p = mkCall(firstseat); + for (const c of calls) P.do_callgrade(p, c); + return p; +} +const bcs = p => [p.banker, p.call, p.step]; + +// 叫5立即上庄 +t.eq('叫5立即上庄', bcs(seq(0, [5])), [0, 5, 2]); +// 暂定庄叫65,另两家都不叫 → 暂定庄(0)上庄 +t.eq('0叫65 两家不叫→0上庄', bcs(seq(0, [65, 0, 0])), [0, 65, 2]); +// 0叫65、1叫更低60、2不叫、0不叫 → 60 最低者(1)上庄 +t.eq('0叫65 1叫60 余不叫→1上庄', bcs(seq(0, [65, 60, 0, 0])), [1, 60, 2]); +// 首家叫5路径:0叫5 → 0直接上庄(其余无需叫) +t.eq('首家叫5直接坐庄', bcs(seq(0, [5])), [0, 5, 2]); +// 递减到5:0叫65,1叫60,2叫5 → 2上庄(叫5立即) +t.eq('2叫5立即上庄', bcs(seq(0, [65, 60, 5])), [2, 5, 2]); +// 不同首家:1起叫,1叫50,2不叫,0不叫 → 1上庄 +t.eq('首家1叫50 余不叫→1上庄', bcs(seq(1, [50, 0, 0])), [1, 50, 2]); + +process.exit(t.done('callgrade') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_config.js b/server/games/erqiwang/test/test_config.js new file mode 100644 index 0000000..3d87e46 --- /dev/null +++ b/server/games/erqiwang/test/test_config.js @@ -0,0 +1,74 @@ +// roomtype 位串解析 + 局数/扣卡映射(design §10.1) +require('./_shim'); +const C = require('../class.config.js'); +const t = require('./_assert')(); + +// 复刻 export 的两个映射(现为2行)做端到端验证 +const needroomcard = rt => { const c = C.parse(rt); return c.deductAA ? (c.aset12 ? 2 : 1) : (c.aset12 ? 4 : 2); }; +const asetcount = rt => { const c = C.parse(rt); return c.aset12 ? 12 : 6; }; + +// --- parse 位串 --- +t.eq('"00000"', C.parse("00000"), { aset12: false, deductAA: false, bangwang: false, climb: false, nocheck: false }); +t.eq('"10100"', C.parse("10100"), { aset12: true, deductAA: false, bangwang: true, climb: false, nocheck: false }); +t.eq('"01011"', C.parse("01011"), { aset12: false, deductAA: true, bangwang: false, climb: true, nocheck: true }); +t.eq('"" 兜底', C.parse(""), { aset12: false, deductAA: false, bangwang: false, climb: false, nocheck: false }); +t.eq('undefined 兜底', C.parse(undefined), { aset12: false, deductAA: false, bangwang: false, climb: false, nocheck: false }); +t.eq('"1" 短串(位长校验后按缺省)', C.parse("1"), { aset12: false, deductAA: false, bangwang: false, climb: false, nocheck: false }); + +// --- 【位长协议 · 2026-09-01 升级 · 第 2 版】扩展位在末尾 --- +// 新版:N+5 位 = 前 5 位协议位(IDX_ASET..IDX_NOCHECK,对应 charAt 0..4)+ 后 N 位扩展位 +// 字串示例:'11011' + '000000' = '11011000000'(默认 N=6 扩展位全 '0') +// 协议位的位索引(IDX_* = 0..4)永远不变,扩展位在协议位之后。 +// 兼容性:旧版 5 位字符串直接走 charAt(0..4);新版 11 位也走 charAt(0..4)(自动兼容)。 +// 前端发 11 位 = 5 位 buildRoomType() 右补 6 位扩展位,服务端按 charAt(0..4) 读协议位。 +t.eq('新版 11 位 "10100000000"(前 5 位 10100 = 12局+房主+傍王)', + C.parse("10100000000"), { aset12: true, deductAA: false, bangwang: true, climb: false, nocheck: false }); +t.eq('新版 11 位 "11011000000"(前 5 位 11011 = 12局+AA+不查+爬坡)', + C.parse("11011000000"), { aset12: true, deductAA: true, bangwang: false, climb: true, nocheck: true }); +t.eq('新版 11 位 "00000000000"(前 5 位 00000 = 全默认)', + C.parse("00000000000"), { aset12: false, deductAA: false, bangwang: false, climb: false, nocheck: false }); + +// 兼容性:旧版 5 位 roomtype +t.eq('旧版 5 位 "10100"(无扩展位)直接解析', + C.parse("10100"), { aset12: true, deductAA: false, bangwang: true, climb: false, nocheck: false }); + +// 兼容性:未来位长增加(追加更多扩展位)→ 前 5 位协议位不变 +t.eq('17 位 "11011000000000000"(前 5 位 11011)', + C.parse("11011000000000000"), { aset12: true, deductAA: true, bangwang: false, climb: true, nocheck: true }); + +// 位长异常 → 兜底(位长 < 5 时整体按 '00000') +t.eq('位长 6("101010",前 5 位 = "10101" → 12局+房主+傍王+不查)', + C.parse("101010"), { aset12: true, deductAA: false, bangwang: true, climb: false, nocheck: true }); +t.eq('位长 4("0000",位长不足)→ 兜底', + C.parse("0000"), { aset12: false, deductAA: false, bangwang: false, climb: false, nocheck: false }); +t.eq('位长 0(空串)→ 兜底', C.parse(""), + { aset12: false, deductAA: false, bangwang: false, climb: false, nocheck: false }); + +// 扩展位允许非 '0'(服务端不校验扩展位内容,由专门读扩展位的代码解释) +t.eq('扩展位含 1 不影响协议位解析', + C.parse("11011111111"), { aset12: true, deductAA: true, bangwang: false, climb: true, nocheck: true }); + +// RESERVE_LEN = 6(前后端 SSOT,仅作 SSOT 标记与未来断言使用) +t.eq('RESERVE_LEN = 6(前后端 SSOT)', C.RESERVE_LEN, 6); + +// --- 局数 --- +t.eq('6局 "0"', asetcount("00000"), 6); +t.eq('12局 "1"', asetcount("10000"), 12); + +// --- 开房房卡 (design §10.1) --- +t.eq('房主6局', needroomcard("00000"), 2); +t.eq('房主12局', needroomcard("10000"), 4); +t.eq('AA6局', needroomcard("01000"), 1); +t.eq('AA12局', needroomcard("11000"), 2); + +// --- 直接调用 export 的接口(替代复刻映射) --- +const exp = require('../class.export.js').new(); +t.eq('export 局数6', exp.get_asetcount("00000"), 6); +t.eq('export 局数12', exp.get_asetcount("10000"), 12); +t.eq('export 房主6局2张', exp.get_needroomcard("00000"), 2); +t.eq('export 房主12局4张', exp.get_needroomcard("10000"), 4); +t.eq('export AA6局1张', exp.get_needroomcard("01000"), 1); +t.eq('export 加入·房主0张', exp.get_needroomcard_joinroom("00000"), 0); +t.eq('export 加入·AA6局1张', exp.get_needroomcard_joinroom("01000"), 1); + +process.exit(t.done('config') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_deal.js b/server/games/erqiwang/test/test_deal.js new file mode 100644 index 0000000..c78c3c6 --- /dev/null +++ b/server/games/erqiwang/test/test_deal.js @@ -0,0 +1,28 @@ +// §2 牌局构成:两副去3/4共92张、三家各28+底8、分值。期望取自 design §2/§6.1 +require('./_shim'); +const P = require('../class.paiju.js'); +const t = require('./_assert')(); + +const pj = P.new({ paiju_list: [] }, 0); // 真实发牌(min_random 由 shim 提供) + +// dealowner 统计:-1 去掉的3/4;0 底牌;1/2/3 三家 +const cnt = {}; +for (const c of pj.cards) cnt[c.dealowner] = (cnt[c.dealowner] || 0) + 1; + +t.eq('发牌 三家各28', [P.get_seat_cards_deal(pj, 0).length, P.get_seat_cards_deal(pj, 1).length, P.get_seat_cards_deal(pj, 2).length], [28, 28, 28]); +t.eq('发牌 底牌8', P.get_bottomcards(pj).length, 8); +t.eq('发牌 总发出92(28×3+8)', (cnt[0] || 0) + (cnt[1] || 0) + (cnt[2] || 0) + (cnt[3] || 0), 92); +t.eq('去掉的3/4共16', cnt[-1], 16); +t.eq('无3/4被发出', pj.cards.filter(c => (c.number === 3 || c.number === 4) && c.dealowner !== -1).length, 0); +t.eq('牌对象总数108', pj.cards.length, 108); + +// 分值 (§6.1):5→5、10→10、K(13)→10、其余0 +const scoreOf = n => pj.cards.find(x => x.number === n).score; +t.eq('分值 5→5', scoreOf(5), 5); +t.eq('分值 10→10', scoreOf(10), 10); +t.eq('分值 K(13)→10', scoreOf(13), 10); +t.eq('分值 A(1)→0', scoreOf(1), 0); +t.eq('分值 6→0', scoreOf(6), 0); +t.eq('分值 大王→0', scoreOf(54), 0); + +process.exit(t.done('deal') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_desk.js b/server/games/erqiwang/test/test_desk.js new file mode 100644 index 0000000..f2fce53 --- /dev/null +++ b/server/games/erqiwang/test/test_desk.js @@ -0,0 +1,46 @@ +// §4.6 坐庄轮换 do_prepare(庄赢连庄 / 闲赢·投降下家)。期望取自 design §4.6 +require('./_shim'); +const D = require('../class.desk.js'); +const t = require('./_assert')(); + +// 造一个牌桌,压入一局"已结束"的牌局(banker/result),三家都准备后开新局,取新局 firstseat +function nextFirstseat(banker, result) { + const o_room = { seatlist: [{ conmode: 0, fromid: 0 }, { conmode: 0, fromid: 1 }, { conmode: 0, fromid: 2 }], asetcount: 6, method: { sendpack_toother: () => { } } }; + const desk = D.new(o_room); + desk.paiju_list.push({ banker, result, tmp_jiesuan_aset: {} }); + D.do_prepare(desk, 0); D.do_prepare(desk, 1); D.do_prepare(desk, 2); + return desk.paiju_list[desk.paiju_list.length - 1].firstseat; +} +t.eq('轮庄 庄1赢(result0)连庄', nextFirstseat(1, 0), 1); +t.eq('轮庄 庄1闲赢(result1)下家2', nextFirstseat(1, 1), 2); +t.eq('轮庄 庄1投降(result2)下家2', nextFirstseat(1, 2), 2); +t.eq('轮庄 庄2赢连庄', nextFirstseat(2, 0), 2); +t.eq('轮庄 庄2闲赢下家(回0)', nextFirstseat(2, 1), 0); +t.eq('轮庄 庄0闲赢下家1', nextFirstseat(0, 1), 1); + +// ============ I-3 发牌包必须自带阶段 step 与控制权 seat ============ +// 红线「状态机唯一在服务端」:阶段与「轮到谁」必须由服务端在包里显式给出。 +// 曾经 fapai 只有 seat 没有 step,前端只能按 rpc 名硬编码 step=1——那就是把阶段推导 +// 搬到了前端。反向验证:把 class.desk.js 里的 msg.data.step 删掉,本组第一条必转红。 +function firstFapai() { + const sent = []; + const o_room = { + seatlist: [{ conmode: 0, fromid: 0 }, { conmode: 0, fromid: 1 }, { conmode: 0, fromid: 2 }], + asetcount: 6, method: { sendpack_toother: () => { } } + }; + const prevApp = global.youle_erqiwang.app; + global.youle_erqiwang.app = { SendPack: m => sent.push(JSON.parse(JSON.stringify(m))) }; + const desk = D.new(o_room); + D.do_new_paiju(desk, 1); // 首个叫分者 = 1 号位(刻意不用 0,避免"恒等于 0"的假绿) + global.youle_erqiwang.app = prevApp; + return { desk, pj: desk.paiju_list[desk.paiju_list.length - 1], fapai: sent.filter(m => m.rpc === 'fapai') }; +} +const fp = firstFapai(); +t.eq('发牌 fapai 带 step,且 = 牌局的真实阶段(1 叫分)', fp.fapai.map(m => m.data.step), [1, 1, 1]); +t.eq('发牌 fapai.step 与 paiju.step 同源(不是写死的常量)', fp.fapai[0].data.step, fp.pj.step); +t.eq('发牌 fapai 控制权 seat = 当前等待叫分者(firstseat=1)', + fp.fapai.map(m => m.data.seat), [1, 1, 1]); +t.eq('发牌 fapai.seat 与 get_callgrade_seat() 同源', + fp.fapai[0].data.seat, fp.pj.method.get_callgrade_seat()); + +process.exit(t.done('desk') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_endgame.js b/server/games/erqiwang/test/test_endgame.js new file mode 100644 index 0000000..535215f --- /dev/null +++ b/server/games/erqiwang/test/test_endgame.js @@ -0,0 +1,182 @@ +// L4 端到端:用真实发牌跑完整一局(叫分→上庄→选主→埋牌→28轮出牌→小局结算), +// 校验 design §12.1 的流程闭合与一批与牌面无关的不变量(牌张守恒/捡分一致/结算零和/分配公式)。 +// 另覆盖 §4.4 投降局与 §12.2 中途解散两条支线。 +// 说明:牌是随机发的,但断言的都是**与具体牌面无关**的不变量,不会因发牌不同而抖动。 +const { mod, setup } = require('./_rpc.js'); +const t = require('./_assert')(); +const P = global.cls_youle_erqiwang_paiju; +const AR = global.cls_youle_erqiwang_arith; + +const pk = (seat, d) => ({ conmode: 0, fromid: seat, data: Object.assign({ agentid: 1, playerid: seat, gameid: 1, roomcode: 1, seat: seat }, d || {}) }); + +// 开局并让 0 号位以 call 分坐庄(另两家不叫) +function seatBanker(roomtype, call) { + const pj = P.new({ paiju_list: [] }, 0); + const ctx = setup(roomtype, pj); + mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: call })); + mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); + mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); + return { pj, ctx }; +} +// 出牌驱动:首家出手上最小的单张,跟家取第一张能通过 can_followcard 的牌 +function playAll(pj) { + let guard = 0; + while (pj.step === 5 && ++guard < 200) { + const seat = pj.playproc.currseat, hand = P.get_seat_cards(pj, seat); + let pick = null; + if (seat === pj.playproc.start) { + pick = [hand[hand.length - 1]]; + } else { + for (const c of hand) { + if (AR.can_followcard(pj.flower, hand, [c], pj.playproc.startcount, pj.playproc.startflower, pj.playproc.starttype).result) { pick = [c]; break; } + } + } + if (!pick) { return false; } + mod.chupai(pk(seat, { cards: pick })); + } + return pj.step === 6; +} + +// ============ §12.1 正常局全流程 ============ +const { pj, ctx } = seatBanker("00000", 65); +t.eq('§4.2 坐庄(banker/call/step)', [pj.banker, pj.call, pj.step], [0, 65, 2]); +t.eq('§4.3 庄家摸暗牌后36张 / 闲家28张', [P.get_seat_cards(pj, 0).length, P.get_seat_cards(pj, 1).length], [36, 28]); +mod.xuanzhu(pk(0, { flower: 1 })); +t.eq('§4.4 选主后进埋牌阶段', [pj.flower, pj.step], [1, 3]); +mod.maipai(pk(0, { cards: P.get_seat_cards(pj, 0).slice(-8) })); +t.eq('§4.5 埋牌后庄家28张、进出牌阶段', [P.get_seat_cards(pj, 0).length, pj.step], [28, 5]); +t.eq('§5.1 第一轮固定由庄家先出', [pj.playproc.round, pj.playproc.start], [1, 0]); +t.eq('§5/§6 28轮打完进入结算', playAll(pj) && pj.playproc.round, 28); +t.eq('§2 牌张守恒(出84 / 埋8 / 去掉3-4共16)', + [pj.cards.filter(c => c.playround > 0).length, pj.cards.filter(c => c.playround === 0).length, pj.cards.filter(c => c.number === 3 || c.number === 4).length], + [84, 8, 16]); + +const last = ctx.sent[ctx.sent.length - 1]; +const aset = last.data.aset; +t.eq('结算包 rpc/success', [last.rpc, last.data.success], ['jiesuan', true]); +t.eq('§11 结束亮出底牌8张', last.data.bottom.cards.length, 8); +// §6.2/§6.3:闲家捡分 = 闲家赢下的分牌 + 扣底(可能翻倍)分 +let jian = 0; +for (const c of pj.cards) { if (c.playowner > -1 && c.playowner !== pj.banker) { jian += c.score; } } +const bonus = last.data.bottom.grade2 || 0; +t.eq('§6.2/§6.3 捡分 = 闲家赢得分牌 + 扣底分', aset.grade, jian + bonus); +// §6.3:扣底只在「闲家用主牌赢下末轮」时发生 +const lastWinIsXian = pj.playproc.maxseat !== pj.banker; +const lastMult = lastWinIsXian ? AR.get_bottom_multiple(pj.flower, pj.playproc.cards[pj.playproc.maxseat]) : 0; +t.eq('§6.3 扣底触发条件(闲家 + 主牌)', lastMult > 0, !!last.data.bottom.multiple); +// §7 第3层:X = 基础子数 × |判定倍率|,庄家 ±2X、闲家各 ∓X +const X = aset.multiple * Math.abs(aset.upgrade); +const win = aset.upgrade > 0 ? 1 : -1; +t.eq('§7 捡分子数分配 庄±2X / 闲各∓X', aset.seatlist.map(p => p.grade_jf), [win * X * 2, -win * X, -win * X]); +// §8.4 第4层:算奖线 X×(2Ni−Nj−Nk) +const N = aset.seatlist.map(p => p.naward); +t.eq('§8.4 算奖分配 X×(2Ni−Nj−Nk)', aset.seatlist.map(p => p.grade_aw), + [X * (2 * N[0] - N[1] - N[2]), X * (2 * N[1] - N[0] - N[2]), X * (2 * N[2] - N[0] - N[1])]); +// S-6:算奖得分按【冲关 / 傍王】两个分量分别再算一次,供大局结算分项展示。 +// 公式对 N 线性,故两分量之和必须恒等于 grade_aw——这是拆分正确性的核心不变量。 +t.eq('S-6 grade_cg + grade_bw == grade_aw', + aset.seatlist.map(p => p.grade_cg + p.grade_bw), aset.seatlist.map(p => p.grade_aw)); +// 分量口径:冲关只计庄家;傍王未勾选(roomtype "00000" 位2=0)时应恒为 0 +t.eq('S-6 未勾傍王 → grade_bw 全 0', aset.seatlist.map(p => p.grade_bw), [0, 0, 0]); +t.eq('S-6 未勾傍王 → grade_cg == grade_aw', aset.seatlist.map(p => p.grade_cg), aset.seatlist.map(p => p.grade_aw)); +t.eq('本局结算零和', aset.seatlist.reduce((s, p) => s + p.grade, 0), 0); +t.eq('S-6 两个分量各自零和', [ + aset.seatlist.reduce((s, p) => s + p.grade_cg, 0), + aset.seatlist.reduce((s, p) => s + p.grade_bw, 0)], [0, 0]); +t.eq('§4.6 result 与判定一致(0庄赢/1闲赢)', pj.result, aset.upgrade > 0 ? 0 : 1); + +// ============ S-6:开傍王局的分量拆分 + 大局累计 ============ +// 上面那局没开傍王,grade_bw 恒 0,只验到平凡情形。这里开傍王(roomtype 位2=1)跑一整局, +// 让两个分量都非零,才算真的验证了「N 拆成冲关/傍王分别代入公式」这件事。 +{ + const bw = seatBanker("00100", 65); // 位2=1 → 傍王开 + mod.xuanzhu(pk(0, { flower: 1 })); + mod.maipai(pk(0, { cards: P.get_seat_cards(bw.pj, 0).slice(-8) })); + t.eq('S-6 傍王局 打满28轮', playAll(bw.pj) && bw.pj.playproc.round, 28); + const a = bw.ctx.sent[bw.ctx.sent.length - 1].data.aset; + t.eq('S-6 傍王局 bangwang 标志', a.bangwang, 1); + // 核心不变量:两分量之和 == grade_aw + t.eq('S-6 傍王局 grade_cg + grade_bw == grade_aw', + a.seatlist.map(p => p.grade_cg + p.grade_bw), a.seatlist.map(p => p.grade_aw)); + // 分量各自零和(每条支付线都是三家之间的转移,不产生也不消灭分数) + t.eq('S-6 傍王局 两分量各自零和', [ + a.seatlist.reduce((s, p) => s + p.grade_cg, 0), + a.seatlist.reduce((s, p) => s + p.grade_bw, 0)], [0, 0]); + // 口径:naward = 冲关(仅庄家) + 王数(傍王开) + const X = a.multiple * Math.abs(a.upgrade); + t.eq('S-6 傍王局 naward 口径', a.seatlist.map((p, i) => p.naward), + a.seatlist.map((p, i) => (i === a.banker ? p.chongguan : 0) + p.wang)); + // 傍王分量必须真的非零,否则这一整块又是平凡情形(三家王数全相等时净额才会是 0) + const wangs = a.seatlist.map(p => p.wang); + const wangAllSame = wangs[0] === wangs[1] && wangs[1] === wangs[2]; + t.eq('S-6 傍王局 王数不全相等时 grade_bw 必有非零', wangAllSame || a.seatlist.some(p => p.grade_bw !== 0), true); + // 逐项复核傍王分量的公式 + const NB = a.seatlist.map(p => p.wang); + t.eq('S-6 傍王分量 == X×(2Ni−Nj−Nk)', a.seatlist.map(p => p.grade_bw), + [X * (2 * NB[0] - NB[1] - NB[2]), X * (2 * NB[1] - NB[0] - NB[2]), X * (2 * NB[2] - NB[0] - NB[1])]); + // 冲关分量:只有庄家有 N_cg,闲家为 0 + const NC = a.seatlist.map((p, i) => (i === a.banker ? p.chongguan : 0)); + t.eq('S-6 冲关分量 == X×(2Ni−Nj−Nk)(仅庄家有 N)', a.seatlist.map(p => p.grade_cg), + [X * (2 * NC[0] - NC[1] - NC[2]), X * (2 * NC[1] - NC[0] - NC[2]), X * (2 * NC[2] - NC[0] - NC[1])]); + // 大局累计:desk.seatlist 后三位分别累计 基础/冲关/傍王,三者之和 == 累积总分 + const dsk = bw.pj.o_desk.seatlist; + t.eq('S-6 大局累计 基础+冲关+傍王 == 累积总分', + dsk.map(s => s[2] + s[3] + s[4]), dsk.map(s => s[0])); +} + +// ============ §4.4 / §7.1 投降局 ============ +const tx = seatBanker("00000", 70); +t.eq('§4.3 70分坐庄:暗牌亮3秒标志 + 闲家也收到暗牌', + [tx.ctx.sent[tx.ctx.sent.length - 1].data.ancard3s, tx.ctx.sent[tx.ctx.sent.length - 2].data.bottomcards.length], [1, 8]); +mod.touxiang(pk(0)); +const txa = tx.ctx.sent[tx.ctx.sent.length - 1].data.aset; +t.eq('§7.1 投降:base=1 / 庄-2、闲各+1', [txa.multiple, txa.seatlist.map(p => p.grade_jf)], [1, [-2, 1, 1]]); +t.eq('§4.4 投降不选主、不埋牌', [tx.pj.flower, tx.pj.cards.filter(c => c.playround === 0).length], [-1, 0]); +t.eq('§8.4 投降算奖按庄家36张快照', P.get_seat_cards_award(tx.pj, 0).length, 36); +t.eq('§4.6 投降 result=2(下家坐庄)', tx.pj.result, 2); + +// ============ §12.2 中途解散 ============ +const ds = seatBanker("00000", 65); +mod.xuanzhu(pk(0, { flower: 1 })); +const EXPORT = require('../class.export.js').new(); +const dsmsg = EXPORT.get_disbandRoom(ds.ctx.o_room); +t.eq('§12.2 解散:不结算子数、result=3', [dsmsg.data.aset.multiple, dsmsg.data.aset.seatlist.map(p => p.grade), ds.pj.result], [0, [0, 0, 0], 3]); +// 平台在 makewar 后立刻置 battlestate=1,而首局是延迟 1 秒创建的:这段窗口内解散必须返回 null, +// 而不是让 curr_paiju() 的 undefined 解引用抛异常打断平台的整条解散链路。 +// 这里必须 try/catch:守卫一旦被去掉,被测函数会直接抛异常,未捕获时整个文件在此中断, +// 后面的断言全不执行——看到的是崩溃而不是某条断言转红(实测过)。 +const noThrow = fn => { try { return fn(); } catch (e) { return '抛异常: ' + e.message; } }; +t.eq('解散·牌局尚未创建 → 返回 null 不抛异常', + noThrow(() => EXPORT.get_disbandRoom({ o_desk: { method: { curr_paiju: () => undefined } } })), null); +t.eq('解散·牌桌尚未创建 → 返回 null 不抛异常', noThrow(() => EXPORT.get_disbandRoom({})), null); +// get_deskinfo 有同一个 o_desk 守卫(平台在开战前也可能回调),一并钉住 +t.eq('重连·牌桌尚未创建 → 返回 null 不抛异常', noThrow(() => EXPORT.get_deskinfo({}, 0)), null); + +// ============ 出牌入参顺序无关性(端到端)============ +// 同一手完全合法的跟牌,仅客户端提交的数组顺序不同,本轮胜者/捡分/扣底/结算判定必须完全一致。 +// 构造:庄家(0)首出 红心KKQQ(副牌两连对,含20分),闲家(1)用 黑桃KKQQ(主拖拉机,含20分)毙牌, +// 闲家(2)垫方块杂牌;这是本局最后一轮,故同时覆盖扣底判定。 +function oneTrick(followOrder) { + const p = P.new({ paiju_list: [] }, 0); + const c = setup("00000", p); + p.banker = 0; p.call = 65; p.flower = 4; p.step = 5; + // 先把所有牌置为「第1轮已由庄家赢走」,再把参战的 12 张发回三家手上 + for (const card of p.cards) { card.playround = 1; card.playowner = 0; } + const give = (ids, owner) => ids.forEach(i => Object.assign(p.cards[i], { dealowner: owner + 1, playround: -1, playowner: -1 })); + give([38, 92, 37, 91], 0); // 红心 K K Q Q + give([51, 105, 50, 104], 1); // 黑桃 K K Q Q(主) + give([5, 7, 8, 10], 2); // 方块杂牌 + P.new_playround(p, 1, 0); + mod.chupai(pk(0, { cards: [38, 92, 37, 91] })); + mod.chupai(pk(1, { cards: followOrder })); + mod.chupai(pk(2, { cards: [5, 7, 8, 10] })); + const m = c.ctx ? c.ctx : c; + const j = m.sent[m.sent.length - 1]; + return [p.playproc.maxseat, j.data.chupai.grade, j.data.aset.grade, j.data.bottom.multiple]; +} +const ordered = oneTrick([51, 105, 50, 104]); // 降序 +const shuffled = oneTrick([51, 50, 105, 104]); // 乱序(把两个对子拆开摆) +t.eq('序无关 端到端·有序提交:闲家毙牌赢下末轮并扣底×4', ordered, [1, 40, 40, 4]); +t.eq('序无关 端到端·乱序提交结果完全一致', shuffled, ordered); + +process.exit(t.done('endgame') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_flow.js b/server/games/erqiwang/test/test_flow.js new file mode 100644 index 0000000..80695db --- /dev/null +++ b/server/games/erqiwang/test/test_flow.js @@ -0,0 +1,195 @@ +// 流程完整性(design §4.2 / §4.6 / §12.1 阶段机) +// +// ① **阶段机迁移矩阵**:8 个 RPC × 5 个阶段 = 40 格,逐格验证「该受理的受理、不该受理的 +// 一律回 STEP 失败包」。此前每个 handler 只测了 1~2 条代表性失败路径,没有系统覆盖 +// 「某个请求在别的阶段会不会被误放行 / 会不会静默丢弃」。 +// ② **叫分起始者**:第一局默认座位 0;之后每局起始叫分者 = 暂定庄家(庄赢连庄 / 庄输下家); +// 每一局的首家都不能"不叫"。连庄分支单独构造(叫 70 分 + 首家出最大牌)才走得到。 +// +// 随机源与发牌都接固定种子,结果可复现。 +const R = require('./_rpc.js'); const mod = R.mod; +const P = global.cls_youle_erqiwang_paiju, A = global.cls_youle_erqiwang_arith; +const D = require('../class.desk.js'); +const t = require('./_assert')(); +let seed = 0x5A5A01; +const rnd = n => { seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; seed >>>= 0; return seed % n; }; +global.min_ontimeout = fn => fn(); +global.min_random = (a, b) => a + rnd(b - a + 1); +const pk = (s, d) => ({ conmode: 0, fromid: s, data: Object.assign({ agentid: 1, playerid: s, gameid: 1, roomcode: 1, seat: s }, d || {}) }); +const ERR = mod.ERR; + +function mkRoom(rt, asetcount) { + const sent = []; + const o_room = { + roomtype: rt, asetcount: asetcount || 6, roomcode: 1, createtime: 'T', makewartime: 'T', + seatlist: [0, 1, 2].map(i => ({ conmode: 0, fromid: i, playerid: 100 + i, nickname: 'P', avatar: '', gameinfo: {} })), + method: { sendpack_toother: m => sent.push(JSON.parse(JSON.stringify(m))) } + }; + const desk = D.new(o_room); o_room.o_desk = desk; + global.youle_erqiwang.app = { SendPack: m => sent.push(JSON.parse(JSON.stringify(m))) }; + global.youle_erqiwang.import = { check_player: () => o_room, deduct_roomcard: () => { }, save_grade: () => { } }; + mod.import = global.youle_erqiwang.import; mod.app = global.youle_erqiwang.app; + return { o_room, desk, sent }; +} +// 把牌局驱动到指定阶段。call=70 时保留投降资格 +function driveTo(step, call) { + const c = mkRoom('00000'); + D.do_new_paiju(c.desk, 0); + const pj = c.desk.method.curr_paiju(); + if (step >= 2) { + mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: call || 65 })); + if (pj.step === 1) mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); + if (pj.step === 1) mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); + } + if (step >= 3) mod.xuanzhu(pk(pj.banker, { flower: 1 })); + if (step >= 5) mod.maipai(pk(pj.banker, { cards: P.get_seat_cards(pj, pj.banker).slice(-8) })); + if (step >= 6) { + let g = 0; + while (pj.step === 5 && ++g < 400) { + const s = pj.playproc.currseat, h = P.get_seat_cards(pj, s); + let p = null; + if (s === pj.playproc.start) p = [h[h.length - 1]]; + else for (const x of h) if (A.can_followcard(pj.flower, h, [x], pj.playproc.startcount, pj.playproc.startflower, pj.playproc.starttype).result) { p = [x]; break; } + if (!p) break; + mod.chupai(pk(s, { cards: p })); + } + } + return { c, pj }; +} +// 各 RPC 在「本阶段的正确发起者 + 正确入参」下发一次;返回是否被受理 +const FIRE = { + jiaofen: (c, pj) => { const s = pj.step === 1 ? pj.method.get_callgrade_seat() : 0; mod.jiaofen(pk(s, { call: 65 })); }, + xuanzhu: (c, pj) => mod.xuanzhu(pk(pj.banker >= 0 ? pj.banker : 0, { flower: 1 })), + touxiang: (c, pj) => mod.touxiang(pk(pj.banker >= 0 ? pj.banker : 0)), + maipai: (c, pj) => { const b = pj.banker >= 0 ? pj.banker : 0; const h = P.get_seat_cards(pj, b); mod.maipai(pk(b, { cards: h.slice(-8) })); }, + chupai: (c, pj) => { + const s = (pj.playproc && pj.playproc.currseat >= 0) ? pj.playproc.currseat : (pj.banker >= 0 ? pj.banker : 0); + const h = P.get_seat_cards(pj, s); + mod.chupai(pk(s, { cards: h.length ? [h[h.length - 1]] : [0] })); + }, + mingpai: (c, pj) => { + // design §9:明牌按钮在【有人报无主之后】才出现,故 step5 还要先满足这个前置 + if (pj.step === 5) { const s2 = (pj.banker + 1) % 3; for (const cd of P.get_seat_zhucards(pj, s2)) pj.cards[cd].playround = 1; pj.seatlist[s2][4][0] = 0; } + mod.mingpai(pk(pj.banker >= 0 ? (pj.banker + 1) % 3 : 1)); + }, + tishi: (c, pj) => mod.tishi(pk(pj.banker >= 0 ? (pj.banker + 1) % 3 : 1, { tip: 1 })), + zhunbei: (c, pj) => mod.zhunbei(pk(0)) +}; +// design 允许的 (rpc, step):其余阶段一律应回 STEP +const ALLOWED = { jiaofen: [1], xuanzhu: [2], touxiang: [2], maipai: [3], chupai: [5], mingpai: [5], tishi: [5], zhunbei: [6] }; +const STEPS = [1, 2, 3, 5, 6]; + +const bad = []; +let cells = 0; // 实际比对的格数,防止驱动失败导致整行"跳过"而假绿 +console.log('阶段机迁移矩阵(行=RPC,列=阶段;✓受理 ×拒(STEP) ?拒(其他码))'); +console.log(' step1 step2 step3 step5 step6'); +for (const rpc in ALLOWED) { + const row = []; + for (const st of STEPS) { + // touxiang 需要 call=70 才有资格;其余用 65 + const { c, pj } = driveTo(st, rpc === 'touxiang' ? 70 : 65); + if (pj.step !== st) { row.push(' 跳过'); continue; } + c.sent.length = 0; + FIRE[rpc](c, pj); + const fails = c.sent.filter(m => m.data && m.data.success === false); + const accepted = c.sent.length > 0 && fails.length === 0; + const stepRejected = fails.length > 0 && fails[0].data.errcode === ERR.STEP; + const should = ALLOWED[rpc].indexOf(st) >= 0; + let mark; + if (accepted) mark = ' ✓ '; + else if (stepRejected) mark = ' × '; + else if (fails.length) mark = ' ?' + fails[0].data.errcode + ' '; + else mark = ' 无包 '; + row.push(mark); cells++; + if (should && !accepted) bad.push(`${rpc} 在 step${st} 应受理却未受理(${mark.trim()})`); + if (!should && accepted) bad.push(`${rpc} 在 step${st} 不应受理却受理了`); + if (!should && !accepted && !stepRejected && fails.length) bad.push(`${rpc} 在 step${st} 被拒但错误码不是 STEP(${fails[0].data.errcode})`); + if (!should && !fails.length) bad.push(`${rpc} 在 step${st} 既没受理也没回失败包(静默丢弃)`); + } + console.log(rpc.padEnd(10) + row.join('')); +} +t.eq('§12.1 阶段机迁移矩阵(8 RPC × 5 阶段 = 40 格)无异常', bad.slice(0, 3), []); +t.eq('阶段机矩阵覆盖 40 格', cells, 40); + +// ===================== 叫分起始者规则 ===================== +console.log('\n叫分起始者规则(design §4.2 + §4.6)'); +const errs2 = []; +const c2 = mkRoom('00000'); +D.do_new_paiju(c2.desk, 0); +let pj = c2.desk.method.curr_paiju(); +if (pj.firstseat !== 0) errs2.push('第一局起始叫分者应为座位0,实为 ' + pj.firstseat); +if (pj.method.get_callgrade_seat() !== 0) errs2.push('第一局待叫者应为座位0'); +// 首家不能不叫 +c2.sent.length = 0; mod.jiaofen(pk(0, { call: 0 })); +if (!(c2.sent.length === 1 && c2.sent[0].data.success === false && c2.sent[0].data.errcode === ERR.RULE)) errs2.push('首家"不叫"未被按 RULE 拒'); +if (pj.method.get_callgrade_seat() !== 0) errs2.push('首家被拒后仍应轮到他自己'); +t.eq('§4.6 第一局起始叫分者 = 座位 0', [pj.firstseat, 0], [0, 0]); +// 跨局:起始叫分者 = 暂定庄家(庄赢连庄 / 庄输下家) +const seen = { 0: 0, 1: 0, 2: 0 }; +for (let r = 1; r <= 8; r++) { + const cur = c2.desk.method.curr_paiju(); + if (cur.method.get_callgrade_seat() !== cur.firstseat) errs2.push('第' + r + '局待叫者≠firstseat'); + // 该局首家不叫必须被拒 + c2.sent.length = 0; mod.jiaofen(pk(cur.firstseat, { call: 0 })); + if (!(c2.sent.length === 1 && c2.sent[0].data.errcode === ERR.RULE)) errs2.push('第' + r + '局首家不叫未被拒'); + // 正常打完这一局 + mod.jiaofen(pk(cur.method.get_callgrade_seat(), { call: 65 })); + if (cur.step === 1) mod.jiaofen(pk(cur.method.get_callgrade_seat(), { call: 0 })); + if (cur.step === 1) mod.jiaofen(pk(cur.method.get_callgrade_seat(), { call: 0 })); + const b = cur.banker; seen[b]++; + mod.xuanzhu(pk(b, { flower: 1 })); + mod.maipai(pk(b, { cards: P.get_seat_cards(cur, b).slice(-8) })); + let g = 0; + while (cur.step === 5 && ++g < 400) { + const s = cur.playproc.currseat, h = P.get_seat_cards(cur, s); + let p = null; + if (s === cur.playproc.start) p = [h[h.length - 1]]; + else for (const x of h) if (A.can_followcard(cur.flower, h, [x], cur.playproc.startcount, cur.playproc.startflower, cur.playproc.starttype).result) { p = [x]; break; } + if (!p) break; + mod.chupai(pk(s, { cards: p })); + } + const expNext = (cur.result === 0) ? b : (b + 1) % 3; + if (r < 6) { + mod.zhunbei(pk(0)); mod.zhunbei(pk(1)); mod.zhunbei(pk(2)); + const nx = c2.desk.method.curr_paiju(); + if (nx === cur) { errs2.push('第' + r + '局后未开新局'); break; } + if (nx.firstseat !== expNext) errs2.push('第' + (r + 1) + '局起始叫分者应为 ' + expNext + ',实为 ' + nx.firstseat); + } else break; +} +t.eq('§4.2/§4.6 叫分起始者与首家必叫(连打 6 局)无异常', errs2.slice(0, 3), []); + +// ===================== 连庄分支(result=0):庄赢时起始叫分者不变 ===================== +console.log('\n连庄分支(design §4.6 庄赢→暂定庄家仍为该玩家)'); +let got = false; +for (let attempt = 0; attempt < 60 && !got; attempt++) { + const c3 = mkRoom('00000'); + D.do_new_paiju(c3.desk, 0); + const p3 = c3.desk.method.curr_paiju(); + // 叫 70:闲家要捡满 70 分才算达标,庄家最易守;首家出最大牌以多赢墩 + mod.jiaofen(pk(p3.method.get_callgrade_seat(), { call: 70 })); + if (p3.step === 1) mod.jiaofen(pk(p3.method.get_callgrade_seat(), { call: 0 })); + if (p3.step === 1) mod.jiaofen(pk(p3.method.get_callgrade_seat(), { call: 0 })); + const b = p3.banker; + mod.xuanzhu(pk(b, { flower: 1 })); + mod.maipai(pk(b, { cards: P.get_seat_cards(p3, b).slice(-8) })); + let g = 0; + while (p3.step === 5 && ++g < 400) { + const s = p3.playproc.currseat, h = P.get_seat_cards(p3, s); + let p = null; + if (s === p3.playproc.start) p = [h[0]]; + else for (let i = h.length - 1; i >= 0; i--) if (A.can_followcard(p3.flower, h, [h[i]], p3.playproc.startcount, p3.playproc.startflower, p3.playproc.starttype).result) { p = [h[i]]; break; } + if (!p) break; + mod.chupai(pk(s, { cards: p })); + } + if (p3.result !== 0) continue; + got = true; + mod.zhunbei(pk(0)); mod.zhunbei(pk(1)); mod.zhunbei(pk(2)); + const nx = c3.desk.method.curr_paiju(); + t.eq('§4.6 庄赢(result=0) → 下一局起始叫分者仍是该庄家(连庄)', nx.firstseat, b); + // 连庄局的首家同样不能不叫 + c3.sent.length = 0; mod.jiaofen(pk(nx.firstseat, { call: 0 })); + t.eq('§4.2 连庄局的首家同样不能"不叫"', c3.sent.length === 1 && c3.sent[0].data.errcode === ERR.RULE, true); +} +t.eq('§4.6 连庄分支已覆盖到(构造出 result=0 的庄赢局)', got, true); + +process.exit(t.done('flow') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_follow.js b/server/games/erqiwang/test/test_follow.js new file mode 100644 index 0000000..e366d80 --- /dev/null +++ b/server/games/erqiwang/test/test_follow.js @@ -0,0 +1,139 @@ +// §5.1/5.2 正常跟牌/毙牌/垫牌(can_followcard 纯函数)。期望值取自 design §5.2 +require('./_shim'); +const A = require('../class.arith.js'); +const t = require('./_assert')(); + +const id = (d, f, n) => (d - 1) * 54 + (f - 1) * 13 + (n - 1); +const MF = 1; // 主花色=方块(1) +// 副花色2的牌(number 避开 2/7,否则是固定主) +const K = d => id(d, 2, 13), Q = d => id(d, 2, 12), J = d => id(d, 2, 11), T = d => id(d, 2, 10), N9 = d => id(d, 2, 9), N8 = d => id(d, 2, 8); +// 主牌(flower1) +const zK = d => id(d, 1, 13), zQ = d => id(d, 1, 12), zJ = d => id(d, 1, 11), z9 = d => id(d, 1, 9); +// 另一副花色3(用于垫牌) +const oth = n => id(1, 3, n); + +// follow(inhand, cards, startcount, startflower, startcardtype) -> can_followcard 结果 +const f = (inhand, cards, cnt, flw, typ) => A.can_followcard(MF, inhand, cards, cnt, flw, typ); +const res = r => r.result; // 能否跟 +const beat = r => (r.cardvalue || 0) > 0; // 是否压过(cardvalue>0) + +// ===================== 单张跟牌(首家 副K 单张,flower2, 101)===================== +// §5.2: 跟同花色任意单张;缺门则任意补(毙牌用主牌可压、垫副牌不压) +t.eq('单-跟同花色单张(合法)', res(f([N9(1), z9(1)], [N9(1)], 1, 2, 101)), true); +t.eq('单-有同花色却出主牌(拒)', res(f([N9(1), zK(1)], [zK(1)], 1, 2, 101)), false); +t.eq('单-缺门垫副牌(合法)', res(f([oth(9), zK(1)], [oth(9)], 1, 2, 101)), true); +t.eq('单-缺门垫副牌不压', beat(f([oth(9), zK(1)], [oth(9)], 1, 2, 101)), false); +t.eq('单-缺门毙牌主单(合法)', res(f([oth(9), zK(1)], [zK(1)], 1, 2, 101)), true); +t.eq('单-缺门毙牌主单压过', beat(f([oth(9), zK(1)], [zK(1)], 1, 2, 101)), true); + +// ===================== 对子跟牌(首家 副K对,flower2, 201, 2张)===================== +// §2: 有同花色对必出对;没对用两张同花色单;不足两张任意补;缺门毙牌必须主对(不能任意两张) +t.eq('对-有同花色对必出对(合法)', res(f([N9(1), N9(2), N8(1)], [N9(1), N9(2)], 2, 2, 201)), true); +t.eq('对-有对却出两散张(拒)', res(f([N9(1), N9(2), N8(1)], [N9(1), N8(1)], 2, 2, 201)), false); +t.eq('对-无对出两同花色单(合法)', res(f([N9(1), N8(1), Q(1)], [N9(1), N8(1)], 2, 2, 201)), true); +t.eq('对-缺门毙主对(合法且压)', beat(f([zK(1), zK(2), oth(9)], [zK(1), zK(2)], 2, 2, 201)), true); +t.eq('对-缺门两散主顶对(可出但不压)', res(f([zK(1), zQ(1), oth(9)], [zK(1), zQ(1)], 2, 2, 201)), true); +t.eq('对-缺门两散主顶对 不压', beat(f([zK(1), zQ(1), oth(9)], [zK(1), zQ(1)], 2, 2, 201)), false); + +// ===================== 拖拉机跟牌(首家 副KQ两连对,flower2, 302, 4张)===================== +// §2: 有同花色拖拉机必出拖;无拖但有两对不连也必出两对 +t.eq('拖-有同长拖必出拖(合法)', res(f([J(1), J(2), T(1), T(2), N8(1)], [J(1), J(2), T(1), T(2)], 4, 2, 302)), true); +t.eq('拖-有拖却出4散(拒)', res(f([J(1), J(2), T(1), T(2), N8(1)], [J(1), T(1), N9(1), N8(1)], 4, 2, 302)), false); +t.eq('拖-无拖两对不连必出两对(合法)', res(f([J(1), J(2), N9(1), N9(2), N8(1)], [J(1), J(2), N9(1), N9(2)], 4, 2, 302)), true); +t.eq('拖-有两对却出散张(拒)', res(f([J(1), J(2), N9(1), N9(2), N8(1)], [J(1), N9(1), N8(1), Q(1)], 4, 2, 302)), false); +t.eq('拖-缺门毙同长主拖(合法且压)', beat(f([zK(1), zK(2), zQ(1), zQ(2), oth(9)], [zK(1), zK(2), zQ(1), zQ(2)], 4, 2, 302)), true); + +const N6 = d => id(d, 2, 6); + +// ===================== 主牌首出的跟牌(followers 必须跟主牌)===================== +t.eq('主单 有主必跟主', res(f([zQ(1), oth(9)], [zQ(1)], 1, 1, 101)), true); +t.eq('主单 有主却垫副→拒', res(f([zQ(1), oth(9)], [oth(9)], 1, 1, 101)), false); +t.eq('主单 缺主垫副(合法不压)', res(f([oth(9), oth(8)], [oth(9)], 1, 1, 101)), true); +t.eq('主单 缺主垫副 不压', beat(f([oth(9), oth(8)], [oth(9)], 1, 1, 101)), false); +t.eq('主对 有主对必出对', res(f([zJ(1), zJ(2), z9(1)], [zJ(1), zJ(2)], 2, 1, 201)), true); +t.eq('主对 有主对却拆散→拒', res(f([zJ(1), zJ(2), z9(1)], [zJ(1), z9(1)], 2, 1, 201)), false); + +// ===================== 对子首出·同花色不足两张(必出仅有的1张+补)===================== +t.eq('对 同花1张必出它+补1', res(f([N9(1), oth(5), oth(6)], [N9(1), oth(5)], 2, 2, 201)), true); +t.eq('对 同花1张却不出它→拒', res(f([N9(1), oth(5), oth(6)], [oth(5), oth(6)], 2, 2, 201)), false); + +// ===================== 拖拉机首出·跟牌方各手牌分支 ===================== +t.eq('拖 对子不够·必出1对+补', res(f([J(1), J(2), N9(1), N8(1), N6(1)], [J(1), J(2), N9(1), N8(1)], 4, 2, 302)), true); +t.eq('拖 对子不够·不出对→拒', res(f([J(1), J(2), N9(1), N8(1), N6(1)], [N9(1), N8(1), N6(1), Q(1)], 4, 2, 302)), false); +t.eq('拖 同花无对·补同花4张', res(f([K(1), Q(1), J(1), T(1), N9(1)], [K(1), Q(1), J(1), T(1)], 4, 2, 302)), true); +// 多候选拖拉机(3连对里选1个2连拖)——回归 can_followcard 曾崩溃的 cantype=3xx 分支 +t.eq('拖 多候选·出合法2连拖', res(f([K(1), K(2), Q(1), Q(2), J(1), J(2)], [K(1), K(2), Q(1), Q(2)], 4, 2, 302)), true); +t.eq('拖 多候选·出不连两对→拒(D1回归)', res(f([K(1), K(2), Q(1), Q(2), J(1), J(2)], [K(1), K(2), J(1), J(2)], 4, 2, 302)), false); +// 3连对首出(303) +t.eq('拖 3连对·有3连拖必出', res(f([K(1), K(2), Q(1), Q(2), J(1), J(2), N8(1)], [K(1), K(2), Q(1), Q(2), J(1), J(2)], 6, 2, 303)), true); +// 3连对首出、跟牌方有富余对子(4对)但含1个3连拖 → 必出该3连拖,拆散→拒 +t.eq('拖 3连对·富余对子中出3连拖', res(f([K(1), K(2), Q(1), Q(2), J(1), J(2), N9(1), N9(2)], [K(1), K(2), Q(1), Q(2), J(1), J(2)], 6, 2, 303)), true); +t.eq('拖 3连对·拆散3连拖(改出99)→拒', res(f([K(1), K(2), Q(1), Q(2), J(1), J(2), N9(1), N9(2)], [K(1), K(2), Q(1), Q(2), N9(1), N9(2)], 6, 2, 303)), false); + +// —— 最大覆盖度校验:修复 get_followcard 降级递归 else 分支欠约束 —— +// 跟牌方该副花色 = AA KK JJ TT 88 66(三组互不相邻的2连对:{A,K} {J,T} {8,6}) +const sA = d => id(d, 2, 1); // 副A(flower2, number1;非固定主) +const H3 = [sA(1), sA(2), K(1), K(2), J(1), J(2), T(1), T(2), N8(1), N8(2), N6(1), N6(2)]; +// 303:无3连拖、有多组2连拖 → 必出1个2连拖+1散对(档案[2,1]);出三孤立对(档案[1,1,1])应拒 +t.eq('拖 303·三组2连·出2连+散(合法)', res(f(H3, [sA(1), sA(2), K(1), K(2), N8(1), N8(2)], 6, 2, 303)), true); +t.eq('拖 303·三组2连·出三孤立对AA/JJ/88→拒(else回归)', res(f(H3, [sA(1), sA(2), J(1), J(2), N8(1), N8(2)], 6, 2, 303)), false); +t.eq('拖 303·三组2连·另组KK/TT/66孤立→拒', res(f(H3, [K(1), K(2), T(1), T(2), N6(1), N6(2)], 6, 2, 303)), false); +// 304:无4连拖、有三组2连拖 → 必出两个2连拖(档案[2,2]);出1个2连+两散对(档案[2,1,1])应拒 +t.eq('拖 304·三组2连·出两2连拖(合法)', res(f(H3, [sA(1), sA(2), K(1), K(2), J(1), J(2), T(1), T(2)], 8, 2, 304)), true); +t.eq('拖 304·三组2连·出1个2连+两散对→拒(else回归)', res(f(H3, [sA(1), sA(2), K(1), K(2), J(1), J(2), N8(1), N8(2)], 8, 2, 304)), false); + +// ===================== 手牌数 == 首出张数:全部必出 ===================== +t.eq('全出 手牌恰1张(单张首出)', res(f([oth(9)], [oth(9)], 1, 2, 101)), true); +t.eq('全出 手牌恰2张(对子首出)', res(f([oth(9), oth(8)], [oth(9), oth(8)], 2, 2, 201)), true); + +// ===================== 垫牌:缺门首家副花色,用其他副牌补,不争夺出牌权 ===================== +// (单张垫牌上文已覆盖;此处补对子/拖拉机首家时的副牌垫) +t.eq('垫 对子缺门出两副散(合法)', res(f([oth(9), oth(8), zK(1)], [oth(9), oth(8)], 2, 2, 201)), true); +t.eq('垫 对子缺门出两副散 不压', beat(f([oth(9), oth(8), zK(1)], [oth(9), oth(8)], 2, 2, 201)), false); +t.eq('垫 拖缺门出四副散(合法)', res(f([oth(9), oth(8), oth(6), oth(5), zK(1)], [oth(9), oth(8), oth(6), oth(5)], 4, 2, 302)), true); +t.eq('垫 拖缺门出四副散 不压', beat(f([oth(9), oth(8), oth(6), oth(5), zK(1)], [oth(9), oth(8), oth(6), oth(5)], 4, 2, 302)), false); + +// ===================== 混合出牌:有该花色但不够,只能补主牌,等效垫牌不争权 ===================== +// 首家副对子:手中仅1张该副花色 + 主牌(无其他副牌)→ 必出该副 + 补主,混合牌压不过 +t.eq('混 对子·1副+补主(合法)', res(f([N9(1), zK(1), zQ(1)], [N9(1), zK(1)], 2, 2, 201)), true); +t.eq('混 对子·1副+补主 不压(等效垫)', beat(f([N9(1), zK(1), zQ(1)], [N9(1), zK(1)], 2, 2, 201)), false); +t.eq('混 对子·丢下必出的副牌只出两主→拒', res(f([N9(1), zK(1), zQ(1)], [zK(1), zQ(1)], 2, 2, 201)), false); +// 首家副拖拉机:手中2张该副花色散张 + 主牌 → 必出2副 + 补2主,混合压不过 +t.eq('混 拖·2副散+补2主(合法)', res(f([N9(1), N8(1), zK(1), zQ(1), zJ(1)], [N9(1), N8(1), zK(1), zQ(1)], 4, 2, 302)), true); +t.eq('混 拖·2副散+补2主 不压', beat(f([N9(1), N8(1), zK(1), zQ(1), zJ(1)], [N9(1), N8(1), zK(1), zQ(1)], 4, 2, 302)), false); +// 首家副拖拉机:手中1对该副花色(数量仍不足) + 主牌 → 有对也压不过(混合非纯主拖) +t.eq('混 拖·副对(不足)+补2主 不压', beat(f([N9(1), N9(2), zK(1), zQ(1), zJ(1)], [N9(1), N9(2), zK(1), zQ(1)], 4, 2, 302)), false); + +// ===================== 入参顺序无关性(服务端不得依赖客户端提交的数组顺序)===================== +// 回归:can_followcard 尾段曾用入参原始顺序的 followcards 算 cardvalue/noflower/nopair, +// 而 get_pairlist/get_tuolaji_list 按降序相邻取对——客户端只要打乱数组顺序, +// 就能把本该压过的主拖拉机算成牌面 0(改写本轮胜者/捡分/扣底),或抹掉缺门标志污染 §9 牌况表。 +const same = (a, b) => JSON.stringify(a) === JSON.stringify(b); + +// 主拖拉机毙牌:有序 vs 乱序,结果必须完全一致且都判为「压过」 +const HT = [zK(1), zK(2), zQ(1), zQ(2), oth(9)]; +const ht1 = f(HT, [zK(1), zK(2), zQ(1), zQ(2)], 4, 2, 302); // 降序 +const ht2 = f(HT, [zK(1), zQ(1), zK(2), zQ(2)], 4, 2, 302); // 乱序(对子被拆开摆放) +t.eq('序无关 主拖毙牌·有序压过', beat(ht1), true); +t.eq('序无关 主拖毙牌·乱序同样压过', beat(ht2), true); +t.eq('序无关 主拖毙牌·两种顺序结果全等', same(ht1, ht2), true); + +// 主对子毙牌 +const HP = [zK(1), zK(2), oth(9)]; +t.eq('序无关 主对毙牌·两种顺序结果全等', + same(f(HP, [zK(1), zK(2)], 2, 2, 201), f(HP, [zK(2), zK(1)], 2, 2, 201)), true); + +// 同花色拖拉机跟牌(非毙牌)的牌面值也不得随顺序变 +const HS = [J(1), J(2), T(1), T(2), N8(1)]; +t.eq('序无关 同花色跟拖·两种顺序结果全等', + same(f(HS, [J(1), J(2), T(1), T(2)], 4, 2, 302), f(HS, [J(1), T(1), J(2), T(2)], 4, 2, 302)), true); + +// 混合出牌:3 张该副花色 + 1 主牌。把副牌摆在首尾也必须记为「已缺该花色」 +const HM = [K(1), Q(1), J(1), zK(1)]; +const hm1 = f(HM, [zK(1), K(1), Q(1), J(1)], 4, 2, 302); // 降序:主牌在首 +const hm2 = f(HM, [K(1), zK(1), Q(1), J(1)], 4, 2, 302); // 乱序:首尾都是副牌 +t.eq('序无关 混合出牌·有序记缺门', hm1.noflower, true); +t.eq('序无关 混合出牌·乱序同样记缺门', hm2.noflower, true); +t.eq('序无关 混合出牌·两种顺序结果全等', same(hm1, hm2), true); + +process.exit(t.done('follow') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_followdiff.js b/server/games/erqiwang/test/test_followdiff.js new file mode 100644 index 0000000..cbe4883 --- /dev/null +++ b/server/games/erqiwang/test/test_followdiff.js @@ -0,0 +1,236 @@ +// §5.1/§5.2 跟牌强制层级 · 穷举差分测试 +// +// 做法:把 design §5.2 的裁定规则**按原文另写一份参考实现**(本文件上半部分,只依据 design +// 的文字,不看 class.arith 的实现),再随机造手牌、**枚举该手牌的全部 C 张出牌组合**, +// 逐个比对「参考实现允许 vs can_followcard 允许」。任何一侧多放行或多拒绝都会被抓到。 +// +// 这类差分比 test_follow.js 的手写正反例更能兜住实现里的欠约束分支 +// (get_followcard 降级递归的 else 分支就曾在这类场景漏判)。 +// 随机源用**固定种子的 PRNG**,不用 Math.random —— 同一份代码每次跑的用例集完全相同,不抖动。 +require('./_shim'); +const A = require('../class.arith.js'); +const t = require('./_assert')(); + +const MF = 4; // 主花色 = 黑桃 +const id = (d, f, n) => (d - 1) * 54 + (f - 1) * 13 + (n - 1); +const WG = (d, big) => (d - 1) * 54 + (big ? 53 : 52); + +// ==================== 参考实现(逐条转写自 design §3/§5.2/§5.3)==================== +const num = c => { const m = c % 54; return m === 52 ? 53 : m === 53 ? 54 : m % 13 + 1; }; +const flw = c => { const m = c % 54; return m >= 52 ? 5 : Math.floor(m / 13) + 1; }; +// §3:主牌 = 大小王 + 所有花色的 2 和 7 + 主花色普通牌 +const isTrump = c => { const n = num(c); return n >= 53 || n === 2 || n === 7 || flw(c) === MF; }; +// 跟牌时的「有效花色」:主牌一律归主花色 +const eflw = c => isTrump(c) ? MF : flw(c); +// §3 主牌顺序 / §5.3 相邻链上的位置(越小越大);副7、副2 各自只占一个等级 +const NORMAL = [1, 13, 12, 11, 10, 9, 8, 6, 5]; // A K Q J 10 9 8 6 5 +function pos(c) { + const n = num(c), f = flw(c); + if (n === 54) return 0; + if (n === 53) return 1; + if (n === 7) return f === MF ? 2 : 3; + if (n === 2) return f === MF ? 4 : 5; + if (f === MF) return 6 + NORMAL.indexOf(n); + return 100 + NORMAL.indexOf(n); // 副牌:只在同花色内比较 +} +// §3「对子指同一张具体牌的两副」= 同花色同点数 +const samecard = (a, b) => num(a) === num(b) && flw(a) === flw(b); +// 取对子(返回每对的代表牌,按大到小) +function pairsOf(cs) { + const used = [], ps = []; + for (let i = 0; i < cs.length; i++) { + if (used[i]) continue; + for (let j = i + 1; j < cs.length; j++) { + if (!used[j] && samecard(cs[i], cs[j])) { used[i] = used[j] = 1; ps.push(cs[i]); break; } + } + } + ps.sort((a, b) => pos(a) - pos(b)); + return ps; +} +// §5.3:相邻对子连成拖拉机 → 各连对段的长度,降序(孤立对子长度 1) +function runsOf(cs) { + const ps = pairsOf(cs), runs = []; + let cur = 1; + for (let i = 0; i + 1 < ps.length; i++) { + if (pos(ps[i + 1]) - pos(ps[i]) === 1) cur++; + else { runs.push(cur); cur = 1; } + } + if (ps.length) runs.push(cur); + runs.sort((a, b) => b - a); + return runs; +} +// §5.2 层级2「能凑多长的拖拉机,就必须先凑多长」→ 最长优先的贪心规格档案 +function bestProfile(runs, k) { + const av = runs.concat().sort((a, b) => b - a), p = []; + let budget = k, i = 0; + while (budget > 0 && i < av.length) { const take = Math.min(av[i], budget); p.push(take); budget -= take; i++; } + return p; +} +function lessProfile(a, b) { + const n = Math.min(a.length, b.length); + for (let i = 0; i < n; i++) { if (a[i] < b[i]) return true; if (a[i] > b[i]) return false; } + return a.length < b.length; +} +// 参考裁定:这手 X 是不是 design §5.2 允许的跟牌 +function refOk(hand, X, C, F, T) { + if (X.length !== C) return false; + const S = hand.filter(c => eflw(c) === F); + if (S.length === 0) return true; // §5.2 完全缺门:整手可任意出 + if (S.length < C) return S.every(c => X.indexOf(c) >= 0); // §5.2 该花色不够:全出 + 缺口任意补 + if (X.filter(c => eflw(c) === F).length !== C) return false; // 够:必须全打同花色 + if (T > 100 && T < 200) return true; // 单张 / 多张单张 + if (T === 201) return pairsOf(S).length === 0 ? true : pairsOf(X).length >= 1; // 有对必出对 + if (T > 201 && T < 300) return true; // 多对(甩牌压平):分量约束在 flush_follow_ok + if (T > 300 && T < 400) { // N 连对拖拉机 + const N = T - 300, k = Math.min(pairsOf(S).length, N); // 能凑几对就必须凑几对(层级 3/4/5) + if (pairsOf(X).length !== k) return false; + return !lessProfile(runsOf(X), bestProfile(runsOf(S), k)); // 且拖拉机规格不得劣于最优(层级 1/2) + } + return true; +} + +// ==================== 穷举差分 ==================== +// 固定种子的 xorshift32,保证用例集可复现 +let _seed = 0x2717CAFE; +function rnd(n) { _seed ^= _seed << 13; _seed ^= _seed >>> 17; _seed ^= _seed << 5; _seed >>>= 0; return _seed % n; } +function combos(arr, k) { + const out = []; + (function rec(st, cur) { + if (cur.length === k) { out.push(cur.slice()); return; } + for (let i = st; i < arr.length; i++) { cur.push(arr[i]); rec(i + 1, cur); cur.pop(); } + })(0, []); + return out; +} + +// 造牌器必须**按牌型结构**造手牌,不能纯随机抽:纯随机几乎抽不出「同花色 3 对以上、 +// 且含多组不同长度拖拉机」的手,而 §5.2 层级 1/2 的强制约束恰恰只在那种手上才生效 +// (实测纯随机版关掉 follow_tractor_cover_ok 也全绿 = 差分形同虚设)。 +// 这里按「等级表」逐级选取 0/1/2 张,天然产出富对子、富连对的手牌。 + +// 副花色(梅花)的等级表:A K Q J 10 9 8 6 5(相邻即连对) +const SUB_RANKS = NORMAL.map(n => [id(1, 2, n), id(2, 2, n)]); +// 主牌的等级表 = design §5.3 的相邻链:大王-小王-正7-副7-正2-副2-主A…主5 +const TRUMP_RANKS = [ + [WG(1, true), WG(2, true)], // 大王 + [WG(1, false), WG(2, false)], // 小王 + [id(1, MF, 7), id(2, MF, 7)], // 正7 + [id(1, 3, 7), id(2, 3, 7)], // 副7(红心) + [id(1, MF, 2), id(2, MF, 2)], // 正2 + [id(1, 3, 2), id(2, 3, 2)] // 副2(红心) +].concat(NORMAL.map(n => [id(1, MF, n), id(2, MF, n)])); +// 同级但不同花色的另一张副7/副2(§3:跨花色不成对,用于压边界) +const SAME_LEVEL = [id(1, 2, 7), id(2, 2, 7), id(1, 2, 2), id(2, 2, 2)]; +// 垫牌用的第三花色 +const FILLER = []; +[1, 13, 12, 10, 8, 5].forEach(n => FILLER.push(id(1, 3, n), id(2, 3, n))); + +function buildHand(F, C) { + const ranks = (F === MF) ? TRUMP_RANKS : SUB_RANKS; + // 覆盖 §5.2 的四类分支:完全缺门 / 该花色不够 / 恰好够 / 有富余 + const mode = rnd(4); + let targetSuit; + if (mode === 0) targetSuit = 0; + else if (mode === 1) targetSuit = 1 + rnd(Math.max(1, C - 1)); + else if (mode === 2) targetSuit = C; + else targetSuit = C + rnd(4); + + const suit = []; + // 从随机起点顺链扫,每级取 0/1/2 张(偏向取 2 张以造出对子与连对) + let idx = rnd(ranks.length); + for (let step = 0; step < ranks.length && suit.length < targetSuit; step++) { + const r = ranks[(idx + step) % ranks.length]; + const w = rnd(10); + const take = w < 5 ? 2 : w < 8 ? 1 : 0; + for (let q = 0; q < take && suit.length < targetSuit; q++) suit.push(r[q]); + } + // 顺链一圈还没凑够就再补(保证 targetSuit 尽量达成) + for (let step = 0; step < ranks.length && suit.length < targetSuit; step++) { + const r = ranks[step]; + for (let q = 0; q < 2 && suit.length < targetSuit; q++) if (suit.indexOf(r[q]) < 0) suit.push(r[q]); + } + // 同级不同花色的副7/副2 偶尔掺一张(只在主牌为跟牌花色时才算同花色) + if (F === MF && rnd(6) === 0) suit.push(SAME_LEVEL[rnd(SAME_LEVEL.length)]); + + // 非该花色的补充牌:垫牌花色 + (副牌首出时)主牌,用于毙牌/垫牌/混合出牌分支 + const others = []; + const otherPool = (F === MF) ? FILLER.concat() : FILLER.concat(TRUMP_RANKS.reduce((a, r) => a.concat(r), [])); + const oc = rnd(4); + for (let i = 0; i < oc && otherPool.length; i++) others.push(otherPool.splice(rnd(otherPool.length), 1)[0]); + + // 去重后返回(同级不同花色那一张可能与 others 撞车) + const seen = {}, hand = []; + suit.concat(others).forEach(c => { if (!seen[c]) { seen[c] = 1; hand.push(c); } }); + return hand; +} + +// [张数, 首家花色, 首家牌型, 说明] +const LEADS = [ + [1, 2, 101, '副牌单张'], + [2, 2, 201, '副牌对子'], + [4, 2, 302, '副牌两连对'], + [6, 2, 303, '副牌三连对'], + [8, 2, 304, '副牌四连对'], + [1, MF, 101, '主牌单张'], + [2, MF, 201, '主牌对子'], + [4, MF, 302, '主牌两连对'], + [6, MF, 303, '主牌三连对'], + [3, MF, 103, '主牌甩三单张(压平)'], + [4, MF, 202, '主牌甩两对(压平)'] +]; + +// 结构化手牌(确定性):随机造牌打不到的关键结构在此显式钉住, +// 不让 PRNG 决定 §5.2 层级 1/2 这类强制约束到底有没有被覆盖。 +// 例如「同花色三组互不相邻的两连对」共 12 张,是唯一能让「只出零散对子」与 +// 「先凑最长拖拉机」产生不同结果的形状——少于 3 组时任取 3 对必含相邻对,测不出差别。 +function structHands(F) { + const R = (F === MF) ? TRUMP_RANKS : SUB_RANKS; + const p = i => R[i]; // 第 i 级的一对 + const one = i => [R[i][0]]; // 第 i 级的单张 + const cat = (...xs) => [].concat(...xs); + return [ + cat(p(0), p(1), p(3), p(4), p(6), p(7)), // 三组两连对(互不相邻) + cat(p(0), p(1), p(2), p(4), p(5), p(7)), // 一个三连对 + 一个两连对 + 一个孤立对 + cat(p(0), p(2), p(4), p(6), p(8)), // 五个孤立对(无任何拖拉机) + cat(p(0), p(1), p(2), p(3), p(5)), // 一个四连对 + 一个孤立对 + cat(p(0), p(1), p(3), one(5), one(7), one(8)), // 两连对 + 孤立对 + 三张单 + cat(p(0), one(2), one(4), one(6), one(8)), // 一对 + 四张单(对子不够) + cat(one(0), one(1), one(2), one(3), one(4), one(5), one(6)), // 全单张(无对) + cat(p(0), p(1), p(2)), // 恰好一个三连对 + cat(p(0), p(1)), // 恰好一个两连对 + cat(one(0), one(1)), // 两张单 + [], // 完全缺门 + cat(one(0)), // 该花色仅 1 张(不够) + cat(p(0), one(2)) // 该花色 3 张(对子 + 单) + ].map(suit => { + // 掺几张别的花色,制造垫牌 / 毙牌 / 混合出牌分支; + // 但同花色已经很多时不再掺——否则手牌被撑过枚举上限、整条用例会被静默跳过, + // 而「三组两连对」这类 12 张的关键结构恰恰就在上限边缘 + const extra = (F === MF) ? FILLER : FILLER.concat([TRUMP_RANKS[0][0], TRUMP_RANKS[2][0], TRUMP_RANKS[6][0], TRUMP_RANKS[7][0]]); + return suit.concat(extra.slice(0, Math.max(0, Math.min(3, 12 - suit.length)))); + }); +} + +const TRIALS_PER_LEAD = 320; +let grandCombos = 0; +for (const [C, F, T, desc] of LEADS) { + let combosCount = 0; + const bad = []; + const hands = structHands(F); + for (let k = 0; k < TRIALS_PER_LEAD; k++) hands.push(buildHand(F, C)); + for (const hand of hands) { + if (hand.length < C || hand.length > 13) continue; // 组合数上界保护 + for (const X of combos(hand, C)) { + combosCount++; + const got = A.can_followcard(MF, hand, X, C, F, T).result === true; + const exp = refOk(hand, X, C, F, T); + if (got !== exp && bad.length < 3) bad.push({ hand: hand.slice(), play: X.slice(), code: got, design: exp }); + else if (got !== exp) bad.push('...'); + } + } + grandCombos += combosCount; + t.eq('§5.2 穷举差分 ' + desc + '(' + combosCount + ' 组)失配', bad, []); +} +// 钉住覆盖规模:枚举数量若骤降说明造牌逻辑退化、差分形同虚设 +t.eq('§5.2 差分覆盖组合数 ≥ 20000', grandCombos >= 20000, true); + +process.exit(t.done('followdiff') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_fuzz.js b/server/games/erqiwang/test/test_fuzz.js new file mode 100644 index 0000000..8e1c882 --- /dev/null +++ b/server/games/erqiwang/test/test_fuzz.js @@ -0,0 +1,290 @@ +// L4 整局模糊 + 每墩胜者差分(design §5/§6/§7/§8/§12) +// +// 为什么需要它:`test_endgame.js` 的驱动器**只会出单张**,所以对子 / 拖拉机 / 甩牌 / 甩错 +// 这些牌型在「整局链路」里从未被跑过——单测虽然逐个覆盖了 can_playcard / can_followcard, +// 但它们与 do_playcard / mod.chupai / 结算的**集成**是空白。本文件用带牌型的随机对局补上, +// 并对**每一墩**用独立参考实现重算「谁最大」与服务端比对(不是抽查,是每墩都比)。 +// +// 不抖动:随机源是固定种子的 xorshift32,同一份代码每次跑的对局序列完全相同。 +// 末尾的「覆盖下限」断言防止造牌/选牌策略退化成全是单张而让本文件形同虚设。 +const R = require('./_rpc.js'); const mod = R.mod; +const P = global.cls_youle_erqiwang_paiju, A = global.cls_youle_erqiwang_arith; +const D = require('../class.desk.js'); +const t = require('./_assert')(); + +const GAMES = 20; +let seed = 0x51EED17; +const rnd = n => { seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; seed >>>= 0; return seed % n; }; +const clone = m => JSON.parse(JSON.stringify(m)); +// 提交顺序随机化:真实客户端可以按任意顺序发 cards,服务端的判定不得依赖入参顺序 +// (曾有缺陷:跟牌牌面值用未排序的入参算,乱序提交能把主拖拉机毙牌算成 0 而翻转本墩胜负) +// 一半随机打乱,一半用**最坏顺序**:按大小排好后取偶数位再取奇数位,把每个对子都拆开摆 +// ([K,K,Q,Q] → [K,Q,K,Q])。这正是按相邻取对的算法会漏判的形状;纯随机打乱在某些牌局下 +// 一次都碰不到,所以必须有确定性的最坏顺序兜底。 +const shuffle = a => { + if (rnd(2) === 0) { + const s = a.concat().sort((x, y) => pos(x) - pos(y)); + return s.filter((_, i) => i % 2 === 0).concat(s.filter((_, i) => i % 2 === 1)); + } + const r = a.concat(); + for (let i = r.length - 1; i > 0; i--) { const j = rnd(i + 1); const x = r[i]; r[i] = r[j]; r[j] = x; } + return r; +}; +const pk = (s, d) => ({ conmode: 0, fromid: s, data: Object.assign({ agentid: 1, playerid: s, gameid: 1, roomcode: 1, seat: s }, d || {}) }); +global.min_ontimeout = fn => fn(); +// 发牌也接到同一个种子上:本文件末尾的「覆盖下限」断言(甩牌/毙牌/扣底各出现多少次)是 +// 发牌相关的,若沿用 _shim 的 Math.random 发牌,稀有事件(尤其扣底)会在某些运行里一次都 +// 不出现而让下限断言随机转红——实测 60 次里有 1 次 0 次扣底。这里固定发牌序列,把覆盖变成 +// 确定事实。随机发牌的整局覆盖由 test_endgame.js 承担,两者互补。 +global.min_random = (a, b) => a + rnd(b - a + 1); + +// ==================== 独立参考实现:按 design §3/§5/§6 重算「本墩谁最大」==================== +let MF; +const num = c => { const m = c % 54; return m === 52 ? 53 : m === 53 ? 54 : m % 13 + 1; }; +const flw = c => { const m = c % 54; return m >= 52 ? 5 : Math.floor(m / 13) + 1; }; +const isTrump = c => { const n = num(c); return n >= 53 || n === 2 || n === 7 || flw(c) === MF; }; +const eflw = c => isTrump(c) ? MF : flw(c); // 主牌一律归主花色 +const NORMAL = [1, 13, 12, 11, 10, 9, 8, 6, 5]; +function pos(c) { // design §3 等级位置(越小越大) + const n = num(c), f = flw(c); + if (n === 54) return 0; if (n === 53) return 1; + if (n === 7) return f === MF ? 2 : 3; // 正7 / 副7(副7 同级) + if (n === 2) return f === MF ? 4 : 5; // 正2 / 副2 + if (f === MF) return 6 + NORMAL.indexOf(n); + return 100 + NORMAL.indexOf(n); // 副牌只在同花色内比较 +} +const samecard = (a, b) => num(a) === num(b) && flw(a) === flw(b); // §3 对子=同花色同点数两副 +function pairsOf(cs) { + const used = [], ps = [], s = cs.concat().sort((a, b) => pos(a) - pos(b)); + for (let i = 0; i < s.length; i++) { + if (used[i]) continue; + for (let j = i + 1; j < s.length; j++) if (!used[j] && samecard(s[i], s[j])) { used[i] = used[j] = 1; ps.push(s[i]); break; } + } + return ps; +} +function isTractor(cs, N) { + const ps = pairsOf(cs); + if (ps.length !== N || cs.length !== 2 * N) return false; + for (let i = 0; i + 1 < ps.length; i++) if (pos(ps[i + 1]) - pos(ps[i]) !== 1) return false; + return true; +} +// 必须取正值:0 是「不参与本墩争夺」的哨兵,rank 若取负数,哨兵 0 反而数值最大(踩过一次) +const topRank = cs => 1000 - Math.min(...cs.map(pos)); +// 某家本墩出牌的牌面值。design §5.1/§5.2:垫牌与混合出牌一律 0(不争夺); +// §5.4.4:首家甩牌时跟牌不可能压过;缺门用纯主牌顶同牌型即「毙牌」,加权盖过任何副牌。 +function valueOf(cs, F, T, C) { + const efs = {}; cs.forEach(c => efs[eflw(c)] = 1); + const ks = Object.keys(efs); + if (ks.length > 1) return 0; // 混合出牌 + const ef = parseInt(ks[0]); + if (ef !== F && ef !== MF) return 0; // 垫别的副牌 + let ok; + if (T > 100 && T < 200) ok = (T === 101 && C === 1); + else if (T === 201) ok = (cs.length === 2 && pairsOf(cs).length === 1); + else if (T > 201 && T < 300) ok = false; + else if (T > 300 && T < 400) ok = isTractor(cs, T - 300); + else ok = false; + if (!ok) return 0; + const r = topRank(cs); + return (ef === MF && F !== MF) ? (100000 + r) : r; // 毙牌 +} + +// design §6.3 扣底倍数:赢末墩的牌须全是主牌;单张 ×1、主对 ×2、N 连对 ×2N; +// 混合甩牌取其中最高规格。返回 0 表示不扣底。 +function refBottomMultiple(cards) { + if (!cards.every(isTrump)) return 0; + const ps = pairsOf(cards); + if (ps.length === 0) return 1; + let best = 1, cur = 1; + for (let i = 0; i + 1 < ps.length; i++) { + if (pos(ps[i + 1]) - pos(ps[i]) === 1) { cur++; if (cur > best) best = cur; } else cur = 1; + } + return 2 * best; +} + +// ==================== 对局装配与选牌策略 ==================== +function mkRoom(rt) { + const sent = []; + const o_room = { + roomtype: rt, asetcount: 6, roomcode: 1, createtime: 'T0', makewartime: 'T1', + seatlist: [0, 1, 2].map(i => ({ conmode: 0, fromid: i, playerid: 100 + i, nickname: 'P' + i, avatar: '', gameinfo: {} })), + method: { sendpack_toother: m => sent.push(clone(m)) } + }; + const desk = D.new(o_room); o_room.o_desk = desk; + // class.desk / class.paiju 走全局 youle_erqiwang,mod.js 走自己的模块对象,两处都要接 + global.youle_erqiwang.app = { SendPack: m => sent.push(clone(m)) }; + global.youle_erqiwang.import = { check_player: () => o_room, deduct_roomcard: () => { }, save_grade: () => { } }; + mod.import = global.youle_erqiwang.import; mod.app = global.youle_erqiwang.app; + return { o_room, desk, sent }; +} +// 首家候选:单张 / 对子 / N 连对 / 随机主牌组合(后者可能是合法甩牌,也可能是甩错) +function leadCandidates(hand) { + const out = hand.map(c => [c]); + const bySuit = {}; + hand.forEach(c => (bySuit[eflw(c)] = bySuit[eflw(c)] || []).push(c)); + for (const f in bySuit) { + const cs = bySuit[f], ps = pairsOf(cs); + const mate = p => cs.filter(x => x !== p && samecard(x, p))[0]; + ps.forEach(p => { if (mate(p) !== undefined) out.push([p, mate(p)]); }); + for (let n = 2; n <= 4; n++) for (let s = 0; s + n <= ps.length; s++) { + const run = ps.slice(s, s + n); + let okr = true; + for (let q = 0; q + 1 < run.length; q++) if (pos(run[q + 1]) - pos(run[q]) !== 1) okr = false; + if (!okr) continue; + const t2 = []; + run.forEach(p => { t2.push(p); t2.push(mate(p)); }); + if (t2.every(x => x !== undefined)) out.push(t2); + } + } + const tr = hand.filter(isTrump); + if (tr.length >= 2) for (let k = 0; k < 6; k++) { + const n = 2 + rnd(3), sel = [], p2 = tr.concat(); + while (sel.length < n && p2.length) sel.push(p2.splice(rnd(p2.length), 1)[0]); + if (sel.length >= 2) out.push(sel); + } + return out; +} +function pickLead(pj, seat) { + const hand = P.get_seat_cards(pj, seat); + const opp = [0, 1, 2].filter(o => o !== seat).map(o => P.get_seat_zhucards(pj, o)); + // 甩错候选也保留:mod.chupai 会按 §5.4.5 收回整套、只强制打出最小一张,这条路径也要跑到 + const cands = leadCandidates(hand).filter(cs => { + const r = A.can_playcard(pj.flower, cs.concat(), seat, pj.seatlist, opp); + return r.result === true || r.shuaicuo === true; + }); + if (!cands.length) return [hand[hand.length - 1]]; + cands.sort((a, b) => b.length - a.length); + return rnd(3) === 0 ? cands[cands.length - 1] : cands[rnd(Math.min(4, cands.length))]; +} +function pickFollow(pj, seat) { + const hand = P.get_seat_cards(pj, seat); + const C = pj.playproc.startcount, F = pj.playproc.startflower, T = pj.playproc.starttype; + const g = A.get_followcard(pj.flower, hand, C, F, T); + const okPick = p => A.can_followcard(pj.flower, hand, p, C, F, T).result === true + && (!pj.playproc.shuai_demand || A.flush_follow_ok(pj.flower, hand, p, pj.playproc.shuai_demand)); + for (let i = 0; i < 400; i++) { // 先按「必出 + 可出」凑(真实客户端的做法) + const pick = (g.mustcard || []).concat(), pool = (g.cancard || []).concat(); + while (pick.length < C && pool.length) pick.push(pool.splice(rnd(pool.length), 1)[0]); + if (pick.length === C && okPick(pick)) return pick; + } + for (let i = 0; i < 800; i++) { // 兜底:整手牌里随机取 C 张 + const h = hand.concat(), p = []; + while (p.length < C && h.length) p.push(h.splice(rnd(h.length), 1)[0]); + if (p.length === C && okPick(p)) return p; + } + return null; +} + +// ==================== 跑局 ==================== +const stats = { rounds: 0, tricks: 0, multi: 0, shuai: 0, shuaicuo: 0, bi: 0, kodi: 0 }; +const errs = []; +const chk = (c, m) => { if (!c) errs.push(m); }; +const CALLS = [70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5]; +const ROOMS = ['00000', '00100', '00010', '00110']; // 常规 / 傍王 / 爬坡 / 傍王+爬坡 + +for (let game = 0; game < GAMES; game++) { + const c = mkRoom(ROOMS[game % ROOMS.length]); + D.do_new_paiju(c.desk, 0); + const pj = c.desk.method.curr_paiju(); + MF = null; + mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: CALLS[rnd(CALLS.length)] })); + if (pj.step === 1) mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); + if (pj.step === 1) mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); + const b = pj.banker; MF = 1 + rnd(4); + mod.xuanzhu(pk(b, { flower: MF })); + mod.maipai(pk(b, { cards: P.get_seat_cards(pj, b).slice(-8) })); + chk(P.get_seat_cards(pj, b).length === 28, `g${game} §4.5 埋牌后庄家应剩 28 张`); + + const allJiesuan = []; + let guard = 0, dead = false, refJian = 0; // refJian:按独立算出的墩胜者累计的闲家捡分 + while (pj.step === 5 && ++guard < 400) { + const st = pj.playproc.start; + c.sent.length = 0; + mod.chupai(pk(st, { cards: shuffle(pickLead(pj, st)) })); + const lp = c.sent.filter(m => m.rpc === 'chupai1')[0]; + chk(!!lp, `g${game} 首出被拒`); + if (!lp) { dead = true; break; } + if (lp.data.shuaicuo) stats.shuaicuo++; + if (lp.data.shuai) stats.shuai++; + if (lp.data.cards.length > 1) stats.multi++; + const C = pj.playproc.startcount, F = pj.playproc.startflower, T = pj.playproc.starttype; + const trick = [{ seat: st, cards: lp.data.cards }]; + let maxseat; + for (let k = 0; k < 2; k++) { + const s2 = pj.playproc.currseat, f2 = pickFollow(pj, s2); + if (f2 === null) { chk(false, `g${game} 找不到合法跟牌 seat${s2} T=${T} C=${C} F=${F}`); dead = true; break; } + chk(f2.length === C, `g${game} §5.2 跟牌张数应等于首出张数`); + const before = c.sent.length; + mod.chupai(pk(s2, { cards: shuffle(f2) })); + const out = c.sent.slice(before); + const ok = out.filter(m => ['chupai2', 'chupai3', 'jiesuan'].indexOf(m.rpc) >= 0); + chk(ok.length > 0 && ok[0].data.success === true, `g${game} 跟牌被拒`); + const fin = out.filter(m => m.rpc === 'jiesuan')[0], c3 = out.filter(m => m.rpc === 'chupai3')[0]; + if (fin) { allJiesuan.push(fin); maxseat = fin.data.chupai.maxseat; } + else if (c3) { maxseat = c3.data.maxseat; } + trick.push({ seat: s2, cards: f2 }); + } + if (dead) break; + stats.tricks++; + if (F !== MF && trick.slice(1).some(x => x.cards.every(isTrump))) stats.bi++; + // —— 每墩都独立重算胜者并比对 —— + const vals = trick.map(x => valueOf(x.cards, F, T, C)); + let bi = 0; for (let v = 1; v < 3; v++) if (vals[v] > vals[bi]) bi = v; + const refWin = trick[bi].seat; + chk(maxseat === refWin, + `g${game} §6.2 墩胜者不符 服务端=${maxseat} 参考=${refWin} T=${T} C=${C} F=${F} MF=${MF} vals=${JSON.stringify(vals)} plays=${JSON.stringify(trick)}`); + // §6.2 本墩全部牌的归属必须是本墩胜者。 + // 注意不能只用 aset.grade 去对「按 playowner 累加的捡分」——那是拿同一个字段自证, + // playowner 被写错时两边一起错、断言照样通过(实测过)。这里用**独立算出的胜者**来钉。 + trick.forEach(x => x.cards.forEach(cid => { + chk(pj.cards[cid].playowner === refWin, `g${game} §6.2 牌 ${cid} 归属应为本墩胜者 ${refWin},实为 ${pj.cards[cid].playowner}`); + })); + if (refWin !== pj.banker) refJian += trick.reduce((a, x) => a + x.cards.reduce((b, cid) => b + pj.cards[cid].score, 0), 0); + } + if (dead) continue; + + chk(pj.step === 6, `g${game} §12.1 未打到结算 step=${pj.step}`); + chk(pj.cards.filter(x => x.playround > 0).length === 84, `g${game} §2 出牌总数应为 84`); + chk(pj.cards.filter(x => x.playround === 0).length === 8, `g${game} §4.5 埋牌应为 8`); + chk(pj.cards.filter(x => x.playround > 0 && x.playowner < 0).length === 0, `g${game} §6.2 有出过的牌无归属`); + chk(allJiesuan.length === 1, `g${game} 结算包应恰好 1 个`); + if (allJiesuan.length === 1) { + const d = allJiesuan[0].data, aset = d.aset; + const bonus = (d.bottom && d.bottom.grade2) || 0; + chk(aset.grade === refJian + bonus, `g${game} §6.2/§6.3 捡分不符 aset=${aset.grade} 独立计=${refJian + bonus}`); + chk(aset.seatlist.reduce((a, p) => a + p.grade, 0) === 0, `g${game} §7/§8 结算不零和`); + // §6.3 扣底:触发条件与**倍数**都要独立算。倍数不能再调 get_bottom_multiple—— + // 那是拿被测函数自证,倍数表被改错(比如恒返回 2)照样通过(实测过)。 + const lastXian = pj.playproc.maxseat !== pj.banker; + const lastMult = lastXian ? refBottomMultiple(pj.playproc.cards[pj.playproc.maxseat]) : 0; + chk((lastMult > 0) === !!d.bottom.multiple, `g${game} §6.3 扣底触发条件不符`); + if (lastMult > 0) { + chk(d.bottom.multiple === lastMult, `g${game} §6.3 扣底倍数不符 服务端=${d.bottom.multiple} 参考=${lastMult}`); + chk(d.bottom.grade2 === d.bottom.multiple * d.bottom.grade1, `g${game} §6.3 扣底得分应为 倍数×底牌分`); + stats.kodi++; + } + const X = aset.multiple * Math.abs(aset.upgrade), N = aset.seatlist.map(p => p.naward); + chk(JSON.stringify(aset.seatlist.map(p => p.grade_aw)) === + JSON.stringify([X * (2 * N[0] - N[1] - N[2]), X * (2 * N[1] - N[0] - N[2]), X * (2 * N[2] - N[0] - N[1])]), + `g${game} §8.4 算奖分配公式不符`); + chk(JSON.stringify(aset.seatlist.map(p => p.grade_jf)) === + JSON.stringify([0, 1, 2].map(s => (aset.upgrade > 0 ? 1 : -1) * X * (s === pj.banker ? 2 : -1))), + `g${game} §7.0 捡分子数分配不符`); + } + stats.rounds++; +} + +t.eq('整局模糊 无异常', errs.slice(0, 3), []); +t.eq('整局模糊 全部跑完', stats.rounds, GAMES); +// 覆盖下限:发牌与选牌都已定种子,这些数字是确定事实(实测 20 局:474 墩 / 多张 53 / +// 甩牌 11 / 甩错 178 / 毙牌 70 / 扣底 6)。阈值取略低于实测值——既能挡住「选牌策略退化成 +// 全是单张」让本文件形同虚设,又不会因无害的重构而误报。 +t.eq('覆盖 墩数 ≥ 400', stats.tricks >= 400, true); +t.eq('覆盖 多张首出 ≥ 40 次', stats.multi >= 40, true); +t.eq('覆盖 合法甩牌 ≥ 8 次', stats.shuai >= 8, true); +t.eq('覆盖 甩错 ≥ 100 次', stats.shuaicuo >= 100, true); +t.eq('覆盖 毙牌墩 ≥ 40 次', stats.bi >= 40, true); +t.eq('覆盖 扣底 ≥ 3 局', stats.kodi >= 3, true); +console.log(` [统计] ${stats.rounds} 局 / ${stats.tricks} 墩:多张首出 ${stats.multi}、合法甩牌 ${stats.shuai}、甩错 ${stats.shuaicuo}、毙牌墩 ${stats.bi}、扣底 ${stats.kodi} 局`); + +process.exit(t.done('fuzz') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_hint.js b/server/games/erqiwang/test/test_hint.js new file mode 100644 index 0000000..f317032 --- /dev/null +++ b/server/games/erqiwang/test/test_hint.js @@ -0,0 +1,229 @@ +// S-1 curmultiple(当前抓分倍数,design §7.2.0)与 S-2 mustcard(跟牌必出牌,design §5.2) +// 两者都是「随现有包下发、供客户端显示/自动选中」的辅助字段: +// curmultiple —— 三家同值(grade 本就公开),随 shangzhuang / chupai1-3 / PushCards 下发 +// mustcard —— 【只发给 nextseat 一家】,是该玩家自己手牌的子集;整表下发会泄露他家手牌结构 +// 除正面用例外,本文件重点覆盖:可见性(谁不该有)、边界(何时不该有)、与结算的一致性。 +const R = require('./_rpc.js'); const mod = R.mod, setup = R.setup, make108 = R.make108, id = R.id; +const P = global.cls_youle_erqiwang_paiju, A = global.cls_youle_erqiwang_arith; +const E = require('../class.export.js').new(); +const t = require('./_assert')(); +const pack = (s, d) => ({ conmode: 0, fromid: s, data: Object.assign({ agentid: 1, playerid: s, gameid: 1, roomcode: 1, seat: s }, d || {}) }); + +// ============================================================================ +// S-1 curmultiple +// ============================================================================ + +// —— 算法层:与 design §7.2.0 判定表逐档对齐(常规算子 Q=40)—— +// curmultiple 是【带符号】的(S-8):正数=庄家赢的三档,负数=闲家升 N 级。 +// 符号就是「谁赢」的区分,客户端判定动画靠它分辨该播哪个。 +const cm = (call, grade, climb) => { + const q = A.get_qvalue(call, climb ? 1 : 0); + return A.get_upgrade(call, grade, q); +}; +t.eq('curmultiple 65分 捡0 → 大光 +3', cm(65, 0), 3); +t.eq('curmultiple 65分 捡35 → 小光 +2', cm(65, 35), 2); +t.eq('curmultiple 65分 捡40 → 过庄 +1', cm(65, 40), 1); +t.eq('curmultiple 65分 捡65 → 升1级 -1', cm(65, 65), -1); +t.eq('curmultiple 65分 捡105 → 升2级 -2', cm(65, 105), -2); +t.eq('curmultiple 65分 捡145 → 升3级 -3', cm(65, 145), -3); +// 爬坡:40分档 Q=20,分界线与常规不同(design §7.3.2) +t.eq('curmultiple 爬坡40分 捡15 → 小光 +2', cm(40, 15, true), 2); +t.eq('curmultiple 爬坡40分 捡20 → 过庄 +1', cm(40, 20, true), 1); +t.eq('curmultiple 爬坡40分 捡60 → 升2级 -2', cm(40, 60, true), -2); + +// —— S-8 的核心:三对「绝对值相同、判定相反」的组合必须靠符号分开 —— +// 取绝对值的话这三对会完全撞在一起,判定动画就无从分辨了 +t.eq('S-8 大光(+3) vs 升3级(-3) 可区分', [cm(65, 0), cm(65, 145)], [3, -3]); +t.eq('S-8 小光(+2) vs 升2级(-2) 可区分', [cm(65, 35), cm(65, 105)], [2, -2]); +t.eq('S-8 过庄(+1) vs 升1级(-1) 可区分', [cm(65, 40), cm(65, 65)], [1, -1]); +// 符号语义:>0 庄家赢、<0 闲家赢、==0 叫分未定 +t.eq('S-8 符号语义 庄赢为正', [cm(65, 0), cm(65, 35), cm(65, 40)].every(v => v > 0), true); +t.eq('S-8 符号语义 闲赢为负', [cm(65, 65), cm(65, 105), cm(65, 145)].every(v => v < 0), true); + +// —— 直接调 get_curmultiple 本身,覆盖【负数】分支 —— +// 必要性:上面的 cm() 走的是 A.get_upgrade,【绕过了 get_curmultiple】; +// 而下面端到端那局恰好是大光(正数),取不取绝对值结果一样。 +// 若没有这一组,get_curmultiple 悄悄退回取绝对值时两处都发现不了。 +function mkCur(call, xianGrade, roomtype) { + const cards = []; + for (let i = 0; i < 108; i++) { + cards.push({ id: i, number: 5, score: 0, playowner: -1, dealowner: 1, playround: -1 }); + } + // 喂给闲家(seat1)足量分牌,凑出指定捡分(每张 5 分,故 xianGrade 须为 5 的倍数) + let need = xianGrade / 5; + for (let i = 0; i < 108 && need > 0; i++) { cards[i].score = 5; cards[i].playowner = 1; need--; } + return P.get_curmultiple({ + call: call, banker: 0, flower: 1, cards: cards, + o_desk: { o_room: { roomtype: roomtype || '00000' } } + }); +} +t.eq('S-8 get_curmultiple 大光 → +3', mkCur(65, 0), 3); +t.eq('S-8 get_curmultiple 小光 → +2', mkCur(65, 35), 2); +t.eq('S-8 get_curmultiple 过庄 → +1', mkCur(65, 40), 1); +t.eq('S-8 get_curmultiple 升1级 → -1', mkCur(65, 65), -1); +t.eq('S-8 get_curmultiple 升2级 → -2', mkCur(65, 105), -2); +t.eq('S-8 get_curmultiple 升3级 → -3', mkCur(65, 145), -3); +t.eq('S-8 get_curmultiple 叫分未定 → 0', mkCur(0, 0), 0); +// 爬坡走 roomtype 位3=1,Q 分段不同 +t.eq('S-8 get_curmultiple 爬坡40分 捡60 → 升2级 -2', mkCur(40, 60, '00010'), -2); + +// —— 端到端:走完整流程,验证包里带且值正确 —— +function driveToPlay(roomtype) { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup(roomtype, pj); + mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); + mod.xuanzhu(pack(0, { flower: 1 })); + mod.maipai(pack(0, { cards: P.get_seat_cards(pj, 0).slice(0, 8) })); + return Object.assign({ pj }, c); +} +const e = driveToPlay("00000"); +// shangzhuang:叫分刚定、捡分为 0 → 大光 x3 +const szPk = e.sent.filter(m => m.rpc === 'shangzhuang')[0]; +t.eq('shangzhuang 带curmultiple', szPk && szPk.data.curmultiple !== undefined, true); +t.eq('shangzhuang curmultiple=3(此刻大光)', szPk && szPk.data.curmultiple, 3); +// 出牌包:三家同值 +e.sent.length = 0; +mod.chupai(pack(0, { cards: [P.get_seat_cards(e.pj, 0)[0]] })); +const cp1 = e.sent.filter(m => m.rpc === 'chupai1'); +t.eq('chupai1 下发给三家', cp1.length, 3); +t.eq('chupai1 都带curmultiple', cp1.every(m => m.data.curmultiple !== undefined), true); +t.eq('chupai1 三家curmultiple同值', new Set(cp1.map(m => m.data.curmultiple)).size, 1); +// 重连 PushCards 也带,且与 chupai 包同值 +const pcNow = E.get_deskinfo(e.o_room, 1).PushCards; +t.eq('重连 PushCards 带curmultiple', pcNow.curmultiple !== undefined, true); +t.eq('重连 PushCards curmultiple 与出牌包一致', pcNow.curmultiple, cp1[0].data.curmultiple); + +// —— 边界:叫分未定时为 0 —— +const pjNoCall = P.new({ paiju_list: [] }, 0); +setup("00000", pjNoCall); +t.eq('叫分未定 curmultiple=0', P.get_curmultiple(pjNoCall), 0); + +// —— 一致性:curmultiple 与结算 aset.upgrade 同源 —— +// 构造:庄家 0 叫 65、闲家一分未捡 → 局末应为大光,|upgrade| 须等于出牌过程中的 curmultiple +{ + const pj = P.new({ paiju_list: [] }, 0); + const c = setup("00000", pj); + mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); + mod.xuanzhu(pack(0, { flower: 1 })); + mod.maipai(pack(0, { cards: P.get_seat_cards(pj, 0).slice(0, 8) })); + const before = pj.method.get_curmultiple(); + const q = A.get_qvalue(pj.call, 0); + const u = A.get_upgrade(pj.call, P.get_jian_grade(pj).grade, q); + t.eq('curmultiple 与 get_upgrade 完全一致(含符号)', before, u); +} + +// ============================================================================ +// S-2 mustcard +// ============================================================================ + +// 构造一个「跟牌方有必出牌」的确定场景: +// 主花色 ♦(1)。庄家 0 首出 ♥ 对子;闲家 1 手上恰有一对 ♥ → 必须打出该对(design §5.2) +function mkFollow() { + const pj = { + idx: 1, step: 5, banker: 0, call: 65, flower: 1, callproc: [], + seatlist: [[[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]]], + playproc: {}, playhistory: [], cards: make108() + }; + for (let i = 0; i < 108; i++) pj.cards[i].dealowner = -1; + const give = (cid, s) => { pj.cards[cid].dealowner = s + 1; pj.cards[cid].playround = -1; }; + give(id(1, 3, 9), 0); give(id(2, 3, 9), 0); // 庄家:♥9 对(首出) + give(id(1, 3, 10), 1); give(id(2, 3, 10), 1); // 闲1:♥10 对 ← 必出 + // 闲2:♣8 对 + ♣9 对共 4 张(缺♥)。必须给够 4 张——若只给 2 张,手牌数恰等于首家张数, + // get_followcard 会判「全是必出」(这是对的),就测不出「缺门方可自由垫」这个点了 + give(id(1, 2, 8), 2); give(id(2, 2, 8), 2); give(id(1, 2, 9), 2); give(id(2, 2, 9), 2); + pj.method = { + check_cards_valid: c => P.check_cards_valid(pj, c), + check_cards_inhand: (c, s) => P.check_cards_inhand(pj, c, s), + do_playcard: c => P.do_playcard(pj, c), + get_seat_cards: s => P.get_seat_cards(pj, s), + get_jian_grade: () => P.get_jian_grade(pj), + get_burycard: () => P.get_burycard(pj), + get_liangpai: () => null, + have_baofu: () => false, + get_curmultiple: () => P.get_curmultiple(pj), + get_mustcard: s => P.get_mustcard(pj, s), + get_pushlist: () => P.get_pushlist(pj) + }; + P.new_playround(pj, 1, 0); + return pj; +} +const f = mkFollow(); const fc = setup("00000", f); +// 首家出牌前:本轮无人出牌 → 谁都没有必出牌 +t.eq('本轮未开始 首家无mustcard', P.get_mustcard(f, 0), null); +mod.chupai(pack(0, { cards: [id(1, 3, 9), id(2, 3, 9)] })); // 庄家首出 ♥9 对 +const fp = fc.sent.filter(m => m.rpc === 'chupai1'); +t.eq('chupai1 下发给三家', fp.length, 3); +const toNext = fp.find(m => m.fromid === 1); // nextseat = 1 +const toOther = fp.filter(m => m.fromid !== 1); +// 正面:nextseat 收到 mustcard,且正是那对 ♥10 +t.eq('mustcard 只有nextseat收到', Array.isArray(toNext.data.mustcard), true); +t.eq('mustcard = 闲1的♥10对', toNext.data.mustcard.slice().sort((a, b) => a - b), + [id(1, 3, 10), id(2, 3, 10)].sort((a, b) => a - b)); +// 可见性(防泄露):另外两家一律没有该字段 +t.eq('mustcard 其他两家都没有', toOther.every(m => m.data.mustcard === undefined), true); +// 反面:mustcard 必须是收包者自己的牌,不得含他家手牌 +t.eq('mustcard 全部属于nextseat自己', toNext.data.mustcard.every(cid => f.cards[cid].dealowner === 2), true); +// 首家自己那一包也不该有(他已经出完了) +t.eq('mustcard 出牌者本人无', fp.find(m => m.fromid === 0).data.mustcard, undefined); + +// 边界:缺门且手牌有富余(闲2 有 4 张 ♣、首家出 2 张)→ 可自由垫,无必出牌 +t.eq('缺门且手牌有富余 无mustcard', P.get_mustcard(f, 2), null); +// 把桩推进到「庄家已首出 ♥9 对、轮到跟牌」的状态。 +// 直接设 playproc 而不走 mod.chupai,是为了让下面每个边界用例【只变动一个因素】—— +// 否则 startcount 仍是 -1,前置守卫就会让所有用例都返回 null 而「假通过」,测不到被测因素。 +function asFollowing(s, currseat) { + s.playproc.start = 0; + s.playproc.currseat = currseat; + s.playproc.startcount = 2; + s.playproc.startflower = 3; // ♥ + s.playproc.starttype = 201; // 一对 + return s; +} +// 先自证:推进后确实能算出必出牌(否则下面的 null 断言仍可能是假通过) +{ + const s = asFollowing(mkFollow(), 1); + setup("00000", s); + t.eq('自证:推进到跟牌态后 闲1有mustcard', Array.isArray(P.get_mustcard(s, 1)), true); +} +// 对照:手牌数恰等于首家张数时,整手都是必出(没有任何选择余地) +{ + const s = asFollowing(mkFollow(), 2); + setup("00000", s); + s.cards[id(1, 2, 9)].dealowner = -1; s.cards[id(2, 2, 9)].dealowner = -1; // 把闲2 削回 2 张 + t.eq('手牌数恰等于首家张数 整手必出', P.get_mustcard(s, 2).slice().sort((a, b) => a - b), + [id(1, 2, 8), id(2, 2, 8)].sort((a, b) => a - b)); +} + +// 边界:甩牌不下发 mustcard(甩牌跟牌走 flush_follow_ok 的逐分量匹配,见 design §5.4.4) +// 与上面的「自证」用例只差 shuai_demand 一个因素 +{ + const s = asFollowing(mkFollow(), 1); + setup("00000", s); + s.playproc.shuai_demand = { tractors: [], pairs: 1, singles: 1 }; + t.eq('甩牌局面 不下发mustcard', P.get_mustcard(s, 1), null); +} + +// 边界:非出牌阶段不下发(与「自证」用例只差 step 一个因素) +{ + const s = asFollowing(mkFollow(), 1); + setup("00000", s); + s.step = 3; + t.eq('非出牌阶段 不下发mustcard', P.get_mustcard(s, 1), null); +} + +// 边界:首家自己没有必出牌(与「自证」用例只差 seat 一个因素) +{ + const s = asFollowing(mkFollow(), 0); + setup("00000", s); + t.eq('本轮首家 无mustcard', P.get_mustcard(s, 0), null); +} + +// 重连 PushCards:轮到本座位才有 mustcard,其他座位没有 +{ + const pcNext = E.get_deskinfo(fc.o_room, 1).PushCards; // 当前轮到 1 + const pcIdle = E.get_deskinfo(fc.o_room, 2).PushCards; + t.eq('重连 轮到本家 PushCards有mustcard', Array.isArray(pcNext.mustcard), true); + t.eq('重连 未轮到 PushCards无mustcard', pcIdle.mustcard, undefined); +} + +t.done('hint'); diff --git a/server/games/erqiwang/test/test_input.js b/server/games/erqiwang/test/test_input.js new file mode 100644 index 0000000..ed755dc --- /dev/null +++ b/server/games/erqiwang/test/test_input.js @@ -0,0 +1,117 @@ +// 客户端入参校验:重复牌id / 越界id / 非数组 / 非整数(check_cards_valid + check_cards_inhand) +// 回归两个真实缺陷:① 重复id 被 can_playcard 当成对子/拖拉机;② 埋牌传重复id 只埋下 1 张 +require('./_shim'); +const { mod, setup, make108, id } = require('./_rpc.js'); +const P = require('../class.paiju.js'); +const t = require('./_assert')(); + +// 造一个最小可用牌局:108 张牌全发给指定座位,主花色=1 +function newPaiju(step, banker, owner) { + const paiju = { + cards: make108(), step, banker, call: 65, flower: 1, + seatlist: [[[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]]], + playproc: {}, playhistory: [], callproc: [], idx: 1 + }; + for (let i = 0; i < 108; i++) paiju.cards[i].dealowner = owner + 1; + paiju.method = { + check_cards_valid: c => P.check_cards_valid(paiju, c), + check_cards_inhand: (c, s) => P.check_cards_inhand(paiju, c, s), + do_burycard: c => P.do_burycard(paiju, c), + do_playcard: c => P.do_playcard(paiju, c), + get_seat_cards: s => P.get_seat_cards(paiju, s), + get_seat_zhucards: s => P.get_seat_zhucards(paiju, s), + have_baofu: () => (paiju.seatlist[0][4][0] == 0 || paiju.seatlist[1][4][0] == 0 || paiju.seatlist[2][4][0] == 0), + get_liangpai: () => null, + get_curmultiple: () => P.get_curmultiple(paiju), + get_mustcard: s => P.get_mustcard(paiju, s) + }; + return paiju; +} + +// ============ L1 check_cards_valid:正 / 反 / 边界 ============ +const pj = newPaiju(5, 0, 0); +const V = c => P.check_cards_valid(pj, c); + +t.eq('valid 正 单张', V([0]), true); +t.eq('valid 正 多张不重复', V([0, 1, 2, 53]), true); +t.eq('valid 边界 最大牌id 107', V([107]), true); +t.eq('valid 边界 最小牌id 0', V([0]), true); + +t.eq('valid 反 undefined', V(undefined), false); +t.eq('valid 反 null', V(null), false); +t.eq('valid 反 字符串', V('012'), false); +t.eq('valid 反 对象', V({ 0: 1, length: 1 }), false); +t.eq('valid 反 空数组', V([]), false); +t.eq('valid 反 重复id', V([5, 5]), false); +t.eq('valid 反 多张中含重复', V([1, 2, 3, 2]), false); +t.eq('valid 反 越界上界108', V([108]), false); +t.eq('valid 反 负数id', V([-1]), false); +t.eq('valid 反 小数id', V([1.5]), false); +t.eq('valid 反 数字字符串', V(['5']), false); + +// ============ L1 check_cards_inhand ============ +t.eq('inhand 正 在手上', P.check_cards_inhand(pj, [0, 1], 0), true); +t.eq('inhand 反 不在手上(别人的)', P.check_cards_inhand(pj, [0], 1), false); +t.eq('inhand 反 重复id(经 valid 拦截)', P.check_cards_inhand(pj, [0, 0], 0), false); +t.eq('inhand 反 越界id(经 valid 拦截)', P.check_cards_inhand(pj, [999], 0), false); +pj.cards[3].playround = 1; // 已打出 +t.eq('inhand 反 已打出的牌', P.check_cards_inhand(pj, [3], 0), false); +pj.cards[4].playround = 0; // 已埋 +t.eq('inhand 反 已埋下的牌', P.check_cards_inhand(pj, [4], 0), false); + +// ============ L2 出牌:重复id 不得被当成一对 ============ +const pj2 = newPaiju(5, 0, 0); +P.new_playround(pj2, 1, 0); +const cid = id(1, 1, 13); // 方块K +t.eq('出牌 反 重复id 被 mod 层拦截(valid)', P.check_cards_valid(pj2, [cid, cid]), false); + +// ============ L3 mod.maipai:8 个重复id 必须被拒、不得进入出牌阶段 ============ +const pj3 = newPaiju(3, 0, 0); +const s3 = setup('00000', pj3); +const dup = id(1, 1, 13); +mod.maipai({ data: { seat: 0, cards: [dup, dup, dup, dup, dup, dup, dup, dup] }, conmode: 0, fromid: 0 }); +t.eq('埋牌 反 8个重复id 被拒(step仍为3)', pj3.step, 3); +t.eq('埋牌 反 8个重复id 未埋下任何牌', pj3.cards.filter(c => c.playround === 0).length, 0); + +// 对照:8 张不同的牌可以正常埋下 +const pj4 = newPaiju(3, 0, 0); +setup('00000', pj4); +mod.maipai({ data: { seat: 0, cards: [0, 1, 2, 3, 4, 5, 6, 7] }, conmode: 0, fromid: 0 }); +t.eq('埋牌 正 8张不同牌 埋牌成功(step进5)', pj4.step, 5); +t.eq('埋牌 正 实际埋下8张', pj4.cards.filter(c => c.playround === 0).length, 8); + +// ============ L3 mod.chupai:越界/非数组入参不得抛异常 ============ +const pj5 = newPaiju(5, 0, 0); +P.new_playround(pj5, 1, 0); +const s5 = setup('00000', pj5); +let threw = null; +try { + mod.chupai({ data: { seat: 0, cards: [999] }, conmode: 0, fromid: 0 }); + mod.chupai({ data: { seat: 0, cards: undefined }, conmode: 0, fromid: 0 }); + mod.chupai({ data: { seat: 0, cards: 'x' }, conmode: 0, fromid: 0 }); + mod.chupai({ data: { seat: 0, cards: [dup, dup] }, conmode: 0, fromid: 0 }); +} catch (e) { threw = String(e); } +t.eq('出牌 反 非法入参不抛异常', threw, null); +t.eq('出牌 反 非法入参不产生任何出牌', pj5.cards.filter(c => c.playround > 0).length, 0); + + + +// ============ design §9:报无主后立即为全体三人刷新主牌统计 ============ +// 造 step5 牌局:seat0 手上只剩 1 张主牌(打掉即报无主),seat1/seat2 各持若干主牌 +const pj6 = newPaiju(5, 0, 0); +for (let i = 0; i < 108; i++) pj6.cards[i].dealowner = -1; // 先全部清空 +const give = (cid, seat) => { pj6.cards[cid].dealowner = seat + 1; pj6.cards[cid].playround = -1; }; +give(id(1, 1, 5), 0); // seat0:主♦5(唯一主牌) +give(id(1, 3, 9), 0); // seat0:副♥9(保证还有牌) +give(id(1, 1, 13), 1); give(id(2, 1, 13), 1); give(id(1, 1, 12), 1); // seat1:主♦K对 + 主♦Q +give(id(1, 1, 7), 2); // seat2:正7(固定主牌) +P.new_playround(pj6, 1, 0); +t.eq('报无主前 三家统计均为初始 [-1,-1]', pj6.seatlist.map(s => s[4]), [[-1, -1], [-1, -1], [-1, -1]]); +P.do_playcard(pj6, [id(1, 1, 5)]); // seat0 打出唯一主牌 → 报无主 +t.eq('报无主 seat0 主牌清空 [0,0]', pj6.seatlist[0][4], [0, 0]); +t.eq('报无主 立即刷新 seat1 [3张,1对]', pj6.seatlist[1][4], [3, 1]); +t.eq('报无主 立即刷新 seat2 [1张,0对]', pj6.seatlist[2][4], [1, 0]); +t.eq('报无主 seat0 主花色标志置1', pj6.seatlist[0][pj6.flower - 1], [1, 1]); +t.eq('报无主 seat2 无主对→无对标志置1', pj6.seatlist[2][pj6.flower - 1][1], 1); + +process.exit(t.done('input') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_leak.js b/server/games/erqiwang/test/test_leak.js new file mode 100644 index 0000000..4ff41cd --- /dev/null +++ b/server/games/erqiwang/test/test_leak.js @@ -0,0 +1,540 @@ +// 下发面泄露审计(server 红线「发全 ≠ 发多,按可见性下发」+ design §4/§9/§11) +// 跑完整局,对每一个「服务器 → 某座位」的包做两层扫描: +// ① 牌 id 层:取出包里出现的所有牌 id,逐个判定「此刻该座位是否有权知道这张牌」。 +// 依据 design §4(底牌只有庄家可见 / 70分亮3秒)、§9(查牌模式)、§11(结束亮埋牌底牌)。 +// ② 统计量层:牌 id 之外,包里还有【由手牌算出来的数】——seatlist 的缺门/无对表、 +// playproc 的 maxseat/maxcard、curmultiple、捡分。它们不含牌 id,①层一个都扫不到, +// 却同样能泄露("某家某花色还剩几张"这种统计,功能正常、玩家看不出、只有抓包的人知道)。 +// 故本层用【公开账本】独立重算这些数:只喂三家都看得见的信息(已广播的 chupai*.cards、 +// chupai1 的 count/flower/cardtype、庄家/叫分/主花色),算出来必须与包内值逐项相等。 +// 能被公开信息算出来的数 = 不含私密信息;算不出来 = 它一定读了别人的手牌。 +// 同时钉死这些结构的【字段集/项数】,防止有人往里加一项新统计而门禁照样全绿。 +const R = require('./_rpc.js'); const mod = R.mod; +const P = global.cls_youle_erqiwang_paiju, A = global.cls_youle_erqiwang_arith; +const CFG = global.cls_youle_erqiwang_config; +const D = require('../class.desk.js'), EX = require('../class.export.js'); +const t = require('./_assert')(); +let seed = 0xBEEF01; +const rnd = n => { seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; seed >>>= 0; return seed % n; }; +global.min_ontimeout = fn => fn(); +global.min_random = (a, b) => a + rnd(b - a + 1); +const pk = (s, d) => ({ conmode: 0, fromid: s, data: Object.assign({ agentid: 1, playerid: s, gameid: 1, roomcode: 1, seat: s }, d || {}) }); +const JS = x => JSON.stringify(x); + +// ========================================================================== +// ①层:牌 id 的可见性 +// ========================================================================== + +// 深度收集一个对象里所有「看起来是牌 id」的整数(0~107),并记下它【出现在哪个字段路径上】。 +// 为避免把 seat/count/grade 之类误当牌 id,只扫描已知承载牌 id 的字段名。 +// 【为什么要记路径】豁免必须按「哪一个字段」给,而不是按「哪一个 rpc」给: +// 同一个包里既有该公开的分组(结算面板的抠底明细/算奖明细),也有绝不该公开的分组, +// 按 rpc 整包放行会把后者一并放掉。路径只由对象键组成、忽略数组下标(如 +// aset.seatlist[1].cards → "aset.seatlist.cards")。 +const CARD_FIELDS = ['cards', 'bottomcards', 'burycards', 'cardsinhand', 'zhucards', 'gradecards', 'pushlist', 'MyCards', 'mustcard']; +function collectCards(node, path, key, out) { + if (node === null || node === undefined) return; + if (Array.isArray(node)) { node.forEach(x => collectCards(x, path, key, out)); return; } + if (typeof node === 'number') { if (CARD_FIELDS.indexOf(key) >= 0 && node >= 0 && node <= 107) out.add(node + '@' + path); return; } + if (typeof node === 'object') { + for (const k in node) { + collectCards(node[k], path ? path + '.' + k : k, CARD_FIELDS.indexOf(k) >= 0 ? k : key, out); + } + } +} +const cidOf = e => parseInt(e.slice(0, e.indexOf('@')), 10); +const pathOf = e => e.slice(e.indexOf('@') + 1); +// 路径尾匹配:deskinfo 会多一层分组前缀(Balance.aset.seatlist.cards vs aset.seatlist.cards) +const pathIs = (path, suffix) => path === suffix || path.endsWith('.' + suffix); + +// design §8.2 亮牌:庄家埋牌后手中【固定主牌】达门槛时,这些牌的牌面对两个闲家公开(仅可查牌模式)。 +// 【独立于 get_liangpai 重算一遍】——若实现误把非固定主牌(主花色普通牌、副牌)塞进 liangpai, +// 下面的集合里不会有它,审计照样报泄露。这是审计的意义所在,不能图省事直接复用被测实现。 +function bankerLiangpaiCards(pj) { + const out = []; + let wang = 0, qi = 0, er = 0; + for (const c of pj.cards) { + // 与 get_seat_cards_award 同口径:庄家手牌 = 发到自己的 + 摸起的底牌,且排除已埋的 + if (!(c.dealowner === pj.banker + 1 || c.dealowner === 0)) continue; + if (c.playround === 0) continue; + if (c.number >= 53) { wang++; out.push(c.id); } + else if (c.number === 7) { qi++; out.push(c.id); } + else if (c.number === 2) { er++; out.push(c.id); } + } + if (out.length < 10 && wang < 3 && qi < 6 && er < 6) return new Set(); // 不达标:一张都不该下发 + return new Set(out); +} +let curLiangpai = new Set(); // 本局亮牌应公开的牌(埋牌后确定,全程固定) +let curNoCheck = false; // 本局是否「不查牌」 +let liangpaiRounds = 0; // 可查牌 + 庄家达标的局数(亮牌真的下发过) +let liangpaiRoundsNoCheck = 0; // 不查牌 + 庄家达标的局数(此时一张都不该发) +let surrenderRuns = 0; // 走过投降结算的局数 +let disbandRuns = 0; // 走过中途解散结算的局数 +let awardExempt = 0; // 「算奖明细」这条显式豁免被真正用到的次数 + +const leaks = []; +let pkts = 0, scanned = 0; +// 判定:seat 此刻是否有权知道 cid(path 见 collectCards 的说明) +function mayKnow(pj, seat, cid, rpc, path) { + const c = pj.cards[cid]; + if (c.dealowner === -1) return true; // 规则去除的 3/4,不可能出现 + if (c.playround > 0) return true; // 已打出,全场可见 + if (c.dealowner === seat + 1) return true; // 自己的牌 + if (seat === pj.banker && c.dealowner === 0) return true; // 庄家可见底牌(含摸起后又埋下的) + // §4 70分坐庄:上庄推送把 8 张底牌向三家亮 3 秒——只放行 shangzhuang 的 bottomcards 这一处 + if (rpc === 'shangzhuang' && pj.call === 70 && c.dealowner === 0 && pathIs(path, 'bottomcards')) return true; + + // §11 结束亮埋牌底牌:本局【正常结算】后,埋牌底牌随抠底明细对全场公开。 + // jiesuan 广播(bottom.cards,sendpack_toother(msg,-1) 三家都发)与结算面板还开着时的 + // 重连包(Balance.bottom.cards)是【同一份数据的两条投递路径】,可见性必须同判。 + // 门控写成「step===6 且 result!==3」而不是「rpc 是 jiesuan/deskinfo」: + // · 只认字段路径 bottom.cards,不整包放行(同一个包里的其他分组仍严格判定); + // · result 3 = 中途解散(class.paiju.js get_paiju_account 的 case 2,o_paiju.result = 3)。解散是【没打完】, + // 埋牌底牌不该亮。当前解散路径的 tmp_jiesuan_bottom 恒为 null、包里本就没有 bottom, + // 所以这一条现在不改变任何判定;它挡的是【将来】有人给解散路径也带上 bottom—— + // 那时门禁必须转红,而不是因为「step 也是 6」被放行。 + // 【措辞更正】上一轮把「给重连包补上这条豁免」记成了"收紧"——其实那一步的净效果是 + // 【放宽】(在原豁免上加了一个 OR 分支,旧放行面 ⊂ 新放行面),只是放行面被限定在 + // step===6 的同一份公开数据上。真正的收紧是这一轮:从「按 rpc 整包放行」改成 + // 「按字段路径 bottom.cards + step6 + result!==3 放行」。 + if (c.playround === 0 && pathIs(path, 'bottom.cards') && pj.step === 6 && pj.result !== 3) return true; + + // 【显式豁免 · 结算算奖明细】aset.seatlist[i].cards(get_paiju_account 的 _cglist[i].cards) + // 是 design §8.1 冲关 / §8.3 傍王的算奖依据牌,三种结算来源都带、且三家都收到。 + // · 正常打完:这些牌早已打出(playround>0),本来就公开,本豁免对它无影响。 + // · 投降(step2 直接结算)与中途解散:牌【一张没打】(playround===-1),于是他家的 + // 王/冲关牌会随算奖明细发给三家。这是既有行为——算奖要向三家自证,牌局此刻已终止、 + // 不再有后续出牌,公开这几张牌不影响任何未决的对局信息。 + // 写成一条【带路径限定的显式豁免】而不是让它落在扫不到的盲区里: + // 若哪天有人把「未打出的手牌」塞到别的字段上,它不在这条路径下,照样报泄露。 + if (pathIs(path, 'aset.seatlist.cards') && pj.step === 6) { awardExempt++; return true; } + + if (rpc === 'mingpai') return true; // §9 明牌:单独在下面按内容校验 + // §8.2 亮牌:可查牌模式下,庄家的固定主牌牌面对闲家公开(只在 maipai / 重连包的 liangpai 分组里下发) + if (!curNoCheck && (rpc === 'maipai' || rpc === 'deskinfo') && pathIs(path, 'liangpai.cards') && curLiangpai.has(cid)) return true; + return false; +} + +// ========================================================================== +// ②层:统计量必须能由「公开账本」独立重算出来 +// ========================================================================== +// 账本里只有【三家都看得见】的东西: +// 庄家 / 庄家叫分(shangzhuang 广播)、主花色(xuanzhu 广播)、 +// 每一手已摊在桌上的牌(chupai1/2/3 的 data.cards,三家同收)、 +// 首家这一手的 张数/花色/牌型(chupai1 的 count/flower/cardtype —— 它就是那几张明牌的描述)。 +// 账本【没有】任何人的手牌。凡是能从账本算出来的数,就不可能夹带手牌信息。 +let LG = null; +let statChecks = 0; +const statBad = []; + +const flowerOf = (fl, id) => { const c = A.id_to_code(fl, id); return c > 1000 ? fl : Math.floor(c / 100); }; +// 牌面分值:5→5、10/K→10,其余 0。只由牌 id 推出(牌已摊在桌上,牌面是公开的) +const scoreOf = id => { const n = A.id_to_number(id); return n === 5 ? 5 : ((n === 10 || n === 13) ? 10 : 0); }; + +// 首家这一手的牌型值 = 这一手【最小一张】的牌编码。 +// 【独立推导,不复用 can_playcard】:那段逐张状态机无论走 单张/对子/拖拉机/甩牌 哪个分支, +// cardvalue 最终都停在「最后处理到的那张牌」上;牌已按编码降序排列,最后一张即最小一张。 +// 换个式子算出同一个数,才谈得上"独立重算"。 +function leadValue(fl, cards) { + const s = A.order_cards(fl, cards.concat()); + return A.id_to_code(fl, s[s.length - 1]); +} + +// 跟家这一手的牌型值:照 design 的规则从【摊在桌上的牌】重算(can_followcard 里这段同样 +// 只吃 _sortfollow / startflower / startcardtype,不碰手牌——本函数是它的独立复述)。 +function followValue(fl, cards, startflower, starttype) { + const s = A.order_cards(fl, cards.concat()); + const top = A.id_to_code(fl, s[0]); + const flTop = top > 1000 ? fl : Math.floor(top / 100); + const sameLine = (flTop === startflower || flTop === fl); + // 副7 / 副2 同级归一(四门的 7 一样大、四门的 2 一样大) + const norm = c => (c > 7000 && c < 8000) ? 7000 : ((c > 2000 && c < 3000) ? 2000 : c); + if (starttype === 101) { return sameLine ? norm(top) : 0; } + if (starttype > 101 && starttype < 200) { return 0; } // 首家甩单张:跟牌一律压不过 + if (starttype === 201) { + if (A.get_pairlist(fl, s).length !== 1) return 0; + return sameLine ? norm(top) : 0; + } + if (starttype > 201 && starttype < 300) { return 0; } // 首家甩多对:同上 + if (starttype > 300 && starttype < 400) { + if (A.get_tuolaji_list(fl, A.get_pairlist(fl, s), starttype).length !== 1) return 0; + return sameLine ? top : 0; + } + return 0; +} + +// 跟家这一手暴露出的「缺门 / 无对」:完全由摊在桌上的牌与首家牌型推出(design §9 的牌况表)。 +// 最大或最小的一张不是首家花色 → 这门没牌了(顺带也没对子) +// 首家出的是对子/拖拉机、跟牌没凑够同样多的对子 → 这门没对子了 +function followFlags(fl, cards, startflower, starttype) { + const s = A.order_cards(fl, cards.concat()); + let noflower = false, nopair = false; + if (flowerOf(fl, s[0]) !== startflower) { noflower = true; nopair = true; } + if (flowerOf(fl, s[s.length - 1]) !== startflower) { noflower = true; nopair = true; } + if (starttype > 200 && A.get_pairlist(fl, s).length !== starttype % 100) { nopair = true; } + return { noflower, nopair }; +} + +// 首家甩牌的分量需求:把桌面上那一手直接分解即可(decompose_trump 只吃牌 id),非甩牌为 null +function expectShuaiDemand() { + if (LG.start < 0 || !LG.cards[LG.start]) return null; + const comps = A.decompose_trump(LG.flower, LG.cards[LG.start].concat()); + if (comps.length <= 1) return null; + const re = { tractors: [], pairs: 0, singles: 0 }; + for (const c of comps) { + if (c.type === 'tractor') { re.tractors.push(c.len); } + else if (c.type === 'pair') { re.pairs++; } + else { re.singles++; } + } + return re; +} + +function ledgerNewRound(round, start) { + LG.round = round; LG.start = start; LG.currseat = start; + LG.startcount = -1; LG.startflower = -1; LG.starttype = -1; + LG.maxseat = -1; LG.maxcard = -1; + LG.cards = [null, null, null]; +} +function ledgerApplyFollow(seat, cards) { + const fl = LG.flower; + const s = A.order_cards(fl, cards.concat()); + LG.cards[seat] = s; + const v = followValue(fl, s, LG.startflower, LG.starttype); + if (v > LG.maxcard) { LG.maxseat = seat; LG.maxcard = v; } + const fg = followFlags(fl, s, LG.startflower, LG.starttype); + const cell = LG.table[seat][LG.startflower - 1]; + if (fg.nopair) { cell[1] = 1; } + if (fg.noflower) { cell[0] = 1; cell[1] = 1; } +} +function ledgerEndRound() { + // 本墩被闲家收走 → 墩里的分归闲家(庄家收走则不计入闲家捡分) + let g = 0; + if (LG.maxseat !== LG.banker) { + for (let i = 0; i < 3; i++) { for (const id of (LG.cards[i] || [])) { g += scoreOf(id); } } + } + LG.grade += g; + LG.lastRoundGrade = g; + ledgerNewRound(LG.round + 1, LG.maxseat); +} +// 当前抓分倍数:算法本身只吃 (叫分, 闲家捡分, 级距) 三个数——叫分与捡分都在账本里 +function ledgerCurmultiple() { + if (!LG.call || LG.call <= 0) return 0; + return A.get_upgrade(LG.call, LG.grade, A.get_qvalue(LG.call, LG.climb)); +} + +// 把一个【逻辑包】喂进公开账本(同一个包会逐座位发 3 份,只喂第一份) +function ledgerFeed(rpc, d) { + if (!LG) return; + switch (rpc) { + case 'shangzhuang': LG.banker = d.banker; LG.call = d.grade; break; + case 'xuanzhu': LG.flower = d.flower; break; + case 'maipai': ledgerNewRound(1, d.seat); break; + case 'chupai1': { + const s = A.order_cards(LG.flower, d.cards.concat()); + LG.startcount = s.length; + LG.startflower = flowerOf(LG.flower, s[0]); + LG.starttype = d.cardtype; + LG.maxseat = d.seat; + LG.maxcard = leadValue(LG.flower, s); + LG.cards[d.seat] = s; + LG.currseat = (LG.start + 1) % 3; + // 首家自报的 张数/花色 必须与桌面上那几张牌一致(同一份公开数据两处口径) + statChecks++; + if (d.count !== s.length || d.flower !== LG.startflower) { + statBad.push(`chupai1 自报 count/flower=(${d.count},${d.flower}) 与桌面牌重算=(${s.length},${LG.startflower}) 不一致`); + } + // 账本自检:首出者必须就是本轮首家(账本若跟丢了轮次,下面所有比对都不再可信) + statChecks++; + if (d.seat !== LG.start) { statBad.push(`chupai1 首出者=${d.seat} 公开账本认为本轮首家=${LG.start}`); } + break; + } + case 'chupai2': ledgerApplyFollow(d.seat, d.cards); LG.currseat = (LG.start + 2) % 3; break; + case 'chupai3': ledgerApplyFollow(d.seat, d.cards); ledgerEndRound(); break; + case 'jiesuan': + // 收尾墩不发 chupai3,整包换成 jiesuan、把这一手放进 data.chupai(协议 §13/§14) + if (d.chupai && d.chupai.cards) { ledgerApplyFollow(d.chupai.seat, d.chupai.cards); ledgerEndRound(); } + break; + } +} + +// playproc 的字段集是【契约】:多一个键就是多一份下发信息,必须显式暴露出来 +const PLAYPROC_KEYS = ['cards', 'currseat', 'maxcard', 'maxseat', 'round', 'shuai_demand', 'start', 'startcount', 'startflower', 'starttype']; + +function checkPlayproc(tag, pp) { + statChecks++; + const keys = Object.keys(pp).sort(); + if (JS(keys) !== JS(PLAYPROC_KEYS)) { statBad.push(`${tag} playproc 字段集变了:${JS(keys)}`); } + const exp = { + round: LG.round, start: LG.start, currseat: LG.currseat, + startcount: LG.startcount, startflower: LG.startflower, starttype: LG.starttype, + maxseat: LG.maxseat, maxcard: LG.maxcard + }; + for (const k in exp) { + statChecks++; + if (pp[k] !== exp[k]) { statBad.push(`${tag} playproc.${k}=${pp[k]} 公开账本重算=${exp[k]}`); } + } + for (let s = 0; s < 3; s++) { + statChecks++; + const got = (pp.cards[s] === undefined || pp.cards[s] === null) ? null : pp.cards[s]; + if (JS(got) !== JS(LG.cards[s])) { statBad.push(`${tag} playproc.cards[${s}]=${JS(got)} 公开账本重算=${JS(LG.cards[s])}`); } + } + statChecks++; + if (JS(pp.shuai_demand) !== JS(expectShuaiDemand())) { + statBad.push(`${tag} playproc.shuai_demand=${JS(pp.shuai_demand)} 桌面牌重算=${JS(expectShuaiDemand())}`); + } +} + +function checkSeatlist(tag, sl) { + statChecks++; + if (!Array.isArray(sl) || sl.length !== 3) { statBad.push(`${tag} seatlist 外层不是定长 3:${JS(sl)}`); return; } + // 报副格(第 5 项)是 design §9 的【有意公开】:一旦有人把主牌打空(报无主), + // 系统就向全体三人公示三家各自的余主数量与余主对数。反过来说——【没人报无主之前 + // 一格都不许透露】,必须还是初值 [-1,-1]。这条门控若被放宽,等于提前泄露三家主牌结构。 + const anyEmpty = sl.some(one => Array.isArray(one[4]) && one[4][0] === 0); + for (let s = 0; s < 3; s++) { + const one = sl[s]; + statChecks++; + if (!Array.isArray(one) || one.length !== 5) { + // 约定恒 5 项:花色 1~4 的 [缺门, 无对] + 报副 [余主数, 余主对数]。 + // 多出来的一项必定是一份【新的统计】——而新统计几乎注定是从真实手牌算出来的, + // 公开账本重算不出来。这里直接判死,不给它蒙混过关的机会。 + statBad.push(`${tag} seat${s} seatlist 项数不是 5(新增统计?):${JS(one)}`); + continue; + } + for (let f = 0; f < 5; f++) { + statChecks++; + const cell = one[f]; + if (!Array.isArray(cell) || cell.length !== 2 || typeof cell[0] !== 'number' || typeof cell[1] !== 'number') { + statBad.push(`${tag} seat${s}[${f}] 形状不是二元数值组:${JS(cell)}`); + } + } + // 前 4 格:只能是公开账本推得出的缺门/无对 + for (let f = 0; f < 4; f++) { + const exp = [LG.table[s][f][0], LG.table[s][f][1]]; + // 主花色那一格另有一条来源:报无主公示之后,「余主数=0」等价于主花色缺门+无对、 + // 「余主对数=0」等价于主花色无对(class.paiju.js do_playcard 的报副刷新)。 + // 它推导自【本包自己给出的】报副格,仍属公开信息,不是额外泄露。 + if (LG.flower > 0 && f === LG.flower - 1 && Array.isArray(one[4])) { + if (one[4][0] === 0) { exp[0] = 1; exp[1] = 1; } + else if (one[4][0] > 0 && one[4][1] === 0) { exp[1] = 1; } + } + statChecks++; + if (one[f][0] !== exp[0] || one[f][1] !== exp[1]) { + statBad.push(`${tag} seatlist[${s}][${f}]=${JS(one[f])} 公开账本重算=${JS(exp)}`); + } + } + statChecks++; + if (!anyEmpty) { + if (JS(one[4]) !== JS([-1, -1])) { statBad.push(`${tag} 无人报无主却下发了 seat${s} 的余主统计:${JS(one[4])}`); } + } else if (one[4][0] < 0 || one[4][1] < 0) { + statBad.push(`${tag} 已报无主但 seat${s} 的余主统计仍是初值:${JS(one[4])}`); + } + } +} + +// 对一个「分组对象」(包的 data,或 deskinfo 的 ChooseMain / BuryCards / PushCards)做统计量审计 +function statAuditGroup(tag, g, opts) { + if (!g || typeof g !== 'object' || !LG) return; + opts = opts || {}; + if (g.hasOwnProperty('curmultiple')) { + statChecks++; + const exp = ledgerCurmultiple(); + if (g.curmultiple !== exp) { statBad.push(`${tag} curmultiple=${g.curmultiple} 公开账本重算=${exp}`); } + } + if (opts.cumulativeGrade && g.hasOwnProperty('grade')) { + statChecks++; + if (g.grade !== LG.grade) { statBad.push(`${tag} grade=${g.grade} 公开账本重算=${LG.grade}`); } + } + // 分组这一层的 seatlist 恒指 design §9 的【牌况表】(maipai / chupai1/2/3 / PushCards); + // 结算包里的 aset.seatlist 是另一回事,嵌在 aset 下、不会走到这里。 + // 【不加形状守卫】形状变了就该由 checkSeatlist 判红,而不是被守卫悄悄跳过 + if (g.hasOwnProperty('seatlist')) { checkSeatlist(tag, g.seatlist); } + if (g.playproc) { checkPlayproc(tag, g.playproc); } +} + +// ========================================================================== + +function run(roomtype, call, opts) { + opts = opts || {}; + const sent = []; + const o_room = { + roomtype, asetcount: 6, roomcode: 1, createtime: 'T', makewartime: 'T', + seatlist: [0, 1, 2].map(i => ({ conmode: 0, fromid: i, playerid: 100 + i, nickname: 'P', avatar: '', gameinfo: {} })), + method: { sendpack_toother: m => sent.push({ to: 'ALL', m: JSON.parse(JSON.stringify(m)) }) } + }; + curNoCheck = (String(roomtype).charAt(4) === '1'); // roomtype 位4:1=不查牌 + curLiangpai = new Set(); + LG = { + climb: CFG.parse(roomtype).climb, banker: -1, call: -1, flower: -1, + round: 0, start: -1, currseat: -1, startcount: -1, startflower: -1, starttype: -1, + maxseat: -1, maxcard: -1, cards: [null, null, null], grade: 0, lastRoundGrade: 0, + table: [0, 1, 2].map(() => [[0, 0], [0, 0], [0, 0], [0, 0]]) + }; + const desk = D.new(o_room); o_room.o_desk = desk; + global.youle_erqiwang.app = { SendPack: m => sent.push({ to: m.fromid, m: JSON.parse(JSON.stringify(m)) }) }; + global.youle_erqiwang.import = { check_player: () => o_room, deduct_roomcard: () => { }, save_grade: () => { } }; + mod.import = global.youle_erqiwang.import; mod.app = global.youle_erqiwang.app; + D.do_new_paiju(desk, 0); + const pj = desk.method.curr_paiju(); + + const audit = () => { + while (sent.length) { + const { to, m } = sent.shift(); + pkts++; + // 同一个逻辑包逐座位发 3 份(i=0,1,2),只用第一份推进公开账本; + // 但【每一份都要审计】,这样三家收到的统计量若有差异也会被抓住 + if (to === 0 || to === 'ALL') { ledgerFeed(m.rpc, m.data); } + const targets = to === 'ALL' ? [0, 1, 2] : [to]; + statAuditGroup(`rpc=${m.rpc}→seat${to}`, m.data); + // chupai3 的 data.grade 是【这一墩】闲家收到的分(>0 才带),与账本的墩内分值同源 + if (m.rpc === 'chupai3') { + statChecks++; + const got = m.data.hasOwnProperty('grade') ? m.data.grade : 0; + if (got !== LG.lastRoundGrade) { statBad.push(`chupai3 本墩 grade=${got} 公开账本重算=${LG.lastRoundGrade}`); } + } + // 结算包的闲家总捡分 = 账本累计捡分 + 抠底(扣底只在最后一墩产生) + if (m.rpc === 'jiesuan' && m.data.aset) { + statChecks++; + const bg = (m.data.bottom && m.data.bottom.grade2) ? m.data.bottom.grade2 : 0; + if (m.data.aset.grade !== LG.grade + bg) { + statBad.push(`jiesuan aset.grade=${m.data.aset.grade} 公开账本重算=${LG.grade}+抠底${bg}`); + } + } + const out = new Set(); + collectCards(m.data, '', null, out); + for (const seat of targets) for (const e of out) { + scanned++; + const cid = cidOf(e); + if (!mayKnow(pj, seat, cid, m.rpc, pathOf(e))) { + leaks.push(`rpc=${m.rpc} @${pathOf(e)} → seat${seat} 泄露牌 ${cid}(dealowner=${pj.cards[cid].dealowner} playround=${pj.cards[cid].playround})`); + } + } + } + }; + // 重连快照的审计(每个座位各取一次)。阶段取【实际的】pj.step:最后一手打完后 step 已转 6, + // 这里拿到的是结算快照,写死 "step5" 会让报错信息指错阶段(曾据此误判过) + const auditDeskinfo = () => { + for (let s = 0; s < 3; s++) { + const di = EX.new().get_deskinfo(o_room, s); + pkts++; + statAuditGroup(`deskinfo(step${pj.step})→seat${s}.ChooseMain`, di.ChooseMain); + statAuditGroup(`deskinfo(step${pj.step})→seat${s}.BuryCards`, di.BuryCards); + statAuditGroup(`deskinfo(step${pj.step})→seat${s}.PushCards`, di.PushCards, { cumulativeGrade: true }); + const out = new Set(); collectCards(di, '', null, out); + for (const e of out) { + scanned++; + const cid = cidOf(e); + if (!mayKnow(pj, s, cid, 'deskinfo', pathOf(e))) { + leaks.push(`deskinfo(step${pj.step}) @${pathOf(e)} → seat${s} 泄露牌 ${cid}(dealowner=${pj.cards[cid].dealowner} playround=${pj.cards[cid].playround})`); + } + } + } + }; + + audit(); + mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call })); audit(); + if (pj.step === 1) { mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); audit(); } + if (pj.step === 1) { mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); audit(); } + const b = pj.banker; + + // ---- 投降结算路径(design §4:70 分坐庄可在选主阶段直接投降)---- + // 这条路径此前从未被审计过:9 局全部「正常打完」,mod.touxiang 一次都没走到。 + if (opts.surrender) { + if (pj.call !== 70) { leaks.push(`投降用例前置不成立:庄家叫分=${pj.call},非 70 分坐庄`); return pj; } + mod.touxiang(pk(b)); audit(); + if (pj.step !== 6 || pj.result !== 2) { leaks.push(`投降用例未真正结算:step=${pj.step} result=${pj.result}`); return pj; } + auditDeskinfo(); + surrenderRuns++; + return pj; + } + + mod.xuanzhu(pk(b, { flower: 1 + rnd(4) })); audit(); + // 亮牌快照在埋牌后才固定;先算出期望集合,再审计这一包(maipai 正是亮牌的下发时机) + mod.maipai(pk(b, { cards: P.get_seat_cards(pj, b).slice(-8) })); + curLiangpai = bankerLiangpaiCards(pj); + if (curLiangpai.size > 0) { if (curNoCheck) { liangpaiRoundsNoCheck++; } else { liangpaiRounds++; } } + audit(); + auditDeskinfo(); + let guard = 0; + while (pj.step === 5 && ++guard < 400) { + if (opts.disbandAfter && guard > opts.disbandAfter) { break; } + const seat = pj.playproc.currseat, hand = P.get_seat_cards(pj, seat); + let pick = null; + if (seat === pj.playproc.start) pick = [hand[hand.length - 1]]; + else for (const c of hand) if (A.can_followcard(pj.flower, hand, [c], pj.playproc.startcount, pj.playproc.startflower, pj.playproc.starttype).result) { pick = [c]; break; } + if (!pick) break; + mod.chupai(pk(seat, { cards: pick })); audit(); + if (guard % 7 === 0) { // 中途穿插重连与明牌 + auditDeskinfo(); + if (pj.method.have_baofu()) { + sent.length = 0; + mod.mingpai(pk(0)); + // §9 明牌:只允许返回「另外两家未出的主牌」,多一张都算越权 + for (const { m } of sent) { + if (m.rpc !== 'mingpai' || !m.data.others) continue; + for (const o of m.data.others) { + for (const cid of o.zhucards) { + scanned++; + const c = pj.cards[cid]; + const isTrumpCard = c.flower === pj.flower || c.flower === 5 || c.number === 2 || c.number === 7; + if (c.dealowner !== o.seat + 1 && !(o.seat === pj.banker && c.dealowner === 0)) leaks.push(`mingpai 返回了非该座位的牌 ${cid}`); + if (c.playround !== -1) leaks.push(`mingpai 返回了已出/已埋的牌 ${cid}`); + if (!isTrumpCard) leaks.push(`mingpai 返回了非主牌 ${cid}`); + } + } + } + sent.length = 0; + } + } + } + audit(); + + // ---- 中途解散结算路径(get_disbandRoom)---- + // 同样此前零覆盖。平台在解散时取这份 deskfree 下发给三家(前端 Game_Modify.Free), + // 故按「广播给三家」审计;随后的 step6 重连快照(result===3)也一并过一遍。 + if (opts.disbandAfter) { + const msg = EX.new().get_disbandRoom(o_room); + if (!msg) { leaks.push('解散用例前置不成立:get_disbandRoom 返回 null'); return pj; } + sent.push({ to: 'ALL', m: JSON.parse(JSON.stringify(msg)) }); + audit(); + if (pj.step !== 6 || pj.result !== 3) { leaks.push(`解散用例未真正结算:step=${pj.step} result=${pj.result}`); return pj; } + auditDeskinfo(); + disbandRuns++; + } + return pj; +} + +for (const rt of ['00000', '00001']) for (const call of [65, 70, 5]) run(rt, call); +// 上面 6 局按 0xBEEF01 的牌序,庄家一次都没达到亮牌门槛(§8.2)。亮牌是【有意的信息公开】, +// 必须真的被审计走过,否则「无越权泄露」对这条路径只是碰巧没触发。故换牌序再补几局: +// 可查牌局验「该发的发了且不越权」,不查牌局验「达标也一张都不发」。 +seed = 0x12345; +for (const call of [65, 5]) run('00000', call); +seed = 0x777001; +run('00000', 65); +run('00001', 65); +// 投降与解散:两条结算路径此前从未进过本门禁(9 局全是「正常打完」) +seed = 0x24680; +run('00000', 70, { surrender: true }); +run('00001', 70, { surrender: true }); +run('00000', 65, { disbandAfter: 11 }); +run('00001', 60, { disbandAfter: 5 }); + +t.eq('下发面无越权泄露', [...new Set(leaks)].slice(0, 5), []); +t.eq('统计量均可由公开账本重算', [...new Set(statBad)].slice(0, 5), []); +// 覆盖下限:若收集器失效(扫不到牌),上面的断言会假绿,这里钉住实际扫描量 +t.eq('审计覆盖 下发面 ≥ 1000 个', pkts >= 1000, true); +t.eq('审计覆盖 可见性判定 ≥ 10000 次', scanned >= 10000, true); +t.eq('审计覆盖 统计量重算 ≥ 10000 次', statChecks >= 10000, true); +// 覆盖下限(续):随机手牌下庄家未必达到亮牌门槛。若一局都没触发, +// 上面「无越权泄露」对亮牌这条路径就是【碰巧没走到】而不是【验证过安全】——必须钉住。 +t.eq('审计覆盖 亮牌路径(可查牌) 至少 1 局', liangpaiRounds >= 1, true); +t.eq('审计覆盖 亮牌路径(不查牌) 至少 1 局', liangpaiRoundsNoCheck >= 1, true); +t.eq('审计覆盖 投降结算路径 至少 1 局', surrenderRuns >= 1, true); +t.eq('审计覆盖 中途解散结算路径 至少 1 局', disbandRuns >= 1, true); +// 「算奖明细」那条显式豁免必须真的被用到,否则它是一条无人走过的死规则, +// 早晚会在无人察觉时开始放行别的东西 +t.eq('审计覆盖 算奖明细豁免确实被走到', awardExempt >= 1, true); +console.log(` [统计] 扫描 ${pkts} 个下发面 / ${scanned} 次「座位×牌」可见性判定 / ${statChecks} 次统计量重算比对`); +console.log(` [统计] 亮牌达标 可查牌${liangpaiRounds}局·不查牌${liangpaiRoundsNoCheck}局 / 投降${surrenderRuns}局 / 解散${disbandRuns}局 / 算奖明细豁免命中 ${awardExempt} 次`); +process.exit(t.done('leak') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_paiju.js b/server/games/erqiwang/test/test_paiju.js new file mode 100644 index 0000000..099c2a8 --- /dev/null +++ b/server/games/erqiwang/test/test_paiju.js @@ -0,0 +1,262 @@ +// paiju 单测:亮牌(§8.2) + 结算 get_paiju_account(§7/§8) 集成 +require('./_shim'); +const P = require('../class.paiju.js'); +const t = require('./_assert')(); + +const id = (d, f, n) => (d - 1) * 54 + (f - 1) * 13 + (n - 1); +const big = d => (d - 1) * 54 + 53; +const small = d => (d - 1) * 54 + 52; + +// ============ §8.2 亮牌 get_liangpai ============ +// 构造庄家(seat0)手牌 o_paiju;副花色中性单张填充到28 +function fill(req, n) { + const u = new Set(req), o = req.slice(); + for (const f of [2, 3, 4]) for (const x of [1, 5, 6, 8, 9, 10, 11, 12, 13]) { + if (o.length >= n) break; + const c = id(1, f, x); + if (!u.has(c)) { u.add(c); o.push(c); } + } + return o; +} +const mkHand = ids => ({ flower: 1, banker: 0, cards: ids.map(x => ({ id: x, playround: -1, dealowner: 1 })) }); +const lp = ids => P.get_liangpai(mkHand(fill(ids, 28))); +// 亮牌返回 { cards: [固定主牌id...] }(按主牌序排好);不达标返回 null。 +// 断言用「排序后的 id 集合」比较,避免把排序细节写死进用例。 +const sortIds = a => a.slice().sort((x, y) => x - y); +const lpCards = ids => { const r = lp(ids); return r === null ? null : sortIds(r.cards); }; + +// —— 四条门槛各自能触发,且亮出的都是【全部固定主牌】—— +{ + const h = [big(1), small(1), small(2)]; // 3 王 + t.eq('亮牌 3王 → 亮出这3张王', lpCards(h), sortIds(h)); +} +{ + const h = [id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 3, 2), id(2, 3, 2)]; // 6 个 2 + t.eq('亮牌 6个2 → 亮出这6张2', lpCards(h), sortIds(h)); +} +{ + const h = [id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]; // 6 个 7 + t.eq('亮牌 6个7 → 亮出这6张7', lpCards(h), sortIds(h)); +} +{ + const h = [big(1), small(1), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), + id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2)]; // 固定主 10(各档单看都不达标) + t.eq('亮牌 固定主10边界 → 亮出全部10张', lpCards(h), sortIds(h)); +} +// —— 不达标 → null —— +t.eq('亮牌 2王2个7 不达标→null', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7)]), null); +t.eq('亮牌 固定主9 不达标→null', + lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2)]), null); + +// —— 关键:亮出的只有【固定主牌】,主花色普通牌【不能】混进来 —— +// 手牌里塞满主花色(♦)普通牌 A/K/Q/J/10/9/8/6/5,它们是主牌但不是固定主牌 +{ + const guding = [big(1), small(1), small(2)]; // 3 王 → 达标 + const zhuNormal = [id(1, 1, 1), id(1, 1, 13), id(1, 1, 12), id(1, 1, 11), id(1, 1, 10)]; // ♦A/K/Q/J/10 + const r = lp(guding.concat(zhuNormal)); + t.eq('亮牌 只含固定主牌 张数=3', r.cards.length, 3); + t.eq('亮牌 主花色普通牌未混入', sortIds(r.cards), sortIds(guding)); +} +// —— 亮出内容与「被哪条门槛触发」无关:4王+6个7 应亮出全部 10 张固定主 —— +{ + const h = [big(1), big(2), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]; + t.eq('亮牌 同时满足多档 → 仍只亮同一份固定主牌', lpCards(h), sortIds(h)); +} +// —— 排序:按本局主牌序从大到小(大王 > 小王 > 正7 > 副7 > 正2 > 副2)—— +{ + const A = require('../class.arith.js'); + const h = [big(1), small(1), id(1, 1, 7), id(1, 2, 7), id(1, 1, 2), id(1, 2, 2), + id(2, 1, 7), id(2, 2, 7), id(2, 1, 2), id(2, 2, 2)]; // 固定主 10,主花色=♦(1) + const r = lp(h); + t.eq('亮牌 按主牌序排好', r.cards, A.order_cards(1, r.cards.slice())); + t.eq('亮牌 首张为大王', r.cards[0], big(1)); +} + +// design §8.2:亮牌依据是【庄家埋牌后】的静态手牌快照,全程固定、不随出牌缩水 +// (闲家断线重连时 get_deskinfo 会再取一次;若按当前剩余手牌重算,亮牌会缩水甚至变 null) +const LIANG28 = fill([big(1), big(2), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)], 28); +const EXPECT_LIANG = sortIds([big(1), big(2), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]); +t.eq('亮牌 埋牌后快照(出牌开始)', sortIds(P.get_liangpai(mkHand(LIANG28)).cards), EXPECT_LIANG); +const played = mkHand(LIANG28); // 同一手牌,已打出2大王+1对♦7 +for (const c of played.cards){ + if (c.id === big(1) || c.id === big(2) || c.id === id(1, 1, 7) || c.id === id(2, 1, 7)){ c.playround = 1; } +} +t.eq('亮牌 打出4张后重连仍按快照', sortIds(P.get_liangpai(played).cards), EXPECT_LIANG); +// 埋下的牌(playround==0)不算在快照里 +const buried = mkHand(LIANG28); +for (const c of buried.cards){ if (c.id === big(1) || c.id === big(2)){ c.playround = 0; } } +t.eq('亮牌 已埋的牌不计入快照', sortIds(P.get_liangpai(buried).cards), + sortIds([small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)])); + +// ============ §7/§8 结算 get_paiju_account 集成 ============ +// 受控 o_paiju/o_desk/o_room:控制庄家算奖手牌(dealowner=1)与闲1捡分(playowner=1,score) +const mkDesk = roomtype => ({ seatlist: [[0, [], 0, 0, 0], [0, [], 0, 0, 0], [0, [], 0, 0, 0]], o_room: { roomtype, asetcount: 6 }, method: { get_desk_account: m => m } }); +const paiju = (call, flower, cards, roomtype) => ({ call, banker: 0, flower, cards, idx: 2, o_desk: mkDesk(roomtype), endtime: null, result: null }); +// bankerIds→庄家算奖手牌(dealowner=1);xian1grade→闲1捡分(每张≤10分) +function mkCards(bankerIds, xian1grade) { + const cs = bankerIds.map(x => ({ id: x, dealowner: 1, playround: -1, playowner: -1, score: 0 })); + let g = xian1grade, i = 0; + while (g > 0) { const s = g >= 10 ? 10 : g; cs.push({ id: 200 + i, dealowner: 2, playround: 1, playowner: 1, score: s }); g -= s; i++; } + return cs; +} +const settle = (call, flower, cards, roomtype, type = 0) => P.get_paiju_account(paiju(call, flower, cards, roomtype), type, {}); +const grades = msg => msg.data.aset.seatlist.map(p => p.grade); +const B0 = [id(1, 1, 1)]; // 庄家仅1张手牌(不足28→无算奖),用于纯算子用例 + +// 常规算子:庄赢/闲赢各档(无算奖) +t.eq('结算 65小光(grade20) X=4', grades(settle(65, 1, mkCards(B0, 20), "00000")), [8, -4, -4]); +t.eq('结算 65 upgrade/multiple', [settle(65, 1, mkCards(B0, 20), "00000").data.aset.upgrade, settle(65, 1, mkCards(B0, 20), "00000").data.aset.multiple], [2, 2]); +t.eq('结算 65大光(grade0)', grades(settle(65, 1, mkCards(B0, 0), "00000")), [12, -6, -6]); +t.eq('结算 65过庄(grade50)', grades(settle(65, 1, mkCards(B0, 50), "00000")), [4, -2, -2]); +t.eq('结算 65升2级(grade110)', grades(settle(65, 1, mkCards(B0, 110), "00000")), [-8, 4, 4]); +// 爬坡局(位3=1):40分大光 base8 → X=24 +t.eq('结算 爬坡40大光', grades(settle(40, 1, mkCards(B0, 0), "00010")), [48, -24, -24]); +// 投降(type1):基础1、X=1 → 庄-2、闲各+1 +t.eq('结算 投降 grade', grades(settle(70, -1, mkCards(B0, 0), "00000", 1)), [-2, 1, 1]); +t.eq('结算 投降 upgrade标识', settle(70, -1, mkCards(B0, 0), "00000", 1).data.aset.upgrade, -99); +// 傍王(位2=1) + 庄家3王:N_庄=1(常规算奖)+3(傍王)=4;65小光 X=4 +// jf 庄+8/闲-4,aw 庄+32/闲-16 → 庄40、闲各-20(零和) +const B3wang = fill([big(1), small(1), small(2)], 28); +t.eq('结算 傍王65小光+庄3王', grades(settle(65, 1, mkCards(B3wang, 20), "00100")), [40, -20, -20]); +// 对照:同局不勾傍王 → N_庄=1(仅常规算奖):aw 庄+8/闲-4,jf 庄+8/闲-4 → 庄16、闲各-8 +t.eq('结算 不傍王65小光+庄3王(仅常规算奖)', grades(settle(65, 1, mkCards(B3wang, 20), "00000")), [16, -8, -8]); + +// ============ §6.3 扣底触发 get_bottom_account ============ +// 仅当闲家(maxseat!=banker)用主牌赢末轮才扣底/翻倍 +function mkBottomPaiju(maxseat, winCards, bottomScoreCards, flower) { + const cards = []; + for (let i = 0; i < 108; i++) cards[i] = { id: i, playround: -1, score: 0, dealowner: 1, playowner: -1 }; + for (const [cid, sc] of bottomScoreCards) { cards[cid].playround = 0; cards[cid].score = sc; } + const pc = [[], [], []]; pc[maxseat] = winCards; + const o = { banker: 0, flower, cards, playproc: { maxseat, cards: pc }, playhistory: [] }; + o.method = { get_burycard: () => P.get_burycard(o), get_grade_incard: c => P.get_grade_incard(o, c) }; + return o; +} +// 闲1用主对(方块Q对)赢末轮,底2张K=20分 → 主对×2、grade2=40 +const bo = P.get_bottom_account(mkBottomPaiju(1, [id(1, 1, 12), id(2, 1, 12)], [[id(1, 1, 13), 10], [id(2, 1, 13), 10]], 1), { data: {} }); +t.eq('扣底 闲家主对赢 multiple', bo.data.bottom.multiple, 2); +t.eq('扣底 grade1/grade2', [bo.data.bottom.grade1, bo.data.bottom.grade2], [20, 40]); +// 闲1用副牌(副K)赢末轮 → 非主牌不扣底 +const bo2 = P.get_bottom_account(mkBottomPaiju(1, [id(1, 2, 13)], [[id(1, 1, 13), 10]], 1), { data: {} }); +t.eq('扣底 副牌赢不扣底', bo2.data.bottom.multiple, undefined); +// 庄家(maxseat==banker)赢末轮 → 不扣底 +const bo3 = P.get_bottom_account(mkBottomPaiju(0, [id(1, 1, 12), id(2, 1, 12)], [[id(1, 1, 13), 10]], 1), { data: {} }); +t.eq('扣底 庄家赢不扣底', bo3.data.bottom.multiple, undefined); +// 闲1用两连对拖拉机赢 → ×4 +const bo4 = P.get_bottom_account(mkBottomPaiju(1, [id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)], [[id(1, 4, 13), 10]], 1), { data: {} }); +t.eq('扣底 两连对拖赢 ×4', bo4.data.bottom.multiple, 4); + +// ============ §5.4.5 甩错惩罚执行 do_playcard ============ +const A = require('../class.arith.js'); +// 造首出场景:庄家(0)手牌与两闲家主牌,step5 本轮首出 +function mkFirstPlay(flower, dealMap) { + const cards = []; + for (let i = 0; i < 108; i++) cards[i] = { id: i, dealowner: 99, playround: -1, playowner: -1, score: 0, flower: A.id_to_flower(i), number: A.id_to_number(i) }; + for (const [cid, owner] of dealMap) cards[cid].dealowner = owner; + const sl = () => [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]]; + const o = { step: 5, banker: 0, flower, cards, seatlist: [sl(), sl(), sl()], playproc: {}, playhistory: [] }; + o.method = { have_baofu: () => o.seatlist.some(s => s[4][0] == 0) }; + P.new_playround(o, 1, 0); + return o; +} +// 庄家甩 大王+主K对;seat1 持主A对(能压主K对) → 甩错:收回甩牌、只强制打出最小一张(主K)、其余留手 +const fp = mkFirstPlay(1, [[big(1), 1], [id(1, 1, 13), 1], [id(2, 1, 13), 1], [id(1, 1, 1), 2], [id(2, 1, 1), 2]]); +const re = P.do_playcard(fp, [big(1), id(1, 1, 13), id(2, 1, 13)]); +t.eq('甩错执行 shuaicuo=true', re.shuaicuo, true); +t.eq('甩错执行 只打1张且为主K', re.cards.length === 1 && A.id_to_number(re.cards[0]) === 13, true); +t.eq('甩错执行 大王未出(留手)', fp.cards[big(1)].playround, -1); +t.eq('甩错执行 打出的主K已出', fp.cards[re.cards[0]].playround > 0, true); +// 对照:合法甩牌 do_playcard 正常打出整套、不甩错 +const fp2 = mkFirstPlay(1, [[big(1), 1], [big(2), 1], [id(1, 1, 13), 1], [id(1, 1, 5), 2], [id(1, 1, 6), 3]]); // 大王对+主K单;对手仅小主牌 +const re2 = P.do_playcard(fp2, [big(1), big(2), id(1, 1, 13)]); +t.eq('合法甩牌 do_playcard 非甩错', !!re2.shuaicuo, false); +t.eq('合法甩牌 整套3张打出', re2.cards.length, 3); + +// §5.4.4 集成:首出合法甩牌 → 跟牌方违反分量拆解 → do_playcard 拒;正确对位 → 接受 +const fpf = mkFirstPlay(1, [ + [big(1), 1], [id(1, 1, 12), 1], [id(2, 1, 12), 1], // 庄家 大王+主Q对(合法甩牌) + [id(1, 1, 11), 2], [id(2, 1, 11), 2], [id(1, 1, 9), 2], [id(1, 1, 8), 2] // seat1 主J对+主9+主8 +]); +const lead = P.do_playcard(fpf, [big(1), id(1, 1, 12), id(2, 1, 12)]); +t.eq('甩牌集成 首出合法(记录shuai_demand)', lead.result && !lead.shuaicuo, true); +const badF = P.do_playcard(fpf, [id(1, 1, 11), id(1, 1, 9), id(1, 1, 8)]); // 拆散主J对 +t.eq('甩牌集成 跟牌拆散主对→拒', badF.result, false); +const goodF = P.do_playcard(fpf, [id(1, 1, 11), id(2, 1, 11), id(1, 1, 9)]); // 用主J对对位 +t.eq('甩牌集成 跟牌用主对→接受', goodF.result, true); + +// ============ §5.1 控制权 + §6.2 捡分归属 ============ +// 造一轮三家各出 1 张同花色副牌的完整墩:主花色=♦(1),本轮花色=♥(3)。 +// deal: [[牌id, 座位], ...];scores: [[牌id, 分值], ...] +function mkTrick(deal, scores) { + const cards = []; + for (let i = 0; i < 108; i++) cards[i] = { id: i, dealowner: 99, playround: -1, playowner: -1, score: 0, flower: A.id_to_flower(i), number: A.id_to_number(i) }; + for (const [cid, seat] of deal) cards[cid].dealowner = seat + 1; + for (const [cid, s] of scores) cards[cid].score = s; + const sl = () => [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]]; + const o = { step: 5, banker: 0, flower: 1, cards, seatlist: [sl(), sl(), sl()], playproc: {}, playhistory: [] }; + o.method = { have_baofu: () => o.seatlist.some(s => s[4][0] == 0) }; + P.new_playround(o, 1, 0); // 第 1 轮由庄家(0)首出 + return o; +} +const H5 = id(1, 3, 5), H8 = id(1, 3, 8), HK = id(1, 3, 13), HA = id(1, 3, 1); // ♥5 ♥8 ♥K ♥A(副牌) +const SC = [[H5, 5], [HK, 10]]; // §6.1:5→5分、K→10分;♥8/♥A 不计分 + +// —— 反:庄家赢下本轮 → 台面分牌全部作废,不计入闲家捡分 —— +const wb = mkTrick([[HA, 0], [HK, 1], [H5, 2]], SC); +P.do_playcard(wb, [HA]); P.do_playcard(wb, [HK]); +const wbRe = P.do_playcard(wb, [H5]); +// 注:第三家出完后 do_playcard 已开新一轮、playproc 被重置,胜者只能从返回值读 +t.eq('§6.2 反 庄家赢本轮 maxseat=庄', wbRe.maxseat, 0); +t.eq('§6.2 反 庄家赢 → 下发面无本轮得分', wbRe.grade, undefined); +t.eq('§6.2 反 庄家赢 → 闲家捡分 0(分牌作废)', P.get_jian_grade(wb).grade, 0); + +// —— 正:闲家1 赢下本轮 → 台面【全部】分牌(含庄家自己打出的♥5)计入闲家 —— +const w1 = mkTrick([[H5, 0], [HK, 1], [H8, 2]], SC); +P.do_playcard(w1, [H5]); P.do_playcard(w1, [HK]); +const w1Re = P.do_playcard(w1, [H8]); +t.eq('§6.2 正 闲1赢本轮 maxseat=1', w1Re.maxseat, 1); +t.eq('§6.2 正 本轮闲家得分=5+10(含庄家打出的♥5)', w1Re.grade, 15); +t.eq('§6.2 正 累计捡分=15', P.get_jian_grade(w1).grade, 15); +// §5.1:下一轮由本轮牌面最大的一方先出(不是固定庄家、也不是顺序下家) +t.eq('§5.1 正 下一轮由本轮最大者(闲1)先出', [w1.playproc.round, w1.playproc.start, w1.playproc.currseat], [2, 1, 1]); + +// —— 边界:两个闲家谁赢都算「闲家捡到」,结果与闲1赢完全一致(design §6.2)—— +const w2 = mkTrick([[H5, 0], [H8, 1], [HK, 2]], SC); +P.do_playcard(w2, [H5]); P.do_playcard(w2, [H8]); +const w2Re = P.do_playcard(w2, [HK]); +t.eq('§6.2 边界 闲2赢本轮 maxseat=2', w2Re.maxseat, 2); +t.eq('§6.2 边界 闲2赢与闲1赢同样计闲家捡分', [w2Re.grade, P.get_jian_grade(w2).grade], [15, 15]); +t.eq('§5.1 边界 下一轮改由闲2先出', [w2.playproc.round, w2.playproc.start], [2, 2]); + +// ============ §8 算奖快照 get_seat_cards_award ============ +// design §8:庄家按【埋牌完成后】的 28 张、闲家按【发牌后】的 28 张;快照全程固定, +// 不随之后的出牌改变。例外:70 分投降局庄家未埋牌,快照为「发牌 + 暗牌」共 36 张。 +function mkAward() { + const cards = []; + for (let i = 0; i < 108; i++) cards[i] = { id: i, dealowner: -1, playround: -1, playowner: -1, score: 0 }; + let n = 0; + const take = k => { const out = []; while (out.length < k) { out.push(n); n++; } return out; }; + const bankerDeal = take(28), an = take(8), x1 = take(28), x2 = take(28); + bankerDeal.forEach(i => cards[i].dealowner = 1); + an.forEach(i => cards[i].dealowner = 0); // 暗牌 + x1.forEach(i => cards[i].dealowner = 2); + x2.forEach(i => cards[i].dealowner = 3); + return { o: { banker: 0, flower: 1, cards }, bankerDeal, an, x1 }; +} +// 投降局:未选主未埋牌 → 庄家快照 = 发牌 28 + 暗牌 8 = 36 +const aw0 = mkAward(); +t.eq('§8 边界 投降局 庄家快照=发牌+暗牌 36 张', P.get_seat_cards_award(aw0.o, 0).length, 36); +// 正常局:埋掉 8 张后 → 庄家快照 28 张,且已埋的不在内 +const aw = mkAward(); +const bury = aw.bankerDeal.slice(0, 5).concat(aw.an.slice(0, 3)); // 从发牌与暗牌里各埋几张 +bury.forEach(i => aw.o.cards[i].playround = 0); +const snap = P.get_seat_cards_award(aw.o, 0); +t.eq('§8 正 庄家快照=埋牌后 28 张', snap.length, 28); +t.eq('§8 正 已埋的 8 张不在快照里', bury.some(i => snap.indexOf(i) >= 0), false); +t.eq('§8 正 闲家快照=发牌后 28 张', [P.get_seat_cards_award(aw.o, 1).length, P.get_seat_cards_award(aw.o, 2).length], [28, 28]); +// 快照是静态的:之后打出的牌仍算在内(否则闲家重连时算奖/亮牌会缩水) +snap.slice(0, 10).forEach(i => aw.o.cards[i].playround = 3); +t.eq('§8 边界 打出 10 张后 庄家快照仍是同样的 28 张', P.get_seat_cards_award(aw.o, 0), snap); +t.eq('§8 边界 打出后闲家快照也不缩水', P.get_seat_cards_award(aw.o, 1).length, 28); + +process.exit(t.done('paiju') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_rpc.js b/server/games/erqiwang/test/test_rpc.js new file mode 100644 index 0000000..ce96289 --- /dev/null +++ b/server/games/erqiwang/test/test_rpc.js @@ -0,0 +1,788 @@ +// L3 RPC 层:§9 明牌/查牌门控、§4 投降条件。期望取自 design §4/§9 +const R = require('./_rpc'); +const P = require('../class.paiju.js'); +const { mod, setup, make108, id } = R; +const t = require('./_assert')(); + +const pack = (seat, extra) => Object.assign({ data: Object.assign({ agentid: 'a', playerid: 1, gameid: 'g', roomcode: 1, seat }, extra), conmode: 0, fromid: seat }); + +// 被拒 = 恰好回 1 个失败包给请求者(data.success=false + errcode),不再是"静默不发包" +const ERR = mod.ERR; +const rej = (c, errcode) => c.sent.length === 1 && c.sent[0].data.success === false && c.sent[0].data.errcode === errcode; + +// ===================== §9 明牌 mingpai ===================== +const dealMP = [[id(1, 1, 13), 2], [id(1, 1, 12), 3]]; // seat1 主K, seat2 主Q +const mkMP = (step, baofu) => ({ step, banker: 0, flower: 1, cards: make108(dealMP), method: { have_baofu: () => baofu } }); +let ctx = setup("00000", mkMP(5, true)); // 可查牌 + 报无主 + 出牌阶段 +mod.mingpai(pack(0)); +t.eq('明牌 合法 下发1包', ctx.sent.length, 1); +t.eq('明牌 others=他家主牌', ctx.sent[0] && ctx.sent[0].data.others, [{ seat: 1, zhucards: [id(1, 1, 13)] }, { seat: 2, zhucards: [id(1, 1, 12)] }]); +ctx = setup("00001", mkMP(5, true)); mod.mingpai(pack(0)); // 不查牌(位4=1) +t.eq('明牌 不查牌 拒(回失败包RULE)', rej(ctx, ERR.RULE), true); +ctx = setup("00000", mkMP(5, false)); mod.mingpai(pack(0)); // 未报无主 +t.eq('明牌 未报无主 拒(回失败包RULE)', rej(ctx, ERR.RULE), true); +ctx = setup("00000", mkMP(3, true)); mod.mingpai(pack(0)); // 非出牌阶段 +t.eq('明牌 非出牌阶段 拒(回失败包STEP)', rej(ctx, ERR.STEP), true); + +// ===================== §4 投降 touxiang 条件 ===================== +const mkTX = (step, banker, call) => ({ step, banker, call, method: { get_paiju_account: (type, msg) => { msg.rpc = 'jiesuan'; msg.data = { type }; return msg; } } }); +let p = mkTX(2, 0, 70); ctx = setup("00000", p); mod.touxiang(pack(0)); // 70分 step2 庄家 +t.eq('投降 70分step2庄家 step→6', p.step, 6); +t.eq('投降 下发jiesuan', ctx.sent.length === 1 && ctx.sent[0].rpc, 'jiesuan'); +p = mkTX(2, 0, 65); ctx = setup("00000", p); mod.touxiang(pack(0)); +t.eq('投降 65分 拒(回失败包RULE)', rej(ctx, ERR.RULE), true); +p = mkTX(3, 0, 70); ctx = setup("00000", p); mod.touxiang(pack(0)); +t.eq('投降 埋牌后step3 拒(回失败包STEP)', rej(ctx, ERR.STEP), true); +p = mkTX(2, 1, 70); ctx = setup("00000", p); mod.touxiang(pack(0)); +t.eq('投降 非庄家 拒(回失败包SEAT)', rej(ctx, ERR.SEAT), true); + +// ===================== §9 出牌 chupai 的查牌门控(seatlist/baozhu)===================== +// 造真实牌局 → 强制到首出(step5),庄家(0)出一张副牌单张;seat1 已报无主 +function mkChupai(roomtype) { + const o_desk_stub = { paiju_list: [] }; + const pj = P.new(o_desk_stub, 0); + pj.banker = 0; pj.call = 65; pj.flower = 1; pj.step = 5; + P.new_playround(pj, 1, 0); // 本轮庄家(0)首出 + const lead = id(1, 3, 9); // 副9(flower3) 作单张首出 + pj.cards[lead].dealowner = 1; // 归庄家 + pj.cards[lead].playround = -1; + // seat1 报无主:必须让它【真的】打空主牌。do_playcard 会按各家实际手牌重算三家的主牌统计 + // (design §9:一旦有人报无主,立即为全体三人刷新),只置标志位会被重算纠正回去 + for (const c of P.get_seat_zhucards(pj, 1)) pj.cards[c].playround = 1; + pj.seatlist[1][4][0] = 0; // seat1 报无主 → have_baofu()=true + const c = setup(roomtype, pj); + mod.chupai(pack(0, { cards: [lead] })); + return c; +} +let cv = mkChupai("00000"); // 可查牌 +t.eq('出牌 可查牌 有seatlist', cv.sent[0] && cv.sent[0].data.seatlist !== undefined, true); +// design §9:报无主后要为全体三人显示【另外两家】的主牌数/对子数 → 下发面必须整表带三家, +// 不能只带出牌者一家(否则另两家的统计要滞后到它们各自出牌才补上) +t.eq('出牌 可查牌 seatlist 带三家', cv.sent[0] && cv.sent[0].data.seatlist.length, 3); +t.eq('出牌 可查牌 seatlist 含非出牌者的报副统计', cv.sent[0] && cv.sent[0].data.seatlist[1][4][0], 0); +t.eq('出牌 可查牌 baozhu=1(有人报无主)', cv.sent[0] && cv.sent[0].data.baozhu, 1); +let cn = mkChupai("00001"); // 不查牌(位4=1) +t.eq('出牌 不查牌 无seatlist', cn.sent[0] && cn.sent[0].data.seatlist, undefined); +t.eq('出牌 不查牌 baozhu=0', cn.sent[0] && cn.sent[0].data.baozhu, 0); + +// ===================== §4.2 mod.jiaofen 入参校验 ===================== +// do_callgrade 被调用=接受;被 return 挡掉=拒绝。step 设 0 跳过后续广播 +function mkJF(currcall) { + const st = { called: null }; + const pj = { + step: 1, banker: -1, call: -1, + method: { + get_callgrade_seat: () => 0, + get_callgrade_value: () => currcall, + do_callgrade: c => { st.called = c; pj.step = 0; } + } + }; + return { pj, st }; +} +function jf(currcall, call) { + const { pj, st } = mkJF(currcall); + setup("00000", pj); + mod.jiaofen(pack(0, { call })); + return st.called; +} +t.eq('叫分 >70 拒', jf(0, 75), null); +t.eq('叫分 非5倍数 拒', jf(0, 63), null); +t.eq('叫分 首家不叫(currcall0,call0) 拒', jf(0, 0), null); +t.eq('叫分 后叫不更低(call>=currcall) 拒', jf(55, 60), null); +t.eq('叫分 首家合法65 接受', jf(0, 65), 65); +t.eq('叫分 后叫更低50 接受', jf(55, 50), 50); +t.eq('叫分 边界70 接受', jf(0, 70), 70); + +// ===================== §4 70分上庄的暗牌亮牌 gating(真实 jiaofen 驱动到上庄)===================== +function driveUp(calls) { + const pj = P.new({ paiju_list: [] }, 0); // 真实牌局 step1 firstseat0 + const c = setup("00000", pj); + for (const [seat, call] of calls) mod.jiaofen(pack(seat, { call })); + return c.sent.filter(m => m.rpc === 'shangzhuang'); +} +// 0叫70、1不叫、2不叫 → 0上庄70 +const sz70 = driveUp([[0, 70], [1, 0], [2, 0]]); +t.eq('上庄70 有shangzhuang下发', sz70.length > 0, true); +const bankerPk = sz70.find(m => m.data.cards !== undefined); +const xianPk = sz70.find(m => m.data.cards === undefined); +t.eq('上庄70 庄家包有手牌', !!bankerPk, true); +t.eq('上庄70 闲家包有暗牌', xianPk && xianPk.data.bottomcards !== undefined, true); +t.eq('上庄70 ancard3s=1', xianPk && xianPk.data.ancard3s, 1); +// 对照:叫5立即上庄(非70) → 闲家无暗牌、无ancard3s +const sz5 = driveUp([[0, 5]]); +const xian5 = sz5.find(m => m.data.cards === undefined); +t.eq('上庄5 闲家无暗牌', xian5 && xian5.data.bottomcards, undefined); +t.eq('上庄5 无ancard3s', xian5 && xian5.data.ancard3s, undefined); + +// ===================== §12 端到端(叫分→上庄→选主→埋牌→出牌ready) + §9 maipai/deskinfo 门控 ===================== +const E = require('../class.export.js').new(); +function driveToPlay(roomtype) { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup(roomtype, pj); + mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); // 0上庄65 + mod.xuanzhu(pack(0, { flower: 1 })); // 选主 方块 + mod.maipai(pack(0, { cards: P.get_seat_cards(pj, 0).slice(0, 8) })); // 埋任意8张 + return { pj, ...c }; +} +const e = driveToPlay("00000"); +t.eq('端到端 叫分→选主→埋牌→step5', e.pj.step, 5); +t.eq('端到端 庄家首出(currseat=0)', e.pj.playproc.currseat, 0); +t.eq('端到端 选主flower=1', e.pj.flower, 1); +t.eq('端到端 埋牌后庄家保留28张', P.get_seat_cards(e.pj, 0).length, 28); +// §9 maipai 亮牌门控:不查牌 → 闲家包无 liangpai +const en = driveToPlay("00001"); +const mpXian = en.sent.filter(m => m.rpc === 'maipai').find(m => m.data.cards === undefined); +t.eq('埋牌 不查牌 闲家无liangpai', mpXian && mpXian.data.liangpai, undefined); +// §9 deskinfo(重连) PushCards 门控:可查牌有 seatlist、不查牌无 +t.eq('重连 可查牌 PushCards有seatlist', E.get_deskinfo(e.o_room, 1).PushCards.seatlist !== undefined, true); +t.eq('重连 不查牌 PushCards无seatlist', E.get_deskinfo(en.o_room, 1).PushCards.seatlist, undefined); + +// ===================== 底牌 vs 埋牌底牌:字段名必须分开(S-4)===================== +// 术语:底牌 = 发牌时没发给玩家、扣在桌面的 8 张(字段 bottomcards) +// 埋牌底牌 = 庄家埋牌扣下的 8 张(字段 burycards) +// 两者曾共用 bottomcards、只靠所在包区分语义,已拆分。以下用例锁死拆分结果,防回归。 +const mpAll = e.sent.filter(m => m.rpc === 'maipai'); +const mpZhuang = mpAll.find(m => m.data.cards !== undefined); // 庄家包(带手牌) +const mpXian2 = mpAll.find(m => m.data.cards === undefined); // 闲家包 +// 正面:庄家的 maipai 包带 burycards,且就是埋下的那 8 张 +t.eq('maipai 庄家有burycards', mpZhuang && Array.isArray(mpZhuang.data.burycards), true); +t.eq('maipai burycards为8张', mpZhuang && mpZhuang.data.burycards.length, 8); +// 反面:maipai 包里不得再出现 bottomcards(改名漏改会被这条抓住) +t.eq('maipai 庄家无bottomcards', mpZhuang && mpZhuang.data.bottomcards, undefined); +// 可见性:闲家两个字段都不该有(埋牌底牌只有庄家可见) +t.eq('maipai 闲家无burycards', mpXian2 && mpXian2.data.burycards, undefined); +t.eq('maipai 闲家无bottomcards', mpXian2 && mpXian2.data.bottomcards, undefined); +// 重连 PushCards:庄家有 burycards、无 bottomcards;闲家两者皆无 +const pcZhuang = E.get_deskinfo(e.o_room, 0).PushCards; +const pcXian = E.get_deskinfo(e.o_room, 1).PushCards; +t.eq('重连 PushCards 庄家有burycards', Array.isArray(pcZhuang.burycards), true); +t.eq('重连 PushCards 庄家burycards为8张', pcZhuang.burycards.length, 8); +t.eq('重连 PushCards 庄家无bottomcards', pcZhuang.bottomcards, undefined); +t.eq('重连 PushCards 闲家无burycards', pcXian.burycards, undefined); +t.eq('重连 PushCards 闲家无bottomcards', pcXian.bottomcards, undefined); +// 对照:底牌一侧字段名不受影响,仍叫 bottomcards(ChooseMain/BuryCards 为庄家专属) +const cmZhuang = E.get_deskinfo(e.o_room, 0); +t.eq('重连 ChooseMain/BuryCards 侧仍用bottomcards命名未被误改', + cmZhuang.PushCards.burycards !== undefined && cmZhuang.PushCards.bottomcards === undefined, true); + +// ===================== 下发面一致性:增量路径(实时推送) 必须与 全量路径(重连 deskinfo) 给出同一份数据 ===================== +// 背景:前端一致性测试(同一局真包,逐包增量回放 vs 一次性 deskinfo 重建)暴露出 4 处服务端下发面缺陷。 +// 这些都不是"算错了",而是"同一份数据两条路径给的不一样 / 有一条根本没给",前端只能自己补—— +// 而自己补就等于把对局推进逻辑搬到前端(前端红线「数据驱动、前端无对局状态机」)。以下逐条钉死。 +const JS = x => JSON.stringify(x); +const AR = require('../class.arith.js'); // 本文件下方另有同模块的 A,此处在其声明之前,故另起名 + +// ---- ① bottomcards:上庄推送 / ChooseMain(step2) / BuryCards(step3) 三处必须【同序】---- +// 曾经的缺陷:get_bottomcards 每次按"当时的 paiju.flower"重排,而底牌是在【选主之前】翻给庄家看的, +// 于是 step2(flower=-1) 与 step3(flower 已定) 给出两种顺序——主花色的那张牌被顶到队首。 +// 现按发牌时冻结的快照下发。反面守卫:主牌花色刻意选成底牌里真实存在的花色,一旦退回"按当时 flower 排"必红。 +function driveBottom(flowerPick) { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup("00000", pj); + mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); + const sz = c.sent.filter(m => m.rpc === 'shangzhuang').find(m => m.data.cards !== undefined).data; // 庄家包 + const step2 = E.get_deskinfo(c.o_room, 0).ChooseMain; + mod.xuanzhu(pack(0, { flower: flowerPick(pj) })); + const step3 = E.get_deskinfo(c.o_room, 0).BuryCards; + return { pj, c, sz, step2, step3 }; +} +// 主牌花色必须挑一个【会真的改变底牌排序】的:否则本组用例是空转的—— +// 退回"按当时 flower 重排"也照样绿(实测:随手挑底牌里第一张的花色就是空转,它本来就排在队首)。 +let btReorders = false; +const bt = driveBottom(pj => { + const snap = P.get_bottomcards(pj); + for (let f = 1; f <= 4; f++) { + if (JS(AR.order_cards(f, snap.concat())) !== JS(snap)) { btReorders = true; return f; } + } + return 1; +}); +// 先钉住前提:所选花色确实会让"按主牌花色排"与"冻结快照"不同序,下面三条才是有效守卫 +t.eq('底牌 用例前提:所选主牌花色会改变排序(否则本组用例空转)', btReorders, true); +t.eq('底牌 上庄推送 8 张', bt.sz.bottomcards.length, 8); +t.eq('底牌 上庄推送 == 重连 ChooseMain(step2) 同序', JS(bt.step2.bottomcards), JS(bt.sz.bottomcards)); +t.eq('底牌 上庄推送 == 重连 BuryCards(step3) 同序(选主后不得重排)', JS(bt.step3.bottomcards), JS(bt.sz.bottomcards)); +// 快照必须是【副本】:order_cards 是原地排序,调用方就地重排不得污染冻结的快照 +const bcCopy = P.get_bottomcards(bt.pj); bcCopy.reverse(); +t.eq('底牌 get_bottomcards 返回副本(调用方重排不污染快照)', JS(P.get_bottomcards(bt.pj)), JS(bt.sz.bottomcards)); + +// ---- ② curmultiple:ChooseMain(step2) / BuryCards(step3) 必须与 shangzhuang 同源同值 ---- +// 此刻一张牌未出、捡分恒为 0 → 必为 +3(大光)。曾经 deskinfo 不带此字段,重连后前端角标掉回 0。 +t.eq('抓分倍数 上庄推送 curmultiple=3(捡分恒0→大光)', bt.sz.curmultiple, 3); +t.eq('抓分倍数 重连 ChooseMain.curmultiple 与上庄同值', bt.step2.curmultiple, bt.sz.curmultiple); +t.eq('抓分倍数 重连 BuryCards.curmultiple 与上庄同值', bt.step3.curmultiple, bt.sz.curmultiple); + +// ---- ③ maipai.seatlist:与 PushCards.seatlist 同表同门控 ---- +// 曾经的缺陷:maipai 不带 seatlist,「埋牌完成 → 庄家首出」这段窗口增量路径为空、重连路径却有表。 +const mpZhuang3 = mpAll.find(m => m.data.cards !== undefined); +const mpXian3 = mpAll.find(m => m.data.cards === undefined); +t.eq('埋牌 可查牌 maipai 带 seatlist(庄闲同表)', + JS(mpZhuang3.data.seatlist), JS(E.get_deskinfo(e.o_room, 0).PushCards.seatlist)); +t.eq('埋牌 可查牌 maipai 闲家同一张表(seatlist 三家公开、不裁剪)', + JS(mpXian3.data.seatlist), JS(mpZhuang3.data.seatlist)); +t.eq('埋牌 seatlist 埋牌完成时为初始化空表', JS(mpZhuang3.data.seatlist), + JS([0, 1, 2].map(() => [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]]))); +// 反面:不查牌模式一律不下发(与 chupai / PushCards 同一道门控,design §9) +t.eq('埋牌 不查牌 maipai 无 seatlist', + en.sent.filter(m => m.rpc === 'maipai').every(m => m.data.seatlist === undefined), true); + +// ---- ③b maipai.playproc:与 PushCards.playproc 同一个快照函数、同结构,且是深拷贝 ---- +// 曾经的缺陷:mod.js 组 maipai 包时没有放 playproc(do_burycard 内部已 new_playround 就地初始化好 +// round-1 的进行态),「埋牌完成 → 庄家首出」这段窗口增量路径的 table.playproc 是 null, +// 重连路径却已有初值——与 seatlist 是同一个窗口、同一条理由,被漏在了同一处。 +t.eq('埋牌 maipai 带 playproc(庄闲同值)', + JS(mpZhuang3.data.playproc), JS(E.get_deskinfo(e.o_room, 0).PushCards.playproc)); +t.eq('埋牌 maipai 闲家同一份 playproc(三家公开、不裁剪)', + JS(mpXian3.data.playproc), JS(mpZhuang3.data.playproc)); +t.eq('埋牌 playproc 埋牌完成时为 round-1 初值(庄家首出、桌面全空)', mpZhuang3.data.playproc, { + round: 1, start: 0, currseat: 0, startcount: -1, startflower: -1, starttype: -1, + maxseat: -1, maxcard: -1, cards: [null, null, null], shuai_demand: null +}); +// 反面:playproc 不受查牌门控——不像 seatlist,它只是桌面公开信息,不查牌模式下也恒有 +t.eq('埋牌 不查牌 maipai 仍带 playproc', + en.sent.filter(m => m.rpc === 'maipai').every(m => m.data.playproc !== undefined), true); +// 深拷贝守卫:playproc 是全程复用的活对象,new_playround 就地重置同一个对象。 +// 若下发挂的是活引用,埋牌后紧跟着打一手牌,已发出的 maipai.playproc 会被回改成下一状态的样子。 +const dcPj = P.new({ paiju_list: [] }, 0); +const dcC = setup("00000", dcPj); +mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); +mod.xuanzhu(pack(0, { flower: 1 })); +const dcInner = mod.app.SendPack; +let dcLive = null; +mod.app.SendPack = m => { if (m.rpc === 'maipai' && m.data.cards !== undefined) dcLive = m.data.playproc; dcInner(m); }; +mod.maipai(pack(0, { cards: P.get_seat_cards(dcPj, 0).slice(0, 8) })); +mod.app.SendPack = dcInner; +const dcSnapAtSend = JS(dcLive); +const dcHand = P.get_seat_cards(dcPj, dcPj.playproc.currseat); +mod.chupai(pack(dcPj.playproc.currseat, { cards: [dcHand[dcHand.length - 1]] })); // 推进出一手,制造"若为活引用会被回改"的条件 +t.eq('埋牌 playproc 下发的是深拷贝快照(已发出的包不被后续出牌回改)', JS(dcLive), dcSnapAtSend); + +// ---- ④ burycards:maipai 必须发服务端权威快照,不得回显客户端提交的 cards 原序 ---- +// 曾经的缺陷:msg.data.burycards = cards(请求包原样回显),而 PushCards.burycards 走 get_burycard() +// 按主牌花色排序 —— 同一副埋牌底牌两条路径两种顺序(SSOT)。 +const bu = (() => { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup("00000", pj); + mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); + mod.xuanzhu(pack(0, { flower: 1 })); + // 刻意打乱提交顺序:手牌是从大到小的,取 8 张后【反转】成从小到大提交 + const raw = P.get_seat_cards(pj, 0).slice(0, 8).reverse(); + mod.maipai(pack(0, { cards: raw })); + const mp = c.sent.filter(m => m.rpc === 'maipai').find(m => m.data.burycards !== undefined); + return { pj, c, raw, mp }; +})(); +t.eq('埋牌底牌 maipai == 重连 PushCards 同序(SSOT)', + JS(bu.mp.data.burycards), JS(E.get_deskinfo(bu.c.o_room, 0).PushCards.burycards)); +t.eq('埋牌底牌 maipai 不回显客户端提交的原序', JS(bu.mp.data.burycards) === JS(bu.raw), false); +t.eq('埋牌底牌 maipai 与提交的是同一批牌(只是顺序不同)', + JS(bu.mp.data.burycards.concat().sort((x, y) => x - y)), JS(bu.raw.concat().sort((x, y) => x - y))); + +// ---- ⑤ chupai1/2/3.playproc:结构与取值必须与同一时刻的 PushCards.playproc 完全一致 ---- +// 没有它,前端在非重连时拿不到「本轮谁先手/谁最大/三家各出了什么/第几轮」,只能自己累积推导。 +// 用真实牌局打满 28 轮,每出一手就拿三家的下发包与三家的重连快照逐一比对。 +const pp = (() => { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup("00000", pj); + mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); + mod.xuanzhu(pack(0, { flower: 1 })); + mod.maipai(pack(0, { cards: P.get_seat_cards(pj, 0).slice(-8) })); + // 深拷贝守卫需要【活引用】:setup 的 SendPack 会 JSON 深拷贝每个包,用 c.sent 验不出活引用问题。 + // 故在其之外再挂一层,原样留住服务端交出来的那个 playproc 对象。 + const live = []; + const innerSend = mod.app.SendPack; + mod.app.SendPack = m => { if (/^chupai[123]$/.test(m.rpc)) live.push(m.data.playproc); innerSend(m); }; + let cmp = 0, mism = 0, missing = 0, rounds = 0, guard = 0, jiesuanHasPlayproc = null, firstSnap = null; + while (pj.step === 5 && ++guard < 400) { + const seat = pj.playproc.currseat, hand = P.get_seat_cards(pj, seat); + let pick = null; + if (seat === pj.playproc.start) pick = [hand[hand.length - 1]]; + else for (const x of hand) if (AR.can_followcard(pj.flower, hand, [x], pj.playproc.startcount, pj.playproc.startflower, pj.playproc.starttype).result) { pick = [x]; break; } + if (!pick) break; + c.sent.length = 0; + mod.chupai(pack(seat, { cards: pick })); + const cps = c.sent.filter(m => /^chupai[123]$/.test(m.rpc)); + const js = c.sent.filter(m => m.rpc === 'jiesuan'); + if (js.length) jiesuanHasPlayproc = js.some(m => m.data.playproc !== undefined); + if (!cps.length) continue; + rounds++; + if (cps.some(m => m.data.playproc === undefined)) missing++; + if (firstSnap === null) firstSnap = JS(live[0]); // 第 1 包发出【当时】的样子 + if (pj.step !== 5) continue; // 已进结算,无"当前进行态"可比 + for (let s = 0; s < 3; s++) { + const rec = E.get_deskinfo(c.o_room, s).PushCards.playproc; + cmp++; + if (JS(cps[s].data.playproc) !== JS(rec)) mism++; + } + } + mod.app.SendPack = innerSend; + return { pj, cmp, mism, missing, rounds, jiesuanHasPlayproc, firstSnap, firstRef: live[0] }; +})(); +t.eq('本轮进行态 28轮打完(用例真的走到底)', pp.pj.playproc.round, 28); +t.eq('本轮进行态 chupai1/2/3 恒带 playproc', [pp.rounds > 80, pp.missing], [true, 0]); +t.eq('本轮进行态 增量下发 == 同时刻重连快照(逐包逐座位)', [pp.cmp > 200, pp.mism], [true, 0]); +t.eq('本轮进行态 chupai3 转 jiesuan 时不带 playproc', pp.jiesuanHasPlayproc, false); +// 深拷贝守卫:playproc 是全程复用的活对象(new_playround 就地重置同一个对象)。 +// 若下发挂的是活引用,打完整局后"第一包里的第 1 轮进行态"会变成最后一轮的样子。 +t.eq('本轮进行态 下发的是深拷贝快照(已发出的包不被后续出牌回改)', JS(pp.firstRef), pp.firstSnap); +t.eq('本轮进行态 第1包就是第1轮、cards 定长3', [pp.firstRef.round, pp.firstRef.cards.length], [1, 3]); + +// ===================== §5.4 甩牌:chupai1 需下发分量构成 shuai(cardtype 表达不了甩牌)===================== +// 主花色=♦(1)。庄家(0) 甩「主♦K对 + 主♦5单张」;两闲家手上一张主牌都没有 → 甩牌合法 +function mkShuai(extraToSeat1) { + const pj = { + idx: 1, step: 5, banker: 0, call: 65, flower: 1, callproc: [], + seatlist: [[[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]]], + playproc: {}, playhistory: [], cards: make108() + }; + for (let i = 0; i < 108; i++) pj.cards[i].dealowner = -1; // 先全部清空 + const give = (cid, s) => { pj.cards[cid].dealowner = s + 1; pj.cards[cid].playround = -1; }; + give(id(1, 1, 13), 0); give(id(2, 1, 13), 0); give(id(1, 1, 5), 0); // 庄家:主♦K对 + 主♦5 + give(id(1, 3, 9), 1); give(id(1, 3, 8), 1); give(id(2, 3, 9), 1); // 闲1:全副♥,无主牌 + give(id(1, 2, 9), 2); give(id(1, 2, 8), 2); give(id(2, 2, 9), 2); // 闲2:全副♣,无主牌 + if (extraToSeat1) give(extraToSeat1, 1); // 需要造甩错时给闲1一张大主牌 + pj.method = { + check_cards_valid: c => P.check_cards_valid(pj, c), + check_cards_inhand: (c, s) => P.check_cards_inhand(pj, c, s), + do_playcard: c => P.do_playcard(pj, c), + get_seat_cards: s => P.get_seat_cards(pj, s), + have_baofu: () => (pj.seatlist[0][4][0] == 0 || pj.seatlist[1][4][0] == 0 || pj.seatlist[2][4][0] == 0), + get_curmultiple: () => P.get_curmultiple(pj), + get_mustcard: s => P.get_mustcard(pj, s) + }; + P.new_playround(pj, 1, 0); + return pj; +} +// 合法甩牌 → 带 shuai 分量 +let sh = mkShuai(null); let shc = setup("00000", sh); +mod.chupai(pack(0, { cards: [id(1, 1, 13), id(2, 1, 13), id(1, 1, 5)] })); +let shPk = shc.sent.filter(m => m.rpc === 'chupai1')[0]; +t.eq('甩牌 合法 下发 chupai1', !!shPk, true); +t.eq('甩牌 打出3张', shPk && shPk.data.cards.length, 3); +t.eq('甩牌 shuai=1对+1单张', shPk && shPk.data.shuai, { tractors: [], pairs: 1, singles: 1 }); +t.eq('甩牌 无 shuaicuo', shPk && shPk.data.shuaicuo, undefined); +// 非甩牌(单一对子)→ 无 shuai +sh = mkShuai(null); shc = setup("00000", sh); +mod.chupai(pack(0, { cards: [id(1, 1, 13), id(2, 1, 13)] })); +shPk = shc.sent.filter(m => m.rpc === 'chupai1')[0]; +t.eq('非甩牌(单一对子) 无 shuai', shPk && shPk.data.shuai, undefined); +// 甩错(闲1 持大王,能压过主K对里的单张分量)→ 收回、只打最小一张、无 shuai +sh = mkShuai((2 - 1) * 54 + 53); shc = setup("00000", sh); // 第二副大王给闲1 +mod.chupai(pack(0, { cards: [id(1, 1, 13), id(2, 1, 13), id(1, 1, 5)] })); +shPk = shc.sent.filter(m => m.rpc === 'chupai1')[0]; +t.eq('甩错 shuaicuo=1', shPk && shPk.data.shuaicuo, 1); +t.eq('甩错 只打出最小一张(主♦5)', shPk && shPk.data.cards, [id(1, 1, 5)]); +t.eq('甩错 无 shuai 分量', shPk && shPk.data.shuai, undefined); + +// ===================== 重连 PushCards.pushlist:与 chupai 下发的 data.cards 同一份数据 ===================== +// 【契约】「这一手打出的牌」只有一个顺序口径:do_playcard 用 order_playcards 归一一次, +// 之后 chupai1/2/3 的 data.cards、playproc.cards、playhistory→PushCards.pushlist 全都读它。 +// 因此重连包里的每一手,必须与当时那个 chupai 包的 cards【逐元素完全相等】。 +// +// 旧实现在这里分岔:pushlist 是从 cards[i].playround/dealowner 反查后自己再排一次序, +// 与 chupai 回显的「客户端提交原序」是两个写入处——一手多张(甩牌/对子)时必然不同, +// 同编码的一对牌更是排序都区分不了。故本用例必须【一手多张 + 乱序提交】才抓得住。 +// +// 【乱序提交】asc() 把选好的一手反转成升序再发,模拟真实客户端的点击顺序; +// 服务端不得回显它,一律给权威降序。 +function ascSubmit(pj, cards) { return AR.order_cards(pj.flower, cards.concat()).reverse(); } + +function drivePushlist(roomtype, hands) { + const c = driveToPlay(roomtype); + const pj = c.pj; + const played = []; // [{round, seat, cards(下发的)}...] + for (let h = 0; h < hands; h++) { + if (pj.step !== 5) break; + const seat = pj.playproc.currseat; + const round = pj.playproc.round; + const hand = P.get_seat_cards(pj, seat); + let pick = null; + if (seat === pj.playproc.start) { + // 首家优先领一对(制造"一手多张";对子两张同编码,排序无法区分,正是分岔点) + const pairs = AR.get_pairlist(pj.flower, hand.concat()); + for (const pr of pairs) { + if (AR.can_playcard(pj.flower, pr.concat(), seat, pj.seatlist, + [0, 1, 2].filter(s => s !== seat).map(s => P.get_seat_zhucards(pj, s))).result) { + pick = pr.concat(); break; + } + } + if (!pick) { pick = [hand[hand.length - 1]]; } + } else { + const n = pj.playproc.startcount; + const get = AR.get_followcard(pj.flower, hand.concat(), n, pj.playproc.startflower, pj.playproc.starttype); + const base = (get && get.mustcard) ? get.mustcard.concat() : []; + if (base.length === n) { pick = base; } + else { + const pool = (get && get.cancard && get.cancard.length ? get.cancard : hand).filter(x => base.indexOf(x) < 0); + const need = n - base.length; + const combo = []; + (function walk(st) { + if (pick) return; + if (combo.length === need) { + const cand = base.concat(combo); + if (AR.can_followcard(pj.flower, hand, cand, n, pj.playproc.startflower, pj.playproc.starttype).result + && (!pj.playproc.shuai_demand || AR.flush_follow_ok(pj.flower, hand, cand, pj.playproc.shuai_demand))) { + pick = cand; + } + return; + } + for (let i = st; i < pool.length && !pick; i++) { combo.push(pool[i]); walk(i + 1); combo.pop(); } + })(0); + } + } + if (!pick) { break; } + const before = c.sent.length; + mod.chupai(pack(seat, { cards: ascSubmit(pj, pick) })); + const cp = c.sent.slice(before).filter(m => /^chupai[123]$/.test(m.rpc))[0]; + if (cp) { played.push({ round, seat, cards: cp.data.cards, submitted: ascSubmit(pj, pick) }); } + } + return { c, pj, played }; +} + +const plv = drivePushlist("00000", 12); +t.eq('pushlist 用例真的出过一手多张', plv.played.some(x => x.cards.length > 1), true); +const di = E.get_deskinfo(plv.c.o_room, 1).PushCards.pushlist; +t.eq('重连 pushlist 每轮恒3个座位', di.every(r => r.length === 3), true); +t.eq('重连 pushlist 不含 undefined', di.some(r => r.some(x => x === undefined)), false); +// 核心:逐手比对「重连归档」与「当时的 chupai 包」——两条路径同一份数据,必须逐元素相等 +let plMismatch = []; +for (const h of plv.played) { + const arc = di[h.round - 1] && di[h.round - 1][h.seat]; + if (JS(arc) !== JS(h.cards)) { plMismatch.push({ round: h.round, seat: h.seat, arc, pkt: h.cards }); } +} +t.eq('重连 pushlist 每一手都与当时的 chupai.cards 逐元素相等', plMismatch, []); +// 反面:服务端不得回显客户端的提交顺序(提交是升序,下发必须是权威降序) +const plMulti = plv.played.filter(x => x.cards.length > 1); +t.eq('chupai.cards 不回显客户端提交原序(一手多张时)', + plMulti.some(x => JS(x.cards) === JS(x.submitted) && JS(x.cards) !== JS(AR.order_cards(plv.pj.flower, x.cards.concat()))), false); +t.eq('chupai.cards 是本局主牌花色的权威降序', + plMulti.every(x => JS(x.cards) === JS(AR.order_cards(plv.pj.flower, x.cards.concat()))), true); +// 客户端提交的数组不被就地改写(can_playcard 是原地排序,do_playcard 必须先拷贝) +const inplace = driveToPlay("00000"); +const inHand = P.get_seat_cards(inplace.pj, inplace.pj.playproc.currseat); +const inArg = [inHand[inHand.length - 1], inHand[inHand.length - 2]]; +const inArgCopy = inArg.concat(); +mod.chupai(pack(inplace.pj.playproc.currseat, { cards: inArg })); +t.eq('chupai 不就地改写 pack.data.cards', inArg, inArgCopy); + +// §9 不查牌模式:出牌历史属于「查牌」,一律不下发;但当前轮桌面上的牌必须照常恢复 +const plv2 = drivePushlist("00001", 2); +const pc2 = E.get_deskinfo(plv2.c.o_room, 1).PushCards; +t.eq('重连 不查牌 无出牌历史 pushlist', pc2.pushlist, undefined); +t.eq('重连 不查牌 当前轮桌面牌仍恢复(playproc.cards)', + JS(pc2.playproc.cards[plv2.played[0].seat]), JS(plv2.played[0].cards)); +const pc1 = E.get_deskinfo(plv.c.o_room, 1).PushCards; +t.eq('重连 可查牌 有出牌历史 pushlist', Array.isArray(pc1.pushlist), true); + +// ===================== 重连 PushCards.baozhu:与 chupai 的 baozhu 同源同门控 ===================== +// baozhu 是「余主公示 + 明牌按钮」的开关(design §9)。它只在 chupai 包里给、重连包不给的话, +// 重连后按钮凭空消失——状态变更没有包承载(server 红线:发全下发面)。 +function mkBaozhuRoom(roomtype) { + const o_desk_stub = { paiju_list: [] }; + const pj = P.new(o_desk_stub, 0); + pj.banker = 0; pj.call = 65; pj.flower = 1; pj.step = 5; + P.new_playround(pj, 1, 0); + const lead = id(1, 3, 9); + pj.cards[lead].dealowner = 1; pj.cards[lead].playround = -1; + for (const cc of P.get_seat_zhucards(pj, 1)) { pj.cards[cc].playround = 1; } + pj.seatlist[1][4][0] = 0; // seat1 报无主 → have_baofu()=true + const c = setup(roomtype, pj); + mod.chupai(pack(0, { cards: [lead] })); + return c; +} +const bzOn = mkBaozhuRoom("00000"); +t.eq('重连 可查牌 PushCards 带 baozhu,且与 chupai 同值', + E.get_deskinfo(bzOn.o_room, 1).PushCards.baozhu, + bzOn.sent.filter(m => /^chupai[123]$/.test(m.rpc))[0].data.baozhu); +t.eq('重连 可查牌 已报无主 → baozhu=1', E.get_deskinfo(bzOn.o_room, 1).PushCards.baozhu, 1); +const bzOff = mkBaozhuRoom("00001"); +t.eq('重连 不查牌 baozhu 恒 0', E.get_deskinfo(bzOff.o_room, 1).PushCards.baozhu, 0); + +// ===================== §7.3 下发面 multiple 必须随「爬坡」开关取值(与结算 aset.multiple 同源)===================== +// 叫 45 分:常规算子基础子数 6(design §7.1「50 及以下统一 6」),爬坡 7(design §7.3.1) +const A = require('../class.arith.js'); +function driveUp45(roomtype) { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup(roomtype, pj); + mod.jiaofen(pack(0, { call: 45 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); + return { pj, c }; +} +t.eq('算子 常规房 get_base_bycall(45,false)=6', A.get_base_bycall(45, false), 6); +t.eq('算子 爬坡房 get_base_bycall(45,true)=7', A.get_base_bycall(45, true), 7); +const n45 = driveUp45("00000"); // 常规算子 +const p45 = driveUp45("00010"); // 位3=1 爬坡 +const szm = c => c.sent.filter(m => m.rpc === 'shangzhuang')[0].data.multiple; +t.eq('上庄包 常规房 multiple=6', szm(n45.c), 6); +t.eq('上庄包 爬坡房 multiple=7', szm(p45.c), 7); +// 重连各阶段同源:选主阶段(step2) ChooseMain.multiple +t.eq('重连 常规房 ChooseMain.multiple=6', E.get_deskinfo(n45.c.o_room, 0).ChooseMain.multiple, 6); +t.eq('重连 爬坡房 ChooseMain.multiple=7', E.get_deskinfo(p45.c.o_room, 0).ChooseMain.multiple, 7); +// 与结算包 aset.multiple 一致(同一 call 同一房间规则) +const aset45 = P.get_paiju_account( + { call: 45, banker: 0, flower: 1, cards: [], idx: 1, endtime: null, result: null, o_desk: { seatlist: [[0, [], 0, 0, 0], [0, [], 0, 0, 0], [0, [], 0, 0, 0]], o_room: { roomtype: "00010", asetcount: 6 }, method: { get_desk_account: m => m } } }, + 0, {}).data.aset.multiple; +t.eq('爬坡房 下发面 multiple 与结算 aset.multiple 一致', szm(p45.c), aset45); + +// ===================== §11 闲家出牌提示 tishi(只转发给对家,不校验)===================== +const mkTip = (step, banker) => ({ step, banker }); +// 闲家1 发"踩"(tip=1),banker=0 → 对家=2;默认 seatlist fromid==座位号 +let tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(1, { tip: 1 })); +t.eq('提示 合法 下发1包', tp.sent.length, 1); +t.eq('提示 只发给对家(fromid=2)', tp.sent[0] && tp.sent[0].fromid, 2); +t.eq('提示 rpc=tishi', tp.sent[0] && tp.sent[0].rpc, 'tishi'); +t.eq('提示 data.seat=发出者1', tp.sent[0] && tp.sent[0].data.seat, 1); +t.eq('提示 data.tip=1(踩)', tp.sent[0] && tp.sent[0].data.tip, 1); +// 闲家2 发"有分"(tip=3) → 对家=1 +tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(2, { tip: 3 })); +t.eq('提示 闲2发→对家1(fromid=1)', tp.sent[0] && tp.sent[0].fromid, 1); +// banker=2 时,闲家0 发"没分"(tip=2) → 对家=1 +tp = setup("00000", mkTip(5, 2)); mod.tishi(pack(0, { tip: 2 })); +t.eq('提示 banker=2·闲0发→对家1', tp.sent[0] && tp.sent[0].fromid, 1); +// 庄家发 → 拒(庄家无对家) +tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(0, { tip: 1 })); +t.eq('提示 庄家发 拒(回失败包RULE)', rej(tp, ERR.RULE), true); +// 非出牌阶段 → 拒 +tp = setup("00000", mkTip(3, 0)); mod.tishi(pack(1, { tip: 1 })); +t.eq('提示 非出牌阶段(step3) 拒(回失败包STEP)', rej(tp, ERR.STEP), true); +// 非法 tip / 缺 tip → 拒 +tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(1, { tip: 9 })); +t.eq('提示 非法tip=9 拒(回失败包PARAM)', rej(tp, ERR.PARAM), true); +tp = setup("00000", mkTip(5, 0)); mod.tishi(pack(1, {})); +t.eq('提示 缺tip 拒(回失败包PARAM)', rej(tp, ERR.PARAM), true); + +// ===================== §4.2 叫分:边界与非法入参补全 ===================== +// design §4.2:范围 5~70、步进 5;暂定庄家必须叫(不能"不叫"),其后才可以"叫更低"或"不叫" +t.eq('叫分 边界 下限5 接受', jf(0, 5), 5); +t.eq('叫分 正 非首家可以不叫(call=0) 接受', jf(65, 0), 0); +t.eq('叫分 反 负数 拒', jf(0, -5), null); +t.eq('叫分 反 缺 call 字段(NaN) 拒', jf(0, undefined), null); +t.eq('叫分 反 步进外的小值1 拒', jf(0, 1), null); +t.eq('叫分 反 与当前叫分相同(不更低) 拒', jf(55, 55), null); + +// ===================== §4.2 已"不叫"的玩家不得再叫 ===================== +// design §4.2:不叫即退出本局叫分,轮次直接跳过他;他再发 jiaofen 时已不是当前叫分位,应被拒。 +const bd = P.new({ paiju_list: [] }, 0); // 真实牌局,暂定庄家 = 0 号座位 +const bdc = setup("00000", bd); +mod.jiaofen(pack(0, { call: 65 })); +t.eq('叫分 正 首家叫65后轮到 seat1', bd.method.get_callgrade_seat(), 1); +mod.jiaofen(pack(1, { call: 0 })); // seat1 不叫 +t.eq('叫分 正 seat1不叫后跳到 seat2', bd.method.get_callgrade_seat(), 2); +const bdLen = bd.callproc.length; +bdc.sent.length = 0; +mod.jiaofen(pack(1, { call: 60 })); // seat1 反悔想再叫 +t.eq('叫分 反 已不叫者再叫 → 拒(SEAT)', rej(bdc, ERR.SEAT), true); +t.eq('叫分 反 已不叫者再叫 不改变叫分过程', bd.callproc.length, bdLen); +t.eq('叫分 反 已不叫者再叫 仍轮到 seat2', bd.method.get_callgrade_seat(), 2); +t.eq('叫分 反 已不叫者再叫 未产生庄家', bd.banker, -1); + +// ===================== §4.5 埋牌 maipai:庄家 / 阶段 / 张数 ===================== +// design §4.5:仅"选主/打牌"路径、由庄家从 36 张里选出 **8 张** 重新扣下 +// 桩要"完整到能走完成功路径":否则一旦校验被改松,这里会**抛异常打断整个文件**, +// 后面的断言全不执行——看到的是崩溃而不是某条断言转红,定位与信号都变差。 +const mkBury = (step, banker) => ({ + step, banker, cards: make108(), playproc: { currseat: banker }, playhistory: [], + method: { + check_cards_valid: c => P.check_cards_valid({ cards: make108() }, c), + check_cards_inhand: () => true, + do_burycard: () => { }, + get_seat_cards: () => [], + get_liangpai: () => null + } +}); +let bc = setup("00000", mkBury(3, 1)); mod.maipai(pack(0, { cards: [0, 1, 2, 3, 4, 5, 6, 7] })); +t.eq('埋牌 反 非庄家 → 拒(SEAT)', rej(bc, ERR.SEAT), true); +bc = setup("00000", mkBury(2, 0)); mod.maipai(pack(0, { cards: [0, 1, 2, 3, 4, 5, 6, 7] })); +t.eq('埋牌 反 选主阶段(step2)就埋 → 拒(STEP)', rej(bc, ERR.STEP), true); +bc = setup("00000", mkBury(5, 0)); mod.maipai(pack(0, { cards: [0, 1, 2, 3, 4, 5, 6, 7] })); +t.eq('埋牌 反 出牌阶段(step5)再埋 → 拒(STEP)', rej(bc, ERR.STEP), true); +bc = setup("00000", mkBury(3, 0)); mod.maipai(pack(0, { cards: [0, 1, 2, 3, 4, 5, 6] })); +t.eq('埋牌 边界 只埋7张 → 拒(PARAM)', rej(bc, ERR.PARAM), true); +bc = setup("00000", mkBury(3, 0)); mod.maipai(pack(0, { cards: [0, 1, 2, 3, 4, 5, 6, 7, 8] })); +t.eq('埋牌 边界 埋9张 → 拒(PARAM)', rej(bc, ERR.PARAM), true); +bc = setup("00000", mkBury(3, 0)); mod.maipai(pack(0, { cards: [] })); +t.eq('埋牌 反 空数组 → 拒(PARAM)', rej(bc, ERR.PARAM), true); + +// ===================== §11 无超时托管:对局阶段不得注册任何定时器 ===================== +// design §11 已确认「倒计时只作展示,服务端不做任何超时动作」。这不是缺口,而是规则—— +// 所以这里放的是**守卫用例**:若将来有人给服务端加「超时自动出牌 / AI 代打」,必然要注册 +// 定时器来推进对局,这条就会转红,从而把违反 §11 的改动拦在提交前。 +// (min_ontimeout 在**结算**阶段另有正当用途:延迟扣房卡与存战绩,属平台事务、不推进对局, +// 故本守卫只覆盖到出牌阶段为止。) +const realTO = global.min_ontimeout; +let timerCount = 0; +global.min_ontimeout = function () { timerCount++; }; +const gd = driveToPlay("00000"); +mod.chupai(pack(0, { cards: [P.get_seat_cards(gd.pj, 0)[0]] })); +global.min_ontimeout = realTO; +t.eq('§11 叫分→选主→埋牌→出牌 全程未注册任何定时器', timerCount, 0); + +// ===================== I-3 阶段 step 与控制权 必须由服务端在包里显式给出 ===================== +// server 红线:「阶段、控制权(轮到谁)… 必须由服务端唯一维护并在相关下发包里显式给出; +// 任何状态变更都要有包承载」。曾经的缺陷:增量推送里【一个 step 都没有】,选主/埋牌阶段 +// 也没有「轮到谁」,前端只能按 rpc 名硬编码 step、并自己套「选主/埋牌 = 庄家」这条规则—— +// 那就是前端持有了一小段对局状态机(服务端将来插一个中间阶段,前端不会红、只会静默画错)。 +// +// 本组逐包钉死两件事:①每个包的 step == 同时刻 paiju.step(不是常量,是权威值); +// ②每个包的「轮到谁」== 服务端各 RPC 校验所用的同一个权威来源。 +// 反向验证:删掉 mod.js/class.desk.js 里任意一处 msg.data.step / nextseat,本组必转红。 +const sp = (() => { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup("00000", pj); + const grab = rpc => c.sent.filter(m => m.rpc === rpc); + const out = {}; + mod.jiaofen(pack(0, { call: 65 })); + out.jiaofen1 = grab('jiaofen')[0]; out.after_jf1 = { step: pj.step, next: pj.method.get_callgrade_seat() }; + mod.jiaofen(pack(1, { call: 0 })); + out.jiaofen2 = grab('jiaofen')[1]; out.after_jf2 = { step: pj.step, next: pj.method.get_callgrade_seat() }; + out.deskinfo1 = E.get_deskinfo(c.o_room, 1); + mod.jiaofen(pack(2, { call: 0 })); // → 0 号位上庄 + out.shangzhuang = grab('shangzhuang')[0]; out.after_sz = { step: pj.step, banker: pj.banker }; + out.deskinfo2 = E.get_deskinfo(c.o_room, 1); // 闲家视角(控制权与视角无关) + mod.xuanzhu(pack(0, { flower: 1 })); + out.xuanzhu = grab('xuanzhu')[0]; out.after_xz = { step: pj.step, banker: pj.banker }; + out.deskinfo3 = E.get_deskinfo(c.o_room, 2); + mod.maipai(pack(0, { cards: P.get_seat_cards(pj, 0).slice(-8) })); + out.maipai = grab('maipai')[0]; out.after_mp = { step: pj.step, curr: pj.playproc.currseat }; + out.deskinfo5 = E.get_deskinfo(c.o_room, 1); + return { pj, c, out }; +})(); +const O = sp.out; +// ① 阶段 step:每个包都有,且等于同时刻牌局的真实阶段 +t.eq('阶段 jiaofen 带 step,且 == 当时的 paiju.step(仍在叫分=1)', [O.jiaofen1.data.step, O.after_jf1.step], [1, 1]); +t.eq('阶段 jiaofen(第二包) step 仍为 1', O.jiaofen2.data.step, O.after_jf2.step); +t.eq('阶段 shangzhuang 带 step,且 == paiju.step(选主/投降=2)', [O.shangzhuang.data.step, O.after_sz.step], [2, 2]); +t.eq('阶段 xuanzhu 带 step,且 == paiju.step(埋牌=3)', [O.xuanzhu.data.step, O.after_xz.step], [3, 3]); +t.eq('阶段 maipai 带 step,且 == paiju.step(出牌=5)', [O.maipai.data.step, O.after_mp.step], [5, 5]); +// 与重连包 deskinfo.step 同值:增量与全量两条路径必须给同一个阶段 +t.eq('阶段 jiaofen.step == 同时刻 deskinfo.step', O.jiaofen2.data.step, O.deskinfo1.step); +t.eq('阶段 shangzhuang.step == 同时刻 deskinfo.step', O.shangzhuang.data.step, O.deskinfo2.step); +t.eq('阶段 xuanzhu.step == 同时刻 deskinfo.step', O.xuanzhu.data.step, O.deskinfo3.step); +t.eq('阶段 maipai.step == 同时刻 deskinfo.step', O.maipai.data.step, O.deskinfo5.step); +// ② 控制权:每个包的「轮到谁」== 服务端自己校验时用的权威来源 +t.eq('控制权 jiaofen.nextseat == get_callgrade_seat()(下一个叫分者)', O.jiaofen1.data.nextseat, O.after_jf1.next); +t.eq('控制权 jiaofen 不叫后跳过该家', O.jiaofen2.data.nextseat, O.after_jf2.next); +t.eq('控制权 shangzhuang.nextseat == banker(选主/投降只由庄家做)', O.shangzhuang.data.nextseat, O.after_sz.banker); +t.eq('控制权 xuanzhu.nextseat == banker(埋牌只由庄家做)', O.xuanzhu.data.nextseat, O.after_xz.banker); +t.eq('控制权 maipai.seat == playproc.currseat(即将首出者)', O.maipai.data.seat, O.after_mp.curr); +// 反面:shangzhuang 的 data.seat 是【最后一个叫分者】,不是控制权——两者必须能区分开, +// 否则"直接读 seat 当控制权"的写法也能蒙混过关(本局:最后叫分者=2、庄家=0) +t.eq('控制权 shangzhuang.seat(最后叫分者) 与 nextseat(庄家) 确实不同 → 用例非空转', + O.shangzhuang.data.seat !== O.shangzhuang.data.nextseat, true); +// 重连包:ChooseMain / BuryCards 也必须显式给控制权,且与增量推送同值 +t.eq('控制权 重连 ChooseMain.seat == shangzhuang.nextseat', O.deskinfo2.ChooseMain.seat, O.shangzhuang.data.nextseat); +t.eq('控制权 重连 BuryCards.seat == xuanzhu.nextseat', O.deskinfo3.BuryCards.seat, O.xuanzhu.data.nextseat); +t.eq('控制权 重连 PushCards 由 playproc.currseat 给出 == maipai.seat', + O.deskinfo5.PushCards.playproc.currseat, O.maipai.data.seat); + +// ===================== I-2 结算阶段(step6)的重连包必须与 jiesuan 推送给同一份结算数据 ===================== +// 曾经的缺陷:deskinfo 的 Balance 只给 readystate + aset,而 jiesuan 推送给的是 +// chupai + bottom + aset(+ 末局 account)——结算面板还开着时断线重连/硬刷新, +// 抠底明细与末局大结算全部空白(同一份数据两条路径给的不一样)。 +function driveToSettle(roomtype, hookDesk) { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup(roomtype, pj); + if (hookDesk) { hookDesk(c.o_desk, pj); } + mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); + mod.xuanzhu(pack(0, { flower: 1 })); + mod.maipai(pack(0, { cards: P.get_seat_cards(pj, 0).slice(-8) })); + let guard = 0; + while (pj.step === 5 && ++guard < 400) { + const seat = pj.playproc.currseat, hand = P.get_seat_cards(pj, seat); + let pick = null; + if (seat === pj.playproc.start) { pick = [hand[hand.length - 1]]; } + else { for (const x of hand) { if (AR.can_followcard(pj.flower, hand, [x], pj.playproc.startcount, pj.playproc.startflower, pj.playproc.starttype).result) { pick = [x]; break; } } } + if (!pick) { break; } + mod.chupai(pack(seat, { cards: pick })); + } + return { pj, c, jiesuan: c.sent.filter(m => m.rpc === 'jiesuan')[0] }; +} +const sv = driveToSettle("00000"); +t.eq('结算 用例真的打到 step6', sv.pj.step, 6); +t.eq('结算 jiesuan 带 step=6(阶段同样由包承载)', sv.jiesuan.data.step, 6); +const bal = [0, 1, 2].map(s => E.get_deskinfo(sv.c.o_room, s).Balance); +t.eq('结算 重连 Balance.aset == jiesuan.aset(三家)', + bal.map(b => JS(b.aset)), [0, 1, 2].map(() => JS(sv.jiesuan.data.aset))); +t.eq('结算 重连 Balance.bottom == jiesuan.bottom(抠底明细,三家)', + bal.map(b => JS(b.bottom)), [0, 1, 2].map(() => JS(sv.jiesuan.data.bottom))); +// 用例非空转:抠底分组必须真的有内容(cards 恒 8 张),否则上面两边"同为 undefined"也会绿 +t.eq('结算 抠底分组真的有内容(8张埋牌底牌) → 用例非空转', bal[0].bottom.cards.length, 8); +t.eq('结算 重连 deskinfo.step == 6', E.get_deskinfo(sv.c.o_room, 0).step, 6); +// 已按下准备的座位不再重发结算数据(原有门控不变) +sv.c.o_desk.prepare[1] = 1; +const balReady = E.get_deskinfo(sv.c.o_room, 1).Balance; +t.eq('结算 已准备的座位不带 aset/bottom/account(门控不变)', + [balReady.aset, balReady.bottom, balReady.account], [undefined, undefined, undefined]); +// 末局:account 也必须能从重连包拿到。用桩 get_desk_account 造出 account(真实实现要写战绩, +// 与本用例无关),验证的是「get_paiju_account 冻结了它 + get_deskinfo 原样给出」这条链路 +const svLast = driveToSettle("00000", (o_desk) => { + o_desk.method.get_desk_account = m => { m.data.account = [{ score: 1 }, { score: 2 }, { score: 3 }]; return m; }; +}); +svLast.pj.idx = 99; // 末局判定:idx >= asetcount(6) +// idx 改在结算之前才有效,故这里重新走一遍结算路径:直接再调一次 get_paiju_account +const lastMsg = P.get_paiju_account(svLast.pj, 0, { data: {} }); +t.eq('结算 末局 jiesuan 带 account', Array.isArray(lastMsg.data.account), true); +t.eq('结算 末局 重连 Balance.account == jiesuan.account', + JS(E.get_deskinfo(svLast.c.o_room, 0).Balance.account), JS(lastMsg.data.account)); +// 反面:非末局不带 account(与 jiesuan 推送的取舍一致,不凭空造分组) +t.eq('结算 非末局 jiesuan 无 account', sv.jiesuan.data.account, undefined); +t.eq('结算 非末局 重连 Balance 无 account', bal[0].account, undefined); + +// ===================== M-2 seatlist 必须以深拷贝下发(不把活对象挂进下发包)===================== +// paiju.seatlist 是一张全程就地改写的活表(do_playcard 每手都在改缺门/无对/报副)。 +// 同一份数组曾同时挂在 maipai 的三个逐座位包、chupai1/2/3 与重连包 PushCards 上—— +// 平台真实链路立刻序列化看不出来,但这是隐患,与 playproc / pushlist / bottomcards 同一条标准。 +const EMPTY_TABLE = JS([0, 1, 2].map(() => [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]])); +const sl = (() => { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup("00000", pj); + mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 })); + mod.xuanzhu(pack(0, { flower: 1 })); + // 深拷贝守卫需要【活引用】:setup 的 SendPack 会 JSON 深拷贝每个包,用 c.sent 验不出问题 + const inner = mod.app.SendPack; + let mpLive = null, cpLive = null; + mod.app.SendPack = m => { + if (m.rpc === 'maipai' && mpLive === null) { mpLive = m.data.seatlist; } + if (/^chupai[123]$/.test(m.rpc) && cpLive === null) { cpLive = m.data.seatlist; } + inner(m); + }; + mod.maipai(pack(0, { cards: P.get_seat_cards(pj, 0).slice(-8) })); + // 重连快照也取一份活引用(此刻还是初始空表) + const diLive = E.get_deskinfo(c.o_room, 1).PushCards.seatlist; + let guard = 0; + while (pj.step === 5 && ++guard < 400) { + const seat = pj.playproc.currseat, hand = P.get_seat_cards(pj, seat); + let pick = null; + if (seat === pj.playproc.start) { pick = [hand[hand.length - 1]]; } + else { for (const x of hand) { if (AR.can_followcard(pj.flower, hand, [x], pj.playproc.startcount, pj.playproc.startflower, pj.playproc.starttype).result) { pick = [x]; break; } } } + if (!pick) { break; } + mod.chupai(pack(seat, { cards: pick })); + } + mod.app.SendPack = inner; + return { pj, c, mpLive, cpLive, diLive }; +})(); +// 用例非空转:整局打完后这张活表必须真的变过,否则"没被回改"是碰巧 +t.eq('座位牌况 整局打完后 paiju.seatlist 确实变过 → 用例非空转', JS(sl.pj.seatlist) !== EMPTY_TABLE, true); +t.eq('座位牌况 maipai 下发的是深拷贝(已发出的包不被后续出牌回改)', JS(sl.mpLive), EMPTY_TABLE); +t.eq('座位牌况 chupai1 下发的是深拷贝(打完整局后仍是发出当时的样子)', + JS(sl.cpLive) !== JS(sl.pj.seatlist), true); +t.eq('座位牌况 重连 PushCards 下发的是深拷贝', JS(sl.diLive), EMPTY_TABLE); +// 正面:深拷贝的内容必须与当时的活表逐格相等(拷贝不能拷错/拷浅) +const slNow = P.get_seatlist(sl.pj); +t.eq('座位牌况 get_seatlist 内容与活表逐格相等', JS(slNow), JS(sl.pj.seatlist)); +slNow[0][0][0] = 9; +t.eq('座位牌况 get_seatlist 是深拷贝(改副本不影响本体)', sl.pj.seatlist[0][0][0] !== 9, true); + +// ===================== Balance.readystate 必须以深拷贝下发(不把活数组挂进下发包)===================== +// o_desk.prepare 是一张全程就地改写的活数组(do_prepare 把某一格置 1、do_new_paiju 就地重建)。 +// 它此前被整根挂进 deskinfo.Balance.readystate,与 playproc / seatlist / pushlist 同级的隐患: +// 已发出的包会随后续准备操作被回改。标准与 get_playproc 一致——下发副本。 +const rs = driveToSettle("00000"); +const rsLive = E.get_deskinfo(rs.c.o_room, 0).Balance.readystate; +t.eq('准备状态 内容与活数组逐格相等', JS(rsLive), JS(rs.c.o_desk.prepare)); +t.eq('准备状态 发包时刻三家皆未准备 → 用例起点明确', JS(rsLive), JS([0, 0, 0])); +rs.c.o_desk.prepare[1] = 1; //模拟「包已发出之后,1 号位按了准备」(do_prepare 的就地写) +t.eq('准备状态 活数组确实变过 → 用例非空转', JS(rs.c.o_desk.prepare), JS([0, 1, 0])); +t.eq('准备状态 下发的是深拷贝(已发出的包不被后续准备操作回改)', JS(rsLive), JS([0, 0, 0])); +// 反面:改副本不得影响本体 +rsLive[2] = 9; +t.eq('准备状态 是深拷贝(改副本不影响本体)', rs.c.o_desk.prepare[2], 0); + +process.exit(t.done('rpc') ? 0 : 1); diff --git a/server/games/erqiwang/test/test_success.js b/server/games/erqiwang/test/test_success.js new file mode 100644 index 0000000..15e0645 --- /dev/null +++ b/server/games/erqiwang/test/test_success.js @@ -0,0 +1,135 @@ +// 成败标志 data.success(dev-guide server 03 §4/§5 硬红线): +// ① 每个下发包(主动推送与应答)都必须自带 data.success; +// ② 每条失败路径都必须回一个 success=false + errcode 的包给请求者,不得静默丢弃。 +require('./_shim'); +const { mod, setup, make108, id } = require('./_rpc.js'); +const P = require('../class.paiju.js'); +const D = require('../class.desk.js'); +const t = require('./_assert')(); +const ERR = mod.ERR; + +const clone = m => JSON.parse(JSON.stringify(m)); +const pack = (seat, extra) => ({ data: Object.assign({ agentid: 'a', playerid: 1, gameid: 'g', roomcode: 1, seat }, extra), conmode: 0, fromid: seat }); + +// ============ ① 成功包一律带 success=true ============ + +// -- 牌桌层主动推送:fapai / zhunbei 走全局 youle_erqiwang.app.SendPack -- +const gsent = []; +global.youle_erqiwang.app.SendPack = m => gsent.push(clone(m)); +const o_room = { + roomtype: '00000', asetcount: 6, + seatlist: [{ conmode: 0, fromid: 0 }, { conmode: 0, fromid: 1 }, { conmode: 0, fromid: 2 }], + method: { sendpack_toother: m => gsent.push(clone(m)) } +}; +const desk = D.new(o_room); +o_room.o_desk = desk; +D.do_new_paiju(desk, 0); +const fapai = gsent.filter(m => m.rpc === 'fapai'); +t.eq('fapai 下发3包(每座位1)', fapai.length, 3); +t.eq('fapai 带 success=true', fapai.every(m => m.data.success === true), true); + +gsent.length = 0; +desk.paiju_list.push({ banker: 0, result: 0, tmp_jiesuan_aset: {} }); +D.do_prepare(desk, 0); // 只有1家准备 → 广播 zhunbei +const zb = gsent.filter(m => m.rpc === 'zhunbei'); +t.eq('zhunbei 带 success=true', zb.length === 1 && zb[0].data.success === true, true); + +// -- RPC 层:真实驱动 叫分→上庄→选主→埋牌→出牌 -- +function driveToPlay() { + const pj = P.new({ paiju_list: [] }, 0); + const c = setup('00000', pj); + mod.jiaofen(pack(0, { call: 65 })); + mod.jiaofen(pack(1, { call: 0 })); + mod.jiaofen(pack(2, { call: 0 })); // 0 上庄 65 + mod.xuanzhu(pack(0, { flower: 1 })); + mod.maipai(pack(0, { cards: P.get_seat_cards(pj, 0).slice(0, 8) })); + return { pj, c }; +} +const e = driveToPlay(); +const rpcs = e.c.sent.map(m => m.rpc); +t.eq('端到端 覆盖 jiaofen/shangzhuang/xuanzhu/maipai', ['jiaofen', 'shangzhuang', 'xuanzhu', 'maipai'].every(r => rpcs.indexOf(r) >= 0), true); +t.eq('端到端 所有下发包均 success=true', e.c.sent.every(m => m.data.success === true), true); + +// 首出一张牌 → chupai1 也带 success +const lead = P.get_seat_cards(e.pj, 0)[0]; +e.c.sent.length = 0; +mod.chupai(pack(0, { cards: [lead] })); +const cp1 = e.c.sent.filter(m => m.rpc === 'chupai1'); +t.eq('chupai1 下发3包', cp1.length, 3); +t.eq('chupai1 带 success=true', cp1.every(m => m.data.success === true), true); + +// -- 结算包 jiesuan:正常/投降/解散 三种 type 都带 success -- +const mkDesk = () => ({ seatlist: [[0, [], 0, 0, 0], [0, [], 0, 0, 0], [0, [], 0, 0, 0]], o_room: { roomtype: '00000', asetcount: 6 }, method: { get_desk_account: m => m } }); +const mkPaiju = () => ({ call: 65, banker: 0, flower: 1, cards: [], idx: 2, o_desk: mkDesk(), endtime: null, result: null }); +t.eq('jiesuan 正常结算 success=true', P.get_paiju_account(mkPaiju(), 0, {}).data.success, true); +t.eq('jiesuan 投降结算 success=true', P.get_paiju_account(mkPaiju(), 1, {}).data.success, true); +t.eq('jiesuan 解散结算 success=true', P.get_paiju_account(mkPaiju(), 2, {}).data.success, true); + +// mingpai / tishi 成功包 +const mp = setup('00000', { step: 5, banker: 0, flower: 1, cards: make108(), method: { have_baofu: () => true } }); +mod.mingpai(pack(0)); +t.eq('mingpai 成功包 success=true', mp.sent.length === 1 && mp.sent[0].data.success === true, true); +const tp = setup('00000', { step: 5, banker: 0 }); +mod.tishi(pack(1, { tip: 1 })); +t.eq('tishi 成功包 success=true', tp.sent.length === 1 && tp.sent[0].data.success === true, true); + +// ============ ② 失败路径回包:success=false + errcode,且只回给请求者 ============ +// 每个 handler 取一条代表性失败路径;断言"恰好1包 + success=false + errcode + 回到请求者" +function failCase(name, o_paiju, call, seat, errcode) { + const c = setup('00000', o_paiju); + call(seat); + const ok = c.sent.length === 1 + && c.sent[0].data.success === false + && c.sent[0].data.errcode === errcode + && c.sent[0].fromid === seat; + t.eq(name, ok, true); +} +const stubJF = { step: 9, method: { get_callgrade_seat: () => 0, get_callgrade_value: () => 0, do_callgrade: () => { } } }; +failCase('jiaofen 阶段不对 → STEP', stubJF, s => mod.jiaofen(pack(s, { call: 65 })), 0, ERR.STEP); +failCase('jiaofen 非当前叫分位 → SEAT', + { step: 1, method: { get_callgrade_seat: () => 1, get_callgrade_value: () => 0, do_callgrade: () => { } } }, + s => mod.jiaofen(pack(s, { call: 65 })), 0, ERR.SEAT); +failCase('jiaofen 叫分>70 → PARAM', + { step: 1, method: { get_callgrade_seat: () => 0, get_callgrade_value: () => 0, do_callgrade: () => { } } }, + s => mod.jiaofen(pack(s, { call: 75 })), 0, ERR.PARAM); +failCase('jiaofen 后叫不更低 → RULE', + { step: 1, method: { get_callgrade_seat: () => 0, get_callgrade_value: () => 55, do_callgrade: () => { } } }, + s => mod.jiaofen(pack(s, { call: 60 })), 0, ERR.RULE); + +failCase('xuanzhu 非庄家 → SEAT', { step: 2, banker: 1 }, s => mod.xuanzhu(pack(s, { flower: 1 })), 0, ERR.SEAT); +failCase('xuanzhu 花色非法 → PARAM', { step: 2, banker: 0 }, s => mod.xuanzhu(pack(s, { flower: 9 })), 0, ERR.PARAM); + +const mkMP8 = () => ({ step: 3, banker: 0, cards: make108(), method: { check_cards_valid: c => P.check_cards_valid({ cards: make108() }, c), check_cards_inhand: () => true } }); +failCase('maipai 张数不是8 → PARAM', mkMP8(), s => mod.maipai(pack(s, { cards: [0, 1, 2] })), 0, ERR.PARAM); +failCase('maipai 入参非数组 → PARAM', mkMP8(), s => mod.maipai(pack(s, { cards: 'x' })), 0, ERR.PARAM); + +failCase('touxiang 非70分 → RULE', { step: 2, banker: 0, call: 65 }, s => mod.touxiang(pack(s)), 0, ERR.RULE); + +const mkCP = () => { + const pj = P.new({ paiju_list: [] }, 0); + pj.banker = 0; pj.call = 65; pj.flower = 1; pj.step = 5; + P.new_playround(pj, 1, 0); + return pj; +}; +failCase('chupai 重复牌id → PARAM', mkCP(), s => mod.chupai(pack(s, { cards: [0, 0] })), 0, ERR.PARAM); +failCase('chupai 牌不在手上 → PARAM', (function () { const pj = mkCP(); for (let i = 0; i < 108; i++) pj.cards[i].dealowner = 2; return pj; })(), + s => mod.chupai(pack(s, { cards: [0] })), 0, ERR.PARAM); +failCase('chupai 非当前出牌位 → SEAT', (function () { const pj = mkCP(); pj.playproc.currseat = 1; return pj; })(), + s => mod.chupai(pack(s, { cards: [0] })), 0, ERR.SEAT); + +failCase('mingpai 未报无主 → RULE', { step: 5, banker: 0, method: { have_baofu: () => false } }, s => mod.mingpai(pack(s)), 0, ERR.RULE); +failCase('tishi 庄家发 → RULE', { step: 5, banker: 0 }, s => mod.tishi(pack(s, { tip: 1 })), 0, ERR.RULE); +failCase('zhunbei 非结算阶段 → STEP', { step: 5 }, s => mod.zhunbei(pack(s)), 0, ERR.STEP); + +// 不查牌房的 mingpai 需要 roomtype '00001',单列 +const mpn = setup('00001', { step: 5, banker: 0, method: { have_baofu: () => true } }); +mod.mingpai(pack(0)); +t.eq('mingpai 不查牌 → 回 RULE 失败包', mpn.sent.length === 1 && mpn.sent[0].data.errcode === ERR.RULE, true); + +// check_player 不通过(房间/座位对不上)也要回包,不能让前端干等 +const cp = setup('00000', { step: 1 }); +mod.import.check_player = () => null; +mod.jiaofen(pack(0, { call: 65 })); +t.eq('check_player 失败 → 回 PLAYER 失败包', cp.sent.length === 1 && cp.sent[0].data.errcode === ERR.PLAYER, true); + +process.exit(t.done('success') ? 0 : 1); diff --git a/server/indexDb.js b/server/indexDb.js new file mode 100644 index 0000000..b9adc6a --- /dev/null +++ b/server/indexDb.js @@ -0,0 +1,199 @@ +(function(){ + var dbObject = {}; + dbObject.open = function(params){ + //数据库名 + dbObject.db_name = params.db_name; + //数据库版本 + dbObject.db_version = params.db_version; + //存储对象名 + dbObject.store_name = params.store_name; + //IndexedDB对象 + dbObject.db = null; + + if (!window.indexedDB){ + console.log("你的浏览器不支持IndexedDB,请更换浏览器"); + } + var request = indexedDB.open(dbObject.db_name, dbObject.db_version); + //打开数据库成功 + request.onsuccess = function(event){ + console.log("打开IndexedDB数据库" + dbObject.db_name + "成功"); + dbObject.db = event.target.result; + if (params.success_callback){ + params.success_callback(); + } + } + //打开数据库失败 + request.onerror = function(event) { + console.log("打开IndexedDB数据库" + dbObject.db_name + "失败"); + console.log(event); + if (params.error_callback){ + params.error_callback(); + } + } + //当打开的数据库版本高于当前版本时触发 + request.onupgradeneeded = function(event){ + console.log("IndexedDB数据库" + dbObject.db_name + "版本升级到" + dbObject.db_version); + dbObject.db = event.target.result; + if(!dbObject.db.objectStoreNames.contains(dbObject.store_name)){ + dbObject.db.createObjectStore(dbObject.store_name); + } + } + } + + //添加或修改 + dbObject.put = function(key, value){ + try{ + if (!dbObject.db){ + return; + } + + //此处必须显式声明事务 + var transaction = dbObject.db.transaction(dbObject.store_name, "readwrite"); + var store = transaction.objectStore(dbObject.store_name); + var request = store.put(value, key); + request.onsuccess = function(){ + // console.log("编辑IndexedDB记录成功:" + key); + } + request.onerror = function(event){ + console.log("编辑IndexedDB记录失败:" + key); + console.log(event); + } + } + catch(e){ + console.log(min_now() + "添加或修改IndexedDB失败"); + console.log(e); + console.log(key); + console.log(value); + } + } + + //删除 + dbObject.delete = function(key){ + try{ + if (!dbObject.db){ + return; + } + var transaction = dbObject.db.transaction(dbObject.store_name, "readwrite"); + var store = transaction.objectStore(dbObject.store_name); + var request = store.delete(key); + request.onsuccess = function(){ + // console.log("删除IndexedDB记录成功:" + key); + } + request.onerror = function(event){ + console.log("删除IndexedDB记录失败:" + key); + console.log(event); + } + } + catch(e){ + console.log(min_now() + "删除IndexedDB失败"); + console.log(e); + console.log(key); + } + } + + //根据key获取value + dbObject.get = function(key, succ_callback){ + try{ + if (!dbObject.db){ + return; + } + var transaction = dbObject.db.transaction(dbObject.store_name, "readwrite"); + var store = transaction.objectStore(dbObject.store_name); + var request = store.get(key); + request.onsuccess = function(){ + succ_callback(request.result); + } + request.onerror = function(event){ + console.log("获取IndexedDB记录失败:" + key); + console.log(event); + } + } + catch(e){ + console.log(min_now() + "获取IndexedDB失败"); + console.log(e); + console.log(key); + } + } + + //获取所有的value + dbObject.get_allvalue = function(success_callback, error_callback){ + try{ + if (!dbObject.db){ + return; + } + var transaction = dbObject.db.transaction(dbObject.store_name, "readwrite"); + var store = transaction.objectStore(dbObject.store_name); + var request = store.getAll(); + request.onsuccess = function(){ + if (success_callback){ + success_callback(request.result); + } + } + request.onerror = function(event){ + console.log("获取IndexedDB记录失败:all"); + console.log(event); + if (error_callback){ + error_callback(); + } + } + } + catch(e){ + console.log(min_now() + "获取全部的IndexedDB失败"); + console.log(e); + } + } + + //游标遍历 + dbObject.get_cursor = function(success_callback, error_callback){ + try{ + if (!dbObject.db){ + return; + } + var transaction = dbObject.db.transaction(dbObject.store_name, "readwrite"); + var store = transaction.objectStore(dbObject.store_name); + var request = store.openCursor(); + + request.onsuccess = function(event){ + if (success_callback){ + success_callback(event.target.result); + } + } + request.onerror = function(event){ + console.log("获取IndexedDB记录失败:游标"); + console.log(event); + if (error_callback){ + error_callback(); + } + } + } + catch(e){ + console.log(min_now() + "获取IndexedDB游标失败"); + console.log(e); + } + } + + //清除整个对象存储(表) + dbObject.clear = function(){ + try{ + if (!dbObject.db){ + return; + } + var transaction = dbObject.db.transaction(dbObject.store_name, "readwrite"); + var store = transaction.objectStore(dbObject.store_name); + var request = store.clear(); + request.onsuccess = function(){ + console.log("清空IndexedDB对象存储成功:" + dbObject.store_name); + } + request.onerror = function(event){ + console.log("清空IndexedDB对象存储失败:" + dbObject.store_name); + console.log(event); + } + } + catch(e){ + console.log(min_now() + "清除IndexedDB失败"); + console.log(e); + } + } + + window.dbObject = dbObject; +})(); \ No newline at end of file diff --git a/server/lz-string.js b/server/lz-string.js new file mode 100644 index 0000000..1194e25 --- /dev/null +++ b/server/lz-string.js @@ -0,0 +1,506 @@ +// Copyright (c) 2013 Pieroxy +// This work is free. You can redistribute it and/or modify it +// under the terms of the WTFPL, Version 2 +// For more information see LICENSE.txt or http://www.wtfpl.net/ +// +// For more information, the home page: +// http://pieroxy.net/blog/pages/lz-string/testing.html +// +// LZ-based compression algorithm, version 1.4.4 +var LZString = (function() { + +// private property +var f = String.fromCharCode; +var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; +var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"; +var baseReverseDic = {}; + +function getBaseValue(alphabet, character) { + if (!baseReverseDic[alphabet]) { + baseReverseDic[alphabet] = {}; + for (var i=0 ; i>> 8; + buf[i*2+1] = current_value % 256; + } + return buf; + }, + + //decompress from uint8array (UCS-2 big endian format) + decompressFromUint8Array:function (compressed) { + if (compressed===null || compressed===undefined){ + return LZString.decompress(compressed); + } else { + var buf=new Array(compressed.length/2); // 2 bytes per character + for (var i=0, TotalLen=buf.length; i> 1; + } + } else { + value = 1; + for (i=0 ; i> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i=0 ; i> 1; + } + + + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + // Add wc to the dictionary. + context_dictionary[context_wc] = context_dictSize++; + context_w = String(context_c); + } + } + + // Output the code for w. + if (context_w !== "") { + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { + if (context_w.charCodeAt(0)<256) { + for (i=0 ; i> 1; + } + } else { + value = 1; + for (i=0 ; i> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i=0 ; i> 1; + } + + + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + } + + // Mark the end of the stream + value = 2; + for (i=0 ; i> 1; + } + + // Flush the last char + while (true) { + context_data_val = (context_data_val << 1); + if (context_data_position == bitsPerChar-1) { + context_data.push(getCharFromInt(context_data_val)); + break; + } + else context_data_position++; + } + return context_data.join(''); + }, + + decompress: function (compressed) { + if (compressed == null) return ""; + if (compressed == "") return null; + return LZString._decompress(compressed.length, 32768, function(index) { return compressed.charCodeAt(index); }); + }, + + _decompress: function (length, resetValue, getNextValue) { + var dictionary = [], + next, + enlargeIn = 4, + dictSize = 4, + numBits = 3, + entry = "", + result = [], + i, + w, + bits, resb, maxpower, power, + c, + data = {val:getNextValue(0), position:resetValue, index:1}; + + for (i = 0; i < 3; i += 1) { + dictionary[i] = i; + } + + bits = 0; + maxpower = Math.pow(2,2); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (next = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2,8); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 1: + bits = 0; + maxpower = Math.pow(2,16); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 2: + return ""; + } + dictionary[3] = c; + w = c; + result.push(c); + while (true) { + if (data.index > length) { + return ""; + } + + bits = 0; + maxpower = Math.pow(2,numBits); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (c = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2,8); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + dictionary[dictSize++] = f(bits); + c = dictSize-1; + enlargeIn--; + break; + case 1: + bits = 0; + maxpower = Math.pow(2,16); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + dictionary[dictSize++] = f(bits); + c = dictSize-1; + enlargeIn--; + break; + case 2: + return result.join(''); + } + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + if (dictionary[c]) { + entry = dictionary[c]; + } else { + if (c === dictSize) { + entry = w + w.charAt(0); + } else { + return null; + } + } + result.push(entry); + + // Add w+entry[0] to the dictionary. + dictionary[dictSize++] = w + entry.charAt(0); + enlargeIn--; + + w = entry; + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + } + } +}; + return LZString; +})(); + +if (typeof define === 'function' && define.amd) { + define(function () { return LZString; }); +} else if( typeof module !== 'undefined' && module != null ) { + module.exports = LZString +} else if( typeof angular !== 'undefined' && angular != null ) { + angular.module('LZString', []) + .factory('LZString', function () { + return LZString; + }); +} diff --git a/server/md5.js b/server/md5.js new file mode 100644 index 0000000..7913db6 --- /dev/null +++ b/server/md5.js @@ -0,0 +1,399 @@ +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ + +//var http://api.smsbao.com/sms?u=13619576943&p=PASSWORD&m=13330103011&c=【苏苏拼】您的验证,码是23466. + + +var md5smm='http://api.smsbao.com/sms?u='; +var md5id='daoqi'; +var md5pass='xieyu2018'; +var md5apikey = '16fce7d89c89423280af90dd540c4bc6'; +var md5toipnone='13330103011'; +var md5memo='【道棋科技】您的验证码是23466.'; + +var sms=md5smm+md5id+'&p='; +sms=sms+str_md5(md5pass)+'&m='; + + +sms=sms+md5toipnone+'&c='; +sms=sms+md5memo; + +function mycallback(data) +{ + console.log(data); +}; + +function md5Geturl( md5smm, md5id, md5pass, md5toipnone,md5memo,time,callback){ + + if (arguments.length<1) + { +var md5smm='http://api.smsbao.com/sms?u='; +var md5id=13619576943; +var md5pass='xieyu2018'; +var md5toipnone='13330103011'; +var md5memo='【葫芦娃】您的验证码是9391,30秒内有效'; +var time=10; +var callback=mycallback; + }; + + + var sms=md5smm+md5id+'&p='; +sms=sms+str_md5(md5pass)+'&m='; + + +sms=sms+md5toipnone+'&c='; +sms=sms+md5memo; +if (!time) +{ +return sms; +}; +sms='http://www.baidu.com'; +timedGetText(sms,time,callback); + +return sms; +} + +function md5Geturl2( md5smm, md5id, md5pass, md5toipnone,md5memo,time,callback){ + + if (arguments.length<1) + { +var md5smm='u='; +var md5id=13619576943; +var md5pass='xieyu2018'; +var md5toipnone='13330103011'; +var md5memo='【葫芦娃】您的验证码是9391,30秒内有效'; +//var time=10; +//var callback=mycallback; + }; + + +var sms=md5smm+md5id+'&p='; +sms=sms+hex_md5(md5pass)+'&m='; + + +sms=sms+md5toipnone+'&c='; +sms=sms+md5memo; +if (!time) +{ +return sms; +}; +sms='http://www.baidu.com'; +timedGetText(sms,time,callback); + +return sms; +} + + +function md5Geturl3(md5id, md5pass, md5toipnone, md5memo){ + if (arguments.length < 1) { + var md5smm = 'u='; + var md5id = 13619576943; + var md5pass ='xieyu2018'; + var md5toipnone = '13576974264'; + var md5memo = '【道棋】您的验证码是9391,30秒内有效'; + }; + + var sms='u='+md5id+'&p='; + sms=sms+hex_md5(md5pass)+'&m='; + + sms=sms+md5toipnone+'&c='; + sms=sms+md5memo; + + return sms; +} +function md5Geturl4(md5toipnone, md5memo){ + // if (arguments.length < 1) { + // var md5smm = 'u='; + // var apikey =md5apikey; + // var md5toipnone = '13576974264'; + // var md5memo = '【道棋科技】您的验证码是9391,30秒内有效'; + // }; + + var sms='u='+md5id+'&p='; + sms=sms+md5apikey+'&m='; + + sms=sms+md5toipnone+'&c='; + sms=sms+md5memo; + + return sms; +} + +function timedGetText( url, time, callback ){ + var request = new XMLHttpRequest(); + var timeout = false; + var timer = setTimeout( function(){ + timeout = true; + request.abort(); + }, time ); + request.open( "GET", url ); + request.onreadystatechange = function(){ + if( request.readyState !== 4 ) return; + if( timeout ) return; + clearTimeout( timer ); + if( request.status === 200 ){ + callback( request.responseText ); + } + } + request.send( null ); +} + + + + + + + +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ +var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));} +function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));} +function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));} +function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } +function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } +function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } + +/* + * Perform a simple self-test to see if the VM is working + */ +function md5_vm_test() +{ + return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; +} + +/* + * Calculate the MD5 of an array of little-endian words, and a bit length + */ +function core_md5(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << ((len) % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + + a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); + d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); + d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); + d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i+10], 17, -42063); + b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); + d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); + d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); + c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); + d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); + c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); + d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); + c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); + d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); + c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); + d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); + d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); + d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); + d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); + d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); + d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); + d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); + d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return Array(a, b, c, d); + +} + +/* + * These functions implement the four basic operations the algorithm uses. + */ +function md5_cmn(q, a, b, x, s, t) +{ + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); +} +function md5_ff(a, b, c, d, x, s, t) +{ + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); +} +function md5_gg(a, b, c, d, x, s, t) +{ + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); +} +function md5_hh(a, b, c, d, x, s, t) +{ + return md5_cmn(b ^ c ^ d, a, b, x, s, t); +} +function md5_ii(a, b, c, d, x, s, t) +{ + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); +} + +/* + * Calculate the HMAC-MD5, of a key and some data + */ +function core_hmac_md5(key, data) +{ + var bkey = str2binl(key); + if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); + return core_md5(opad.concat(hash), 512 + 128); +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +/* + * Convert a string to an array of little-endian words + * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. + */ +function str2binl(str) +{ + var bin = Array(); + var mask = (1 << chrsz) - 1; + for(var i = 0; i < str.length * chrsz; i += chrsz) + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); + return bin; +} + +/* + * Convert an array of little-endian words to a string + */ +function binl2str(bin) +{ + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); + return str; +} + +/* + * Convert an array of little-endian words to a hex string. + */ +function binl2hex(binarray) +{ + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); + } + return str; +} + +/* + * Convert an array of little-endian words to a base-64 string + */ +function binl2b64(binarray) +{ + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i += 3) + { + var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) + | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) + | ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; + else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); + } + } + return str; +} diff --git a/server/minhttp.js b/server/minhttp.js new file mode 100644 index 0000000..da0e1bd --- /dev/null +++ b/server/minhttp.js @@ -0,0 +1,822 @@ +(function(wnd, undef){ + + //复制json对象 + function min_copyjson(json) + { + return JSON.parse(JSON.stringify(json)); + } + + function min_clone(myObj){ + if(!myObj) + return myObj; + if(typeof(myObj) != 'object') + return myObj; + + var myNewObj = new Object(); + + for(var i in myObj) + myNewObj[i] = min_clone(myObj[i]); + + return myNewObj; + } + + //json对象转字符串 + function min_jsontostr(json) { + return JSON.stringify(json); + } + + //字符串转json对象 + function min_strtojson(str) { + return JSON.parse(str); + } + + //字符串转整型 def:转换失败时返回的默认值 + function min_strtoint(str, def) { + var i = parseInt(str); + if (i == 0) { + return 0; + }; + if (!i) { //=0也会进来 + if (!def) { + def = 0; + }; + i = def; + } + return i; + }; + + //整型转字符串 + function min_inttostr(i) { + return i.toString(); + }; + + //去左空格 + function min_ltrim(s){ + return s.replace(/(^\s*)/g, ""); + }; + + //去右空格; + function min_rtrim(s){ + return s.replace(/(\s*$)/g, ""); + }; + + //去左右空格; + function min_trim(s){ + return s.replace(/(^\s*)|(\s*$)/g, ""); + }; + + //整除 + function min_div(i, b) + { + if (!b) { + return parseInt(i); + } + return parseInt(i / b); + }; + + //取余数 + function min_mod(a, b){ + return a % b; + }; + + //取绝对值 + function min_abs(b) { + return Math.abs(b); + }; + + //取随机数(范围包含了min和max) + function min_random(min, max) { + var Range = max - min; + var Rand = Math.random(); + return (min + Math.round(Rand * Range)); + }; + //取随机数1 + function min_random1(num) { + return parseInt(Math.random()*num); + }; + + //随机字符串 + function min_randomChar(length){ + var x = "0123456789"; + var y = "qwertyuioplkjhgfdsazxcvbnm"; + var z = "QWERTYUIOPLKJHGFDSAZXCVBNM"; + var tmp = ""; + for (var i = 0; i < length; i++) { + switch(min_random(0, 2)) { + case 0: + tmp += x.charAt(Math.ceil(Math.random()*100000000)%x.length); + break; + case 1: + tmp += y.charAt(Math.ceil(Math.random()*100000000)%y.length); + break; + case 2: + tmp += z.charAt(Math.ceil(Math.random()*100000000)%z.length); + break; + default: + break; + } + } + // var timestamp = new Date().getTime(); + // return timestamp + tmp; + return tmp; + } + + //取长度 + function min_length(key) { + if (typeof(key) != "string") { + var key = key + ""; + } + return key.length; + } + + //字符全替换 ignoreCase:true忽略大小写 false不忽略大小写 + function min_replaceAll(str, str_old, str_new, ignoreCase) + { + if (!RegExp.prototype.isPrototypeOf(str_old)) { + return str.replace(new RegExp(str_old, (ignoreCase ? "gi": "g")), str_new); + } else { + return str.replace(str_old, str_new); + } + } + + //取本地当前时间,格式yyyy-MM-dd HH:MM:SS + function min_now(timestamp) + { + var date; + if (timestamp){ + date = new Date(timestamp); + } else { + date = new Date(); + } + var seperator1 = "-"; + var seperator2 = ":"; + var month = date.getMonth() + 1; + var strDate = date.getDate(); + if (month >= 1 && month <= 9) { + month = "0" + month; + } + if (strDate >= 0 && strDate <= 9) { + strDate = "0" + strDate; + } + var currentdate = date.getFullYear() + seperator1 + month + seperator1 + strDate + + " " + date.getHours() + seperator2 + date.getMinutes() + + seperator2 + date.getSeconds(); + return currentdate; + } + + //时间戳转时间,格式yyyy-MM-dd HH:MM:SS + function min_timestampToTime(timestamp){ + return min_now(timestamp); + } + + //取本地当前日期,格式yyyy-MM-dd + function min_day() + { + var date = new Date(); + var seperator1 = "-"; + var month = date.getMonth() + 1; + var strDate = date.getDate(); + if (month >= 1 && month <= 9) { + month = "0" + month; + } + if (strDate >= 0 && strDate <= 9) { + strDate = "0" + strDate; + } + var currentdate = date.getFullYear() + seperator1 + month + seperator1 + strDate; + return currentdate; + } + + //获取时间差 + function min_datediff(startTime, endTime, diffType) { + //将xxxx-xx-xx的时间格式,转换为 xxxx/xx/xx的格式 + startTime = startTime.replace(/\-/g, "/"); + endTime = endTime.replace(/\-/g, "/"); + + //将计算间隔类性字符转换为小写 + diffType = diffType.toLowerCase(); + var sTime = new Date(startTime); //开始时间 + var eTime = new Date(endTime); //结束时间 + //作为除数的数字 + var divNum = 1; + switch (diffType) { + case "second": + divNum = 1000; + break; + case "minute": + divNum = 1000 * 60; + break; + case "hour": + divNum = 1000 * 3600; + break; + case "day": + divNum = 1000 * 3600 * 24; + break; + default: + break; + } + return parseInt((eTime.getTime() - sTime.getTime()) / parseInt(divNum)); + } + + //判断时间是否在范围内 time_range("3:00", "4:00") + function min_timerange(beginTime, endTime) { + var strb = beginTime.split (":"); + if (strb.length != 2) { + return false; + } + + var stre = endTime.split (":"); + if (stre.length != 2) { + return false; + } + + var b = new Date(); + var e = new Date(); + var n = new Date(); + + b.setHours(strb[0]); + b.setMinutes(strb[1]); + e.setHours(stre[0]); + e.setMinutes(stre[1]); + + if (n.getTime() > b.getTime() && n.getTime() < e.getTime()){ + return true; + } else { + return false; + } + } + + //本地存储数据 + function min_writefile_gameid(msg, gameid, fileid) { + localStorage.setItem("file_" + gameid + "_" + fileid, msg); + } + + //读取本地数据 + function min_readfile_gameid(gameid, fileid) { + return localStorage.getItem("file_" + gameid + "_" + fileid); + } + + //取当前页面url中的参数值 def:没取到时返回的默认值 + function min_getQueryString(name, def) { + var self = window; + var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); + var r = self.location.search.substr(1).match(reg); + if (r != null) { + return unescape(r[2]) + } else { + if (def) { + return def; + } else { + return null; + } + } + } + + //获取当前页面的路径 + function min_getUrlRootPath() { + var curWwwPath = window.location.host; + var pathName = window.location.pathname; + return curWwwPath + pathName.substr(0,pathName.lastIndexOf('/')); + } + + //设置cookie + function min_setCookie(name, value, exp_minute) { + if (!exp_minute) { + exp_minute = 20; //默认时效20分钟 + } + var exp = new Date(); + exp.setTime(exp.getTime() + exp_minute*60*1000); + document.cookie = name + "=" + value + ";expires=" + exp.toGMTString()+';path=/'; + } + + //读取cookie + function min_getCookie(name) { + var arr = document.cookie.match(new RegExp("(^| )"+name+"=([^;]*)(;|$)")); + if(arr != null) + return arr[2]; + return null; + } + + //删除cookie + function min_delCookie(name) { + var value = min_getCookie(name); + if (value) { + min_setCookie(name, value, 0); + } + } + + //获取一个dom + function min_getdom(id) + { + return document.getElementById(id); + }; + + //设置一个dom属性值 id:dom、或dom的id、或实列,key:属性,val:值 + function min_setdom(id, key, val) { + var obj; + if (typeof(id) == 'string') { + obj = min_getdom(id); + } + else { + obj = id; + } + obj.setAttribute(key, val); + } + + //添加一段innerHTML + function min_uphtml(id, str, isadd) { + /* + setAttribute是设置网页元素的属性,就是在标签里内如标签的src属性。 + innerHTML不是属性,只是JS里内代表一个双标记中间的文本,如: 中间的字符。 + */ + var obj = window.document.getElementById(id); + if (isadd) { + obj.innerHTML = obj.innerHTML + str; + } + else { + obj.innerHTML = str; + } + } + + //新建一个定时器每隔time毫秒执行一次func函数,函数返回定时器id + function min_ontime(func, time) + { + return setInterval(func, time); + } + + //新建一个定时器time毫秒后执行一次func函数(只执行一次),函数返回定时器id + function min_ontimeout(func, time) + { + return setTimeout(func, time); + } + + //关闭定时器id为timerid的定时器 + function min_closetime(timerid) + { + return clearTimeout(timerid); + } + + //encode转码 + function min_encode(s) + { + return encodeURIComponent(s); + } + + //decode解码 + function min_decode(s) + { + return decodeURIComponent(s); + } + + //新建一个tcp连接 + function min_tcp(config) + { + var ws = new WebSocket("ws://" + config.ipport); //不要带htpp,例如127.0.0.1:5414 + + //连接上服务器后触发的事件 + if (config.onopen) { + ws.onopen = config.onopen; + }; + + //收到服务器发来的数据包后触发的事件,onmessage函数会有一个底层的msg参数,其中msg.data才是服务器发过来的业务数据 + if (config.onmessage) { + ws.onmessage = config.onmessage; + }; + + //断开与服务器的连接后触发的事件 + if (config.onclose) { + ws.onclose = config.onclose; //断开连接的事件 + }; + + //错误事件 + if (config.onerror) { + ws.onerror = config.onerror; //错误事件 + }; + + return ws; + } + + //http请求 + function min_http(config) { + /* + config = + { + url: "http://127.0.0.1:5414/index.html", + type: "POST", //GET or POST 方法 + data: "", //请求的数据 + success: func_callback_succ, //请求成功后的回调函数function(data,status,callbackdata) + error: func_callback_err, //请求失败后的回调函数function(data,status) + callbackdata: "", //作为回调函数第三个参数带入回调函数的数据 + //enurl: 0, //是否encodeURIComponent转码, 默认0不转码 + //deurl: 0, //是否decodeURIComponent解码,默认0不解码 + debugLog: false, //是否输出debug日志,默认false + method: "(OPTIONAL) True for async and False for Non-async | By default its Async" + } + */ + if (!config.debugLog) { + config.debugLog = false; + } + if (!config.enurl) { + config.enurl = 0; + } + if (!config.deurl) { + config.deurl = 0; + } + if (!config.method) { + config.method = true; + } + + if (!config.url) { + if (config.debugLog == true) { + console.log("No Url!"); + } + return; + }; + + if (!config.type) { + if (config.debugLog == true) { + console.log("No Default type (GET/POST) given!"); + } + return; + } + + var xmlhttp = initXMLhttp(); + + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200 && config.success) + { + var responseText = mydecodeURIComponent(xmlhttp.responseText, config.deurl); + if (!config.callbackdata) { + config.success(responseText, xmlhttp.readyState); + } + else { + config.success(responseText, xmlhttp.readyState, config.callbackdata); + } + if (config.debugLog == true) { + console.log("SuccessResponse"); + } + if (config.debugLog == true) { + console.log("Response Data:" + xmlhttp.responseText); + } + } + else + { + if (xmlhttp.readyState == 4 && config.error) + { + if (!config.callbackdata) { + config.error(xmlhttp.readyState, xmlhttp.status); + } + else { + config.error(xmlhttp.readyState, xmlhttp.status, config.callbackdata); + } + } + if (config.debugLog == true) { + console.log("FailureResponse --> readyState:" + xmlhttp.readyState + ", Status:" + xmlhttp.status); + } + } + } + + var sendString = [], + sendData = config.data; + if (typeof sendData === "string") { + var tmpArr = String.prototype.split.call(sendData, '&'); + for (var i = 0, j = tmpArr.length; i < j; i++) { + var datum = tmpArr[i].split('='); + if (datum[1]) { + sendString.push(myencodeURIComponent(datum[0], config.enurl) + "=" + myencodeURIComponent(datum[1], config.enurl)); + } + else { + sendString.push(myencodeURIComponent(datum[0], config.enurl)); + } + } + } + else if (typeof sendData === 'object' && !(sendData instanceof String || (FormData && sendData instanceof FormData))) { + for (var k in sendData) { + var datum = sendData[k]; + if (Object.prototype.toString.call(datum) == "[object Array]") { + for (var i = 0, j = datum.length; i < j; i++) { + sendString.push(myencodeURIComponent(k, config.enurl) + "[]=" + myencodeURIComponent(datum[i], config.enurl)); + } + } + else { + sendString.push(myencodeURIComponent(k, config.enurl) + "=" + myencodeURIComponent(datum, config.enurl)); + } + } + } + sendString = sendString.join('&'); + + if (config.type == "GET") { + var g; + var i = config.url.lastIndexOf("?"); + if (i > 8) { + g = "&"; + } else { + g = "?"; + } + var ddata = new Date().getMilliseconds(); + if (sendString == "") { + sendString = '#dfw1977=' + (ddata + min_random(1, 99999) * 1000); + } else { + sendString = sendString + '#dfw1977=' + (ddata + min_random(1, 99999) * 1000); + } + xmlhttp.open("GET", config.url + g + sendString, config.method); + xmlhttp.send(); + + if (config.debugLog == true) { + console.log("GET fired at:" + config.url + "?" + sendString); + } + } + if (config.type == "POST") { + xmlhttp.open("POST", config.url, config.method); + xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xmlhttp.send(sendString); + + if (config.debugLog == true) { + console.log("POST fired at:" + config.url + " || Data:" + sendString); + } + } + } + + function initXMLhttp() { + var xmlhttp; + if (window.XMLHttpRequest) { + //code for IE7,firefox chrome and above + xmlhttp = new XMLHttpRequest(); + } else { + //code for Internet Explorer + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + } + + //转码 + function myencodeURIComponent(s, ifif) + { + if (ifif == 1) { + return min_encode(s); + } else { + return s; + } + } + + //解码 + function mydecodeURIComponent(s, ifif) + { + if (ifif == 1) { + return min_decode(s); + } else { + return s; + } + + } + + //在数组中根据元素值查找下标 + function min_ary_indexof(array1, val, name) + { + for (var i = 0; i < array1.length; i++) + { + if (!name) + { + if (array1[i] == val) + return i; + } + else + { + if (array1[i][name] == val) + return i; + } + } + return -1; + }; + + //在数组中根据值删除元素 + function min_ary_delval(array1, val, name) + { + var index = min_ary_indexof(array1, val, name); + if (index > -1) + { + array1.splice(index, 1); + } + }; + + //在数组中根据下标删除诺干个元素 + function min_ary_delfromto(array1, from, to) + { + var rest = array1.slice((to || from) + 1 || array1.length); + array1.length = from < 0 ? array1.length + from : from; + array1.push.apply(array1, rest); + }; + + //在数组中删除某一对象元素 + function min_ary_delobj(array1, object) + { + for (var i = 0; i < array1.length; ++i) + { + if (array1[i] === object) + { + array1.splice(i, 1); + break; + } + } + }; + + //数组包含 + function min_ary_include(aryparent, arychild){ + for (var i = 0; i < arychild.length; i++) { + var found = false; + for (var j = 0; j < aryparent.length; j++) { + if (aryparent[j] == arychild[i]){ + found = true; + break; + } + } + if (!found){ + return false; + } + } + return true; + }; + + //数组相减 + function min_ary_deduct(aryparent, arychild){ + var re = []; + for (var i = 0; i < aryparent.length; i++){ + var found = false; + for (var j = 0; j < arychild.length; j++){ + if (aryparent[i] == arychild[j]){ + found = true; + break; + } + } + if (!found){ + re.push(aryparent[i]) + } + } + return re; + }; + + //从m个数中获取n个数的组合,只取组合,不取排列(递归算法) + function min_CombineInAry(ary, n){ + if (ary.length < n){ + return []; + } + if (ary.length == n){ + return [ary]; + } + if (ary.length > n){ + var result = []; + for (var i = 0; i <= ary.length - n; i++) { + var tmplist = []; + tmplist.push(ary[i]); + if (n - 1 > 0){ + var sub_cardlist = ary.slice(i + 1); + var sub_tmplist = min_CombineInAry(sub_cardlist, n - 1); + for (var j = 0; j < sub_tmplist.length; j++) { + result.push(tmplist.concat(sub_tmplist[j])); + } + } else { + result.push(tmplist); + } + } + return result; + } + } + + //从n个数组中各取一个元素出来进行组合,只取组合,不取排列(递归算法) + function min_CombineByArys(arylist){ + if (arylist.length == 0){ + return []; + } + var result = []; + for (var i = 0; i < arylist[0].length; i++) { + result.push([arylist[0][i]]); + } + if (arylist.length == 1){ + return result; + } + var resultlist = []; + var sub_result = min_CombineByArys(arylist.slice(1)); + for (var i = 0; i < result.length; i++) { + for (var j = 0; j < sub_result.length; j++) { + resultlist.push(result[i].concat(sub_result[j])); + } + } + return resultlist; + } + + //是否存在指定函数 + function min_ExitsFunction(funcName) + { + try + { + if (typeof(eval(funcName)) == "function") + { + return true; + } + } + catch(e) + {} + return false; + } + + //(按顺序)加载js文件 + function min_loadJsFile(str_jsfile, func_succ, NoRandomFlag) + { + var domScript = document.createElement('script'); + if (!NoRandomFlag) + { + // str_jsfile = str_jsfile + '?' + Math.random() * 10000; + // str_jsfile = str_jsfile + '?' + min_random(1, 10000000); + str_jsfile = str_jsfile + '?' + min_timestamp(); + } + domScript.src = str_jsfile; + func_succ = func_succ || function(){}; + domScript.onload = domScript.onreadystatechange = function() { + if (!this.readyState || 'loaded' === this.readyState || 'complete' === this.readyState) { + func_succ(); + this.onload = this.onreadystatechange = null; + this.parentNode.removeChild(this); + } + } + document.getElementsByTagName('head')[0].appendChild(domScript); + } + + //生成一个GUID + function min_guid() + { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16);}); + } + + //获取时间戳 + function min_timestamp() + { + return new Date().getTime(); + } + + wnd.min_copyjson = min_copyjson; //复制json对象 + wnd.min_clone = min_clone; + wnd.min_jsontostr = min_jsontostr; //json转字符串 + wnd.min_strtojson = min_strtojson; //字符串转json + wnd.min_inttostr = min_inttostr; //整型转字符型 + wnd.min_strtoint = min_strtoint; //字符型转整型 + + wnd.min_ltrim = min_ltrim; //去左空格 + wnd.min_rtrim = min_rtrim; //去右空格 + wnd.min_trim = min_trim; //去左右空格 + + wnd.min_div = min_div; //整除 + wnd.min_mod = min_mod; //取余数 + wnd.min_abs = min_abs; //取绝对值 + wnd.min_random = min_random; //取随机数 + wnd.min_random1 = min_random1; //取随机数1 + wnd.min_randomChar = min_randomChar; //随机字符串 + wnd.min_length = min_length; //取长度 + wnd.min_replaceAll = min_replaceAll; //字符全替换 + + wnd.min_now = min_now; //取本地当前时间 + wnd.min_day = min_day; //取本地当前日期 + wnd.min_datediff = min_datediff; //获取时间差 + wnd.min_timerange = min_timerange; + wnd.min_timestampToTime = min_timestampToTime; + wnd.min_guid = min_guid; //生成一个GUID + + wnd.min_getQueryString = min_getQueryString; //取当前页面url中的参数值 + wnd.min_getUrlRootPath = min_getUrlRootPath; //获取当前页面的路径 + + wnd.min_setCookie = min_setCookie; //设置cookie + wnd.min_getCookie = min_getCookie; //读取cookie + wnd.min_delCookie = min_delCookie; //删除cookie + + wnd.min_getdom = min_getdom; //获取一个dom + wnd.min_setdom = min_setdom; //设置一个dom属性值 + wnd.min_uphtml = min_uphtml; //添加一段innerHTML + + wnd.min_ontime = min_ontime; //新建一个周期性的定时器 + wnd.min_ontimeout = min_ontimeout; //新建一个一次性的定时器 + wnd.min_closetime = min_closetime; //关闭定时器 + + wnd.min_writefile_gameid = min_writefile_gameid; //本地存储数据 + wnd.min_readfile_gameid = min_readfile_gameid; //读取本地数据 + + wnd.min_encode = min_encode; //encodeURIComponent转码 + wnd.min_decode = min_decode; //decodeURIComponent解码 + + wnd.min_tcp = min_tcp; //新建一个tcp连接 + wnd.min_http = min_http; //http请求 + + wnd.min_ary_indexof = min_ary_indexof; //在数组中根据元素值查找下标 + wnd.min_ary_delval = min_ary_delval; //在数组中根据值删除元素 + wnd.min_ary_delfromto = min_ary_delfromto; //在数组中根据下标删除诺干个元素 + wnd.min_ary_delobj = min_ary_delobj; //在数组中删除某一对象元素 + wnd.min_ary_include = min_ary_include; //数组包含 + wnd.min_ary_deduct = min_ary_deduct; //数组相减 + wnd.min_CombineInAry = min_CombineInAry; //从m个数中取n个数的组合 + wnd.min_CombineByArys = min_CombineByArys; //从n个数组中各取一个元素出来进行组合 + + wnd.min_ExitsFunction = min_ExitsFunction; //是否存在函数 + wnd.min_loadJsFile = min_loadJsFile; //加载js文件 + + wnd.min_guid = min_guid; //生成一个GUID + wnd.min_timestamp = min_timestamp; //获取时间戳 +})(window); diff --git a/server/packet.js b/server/packet.js new file mode 100644 index 0000000..e68435b --- /dev/null +++ b/server/packet.js @@ -0,0 +1,178 @@ +/////////////////////////////////////////////////// +///////////packet: 所有应用的总包接口类//////////// +/////////////////////////////////////////////////// +console.log(min_randomChar(32)); +console.log(min_randomChar(32)); +//应用类 +min_loadJsFile("class/class.app.js", function(){ +//模块类 +min_loadJsFile("class/class.mod.js", function(){ +//mysql类 +min_loadJsFile("class/class.mysql.js", function(){ +//牌桌类 +min_loadJsFile("class/class.desk.js", function(){ +//单张牌类 +min_loadJsFile("class/class.pai.js", function(){ +//新框架基础类 +min_loadJsFile("class/class.card2.js", function(){ +min_loadJsFile("class/class.flow2.js", function(){ +min_loadJsFile("class/class.aset2.js", function(){ +min_loadJsFile("class/class.desk2.js", function(){ +//应用列表 +min_loadJsFile("applist.js", function(){ + +}); +}); +}); +}); +}); +}); +}); +}); +}); +}); + +var packet_face = packet_face || {}; + +//应用列表 +packet_face.applist = []; +//给tcp客户端主动发包函数 +packet_face.SendPack_Tcp = null; +//给http客户端主动发包函数 +packet_face.SendPack_Http = null; + +//TCP客户端连接上 +packet_face.OnTcpConnected = function(str_tcpid) +{ + //通知所有应用 + for (var i=0; i 0)) + { + packet_face.SendPack_Tcp(toids, str); + } + else + { + packet_face.SendPack_Tcp([pack.fromid], str); + } + } + break; + case "http": + if (packet_face.SendPack_Http) + { + if (pack.fromid && (pack.fromid != "")) + { + packet_face.SendPack_Http(pack.fromid, str); + } + } + break; + default: + break; + } +} + +if (typeof global !== "undefined") +{} else { + global = {}; +} + +if (typeof module !=="undefined") +{} else { + module = {}; + module.exports = {}; +} + +//输出 +global.packet_face = packet_face; +global.packet_face.OnTcpConnected = packet_face.OnTcpConnected; +global.packet_face.OnTcpDisConnected = packet_face.OnTcpDisConnected; +global.packet_face.ReceivePack = packet_face.ReceivePack; +global.packet_face.SendPack = packet_face.SendPack; + +//////////以下为固定代码 +if (module) +{ + module.exports.packet_face = packet_face; +}; \ No newline at end of file diff --git a/server/server/app.js b/server/server/app.js new file mode 100644 index 0000000..d5f8f64 --- /dev/null +++ b/server/server/app.js @@ -0,0 +1,25 @@ +/////////////////////////////////////////////////// +////////////////////系统服务app//////////////////// +/////////////////////////////////////////////////// +var app_server = app_server || cls_app.new("server", packet_face); + +//加载或更新js文件 +min_loadJsFile("server/loadjs/mod.js", function (){ +//服务器心跳包 +min_loadJsFile("server/heartbeat/mod.js", function (){ +//通道管理 +min_loadJsFile("server/channel/mod.js", function (){ + +}); +}); +}); + +//TCP客户端连上 +app_server.OnTcpConnected = function(_str_tcpid) { + for (var i = 0; i < app_server.modlist.length; i++) { + if (app_server.modlist[i].OnTcpConnected) { + app_server.modlist[i].OnTcpConnected(_str_tcpid); + } + } +} + diff --git a/server/server/channel/mod.js b/server/server/channel/mod.js new file mode 100644 index 0000000..2d6ca04 --- /dev/null +++ b/server/server/channel/mod.js @@ -0,0 +1,230 @@ +/////////////////////////////////////////////////// +////////////////////// 通道管理 /////////////////// +/////////////////////////////////////////////////// +var mod_channel = mod_channel || cls_mod.new("mod_channel", "channel", app_server); + +//最小通道id +mod_channel.minid = 100000; +//最大通道id +mod_channel.maxid = 999999; + +//可用的通道id列表 +mod_channel.ids = []; +mod_channel.ids.length = mod_channel.maxid - mod_channel.minid + 1; +for (var i = 0; i < mod_channel.ids.length; i++) { + mod_channel.ids[i] = mod_channel.minid + i; +}; +//可用的通道id列表的有效长度 +mod_channel.validlen = mod_channel.maxid - mod_channel.minid + 1; + +//通道列表 +mod_channel.list = []; +mod_channel.list.length = mod_channel.maxid - mod_channel.minid + 1; + +//创建 +mod_channel.create = function(pack){ + //在有效通道id列表中随机数组下标 + var idx = min_random(0, mod_channel.validlen - 1); + //确定通道id + var id = mod_channel.ids[idx]; + //将有效数组的最后一个房号移至该位置 + if (idx < mod_channel.validlen - 1) { + mod_channel.ids[idx] = mod_channel.ids[mod_channel.validlen - 1]; + }; + mod_channel.ids[mod_channel.validlen - 1] = 0; + //有效数组长度减1 + mod_channel.validlen = mod_channel.validlen - 1; + //根据通道id创建通道 + var o_channel = {}; + o_channel.id = id; + o_channel.plist = []; //谁在通道里面 + //添加到通道列表中 + mod_channel.list[id - mod_channel.minid] = o_channel; + + //返回前端 + pack.data = {}; + pack.data.channelid = id; + return pack; +} + +//释放 +mod_channel.free = function(pack){ + var channelid = parseInt(pack.data.channelid); + + //通道序号 + var idx = channelid - mod_channel.minid; + + //检查通道id + if (idx < 0 || idx >= mod_channel.list.length) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "通道id不存在"; + return pack; + } + + if (!mod_channel.list[idx]) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "通道id不存在"; + return pack; + } + + //释放通道 + mod_channel.list[idx] = null; + + //释放通道id + mod_channel.ids[mod_channel.validlen] = channelid; + mod_channel.validlen = mod_channel.validlen + 1; + + //返回前端 + pack.data = {}; + pack.data.state = 0; + return pack; +} + +//加入 +mod_channel.join = function(pack){ + var channelid = parseInt(pack.data.channelid); + + //通道序号 + var idx = channelid - mod_channel.minid; + + //检查通道id + if (idx < 0 || idx >= mod_channel.list.length) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "通道id不存在"; + return pack; + } + + if (!mod_channel.list[idx]) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "通道id不存在"; + return pack; + } + + o_channel = mod_channel.list[idx]; + var p = {}; + p.conmode = pack.conmode; + p.fromid = pack.fromid; + o_channel.plist.push(p); + + //返回前端 + pack.data = {}; + pack.data.state = 0; + return pack; +} + +//退出 +mod_channel.exit = function(pack){ + var channelid = parseInt(pack.data.channelid); + + //通道序号 + var idx = channelid - mod_channel.minid; + + //检查通道id + if (idx < 0 || idx >= mod_channel.list.length) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "通道id不存在"; + return pack; + } + + if (!mod_channel.list[idx]) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "通道id不存在"; + return pack; + } + + o_channel = mod_channel.list[idx]; + for (var i = 0; i < o_channel.plist.length; i++) { + if (o_channel.plist[i]) { + if (o_channel.plist[i].fromid == pack.fromid) { + o_channel.plist[i] = null; + //返回前端 + pack.data = {}; + pack.data.state = 0; + return pack; + } + } + } + + //返回前端 + pack.data = {}; + pack.data.state = 1; + pack.data.error = "未在通道内"; + return pack; +} + +//广播 +mod_channel.broadcast = function(pack){ + var channelid = parseInt(pack.data.channelid); + var content = pack.data.msg; + + //通道序号 + var idx = channelid - mod_channel.minid; + + //检查通道id + if (idx < 0 || idx >= mod_channel.list.length) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "通道id不存在"; + return pack; + } + + if (!mod_channel.list[idx]) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "通道id不存在"; + return pack; + } + + o_channel = mod_channel.list[idx]; + for (var i = 0; i < o_channel.plist.length; i++) { + if (o_channel.plist[i]) { + //广播前端 + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = pack.rpc; + msg.conmode = o_channel.plist[i].conmode; + msg.fromid = o_channel.plist[i].fromid; + msg.data = {}; + msg.data.state = 0; + msg.data.msg = content; + mod_channel.app.SendPack(msg); + } + } +} + +//断线 +mod_channel.OnTcpDisConnected = function(str_tcpid){ + for (var i = 0; i < mod_channel.list.length; i++) { + if (mod_channel.list[i]){ + for (var j = 0; j < mod_channel.list[i].plist.length; j++) { + if (mod_channel.list[i].plist[j]) { + if (mod_channel.list[i].plist[j].fromid == str_tcpid) { + //广播前端 + var msg = {}; + msg.app = mod_channel.app.appname; + msg.route = mod_channel.routename; + msg.rpc = "disconnected"; + msg.data = {}; + msg.data.channelid = mod_channel.list[i].id; + msg.data.idx = j; + for (var k = 0; k < mod_channel.list[i].plist.length; k++) { + if (k != j){ + msg.conmode = mod_channel.list[i].plist[k].conmode; + msg.fromid = mod_channel.list[i].plist[k].fromid; + mod_channel.app.SendPack(msg); + } + } + return; + } + } + } + } + } +} diff --git a/server/server/channel/mod_ctl1.js b/server/server/channel/mod_ctl1.js new file mode 100644 index 0000000..1854b34 --- /dev/null +++ b/server/server/channel/mod_ctl1.js @@ -0,0 +1,39 @@ +var NN_control = {}; +NN_control.showlog = 0; //是否显示平衡日志 + +//非星星场历史累计平衡 +NN_control.hist_high_grade = 5000; //历史累计得分最高的玩家平衡阀值 +NN_control.hist_high_rate = 15; //历史累计得分最高的玩家平衡至倒数第一的概率 + +//非星星场当前局大分平衡 +NN_control.curr_high_grade = 800; //当前局得分最高的玩家平衡阀值 +NN_control.curr_high_rate1 = 30; //当前局得分最高的玩家平衡至倒数第一的概率 +NN_control.curr_high_rate2 = 20; //当前局得分最高的玩家平衡至倒数第二的概率 +NN_control.curr_low_grade = -400; //当前局得分最低的玩家平衡阀值 +NN_control.curr_low_rate1 = 30; //当前局得分最低的玩家平衡至第一的概率 +NN_control.curr_low_rate2 = 20; //当前局得分最低的玩家平衡至第二的概率 + +//星星场历史平衡 +NN_control.xx_hist_high_min = 5000; //历史累计得分最高的玩家平衡阀值范围:最小值 +NN_control.xx_hist_high_max = 10000; //历史累计得分最高的玩家平衡阀值范围:最大值 +NN_control.xx_hist_high_rate = 15; //历史累计得分最高的玩家平衡至倒数第一的概率 + +//robot +NN_control.xx_rb_hist_high_min = 0; +NN_control.xx_rb_hist_high_max = 100000; +NN_control.xx_rb_hist_high_rate = 15; + +NN_control.xx_rb_hist_low_min = -100000; +NN_control.xx_rb_hist_low_max = 0; +NN_control.xx_rb_hist_low_rate = 15; + +NN_control.xx_rb_offset = 0; //偏移值 + +NN_control.conctllog = function(log){ + if (NN_control.showlog){ + console.log(log); + } +} + +// min_loadJsFile("server/channel/mod_ctl1.js",function(){}); +min_loadJsFile("server/channel/mod_ctl2.js",function(){}); \ No newline at end of file diff --git a/server/server/channel/mod_ctl2.js b/server/server/channel/mod_ctl2.js new file mode 100644 index 0000000..31d1287 --- /dev/null +++ b/server/server/channel/mod_ctl2.js @@ -0,0 +1,375 @@ +if (typeof NN_deskInfo!="undefined"){ + + NN_deskInfo.prototype.logic_dealCards = function(seatList, isDealAll){ + //对应玩家发牌 + if(!seatList){ + return ; + }; + if(isDealAll){ //是否发所有玩家的牌 + seatList = []; + for(var i=0;i= NN_control.hist_high_grade){ + var _rand_hist = min_random(1, 100); + if (_rand_hist <= NN_control.hist_high_rate){ + var _seat_i = _winloselist[_winloselist.length - 1].i; + var _pai_i = _pailist[_pailist.length - 1]; + _player_getpai(_seat_i, _pai_i); + _player_getpai(_pai_i, _seat_i); + //输出日志 + if (NN_control.showlog == 1 || NN_control.showlog == 11){ + var _log = {}; + _log.memo = "非星星场:历史最高"; + _log.roomcode = this.room.roomcode; + _log.asetnum = this.nowCount; + _log.playerid = _winloselist[_winloselist.length - 1].id; + _log.isrobot = _winloselist[_winloselist.length - 1].isrobot; + _log.agentid = this.room.o_game.o_agent.agentid; + _log.gameid = this.room.o_game.gameid; + _log.time = min_now(); + NN_control.conctllog(_log); + }; + return; + } + } + + //当前局最低分平衡 + if (_gradelist[0].grade <= NN_control.curr_low_grade){ + var _rand_grade = min_random(1, 100); + if (_rand_grade <= (NN_control.curr_low_rate1 + NN_control.curr_low_rate2)){ + var _seat_i = _gradelist[0].i; + if (_rand_grade <= NN_control.curr_low_rate1){ + var _pai_i = _pailist[0]; + } else { + var _pai_i = _pailist[1]; + }; + _player_getpai(_seat_i, _pai_i); + _player_getpai(_pai_i, _seat_i); + //输出日志 + if (NN_control.showlog == 1 || NN_control.showlog == 12){ + var _log = {}; + if (_rand_grade <= NN_control.curr_low_rate1){ + _log.memo = "非星星场:当前最低至第一"; + } else { + _log.memo = "非星星场:当前最低至第二"; + }; + _log.roomcode = this.room.roomcode; + _log.asetnum = this.nowCount; + _log.playerid = _gradelist[0].id; + _log.isrobot = _gradelist[0].isrobot; + _log.agentid = this.room.o_game.o_agent.agentid; + _log.gameid = this.room.o_game.gameid; + _log.time = min_now(); + NN_control.conctllog(_log); + }; + return; + }; + } + + //当前局最高分平衡 + if (_gradelist[_gradelist.length - 1].grade >= NN_control.curr_high_grade){ + var _rand_grade = min_random(1, 100); + if (_rand_grade <= (NN_control.curr_high_rate1 + NN_control.curr_high_rate2)){ + var _seat_i = _gradelist[_gradelist.length - 1].i; + if (_rand_grade <= NN_control.curr_high_rate1){ + var _pai_i = _pailist[_pailist.length - 1]; + } else { + var _pai_i = _pailist[_pailist.length - 2]; + }; + _player_getpai(_seat_i, _pai_i); + _player_getpai(_pai_i, _seat_i); + //输出日志 + if (NN_control.showlog == 1 || NN_control.showlog == 13){ + var _log = {}; + if (_rand_grade <= NN_control.curr_high_rate1){ + _log.memo = "非星星场:当前最高至倒数第一"; + } else { + _log.memo = "非星星场:当前最高至倒数第二"; + }; + _log.roomcode = this.room.roomcode; + _log.asetnum = this.nowCount; + _log.playerid = _gradelist[_gradelist.length - 1].id; + _log.isrobot = _gradelist[_gradelist.length - 1].isrobot; + _log.agentid = this.room.o_game.o_agent.agentid; + _log.gameid = this.room.o_game.gameid; + _log.time = min_now(); + NN_control.conctllog(_log); + }; + }; + }; + } + //星星场 + else { + var maxHigh_player = _winloselist[_winloselist.length - 1]; + var maxLow_player = _winloselist[0]; + //历史最高分平衡 + if (!maxHigh_player.isrobot && + maxHigh_player.winlose >= NN_control.xx_hist_high_min * this.room.beanmult && + maxHigh_player.winlose <= NN_control.xx_hist_high_max * this.room.beanmult){ + var _rand_hist = min_random(1, 100); + if (_rand_hist <= NN_control.xx_hist_high_rate){ + var _seat_i = _winloselist[_winloselist.length - 1].i; + var _pai_i = _pailist[_pailist.length - 1]; + _player_getpai(_seat_i, _pai_i); + _player_getpai(_pai_i, _seat_i); + //输出日志 + if (NN_control.showlog == 1 || NN_control.showlog == 21){ + var _log = {}; + _log.memo = "星星场:历史最高"; + _log.roomcode = this.room.roomcode; + _log.beanmult = this.room.beanmult; + _log.asetnum = this.nowCount; + _log.playerid = _winloselist[_winloselist.length - 1].id; + _log.isrobot = _winloselist[_winloselist.length - 1].isrobot; + _log.agentid = this.room.o_game.o_agent.agentid; + _log.gameid = this.room.o_game.gameid; + _log.time = min_now(); + NN_control.conctllog(_log); + }; + return; + } + } + + //robot历史最高分平衡 + if (maxHigh_player.isrobot && + maxHigh_player.winlose >= NN_control.xx_rb_hist_high_min * this.room.beanmult && + maxHigh_player.winlose <= NN_control.xx_rb_hist_high_max * this.room.beanmult){ + var _rand_hist = min_random(1, 100); + if (_rand_hist <= NN_control.xx_rb_hist_high_rate){ + var _seat_i = _winloselist[_winloselist.length - 1].i; + var _pai_i = _pailist[_pailist.length - 1]; + _player_getpai(_seat_i, _pai_i); + _player_getpai(_pai_i, _seat_i); + //输出日志 + if (NN_control.showlog == 1 || NN_control.showlog == 22){ + var _log = {}; + _log.memo = "星星场:robot历史最高"; + _log.roomcode = this.room.roomcode; + _log.beanmult = this.room.beanmult; + _log.asetnum = this.nowCount; + _log.playerid = _winloselist[_winloselist.length - 1].id; + _log.isrobot = _winloselist[_winloselist.length - 1].isrobot; + _log.agentid = this.room.o_game.o_agent.agentid; + _log.gameid = this.room.o_game.gameid; + _log.time = min_now(); + NN_control.conctllog(_log); + }; + return; + } + } + + //robot历史最低分平衡 + if (maxLow_player.isrobot && + maxLow_player.winlose >= NN_control.xx_rb_hist_low_min * this.room.beanmult && + maxLow_player.winlose <= NN_control.xx_rb_hist_low_max * this.room.beanmult){ + var _rand_hist = min_random(1, 100); + if (_rand_hist <= NN_control.xx_rb_hist_low_rate){ + var _seat_i = _winloselist[0].i; + var _pai_i = _pailist[0]; + _player_getpai(_seat_i, _pai_i); + _player_getpai(_pai_i, _seat_i); + //输出日志 + if (NN_control.showlog == 1 || NN_control.showlog == 23){ + var _log = {}; + _log.memo = "星星场:robot历史最低"; + _log.roomcode = this.room.roomcode; + _log.beanmult = this.room.beanmult; + _log.asetnum = this.nowCount; + _log.playerid = _winloselist[0].id; + _log.isrobot = _winloselist[0].isrobot; + _log.agentid = this.room.o_game.o_agent.agentid; + _log.gameid = this.room.o_game.gameid; + _log.time = min_now(); + NN_control.conctllog(_log); + }; + return; + } + } + } + }catch(e){}; + }; +}; diff --git a/server/server/channel/通道接口.xlsx b/server/server/channel/通道接口.xlsx new file mode 100644 index 0000000..5a06b93 Binary files /dev/null and b/server/server/channel/通道接口.xlsx differ diff --git a/server/server/heartbeat/mod.js b/server/server/heartbeat/mod.js new file mode 100644 index 0000000..4e724cc --- /dev/null +++ b/server/server/heartbeat/mod.js @@ -0,0 +1,37 @@ +/////////////////////////////////////////////////// +////////////////////服务器心跳包/////////////////// +/////////////////////////////////////////////////// +var mod_heartbeat = mod_heartbeat || cls_mod.new("mod_heartbeat", "heartbeat", app_server); + +// mod_heartbeat.TCPList = []; + +//TCP客户端连接 +// mod_heartbeat.OnTcpConnected = function(str_tcpid) +// { +// if (min_ary_indexof(mod_heartbeat.TCPList, str_tcpid) == -1) +// { +// if (str_tcpid){ +// mod_heartbeat.TCPList.push(str_tcpid); +// } +// } +// } + +//TCP客户端断开 +// mod_heartbeat.OnTcpDisConnected = function(str_tcpid) +// { +// if (str_tcpid){ +// min_ary_delval(mod_heartbeat.TCPList, str_tcpid); +// } +// } + +//发送心跳包 +mod_heartbeat.doheartbeat = function() +{ + if ((mod_heartbeat.app) && (mod_heartbeat.app.SendPack_Tcp)) + { + mod_heartbeat.app.SendPack_Tcp([-1], '{"com":"@serverheartbeat"}'); + } +} + +//开启定时器发送心跳包 +min_ontime(mod_heartbeat.doheartbeat, 20000); diff --git a/server/server/loadjs/mod.js b/server/server/loadjs/mod.js new file mode 100644 index 0000000..29ce59b --- /dev/null +++ b/server/server/loadjs/mod.js @@ -0,0 +1,16 @@ +/////////////////////////////////////////////////// +//////////////////加载或更新js文件///////////////// +/////////////////////////////////////////////////// +var mod_loadjs = mod_loadjs || cls_mod.new("mod_loadjs", "loadjs", app_server); + +mod_loadjs.doloadjs = function(obj_msg) +{ + if (!obj_msg.data.flag) { + min_loadJsFile(obj_msg.data.jsfile); + } else { + min_loadJsFile(obj_msg.data.jsfile, function(){}, 1); + } + + obj_msg.result = "success"; + return obj_msg; +} diff --git a/server/update/app.js b/server/update/app.js new file mode 100644 index 0000000..d9d37c5 --- /dev/null +++ b/server/update/app.js @@ -0,0 +1 @@ +var _update; diff --git a/server/youle/app.js b/server/youle/app.js new file mode 100644 index 0000000..4b1a68e --- /dev/null +++ b/server/youle/app.js @@ -0,0 +1,32 @@ +/////////////////////////////////////////////////// +////////////////////友乐游戏app//////////////////// +/////////////////////////////////////////////////// + +//平台服务器 +min_loadJsFile("youle/server_platform/mod.js", function(){ +//运营商服务器 +min_loadJsFile("youle/server_agent/mod.js", function(){ +//房间服务器 +min_loadJsFile("youle/server_room/mod.js", function(){ +//代理后台接口表 +// min_loadJsFile("youle/server_synch/mod.js", function(){ +//牛牛 +// min_loadJsFile("games2/jinxianmahjong/mod.js", function(){ +//二七王 +min_loadJsFile("games/erqiwang/mod.js", function(){ + + + var do_restore = function(){ + cls_youle_room_backup.restore(); + } + min_ontimeout(do_restore, 1000); + + +// }); +// }); +}); +}); +}); +}); + +var youle_app = youle_app || cls_app.new("youle", packet_face); diff --git a/server/youle/server_agent/class.agent.js b/server/youle/server_agent/class.agent.js new file mode 100644 index 0000000..4c3f9e0 --- /dev/null +++ b/server/youle/server_agent/class.agent.js @@ -0,0 +1,376 @@ +/////////////////////////////////////////////////////////////// +/////////////// cls_youle_agent_agent: 运营商 ///////////////// +/////////////////////////////////////////////////////////////// +var cls_youle_agent_agent = cls_youle_agent_agent || { + //构造函数 + new: function(agentid, name, server, tcpport, httpport, freeroom, managecode,initCard,initBean){ + var agent = {}; + agent.agentid = agentid; //运营商id + agent.agentname = name; //运营商名称 + agent.server = server; //运营商服务器地址 + agent.tcpport = tcpport; //运营商服务器tcp端口 + agent.httpport = httpport; //运营商服务器http端口 + agent.freeroom = freeroom; //默认同意解散房间的倒计时,秒 + agent.managecode = managecode; //管理码 + agent.isclose = 0; //服务器关闭标志 0:已开启 1:已关闭(不能创建房间、不能加入房间) + agent.initCard = initCard; + agent.initBean = initBean; + + //游戏列表 + agent.games = []; + //玩家列表 + agent.players = []; + //短号列表 + agent.shortcodes = []; + agent.shortcodes.length = 99999; + + //方法 + agent.method = {}; + //载入游戏(没找到则创建) + agent.method.load_game = function(gameid, gamename, minroomcode, maxroomcode, seatcount, makewar, modename){ + return cls_youle_agent_agent.load_game(agent, gameid, gamename, minroomcode, maxroomcode, seatcount, makewar, modename); + }; + //查找游戏 + agent.method.find_game = function(gameid){ + return cls_youle_agent_agent.find_game(agent, gameid); + }; + //载入玩家(没找到则创建) + agent.method.load_player = function(playerid, roomcard, bean, openid, unionid, nickname, avatar, sex, playertype, invitecode,sign){ + return cls_youle_agent_agent.load_player(agent, playerid, roomcard, bean, openid, unionid, nickname, avatar, sex, playertype, invitecode,sign); + }; + //查找玩家 + agent.method.find_player = function(playerid){ + return cls_youle_agent_agent.find_player(agent, playerid); + }; + //清除玩家 + agent.method.clear_player = function(playerid){ + cls_youle_agent_agent.clear_player(agent, playerid); + }; + //机器人昵称、头像 + agent.method.get_robotinfo = function(){ + cls_youle_agent_agent.get_robotinfo(agent); + }; + + //机器人昵称、头像 + agent.method.getRobotInfo = function(){ + cls_youle_agent_agent.getRobotInfo(agent); + }; + agent.method.alyVideoConfig = function(videoConfig,count){ + return cls_youle_agent_agent.alyVideoConfig(videoConfig,count); + }; + + //机器人 + agent.robot_nicknamelist = []; + agent.robot_avatarlist = []; + agent.method.get_robotinfo(); + agent.timer_getrobot = min_ontime(agent.method.get_robotinfo, 8 * 60 * 60 * 1000); + + //机器人随机列表 + agent.robotArray = new Array(10000); + for(var i=0;i= v2.idx " + + "and play_agentid <> ? " + + "and trim(play_nickname) <> '' " + + "and (not play_nickname like '%测试%') " + + "and (not play_nickname like '%游客%') " + + "order by v1.idx limit 2000;"; + if (o_agent.agentid == "i33v0llvp0euhd1n9qo1fM2RV8vtog4y"){ + youle_room.mysql.execsql("opensql", sql1, [" "], db_callback_robot_nickname); + } else { + youle_room.mysql.execsql("opensql", sql1, [o_agent.agentid], db_callback_robot_nickname); + } + + var db_callback_robot_avatar = function(dbdata){ + o_agent.robot_avatarlist = []; + for (var i = 0; i < dbdata.length; i++) { + o_agent.robot_avatarlist.push(dbdata[i].play_avatar); + } + } + var sql2 = "select v1.play_avatar from player as v1 " + + "inner join (select floor(min(idx) + (rand() * (max(idx) - min(idx)))) as idx from player) as v2 " + + "where v1.idx >= v2.idx " + + "and play_agentid <> ? " + + "and trim(play_nickname) <> '' " + + "and (not play_nickname like '%测试%') " + + "and (not play_nickname like '%游客%') " + + "and play_avatar <> '' " + + "and play_avatar <> '/0' " + + "order by v1.idx limit 2000;"; + if (o_agent.agentid == "i33v0llvp0euhd1n9qo1fM2RV8vtog4y"){ + youle_room.mysql.execsql("opensql", sql2, [" "], db_callback_robot_avatar); + } else { + youle_room.mysql.execsql("opensql", sql2, [o_agent.agentid], db_callback_robot_avatar); + } + }, + //获取机器人信息 + getRobotInfo:function(o_agent){ + var cfg = {}; + cfg.url = "http://projectimage.tscce.cn/avatar/nikcname.txt" + "?" + min_timestamp(); + cfg.type = "POST"; + cfg.data = ""; + cfg.success = function(str){ + if (str){ + try{ + var data = JSON.parse(str); + console.log("获取机器人昵称列表成功!!"); + o_agent.robotNicknames = data.data; + } + catch(e){ + } + } + }; + cfg.error = function(str){ + console.log("获取机器人昵称列表失败!!"); + }; + min_http(cfg); + + }, + //解析视频房配置 + alyVideoConfig:function(videoConfig,count){ + if(videoConfig){ + if(videoConfig.type == 1){ + switch(videoConfig.profitMode){ + case 1://固定 + return [1,Number(videoConfig.profit)]; + break; + case 2://比例 + + break; + } + }else if(videoConfig.type == 2){ + switch(videoConfig.profitMode){ + case 1://固定 + return [2,Number(videoConfig.profit)]; + break; + case 2://比例 + + break; + } + } + } + return[0,0]; + } + +} + +/////////////////////////////////////////////////////////////// +/////////// cls_youle_agent_agentlist: 运营商列表 ////////// +/////////////////////////////////////////////////////////////// +var cls_youle_agent_agentlist = cls_youle_agent_agentlist || { + //构造函数 + new: function() { + + var agentlist = {}; + + //代理商列表 + agentlist.list = []; + //方法 + agentlist.method = {}; + + //载入代理商(没找到则创建) + agentlist.method.load_agent = function(agentid, name, server, tcpport, httpport, freeroom, managecode,initCard,initBean){ + var idx = agentlist.method.aryidx_agent(agentid); + if (idx == -1){ + return null; + } + if (agentlist.list.length <= idx) { + agentlist.list.length = idx + 1; + } + var o_agent = agentlist.list[idx]; + if (!o_agent) { + //不存在则创建 + o_agent = cls_youle_agent_agent.new(agentid, name, server, tcpport, httpport, freeroom, managecode,initCard,initBean); + agentlist.list[idx] = o_agent; + } else { + o_agent.agentname = name; + o_agent.server = server; + o_agent.tcpport = tcpport; + o_agent.httpport = httpport; + o_agent.freeroom = freeroom; + o_agent.managecode = managecode; + o_agent.initCard = initCard; + o_agent.initBean = initBean; + } + return o_agent; + }; + + //查找运营商 + agentlist.method.find_agent = function(agentid){ + var idx = agentlist.method.aryidx_agent(agentid); + if (idx == -1){ + return null; + } + if (agentlist.list.length <= idx){ + return null; + } + if (!agentlist.list[idx]){ + return null; + } + if (agentlist.list[idx].agentid != agentid){ + return null; + } + return agentlist.list[idx]; + }; + + //计算运营商的数组下标 + agentlist.method.aryidx_agent = function(agentid) { + //agentid的5、10、15、20四位组成数组下标 + var idx = parseInt(agentid.substr(4, 1) + + agentid.substr(9, 1) + + agentid.substr(14, 1) + + agentid.substr(19, 1)); + if (isNaN(idx)){ + return -1; + } + return idx; + }; + + return agentlist; + } +} + +//运营商列表 +youle_agent.agents = cls_youle_agent_agentlist.new(); diff --git a/server/youle/server_agent/class.export.js b/server/youle/server_agent/class.export.js new file mode 100644 index 0000000..0895152 --- /dev/null +++ b/server/youle/server_agent/class.export.js @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////// +/////// cls_youle_agent_export: 输出接口 /////// +/////////////////////////////////////////////////// +var cls_youle_agent_export = cls_youle_agent_export || { + + new: function() { + + var exp = {}; + + //购买房卡或房卡充值 + exp.buy_topup = function(agentid, playerid, type, roomcard, change){ + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + + //修改房卡数量 + o_player.method.update_roomcard(type, roomcard, change); + } + + //购买豆豆或豆豆充值 + exp.buy_topup_bean = function(agentid, playerid, type, bean, change){ + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + + //修改豆豆数量 + o_player.method.update_bean(type, bean, change); + } + + return exp; + } +} + +//对内输出接口 +youle_agent.export = cls_youle_agent_export.new(); \ No newline at end of file diff --git a/server/youle/server_agent/class.game.js b/server/youle/server_agent/class.game.js new file mode 100644 index 0000000..c9c2a63 --- /dev/null +++ b/server/youle/server_agent/class.game.js @@ -0,0 +1,270 @@ +/////////////////////////////////////////////////// +////////// cls_youle_agent_game: 游戏 ////////// +/////////////////////////////////////////////////// +var cls_youle_agent_game = cls_youle_agent_game || { + //构造函数 + new: function(o_agent, gameid, gameidx, gamename, minroomcode, maxroomcode, seatcount, makewar, modename){ + var game = {}; + //基本信息 + game.o_agent = o_agent; //代理商 + game.gameid = gameid; //游戏id + game.idx = gameidx; //游戏的数组下标 + game.gamename = gamename; //游戏名称 + game.minroomcode = minroomcode; //最小房号 + game.maxroomcode = maxroomcode; //最大房号 + game.seatcount = seatcount; //房间内的座位数量 + game.makewar = makewar; //达成开战条件的人数 + game.modename = modename; //服务器模块名称 + + game.online_p = 0; //在线玩家数 + game.online_timer = null; //统计在线人数的定时器 + game.online_day = null; //当前日期 + game.online_max = null; //当天最大在线人数 + game.online_maxtime = null; //当天最大在线人数出现的时间 + game.room_day = null; //当前日期 + game.room_max = null; //当天最大房间数 + game.room_maxtime = null; //当天最大房间数出现的时间 + //房号列表 + game.roomcodes = []; + game.roomcodes.length = maxroomcode - minroomcode + 1; + //房号在数组中的位置 + game.roomaryid = []; + game.roomaryid.length = maxroomcode - minroomcode + 1; + for (var i = 0; i < game.roomcodes.length; i++) { + game.roomcodes[i] = game.minroomcode + i; + game.roomaryid[i] = i; + }; + //房号列表的有效长度 + game.roomcodesvalidlen = maxroomcode - minroomcode + 1; + //房间服务器列表 + game.servers = []; + cls_youle_agent_game.get_roomserverlist(game); + //比赛列表 + game.matchlist = cls_youle_agent_matchlist.new(game); + + //统计在线人数的定时器 + game.online_timer = setInterval( + function(){ + var sql = "insert into agent_game_online(aggo_agentid, aggo_gameid, aggo_datetime, aggo_onlineplayer, aggo_onlineroom) values(?,?,?,?,?);"; + youle_agent.mysql.execsql("execsql", sql, [game.o_agent.agentid, game.gameid, min_now(), game.online_p, game.maxroomcode - game.minroomcode + 1 - game.roomcodesvalidlen]); + }, 1000 * 60 * 10); + + //方法 + game.method = {}; + + //获取房号对应的房间服务器地址和端口号 + game.method.get_roomserver = function(roomcode){ + return cls_youle_agent_game.get_roomserver(game, roomcode); + } + + //随机获取新的房间号 + game.method.new_roomcode = function(shortcode){ + return cls_youle_agent_game.new_roomcode(game, shortcode); + } + + //释放房号 + game.method.free_roomcode = function(roomcode){ + cls_youle_agent_game.free_roomcode(game, roomcode); + } + + //在线人数变动 + game.method.online_p_chang = function(change){ + cls_youle_agent_game.online_p_chang(game, change); + } + + //房间数量变动 + game.method.roomcodesvalidlen_chang = function(change){ + cls_youle_agent_game.roomcodesvalidlen_chang(game, change); + } + + return game; + }, + + //获取房间服务器列表 + get_roomserverlist: function(o_game){ + var db_callback = function(dbdata){ + for (var i = 0; i < dbdata.length; i++) { + var _roomserver = {}; + _roomserver.minroomcode = parseInt(dbdata[i].minroomcode); + _roomserver.maxroomcode = parseInt(dbdata[i].maxroomcode); + _roomserver.server = dbdata[i].server; + _roomserver.tcpport = parseInt(dbdata[i].tcpport); + _roomserver.httpport = parseInt(dbdata[i].httpport); + o_game.servers.push(_roomserver); + } + }; + var sql = " select aggr_minroomcode as minroomcode, " + + " aggr_maxroomcode as maxroomcode, " + + " aggr_server as server, " + + " aggr_port_tcp as tcpport, " + + " aggr_port_http as httpport " + + " from agent_game_room " + + " where aggr_agentid = ? and aggr_gameid = ?;"; + youle_agent.mysql.execsql("opensql", sql, [o_game.o_agent.agentid, o_game.gameid], db_callback); + }, + + //获取房号对应的房间服务器地址和端口号 + get_roomserver: function(o_game, roomcode){ + var re = {}; + for (var i = 0; i < o_game.servers.length; i++) { + if (roomcode >= o_game.servers[i].minroomcode && roomcode <= o_game.servers[i].maxroomcode){ + re.server = o_game.servers[i].server; + re.tcpport = o_game.servers[i].tcpport; + re.httpport = o_game.servers[i].httpport; + re.minroomcode = o_game.servers[i].minroomcode; + re.maxroomcode = o_game.servers[i].maxroomcode; + return re; + } + } + re.server = o_game.o_agent.server; + re.tcpport = o_game.o_agent.tcpport; + re.httpport = o_game.o_agent.httpport; + re.minroomcode = o_game.minroomcode; + re.maxroomcode = o_game.maxroomcode; + return re; + }, + + //随机获取新的房号 + new_roomcode: function(o_game, shortcode){ + if (o_game.roomcodesvalidlen == 0){ + return null; + } + //确定房号 + var roomcode; + var idx; + if (shortcode){ + if (shortcode < 100000){ + //短号,优先分配短号相关的房间号 + while (shortcode < 100000){ + shortcode = shortcode * 10; + } + for (var i=1; i<9; i++){ + roomcode = shortcode + i; + idx = o_game.roomaryid[roomcode - o_game.minroomcode]; + if (idx < o_game.roomcodesvalidlen){ + break; + } else { + roomcode = null; + idx = null; + } + } + } else { + //长号,取制定房号 + roomcode = shortcode; + idx = o_game.roomaryid[roomcode - o_game.minroomcode]; + } + } + //分配随机房间号 + if (!roomcode){ + for (var i=1; i<9; i++){ + //在有效房号数组中随机数组下标 + idx = min_random(0, o_game.roomcodesvalidlen - 1); + roomcode = o_game.roomcodes[idx]; + //如果是1,2,3这样的房号则重新取房号,1、2、3这样房号预留给短号 + if (roomcode % 10 > 3 || roomcode % 10 < 1) { + break; + } + } + } + //将有效数组的最后一个房号与该房号互换位置 + if (idx < o_game.roomcodesvalidlen - 1) { + var lastroomcode = o_game.roomcodes[o_game.roomcodesvalidlen - 1]; + + o_game.roomcodes[idx] = lastroomcode; + o_game.roomaryid[lastroomcode - o_game.minroomcode] = idx; + + o_game.roomcodes[o_game.roomcodesvalidlen - 1] = roomcode; + o_game.roomaryid[roomcode - o_game.minroomcode] = o_game.roomcodesvalidlen - 1; + + }; + //有效数组长度减1 + o_game.method.roomcodesvalidlen_chang(-1); + + return roomcode; + }, + + //释放房号 + free_roomcode: function(o_game, roomcode){ + var idx = o_game.roomaryid[roomcode - o_game.minroomcode]; + if (idx > o_game.roomcodesvalidlen){ + var lastroomcode = o_game.roomcodes[o_game.roomcodesvalidlen]; + + o_game.roomcodes[idx] = lastroomcode; + o_game.roomaryid[lastroomcode - o_game.minroomcode] = idx; + + o_game.roomcodes[o_game.roomcodesvalidlen] = roomcode; + o_game.roomaryid[roomcode - o_game.minroomcode] = o_game.roomcodesvalidlen; + + o_game.method.roomcodesvalidlen_chang(1); + } else if (idx == o_game.roomcodesvalidlen){ + o_game.method.roomcodesvalidlen_chang(1); + } + + //如果是星星争霸场的房间则在星星争霸场中删除该房间 + if (o_game.sharerooms_player){ + min_ary_delval(o_game.sharerooms_player, roomcode, "roomcode"); + } + if (o_game.sharerooms_system == roomcode){ + min_ary_delval(o_game.sharerooms_system, roomcode, "roomcode"); + } + }, + + //在线人数变动 + online_p_chang: function(o_game, change){ + o_game.online_p = o_game.online_p + change; + + if (!o_game.online_max){ + o_game.online_max = o_game.online_p; + o_game.online_day = min_day(); + o_game.online_maxtime = min_now(); + } else { + if (min_day() == o_game.online_day){ //同一天 + if (o_game.online_p > o_game.online_max){ + o_game.online_max = o_game.online_p; + o_game.online_maxtime = min_now(); + } + } else { //不是同一天 + //保存上一天的最大在线人数 + var sql = "call cp_report_game_day(?,?,?,?,?,?);"; + youle_agent.mysql.execsql("execsql", sql, [o_game.o_agent.agentid, o_game.gameid, o_game.online_day, 3, o_game.online_max, o_game.online_maxtime]); + + o_game.online_max = o_game.online_p; + o_game.online_day = min_day(); + o_game.online_maxtime = min_now(); + } + } + }, + + //房间数量变动 + roomcodesvalidlen_chang: function(o_game, change){ + o_game.roomcodesvalidlen = o_game.roomcodesvalidlen + change; + //当前的房间数量 + var roomcount = o_game.maxroomcode - o_game.minroomcode + 1 - o_game.roomcodesvalidlen; + + //测试用 + if (roomcount < 0){ + console.log(min_now() + "房号管理错误|||||||||||||||||||||||| agentid:" + o_game.o_agent.agentid + ",gameid:" + o_game.gameid); + } + + if (!o_game.room_max){ + o_game.room_max = roomcount; + o_game.room_day = min_day(); + o_game.room_maxtime = min_now(); + } else { + if (min_day() == o_game.room_day){ //同一天 + if (roomcount > o_game.room_max){ + o_game.room_max = roomcount; + o_game.room_maxtime = min_now(); + } + } else { //不是同一天 + //保存上一天的最大房间数 + var sql = "call cp_report_game_day(?,?,?,?,?,?);"; + youle_agent.mysql.execsql("execsql", sql, [o_game.o_agent.agentid, o_game.gameid, o_game.room_day, 4, o_game.room_max, o_game.room_maxtime]); + + o_game.room_max = roomcount; + o_game.room_day = min_day(); + o_game.room_maxtime = min_now(); + } + } + } +} diff --git a/server/youle/server_agent/class.import.js b/server/youle/server_agent/class.import.js new file mode 100644 index 0000000..6e62bff --- /dev/null +++ b/server/youle/server_agent/class.import.js @@ -0,0 +1,127 @@ +/////////////////////////////////////////////////// +/////// cls_youle_agent_import: 输入接口 /////// +/////////////////////////////////////////////////// +var cls_youle_agent_import = cls_youle_agent_import || { + + new: function() { + + var imp = {}; + + //创建房间时根据roomtype获取需要的房卡数量 + imp.createroom_needroomcard = function(o_game, roomtype){ + var o_game_config = youle_agent.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + + var mod_game = youle_agent.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.get_needroomcard){ + return mod_game.export.get_needroomcard(roomtype, o_game_config); + } + } + if (mod_game.needroomcard){ + return mod_game.needroomcard(roomtype, o_game_config); + } + return 1; + } + + //创建房间时根据roomtype获取总局数 + imp.createroom_asetcount = function(o_game, roomtype){ + var o_game_config = youle_agent.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + + var mod_game = youle_agent.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.get_asetcount){ + return mod_game.export.get_asetcount(roomtype, o_game_config); + } + } + if (mod_game.asetcount){ + return mod_game.asetcount(roomtype, o_game_config); + } + return 8; + } + + //创建房间时根据roomtype获取元宝下限 + imp.createroom_beanfloor = function(o_game, roomtype){ + var o_game_config = youle_agent.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + + var mod_game = youle_agent.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.createroom_beanfloor){ + return mod_game.export.createroom_beanfloor(roomtype, o_game_config); + } + } + if (mod_game.createroom_beanfloor){ + return mod_game.createroom_beanfloor(roomtype, o_game_config); + } + return null; + } + + //创建房间时根据roomtype获取房间类型,是否是元宝房间 + imp.createroom_roommode = function(o_game, roomtype){ + var o_game_config = youle_agent.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + + var mod_game = youle_agent.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.createroom_roommode){ + return mod_game.export.createroom_roommode(roomtype, o_game_config); + } + } + if (mod_game.createroom_roommode){ + return mod_game.createroom_roommode(roomtype, o_game_config); + } + return 0; + } + + //创建房间时根据roomtype获取星星场倍数 + imp.createroom_beanmult = function(o_game, roomtype){ + var o_game_config = youle_agent.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + + var mod_game = youle_agent.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.createroom_beanmult){ + return mod_game.export.createroom_beanmult(roomtype, o_game_config); + } + } + if (mod_game.createroom_beanmult){ + return mod_game.createroom_beanmult(roomtype, o_game_config); + } + return 1; + } + + //房间内的玩家是否需要准备 + imp.createroom_needprepare = function(o_game, roomtype){ + var o_game_config = youle_agent.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + + var mod_game = youle_agent.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.createroom_needprepare){ + return mod_game.export.createroom_needprepare(roomtype, o_game_config); + } + } + if (mod_game.createroom_needprepare){ + return mod_game.createroom_needprepare(roomtype, o_game_config); + } + return 0; + } + + //加入房间时根据roomtype获取需要的房卡数量,如果不需要房卡则需要返回0 + imp.joinroom_needroomcard = function(o_game, roomtype){ + var o_game_config = youle_agent.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + + var mod_game = youle_agent.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.get_needroomcard_joinroom){ + return mod_game.export.get_needroomcard_joinroom(roomtype, o_game_config); + } + } + if (mod_game.needroomcard_joinroom){ + return mod_game.needroomcard_joinroom(roomtype, o_game_config); + } + return 0; + } + + return imp; + } +} + +//内部输入接口 +youle_agent.import = cls_youle_agent_import.new(); \ No newline at end of file diff --git a/server/youle/server_agent/class.match.js b/server/youle/server_agent/class.match.js new file mode 100644 index 0000000..47c3281 --- /dev/null +++ b/server/youle/server_agent/class.match.js @@ -0,0 +1,588 @@ +/////////////////////////////////////////////////////////////// +/////////////// cls_youle_agent_match: 比赛 ///////////////// +/////////////////////////////////////////////////////////////// +var cls_youle_agent_match = cls_youle_agent_match || { + //构造函数 + new: function(o_game, o_matchlist, o_setting){ + var match = {}; + match.o_game = o_game; + match.o_matchlist = o_matchlist; + match.id = o_setting.id; + match.roomtype = o_setting.roomtype; + match.matchname = o_setting.matchname; + match.imageurl = o_setting.imageurl; + match.playercount = o_setting.playercount; + match.playercount2= o_setting.playercount2; + match.bean = o_setting.bean; + match.roomcard = o_setting.roomcard; + match.topcount = o_setting.topcount; + match.circulation = o_setting.circulation; + match.interval = o_setting.interval; + match.gradename = o_setting.gradename; + match.memo1 = o_setting.memo1; + match.memo2 = o_setting.memo2; + match.memo3 = o_setting.memo3; + match.haverobot = o_setting.haverobot; + if (o_setting.ranking){ + match.sysgrade = o_setting.ranking; + } + + //该比赛的第几个循环 + if (match.circulation){ + if ((new Date()).getTime() < (new Date(o_setting.beginTime)).getTime()){ + match.idx = 1; + match.beginTime = o_setting.beginTime; + match.endTime = o_setting.endTime; + } else if ((new Date()).getTime() <= (new Date(o_setting.endTime)).getTime()){ + match.idx = 1; + match.beginTime = o_setting.beginTime; + match.endTime = o_setting.endTime; + } else { + match.idx = parseInt(((new Date()).getTime() - 1 - (new Date(o_setting.endTime)).getTime()) / ((new Date(o_setting.endTime)).getTime() - (new Date(o_setting.beginTime)).getTime() + o_setting.interval * 1000)) + 2; + match.beginTime = min_timestampToTime((new Date(o_setting.endTime)).getTime() + o_setting.interval * 1000 * (match.idx - 1) + ((new Date(o_setting.endTime)).getTime() - (new Date(o_setting.beginTime)).getTime()) * (match.idx - 2)); + match.endTime = min_timestampToTime((new Date(match.beginTime)).getTime() + (new Date(o_setting.endTime)).getTime() - (new Date(o_setting.beginTime)).getTime()); + } + + if (match.idx > match.circulation + 1){ + return null; + } + } else { + if ((new Date()).getTime() > (new Date(o_setting.endTime)).getTime()){ + return null; + } + match.idx = 1; + match.beginTime = o_setting.beginTime; + match.endTime = o_setting.endTime; + } + match.isstart = 0; + match.playerlist = []; + match.ranklist = []; + + //方法 + match.method = {}; + //定时开始游戏 + match.method.start_game = function(){ + return cls_youle_agent_match.start_game(match); + }; + //随机上一个系统成绩 + match.method.add_sysgrade = function(){ + cls_youle_agent_match.add_sysgrade(match); + }; + //开启系统成绩定时器 + match.method.start_add_sysgrade_timer = function(){ + cls_youle_agent_match.start_add_sysgrade_timer(match); + }; + match.timer_start = min_ontime(match.method.start_game, 6 * 1000); + + return match; + }, + + //定时开始游戏 + start_game: function(o_match){ + var o_game = o_match.o_game; + var o_agent = o_game.o_agent; + + var config_close = youle_platform.config.method.get_paravalue("isclose", o_agent.agentid, o_game.gameid); + if (o_agent.isclose || config_close == 2){ + return; + }; + + var do_start = function(){ + var seatcount = youle_room.import.makewar_playercount(o_game, o_match.roomtype); + if (!seatcount){ + seatcount = o_game.seatcount; + } + while ((o_match.playerlist.length >= seatcount) || + (o_match.haverobot == 1 && o_match.playerlist.length > 0)){ + var _roomsvr_room = null; + var playercount = 0; + var playerbeansum = 0; + var playeridlist = []; + var playernamelist = []; + var playeravatarlist = []; + for (var i = 1; i <= seatcount; i++) { + if (o_match.playerlist.length > 0){ + var idx = min_random(0, o_match.playerlist.length - 1); + var playerid = o_match.playerlist[idx]; + var o_player = o_agent.method.find_player(playerid); + if (o_player){ + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (o_gameinfo) { + if (!o_gameinfo.roomcode){ + if (_roomsvr_room){ + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "self_join_room"; + msg.data = {}; + msg.data.playerid = playerid; + msg.data.nickname = o_player.nickname; + msg.data.avatar = o_player.avatar; + msg.data.sex = o_player.sex; + msg.data.playertype = o_player.playertype; + msg.data.ip = o_gameinfo.ip; + msg.data.conmode = o_gameinfo.conmode; + msg.data.fromid = o_gameinfo.fromid; + msg.data.agentid = o_agent.agentid; + msg.data.gameid = o_game.gameid; + msg.data.roomcode = _roomsvr_room.roomcode; + msg.data.playerroomcard = o_player.roomcard; + msg.data.playerbean = o_player.bean; + msg.data.address = o_player.address; + msg.data.latitude = o_player.latitude; + msg.data.longitude = o_player.longitude; + + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + //返回数据 + if (roomserver_data.data.state == 0){ + //加入房间成功 + o_gameinfo.roomcode = _roomsvr_room.roomcode; + delete o_gameinfo.matchid; + delete o_gameinfo.matchidx; + playercount = playercount + 1; + playerbeansum = playerbeansum + o_player.bean; + playeridlist.push(o_player.playerid); + playernamelist.push(o_player.nickname); + playeravatarlist.push(o_player.avatar); + + var pack = {}; + pack.ip = o_gameinfo.match_ip; + pack.conmode = o_gameinfo.match_conmode; + pack.fromid = o_gameinfo.match_fromid; + pack.app = "youle"; + pack.route = "agent"; + pack.rpc = "self_join_room"; + pack.data = roomserver_data.data; + pack.data.matchid = o_match.id; + pack.data.matchidx = o_match.idx; + youle_agent.app.SendPack(pack); + } + } else { + var roomcode = o_game.method.new_roomcode(); + if (roomcode){ + var o_roomserver = o_game.method.get_roomserver(roomcode); + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "create_room"; + msg.data = {}; + + msg.data.agent = {}; + msg.data.agent.agentid = o_agent.agentid; + msg.data.agent.name = o_agent.agentname; + msg.data.agent.server = o_agent.server; + msg.data.agent.tcpport = o_agent.tcpport; + msg.data.agent.httpport = o_agent.httpport; + msg.data.agent.freeroom = o_agent.freeroom; + + msg.data.game = {}; + msg.data.game.gameid = o_game.gameid; + msg.data.game.name = o_game.gamename; + msg.data.game.seatcount = o_game.seatcount; + msg.data.game.makewar = o_game.makewar; + msg.data.game.modename = o_game.modename; + msg.data.game.server = o_roomserver.server; + msg.data.game.tcpport = o_roomserver.tcpport; + msg.data.game.httpport = o_roomserver.httpport; + msg.data.game.minroomcode = o_roomserver.minroomcode; + msg.data.game.maxroomcode = o_roomserver.maxroomcode; + + msg.data.player = {}; + msg.data.player.playerid = playerid; + msg.data.player.nickname = o_player.nickname; + msg.data.player.avatar = o_player.avatar; + msg.data.player.sex = o_player.sex; + msg.data.player.playertype = o_player.playertype; + msg.data.player.ip = o_gameinfo.ip; + msg.data.player.conmode = o_gameinfo.conmode; + msg.data.player.fromid = o_gameinfo.fromid; + msg.data.player.address = o_player.address; + msg.data.player.latitude = o_player.latitude; + msg.data.player.longitude = o_player.longitude; + msg.data.player.bean = o_player.bean; + + var needroomcard = youle_agent.import.createroom_needroomcard(o_game, o_match.roomtype); + //获取总局数 + var asetcount = youle_agent.import.createroom_asetcount(o_game, o_match.roomtype); + //获取豆豆下限 + var beanfloor = youle_agent.import.createroom_beanfloor(o_game, o_match.roomtype); + //获取房间类型 + var roommode = youle_agent.import.createroom_roommode(o_game, o_match.roomtype); + //获取星星场倍数 + var beanmult = youle_agent.import.createroom_beanmult(o_game, o_match.roomtype); + //不是星星场 + if (!roommode){ + beanmult = 0; + } + //玩家是否需要准备 + var needprepare = youle_agent.import.createroom_needprepare(o_game, o_match.roomtype); + //比赛场写死不需要准备 + needprepare = 0; + + msg.data.room = {}; + msg.data.room.roomcode = roomcode; + msg.data.room.roomtype = o_match.roomtype; + msg.data.room.needroomcard = needroomcard; + msg.data.room.asetcount = asetcount; + msg.data.room.infinite = 0; + msg.data.room.roommode = roommode; + msg.data.room.beanfloor = beanfloor; + msg.data.room.beanmult = beanmult; + msg.data.room.needprepare = needprepare; + msg.data.room.advanced_type= 0; + msg.data.room.advanced_teabean = 0; + msg.data.room.match_id = o_match.id; + msg.data.room.match_idx = o_match.idx; + + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + //返回数据 + if (roomserver_data.data.state == 0){ + //加入房间成功 + o_gameinfo.roomcode = roomcode; + delete o_gameinfo.matchid; + delete o_gameinfo.matchidx; + playercount = playercount + 1; + playerbeansum = playerbeansum + o_player.bean; + playeridlist.push(o_player.playerid); + playernamelist.push(o_player.nickname); + playeravatarlist.push(o_player.avatar); + + var pack = {}; + pack.ip = o_gameinfo.match_ip; + pack.conmode = o_gameinfo.match_conmode; + pack.fromid = o_gameinfo.match_fromid; + pack.app = "youle"; + pack.route = "agent"; + pack.rpc = "create_room"; + pack.data = roomserver_data.data; + pack.data.matchid = o_match.id; + pack.data.matchidx = o_match.idx; + youle_agent.app.SendPack(pack); + + var _roomsvr_agent = youle_room.agents.method.find_agent(o_agent.agentid); + var _roomsvr_game = _roomsvr_agent.method.find_game(o_game.gameid); + _roomsvr_room = _roomsvr_game.method.find_room(roomcode); + } + } + } + } + } + } + o_match.playerlist.splice(idx, 1); + } else { + o_match.playerlist.splice(idx, 1); + } + } else { + //加入机器人 + var robot_playerid = null; + for (var j = 1; j < 10; j++) { + robot_playerid = min_random(200000, 899999); + if (min_ary_indexof(playeridlist, robot_playerid) > -1){ + robot_playerid = null; + } else { + break; + } + } + if (robot_playerid == null){ + return; + } + + var idx + var robot_nickname = null; + for (var j = 1; j < 10; j++) { + idx = min_random(0, o_agent.robot_nicknamelist.length - 1); + robot_nickname = o_agent.robot_nicknamelist[idx]; + if (min_ary_indexof(playernamelist, robot_nickname) > -1){ + robot_nickname = null; + } else { + break; + } + } + if (robot_nickname == null){ + return; + } + + var robot_avatar = null; + for (var j = 1; j < 10; j++) { + idx = min_random(0, o_agent.robot_avatarlist.length - 1); + robot_avatar = o_agent.robot_avatarlist[idx]; + if (min_ary_indexof(playeravatarlist, robot_avatar) > -1){ + robot_avatar = null; + } else { + break; + } + } + if (robot_avatar == null){ + return; + } + + var robot_sex = min_random(1, 2); + var robot_ip = min_random(10, 240) + "." + min_random(10, 240) + "." + min_random(10, 240) + "." + min_random(10, 240); + var robot_bean = parseInt(playerbeansum / playercount) + min_random(100, 3000); + if (!robot_bean){ + robot_bean = 0; + } + var o_robot = cls_youle_room_player.new(robot_playerid, robot_nickname, robot_avatar, robot_sex, 0, robot_ip, 0, null, null); + o_robot.bean = robot_bean; + o_robot.isrobot = 1; + for (var j = 0; j < _roomsvr_room.seatlist.length; j++) { + if (!_roomsvr_room.seatlist[j]){ + // console.log(_roomsvr_room.roomcode + "比赛场加入机器人" + robot_playerid); + o_robot.method.enter_room(_roomsvr_room, j, 0); + if (_roomsvr_room.battlestate){ + youle_room.import.player_enter(_roomsvr_room, j); + } + youle_room.return.self_join_room({}, o_robot, robot_bean); + + playeridlist.push(o_robot.playerid); + playernamelist.push(o_robot.nickname); + playeravatarlist.push(o_robot.avatar); + + if (_roomsvr_room.needprepare){ + (function(){ + var o_robot2 = o_robot; + o_robot2.tmr_prepare = min_ontimeout( + function(){ + if (!o_robot2.gameinfo.isprepare){ + var pack = {}; + pack.app = "youle"; + pack.route = "room"; + pack.rpc = "player_prepare"; + pack.data = {}; + pack.data.agentid = o_robot2.gameinfo.o_room.o_game.o_agent.agentid; + pack.data.playerid = o_robot2.playerid; + pack.data.gameid = o_robot2.gameinfo.o_room.o_game.gameid; + pack.data.roomcode = o_robot2.gameinfo.o_room.roomcode; + youle_room.player_prepare(pack); + } + }, min_random(2000, 4000)); + })(o_robot); + } + break; + } + } + } + } + } + } + + if ((new Date()).getTime() >= (new Date(o_match.beginTime)).getTime() && + (new Date()).getTime() <= (new Date(o_match.endTime)).getTime()){ + if (!o_match.isstart){ + if (o_match.playerlist.length >= o_match.playercount || o_match.haverobot == 1){ + do_start(); + o_match.isstart = 1; + } + } else { + if (o_match.playerlist.length >= o_match.playercount2 || o_match.haverobot == 1){ + do_start(); + } + } + } else if ((new Date()).getTime() > (new Date(o_match.endTime)).getTime()){ + cls_youle_room_backup.deletebackup_match(o_match); + min_closetime(o_match.timer_start); + if (o_match.timer_add_sysgrade){ + min_closetime(o_match.timer_add_sysgrade); + } + + if (o_match.ranklist.length > 0){ + var ary_playerid = ""; + var ary_nickname = ""; + var ary_avatar = ""; + var ary_roomcode = ""; + var ary_overtime = ""; + var ary_grade = ""; + var ary_isrobot = ""; + for (var i = 0; i < o_match.ranklist.length; i++) { + if (i == 0){ + ary_playerid = o_match.ranklist[i].playerid; + ary_nickname = o_match.ranklist[i].nickname; + ary_avatar = o_match.ranklist[i].avatar; + ary_roomcode = o_match.ranklist[i].roomcode; + ary_overtime = o_match.ranklist[i].overtime; + ary_grade = o_match.ranklist[i].grade; + ary_isrobot = o_match.ranklist[i].isr; + } else { + ary_playerid = ary_playerid + "," + o_match.ranklist[i].playerid; + ary_nickname = ary_nickname + "," + o_match.ranklist[i].nickname; + ary_avatar = ary_avatar + "," + o_match.ranklist[i].avatar; + ary_roomcode = ary_roomcode + "," + o_match.ranklist[i].roomcode; + ary_overtime = ary_overtime + "," + o_match.ranklist[i].overtime; + ary_grade = ary_grade + "," + o_match.ranklist[i].grade; + ary_isrobot = ary_isrobot + "," + o_match.ranklist[i].isr; + } + }; + + var sql = "call cp_game_save_matchranking(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [o_agent.agentid, o_game.gameid, o_match.id, o_match.idx, o_match.matchname, o_match.beginTime, o_match.endTime, JSON.stringify(o_match.roomtype), o_match.imageurl, o_match.playercount, o_match.playercount2, o_match.bean, o_match.roomcard, o_match.topcount, o_match.circulation, o_match.interval, o_match.gradename, o_match.memo1, o_match.memo2, o_match.memo3, JSON.stringify(o_match.ranklist), ary_playerid, ary_nickname, ary_avatar, ary_roomcode, ary_overtime, ary_grade, ary_isrobot]); + } + + var idx = min_ary_indexof(o_match.o_matchlist.list, o_match.id, "id"); + if (idx > -1){ + o_match.o_matchlist.list.splice(idx, 1); + } + } + }, + + //随机上一个系统成绩 + add_sysgrade: function(o_match){ + if (o_match.sysgrade && o_match.sysgrade.length > 0){ + var robot_playerid = null; + for (var j = 1; j < 10; j++) { + robot_playerid = min_random(200000, 899999); + if (min_ary_indexof(o_match.ranklist, robot_playerid, "playerid") > -1){ + robot_playerid = null; + } else { + break; + } + } + if (robot_playerid == null){ + return; + } + + var o_agent = o_match.o_game.o_agent; + var robot_nickname = null; + for (var j = 1; j < 10; j++) { + var idx = min_random(0, o_agent.robot_nicknamelist.length - 1); + robot_nickname = o_agent.robot_nicknamelist[idx]; + if (min_ary_indexof(o_match.ranklist, robot_nickname, "nickname") > -1){ + robot_nickname = null; + } else { + break; + } + } + if (robot_nickname == null){ + return; + } + + var robot_avatar = null; + for (var j = 1; j < 10; j++) { + var idx = min_random(0, o_agent.robot_avatarlist.length - 1); + robot_avatar = o_agent.robot_avatarlist[idx]; + if (min_ary_indexof(o_match.ranklist, robot_avatar, "avatar") > -1){ + robot_avatar = null; + } else { + break; + } + } + if (robot_avatar == null){ + return; + } + + var rand = min_random(0, o_match.sysgrade.length - 1); + var robot_grade = min_random(o_match.sysgrade[rand].min, o_match.sysgrade[rand].max); + + var o_sysplayer = {}; + o_sysplayer.playerid = robot_playerid; + o_sysplayer.nickname = robot_nickname; + o_sysplayer.avatar = robot_avatar; + o_sysplayer.roomcode = min_random(100000, 999999); + o_sysplayer.overtime = min_now(); + o_sysplayer.grade = robot_grade; + o_sysplayer.isr = 2; + + var isfound = false; + for (var j = 0; j < o_match.ranklist.length; j++) { + if (o_sysplayer.grade > o_match.ranklist[j].grade){ + o_match.ranklist.splice(j, 0, o_sysplayer); + if (o_match.ranklist.length > parseInt(o_match.topcount)){ + o_match.ranklist.length = parseInt(o_match.topcount); + } + isfound = true; + break; + } + } + if (!isfound){ + if (o_match.ranklist.length < parseInt(o_match.topcount)){ + o_match.ranklist.push(o_sysplayer); + } + } + + o_match.sysgrade[rand].count = o_match.sysgrade[rand].count - 1; + if (o_match.sysgrade[rand].count == 0){ + o_match.sysgrade.splice(rand, 1); + } + + cls_youle_room_backup.backup_match(o_match); + + if (o_match.sysgrade.length == 0){ + if (o_match.timer_add_sysgrade){ + min_closetime(o_match.timer_add_sysgrade); + o_match.timer_add_sysgrad = null; + } + } + } + }, + + //开启系统成绩定时器 + start_add_sysgrade_timer: function(o_match){ + if (o_match.sysgrade && !o_match.timer_add_sysgrade){ + var sum_count = 0; + for (var i = 0; i < o_match.sysgrade.length; i++) { + sum_count = sum_count + o_match.sysgrade[i].count; + }; + var interval = (new Date(o_match.endTime)).getTime() - (new Date()).getTime(); + if (interval > 0 && sum_count > 0){ + interval = parseInt(interval / sum_count); + o_match.timer_add_sysgrade = min_ontime(o_match.method.add_sysgrade, interval); + } + } + } +} + +/////////////////////////////////////////////////////////////// +/////////// cls_youle_agent_matchlist: 比赛列表 ////////// +/////////////////////////////////////////////////////////////// +var cls_youle_agent_matchlist = cls_youle_agent_matchlist || { + //构造函数 + new: function(o_game) { + + var matchlist = {}; + matchlist.o_game = o_game; + matchlist.list = []; + + matchlist.method = {}; + //载入一个比赛 + matchlist.method.load_match = function(o_setting){ + return cls_youle_agent_matchlist.load_match(matchlist, o_setting); + }; + + //查找一个比赛 + matchlist.method.find_match = function(id, idx){ + return cls_youle_agent_matchlist.find_match(matchlist, id, idx); + }; + + return matchlist; + }, + + //载入一个比赛 + load_match: function(o_matchlist, o_setting){ + var o_match; + var idx = min_ary_indexof(o_matchlist.list, o_setting.id, "id"); + if (idx == -1) { + o_match = cls_youle_agent_match.new(o_matchlist.o_game, o_matchlist, o_setting); + if (o_match){ + o_matchlist.list.push(o_match); + } + } else { + o_match = o_matchlist.list[idx]; + } + return o_match; + }, + + //查找一个比赛 + find_match: function(o_matchlist, id, idx){ + var i = min_ary_indexof(o_matchlist.list, id, "id"); + if (i > -1){ + if (o_matchlist.list[i].idx == idx){ + return o_matchlist.list[i]; + } + } + return null; + } +} + + diff --git a/server/youle/server_agent/class.player.js b/server/youle/server_agent/class.player.js new file mode 100644 index 0000000..f7b6119 --- /dev/null +++ b/server/youle/server_agent/class.player.js @@ -0,0 +1,426 @@ +/////////////////////////////////////////////////// +///////// cls_youle_agent_player: 玩家 ///////// +/////////////////////////////////////////////////// +var cls_youle_agent_player = cls_youle_agent_player || { + //构造函数 + new: function(o_agent, playerid, roomcard, bean, openid, unionid, nickname, avatar, sex, playertype, invitecode) { + var player = {}; + //基本信息 + player.o_agent = o_agent; //代理商 + player.playerid = playerid; //玩家id + player.roomcard = roomcard; //房卡数量 + player.bean = bean; //星星数量 + player.openid = openid; + player.unionid = unionid; + player.nickname = nickname; + player.avatar = avatar; + player.sex = sex; + player.playertype = playertype; //0:普通用户 1:vip开房不扣房卡 + player.invitecode = invitecode; //绑定的邀请码 + player.freetimer = null; //释放玩家的定时器 + //登录的游戏列表 + player.gameinfo = []; + + //方法 + player.method = {}; + + //登录游戏 + player.method.login_game = function(o_game, ip, conmode, fromid, machineid, machineroom){ + cls_youle_agent_player.login_game(player, o_game, ip, conmode, fromid, machineid, machineroom); + } + + //获取玩家某个游戏的游戏信息 + player.method.get_gameinfo = function(o_game){ + return cls_youle_agent_player.get_gameinfo(player, o_game); + } + + //修改玩家房卡数量 + player.method.update_roomcard = function(type, newroomcrad, change, memo){ + cls_youle_agent_player.update_roomcard(player, type, newroomcrad, change, memo); + } + + //修改玩家豆豆数量 + player.method.update_bean = function(type, newbean, change, push, showrebate){ + cls_youle_agent_player.update_bean(player, type, newbean, change, push, showrebate); + } + + //打开释放玩家的定时器 + player.method.open_free_timer = function(){ + cls_youle_agent_player.open_free_timer(player); + } + + //关闭释放玩家的定时器 + player.method.close_free_timer = function(){ + cls_youle_agent_player.close_free_timer(player); + } + + return player; + }, + + //登录游戏 + login_game: function(o_player, o_game, ip, conmode, fromid, machineid, machineroom){ + if (o_player.gameinfo.length <= o_game.idx) { + o_player.gameinfo.length = o_game.idx + 1; + } + var _game_info = o_player.gameinfo[o_game.idx]; + if (!_game_info){ + //初始化游戏信息 + _game_info = {}; + _game_info.conmode = conmode; + _game_info.fromid = fromid; + _game_info.fromidtime = (new Date()).getTime(); + _game_info.ip = ip; + _game_info.machineid = machineid; + _game_info.machineroom = machineroom; + _game_info.roomcode = null; + o_player.gameinfo[o_game.idx] = _game_info; + //统计在线玩家数 + o_game.method.online_p_chang(1); + } else { + //踢原有连接下线 + // if (fromid && o_game.gameid != "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO"){ + if (fromid){ + if (_game_info.conmode == "tcp" && _game_info.fromid && _game_info.fromid != fromid){ + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.routename; + msg.rpc = "kick_offline"; + msg.conmode = _game_info.conmode; + msg.fromid = _game_info.fromid; + msg.data={}; + msg.data.fromOther = 0; + youle_agent.app.SendPack(msg); + + //测试用 + // var agent_kick_offline = {}; + // agent_kick_offline.agentid = o_game.o_agent.agentid; + // agent_kick_offline.gameid = o_game.gameid; + // agent_kick_offline.playerid = o_player.playerid; + // agent_kick_offline.oldfromid = _game_info.fromid; + // agent_kick_offline.oldtime = _game_info.fromidtime; + // agent_kick_offline.oldmachineid = _game_info.machineid; + // agent_kick_offline.oldmachineroom = _game_info.machineroom; + // agent_kick_offline.newfromid = fromid; + // agent_kick_offline.newtime = (new Date()).getTime(); + // agent_kick_offline.newmachineid = machineid; + // agent_kick_offline.newmachineroom = machineroom; + // console.log(min_now() + " agentserver kick_offline"); + // console.log(agent_kick_offline); + } + + //更新游戏信息 + _game_info.conmode = conmode; + _game_info.fromid = fromid; + _game_info.fromidtime = (new Date()).getTime(); + _game_info.ip = ip; + } + } + + //更新tcp连接列表 + if (conmode == "tcp"){ + youle_agent.tcps.method.load_tcp(fromid, o_player, o_game.idx); + } + }, + + //获取玩家某个游戏的游戏信息 + get_gameinfo: function(o_player, o_game){ + if (o_player.gameinfo.length <= o_game.idx){ + return null; + } + return o_player.gameinfo[o_game.idx]; + }, + + //修改玩家房卡数量 + update_roomcard: function(o_player, type, newroomcard, change, memo){ + var roomCardName = youle_agent.app.youle_platform.config.method.get_paravalue("roomCardName", o_player.o_agent.agentid); + if (!roomCardName){ + roomCardName = "房卡"; + } + o_player.roomcard = newroomcard; + //通知游戏前端 + for (var i = 0; i < o_player.gameinfo.length; i++) { + var _game_info = o_player.gameinfo[i]; + if (_game_info){ + if (_game_info.roomcode){ + //获取房号对应的房间服务器地址 + var o_game = o_player.o_agent.games[i]; + if (!o_game) { + continue; + } + var o_roomserver = o_game.method.get_roomserver(_game_info.roomcode); + + //通过房间服务器发送修改房卡的消息 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "update_roomcard"; + msg.data = {}; + msg.data.agentid = o_player.o_agent.agentid; + msg.data.playerid = o_player.playerid; + msg.data.gameid = o_game.gameid; + msg.data.roomcode = _game_info.roomcode; + msg.data.roomcard = newroomcard; + // msg.data.type = type; + // msg.data.change = change; + switch(type){ + case 0: + msg.data.text = "扣除" + roomCardName + ":" + change; + if (memo){ + msg.data.text = memo + msg.data.text; + } + break; + case 1: + msg.data.text = "购买" + roomCardName + ":" + change; + break; + case 2: + msg.data.text = roomCardName + "充值:" + change; + break; + case 3: + msg.data.text = "领取" + roomCardName + "奖励:" + change; + break; + case 4: + msg.data.text = "修改" + roomCardName + ":" + change; + break; + } + + if (o_roomserver.server == o_player.o_agent.server){ + //房间服务器与代理商服务器是同一地址 + youle_agent.app.youle_room[msg.rpc](msg); + msg.data.issameserver = 1; + } else { + //房间服务器与代理商服务器不是同一地址 + msg.data.issameserver = 0; + var cfg = {}; + cfg.url = "http://" + o_roomserver.server + ":" + o_roomserver.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){}; + cfg.error = function(str){}; + min_http(cfg); + } + } else if (_game_info.conmode && _game_info.fromid){ + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.routename; + msg.rpc = "update_roomcard"; + msg.conmode = _game_info.conmode; + msg.fromid = _game_info.fromid; + msg.data = {}; + msg.data.roomcard = newroomcard; + // msg.data.type = type; + // msg.data.change = change; + switch(type){ + case 0: + msg.data.text = "扣除" + roomCardName + ":" + change; + if (memo){ + msg.data.text = memo + msg.data.text; + } + break; + case 1: + msg.data.text = "购买" + roomCardName + ":" + change; + break; + case 2: + msg.data.text = roomCardName + "充值:" + change; + break; + case 3: + msg.data.text = "领取" + roomCardName + "奖励:" + change; + break; + case 4: + msg.data.text = "修改" + roomCardName + ":" + change; + break; + } + youle_agent.app.SendPack(msg); + } + } + } + }, + + //修改玩家豆豆数量 + update_bean: function(o_player, type, newbean, change, push, showrebate){ + o_player.bean = parseInt(newbean); + var starName = youle_agent.app.youle_platform.config.method.get_paravalue("starName", o_player.o_agent.agentid); + if (!starName){ + starName = "星星"; + } + //通知游戏前端 + for (var i = 0; i < o_player.gameinfo.length; i++) { + var _game_info = o_player.gameinfo[i]; + if (_game_info){ + if (_game_info.roomcode){ + //获取房号对应的房间服务器地址 + var o_game = o_player.o_agent.games[i]; + var o_roomserver = o_game.method.get_roomserver(_game_info.roomcode); + + //通过房间服务器发送修改星星的消息 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = o_player.o_agent.agentid; + msg.data.playerid = o_player.playerid; + msg.data.gameid = o_game.gameid; + msg.data.roomcode = _game_info.roomcode; + msg.data.bean = parseInt(newbean); + msg.data.type = type; + switch(type){ + case 0: //游戏输赢豆豆 + var _text = ""; + if (!showrebate){ + if (change > 0){ + _text = "游戏赢取" + starName + ":" + change; + } else if (change < 0) { + _text = "游戏扣除" + starName + ":" + change; + } + var _showrebate_setting = youle_agent.app.youle_platform.config.method.get_paravalue("shieldShowRebate", o_player.o_agent.agentid); + if (!_showrebate_setting){ + if (push > 0){ + _text = _text + " 获取" + starName + "房间费:" + push; + } else if (push < 0) { + _text = _text + " 扣除" + starName + "房间费:" + push; + } + } + } else if (showrebate == 1){ + if (change + push > 0){ + _text = "游戏赢取" + starName + ":" + (change + push); + } else if (change + push < 0) { + _text = "游戏扣除" + starName + ":" + (change + push); + } + } else if (showrebate == 2){ + var _showrebate_setting = youle_agent.app.youle_platform.config.method.get_paravalue("shieldShowRebate", o_player.o_agent.agentid); + if (!_showrebate_setting){ + if(push){ + _text = "扣除房间费:" + push + starName; + } + } + } + msg.data.text = _text; + break; + case 1: + msg.data.text = "购买" + starName + ":" + change; + break; + case 2: + msg.data.text = starName + "充值:" + change; + break; + case 3: + break; + case 4: + // msg.data.text = "扣除发送系统消息费用:" + change + starName; + msg.data.text=""; + break; + case 5: + msg.data.text = "扣除比赛报名费用:" + push + starName; + break; + //需要语音提示的房主抽成金币到账 + case 6: + //msg.data.text = "扣除比赛报名费用:" + push + starName; + break; + case 7: + if(change > 0){ + msg.data.text = "获赠" + change + starName; + }else{ + msg.data.text = "送出" + Math.abs(change) + starName; + } + break; + } + + if (o_roomserver.server == o_player.o_agent.server){ + //房间服务器与代理商服务器是同一地址 + youle_agent.app.youle_room[msg.rpc](msg); + } else { + //房间服务器与代理商服务器不是同一地址 + var cfg = {}; + cfg.url = "http://" + o_roomserver.server + ":" + o_roomserver.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){}; + cfg.error = function(str){}; + min_http(cfg); + } + } else if (_game_info.conmode && _game_info.fromid){ + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.routename; + msg.rpc = "update_bean"; + msg.conmode = _game_info.conmode; + msg.fromid = _game_info.fromid; + msg.data = {}; + msg.data.bean = parseInt(newbean); + msg.data.type=type; + switch(type){ + case 0: //游戏输赢豆豆 + var _text = ""; + if (!showrebate){ + if (change > 0){ + _text = "游戏赢取" + starName + ":" + change; + } else if (change < 0) { + _text = "游戏扣除" + starName + ":" + change; + } + var _showrebate_setting = youle_agent.app.youle_platform.config.method.get_paravalue("shieldShowRebate", o_player.o_agent.agentid); + if(!_showrebate_setting){ + if (push > 0){ + _text = _text + " 获取" + starName + "房间费:" + push; + } else if (push < 0) { + _text = _text + " 扣除" + starName + "房间费:" + push; + } + } + } else if (showrebate == 1){ + if (change + push > 0){ + _text = "游戏赢取" + starName + ":" + (change + push); + } else if (change + push < 0) { + _text = "游戏扣除" + starName + ":" + (change + push); + } + } else if (showrebate == 2){ + var _showrebate_setting = youle_agent.app.youle_platform.config.method.get_paravalue("shieldShowRebate", o_player.o_agent.agentid); + if (!_showrebate_setting){ + if(push){ + _text = "扣除房间费:" + push + starName; + } + } + } + msg.data.text = _text; + break; + case 1: + msg.data.text = "购买" + starName + ":" + change; + break; + case 2: + msg.data.text = starName + "充值:" + change; + break; + case 3: + msg.data.text = "领取" + starName +"奖励:" + change; + break; + case 7: + if(change > 0){ + msg.data.text = "获赠" + change + starName; + }else{ + msg.data.text = "送出" + Math.abs(change) + starName; + } + break; + } + youle_agent.app.SendPack(msg); + } + } + } + }, + + //打开释放玩家的定时器 + open_free_timer: function(o_player){ + var do_freeplayer = function(){ + o_player.freetimer = null; + o_player.o_agent.players[o_player.playerid - 100001] = null; + } + + if (!o_player.advanced){ + //4小时后释放玩家 + o_player.freetimer = min_ontimeout(do_freeplayer, 7 * 24 * 60 * 60 * 1000); + } + }, + + //关闭释放玩家的定时器 + close_free_timer: function(o_player){ + if (o_player.freetimer){ + min_closetime(o_player.freetimer); + o_player.freetimer = null; + } + } +} diff --git a/server/youle/server_agent/class.tcplist.js b/server/youle/server_agent/class.tcplist.js new file mode 100644 index 0000000..e1279b2 --- /dev/null +++ b/server/youle/server_agent/class.tcplist.js @@ -0,0 +1,120 @@ +/////////////////////////////////////////////////// +//////////////cls_youle_agent_tcplist: tcp连接列表类/////////// +/////////////////////////////////////////////////// +var cls_youle_agent_tcplist = cls_youle_agent_tcplist || { + new: function() { + var tcplist = {}; + tcplist.list = []; + + //方法 + tcplist.method = {}; + + //载入tcp连接 + tcplist.method.load_tcp = function(str_tcpid, o_player, gameidx){ + return cls_youle_agent_tcplist.load_tcp(tcplist, str_tcpid, o_player, gameidx); + } + + //查找tcp连接 + tcplist.method.find_tcp = function(str_tcpid){ + return cls_youle_agent_tcplist.find_tcp(tcplist, str_tcpid); + } + + //删除tcp连接 + tcplist.method.delete_tcp = function(str_tcpid){ + return cls_youle_agent_tcplist.delete_tcp(tcplist, str_tcpid); + } + + return tcplist; + }, + + //载入tcp连接 + load_tcp: function(o_tcplsit, str_tcpid, o_player, gameidx){ + var idx = cls_youle_agent_tcplist.aryidx(str_tcpid); + if (idx == -1){ + return null; + } + if (o_tcplsit.list.length <= idx){ + o_tcplsit.list.length = idx + 1; + } + + var old_tcp = o_tcplsit.list[idx]; + if (old_tcp){ + var _player = old_tcp.o_player; + if (_player.playerid != o_player.playerid){ + var _gameidx = old_tcp.gameidx; + if (_player.gameinfo[_gameidx]){ + if (_player.gameinfo[_gameidx].roomcode){ + _player.gameinfo[_gameidx].ip = null; + _player.gameinfo[_gameidx].conmode = null; + _player.gameinfo[_gameidx].fromid = null; + } else { + //如果不在房间内 + _player.gameinfo[_gameidx] = null; + //判断是否要释放玩家 + var freeflag = true; + for (var m = 0; m < _player.gameinfo.length; m++) { + if (_player.gameinfo[m]){ + freeflag = false; + break; + } + } + if (freeflag){ + // _player.o_agent.players[_player.playerid - 100001] = null; + _player.method.open_free_timer(); + } + + //统计在线玩家数 + var o_game = _player.o_agent.games[_gameidx]; + if (o_game){ + o_game.method.online_p_chang(-1); + } + } + } + } + } + + var new_tcp = {}; + new_tcp.tcpid = str_tcpid; + new_tcp.o_player = o_player; + new_tcp.gameidx = gameidx; + o_tcplsit.list[idx] = new_tcp; + o_player.method.close_free_timer(); + return new_tcp; + }, + + //查找tcp连接 + find_tcp: function(o_tcplsit, str_tcpid){ + var idx = cls_youle_agent_tcplist.aryidx(str_tcpid); + if (idx == -1){ + return null; + } + if (o_tcplsit.list.length <= idx){ + return null; + } + return o_tcplsit.list[idx]; + }, + + //删除tcp连接 + delete_tcp: function(o_tcplsit, str_tcpid){ + var idx = cls_youle_agent_tcplist.aryidx(str_tcpid); + if (idx == -1){ + return; + } + if (o_tcplsit.list.length <= idx){ + return; + } + o_tcplsit.list[idx] = null; + }, + + //计算tcp连接的数组下标 + aryidx: function(str_tcpid){ + var idx = parseInt(str_tcpid.replace("tcp_", "")); + if (isNaN(idx)){ + return -1; + } + return idx - 10000; + } +} + +//tcp连接列表 +youle_agent.tcps = cls_youle_agent_tcplist.new(); \ No newline at end of file diff --git a/server/youle/server_agent/mod.js b/server/youle/server_agent/mod.js new file mode 100644 index 0000000..3119783 --- /dev/null +++ b/server/youle/server_agent/mod.js @@ -0,0 +1,854 @@ +////////////////////////////////////////////////////////////////// +//////////////////////////游戏平台服务器////////////////////////// +////////////////////////////////////////////////////////////////// +//rpc +min_loadJsFile("youle/server_agent/rpc.js", function(){ +//tcp连接 +min_loadJsFile("youle/server_agent/class.tcplist.js", function(){ +//运营商 +min_loadJsFile("youle/server_agent/class.agent.js", function(){ +//游戏 +min_loadJsFile("youle/server_agent/class.game.js", function(){ +//玩家 +min_loadJsFile("youle/server_agent/class.player.js", function(){ +//比赛 +min_loadJsFile("youle/server_agent/class.match.js", function(){ +//对内的输入接口类 +min_loadJsFile("youle/server_agent/class.import.js", function(){ +//对内的输出接口类 +min_loadJsFile("youle/server_agent/class.export.js", function(){ + +}); +}); +}); +}); +}); +}); +}); +}); + +var youle_agent = youle_agent || cls_mod.new("youle_agent", "agent", youle_app); + +//运营商列表 +youle_agent.agents = null; +//tcp连接 +youle_agent.tcps = null; +//mysql操作类 +youle_agent.mysql = cls_mysql.new("http://ylyxdata.daoqi88.cn/mysql.php", youle_agent.app.SendPack); +youle_agent.mysql_grade = cls_mysql.new("http://ylyxdata.daoqi88.cn/mysql_grade.php", youle_agent.app.SendPack); +youle_agent.mysql_agent = cls_mysql.new("http://ylyxdata.daoqi88.cn/mysql_agent.php", youle_agent.app.SendPack); +//对内的输入接口类(需要的外部接口) +youle_agent.import = null; +//对内输出接口 +youle_agent.export = null; +//比赛 +youle_agent.matchs = []; +//服务器id +youle_agent.youleid = min_randomChar(32); +//手机验证码列表 +youle_agent.phonecodes = []; + + + +var blacknamelist = [ +"oLVKis2bsKkiHY1435uNiBwJaz7w", +"oLVKisy7GuH1C4emgrF8A2FVcumk", //112475,00bA05haB0d9ZC0fwGD09Q2OA30insbQ,薄情少年葬空城 +"oLVKis_nxA6uXlHvUaFK-73WZZms", //111771,00bA05haB0d9ZC0fwGD09Q2OA30insbQ,古月 +"oLVKiszvCtvX02MbiWYZju2ilb4s", //112289,00bA05haB0d9ZC0fwGD09Q2OA30insbQ,么么哒 +"oLVKisx4CHjrLLNyaffo3kPmfejo", //113119,00bA05haB0d9ZC0fwGD09Q2OA30insbQ,情售.! +"oLVKisy9uOS-VRWNEt1gaPrZuIhI", //114204,00bA05haB0d9ZC0fwGD09Q2OA30insbQ,痛彻心扉 +"oLVKis-lZIxgdWSmMeBTCmB6IyD8", //113790,00bA05haB0d9ZC0fwGD09Q2OA30insbQ,百合 +"oLVKis5ctLEyEYn3_2l0e0QGtM0A", //114522,00bA05haB0d9ZC0fwGD09Q2OA30insbQ,饿了又胖了 +"oLVKis3LxiKbR5TopsKwUagsOqcQ" //114248,00bA05haB0d9ZC0fwGD09Q2OA30insbQ,奔波儿霸 +]; + +//获取星星争霸场玩家房间数量 +youle_agent.get_share_room_count = 50; +// //代理后台2.0接口输出日志标志 +// youle_agent.showlog_readdata = 0; +// //代理后台2.0接口定时器 +// youle_agent.readdata_timer = min_ontime( +// function(){ +// //充房卡充星星 +// var update_gamedb_roomcard = function(agentid, playerid, changeroomcard, changebean, opttype, optid){ +// var db_callback_game1 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// switch(parseInt(opttype)){ +// case 1: //购买房卡 +// youle_agent.export.buy_topup(agentid, playerid, 1, parseInt(dbdata[0].play_roomcard), changeroomcard); +// log.m = "玩家购卡成功"; +// break; +// case 2: //代理转卡 +// youle_agent.export.buy_topup(agentid, playerid, 2, parseInt(dbdata[0].play_roomcard), changeroomcard); +// log.m = "代理充卡成功"; +// break; +// case 3: //索要房卡 +// youle_agent.export.buy_topup(agentid, playerid, 2, parseInt(dbdata[0].play_roomcard), changeroomcard); +// log.m = "玩家索卡成功"; +// break; +// case 11: //购买星星 +// youle_agent.export.buy_topup_bean(agentid, playerid, 1, parseInt(dbdata[0].play_bean), changebean); +// log.m = "玩家购买星星成功"; +// break; +// case 12: //代理转星星 +// youle_agent.export.buy_topup_bean(agentid, playerid, 2, parseInt(dbdata[0].play_bean), changebean); +// log.m = "代理充值星星成功"; +// break; +// case 13: //索要星星 +// youle_agent.export.buy_topup_bean(agentid, playerid, 2, parseInt(dbdata[0].play_bean), changebean); +// log.m = "玩家索要星星成功"; +// break; +// } + +// log.id = optid; +// log.type = opttype; +// log.agentid = agentid; +// log.playerid = playerid; +// log.changeroomcard = changeroomcard; +// log.changebean = changebean; +// log.newroomcard = dbdata[0].play_roomcard; +// log.newbean = dbdata[0].play_bean; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// switch(parseInt(opttype)){ +// case 1: //购买房卡 +// log.m = "玩家购卡失败"; +// break; +// case 2: //代理转卡 +// log.m = "代理充卡失败"; +// break; +// case 3: //索要房卡 +// log.m = "玩家索卡失败"; +// break; +// case 11: //购买星星 +// log.m = "玩家购买星星失败"; +// break; +// case 12: //代理转星星 +// log.m = "代理充值星星失败"; +// break; +// case 13: //索要星星 +// log.m = "玩家索要星星失败"; +// break; +// } + +// log.id = optid; +// log.type = opttype; +// log.agentid = agentid; +// log.playerid = playerid; +// log.changeroomcard = changeroomcard; +// log.changebean = changebean; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var sql_game = "call cp_sales2_player_topup(?,?,?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, playerid, changeroomcard, changebean], db_callback_game1); +// } +// //玩家绑定邀请码 +// var update_gamedb_bindinginvitecode = function(agentid, unionid, invitecode, optid){ +// var db_callback_game2 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "玩家绑定邀请码成功"; +// log.id = optid; +// log.agentid = agentid; +// log.unionid = unionid; +// log.invitecode = invitecode; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "玩家绑定邀请码失败"; +// log.id = optid; +// log.agentid = agentid; +// log.unionid = unionid; +// log.invitecode = invitecode; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var sql_game = "call cp_sales2_player_bindinginvitecode(?,?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, unionid, invitecode], db_callback_game2); +// } +// //新代理 +// var update_gamedb_newsalesman = function(agentid, channelid, salesid, salesinfo, optid){ +// var db_callback_game3 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "新增代理成功"; +// log.id = optid; +// log.agentid = agentid; +// log.salesid = salesid; +// log.salesinfo = salesinfo; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "新增代理失败"; +// log.id = optid; +// log.agentid = agentid; +// log.salesid = salesid; +// log.salesinfo = salesinfo; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var o_salesinfo = JSON.parse(salesinfo); +// var sql_game = "call cp_sales2_new_salesman(?,?,?,?,?,?,?,?,?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, channelid, o_salesinfo.openid, o_salesinfo.unionid, o_salesinfo.nickname, o_salesinfo.avatar, o_salesinfo.sex, o_salesinfo.province, o_salesinfo.city, salesid], db_callback_game3); +// } +// //封禁代理 +// var update_gamedb_disabledsales = function(agentid, salesid, optid){ +// var db_callback_game4 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "封禁代理成功"; +// log.id = optid; +// log.agentid = agentid; +// log.salesid = salesid; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "封禁代理失败"; +// log.id = optid; +// log.agentid = agentid; +// log.salesid = salesid; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var sql_game = "call cp_sales2_salesman_disabled(?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, salesid], db_callback_game4); +// } +// //解封代理 +// var update_gamedb_abledsales = function(agentid, salesid, optid){ +// var db_callback_game5 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "解封代理成功"; +// log.id = optid; +// log.agentid = agentid; +// log.salesid = salesid; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "解封代理失败"; +// log.id = optid; +// log.agentid = agentid; +// log.salesid = salesid; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var sql_game = "call cp_sales2_salesman_abled(?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, salesid], db_callback_game5); +// } +// //解绑代理 +// var update_gamedb_disbinding_sales = function(agentid, salesid, optid){ +// var db_callback_game6 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "解绑代理成功"; +// log.id = optid; +// log.agentid = agentid; +// log.salesid = salesid; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "解绑代理失败"; +// log.id = optid; +// log.agentid = agentid; +// log.salesid = salesid; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var sql_game = "call cp_sales2_disbinding_sales(?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, salesid], db_callback_game6); +// } +// //解绑玩家 +// var update_gamedb_disbinding_player = function(agentid, playerid, optid){ +// var db_callback_game7 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "解绑玩家成功"; +// log.id = optid; +// log.agentid = agentid; +// log.playerid = playerid; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var _o_agent = youle_agent.agents.method.find_agent(agentid); +// if (_o_agent){ +// _o_player = _o_agent.method.find_player(playerid); +// if (_o_player){ +// _o_player.invitecode = null; +// } +// } + +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "解绑玩家失败"; +// log.id = optid; +// log.agentid = agentid; +// log.playerid = playerid; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var sql_game = "call cp_sales2_disbinding_player(?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, playerid], db_callback_game7); +// } +// //封禁玩家 +// var update_gamedb_disabledplayer = function(agentid, playerid, optid){ +// var db_callback_game8 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "封禁玩家成功"; +// log.id = optid; +// log.agentid = agentid; +// log.playerid = playerid; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var _o_agent = youle_agent.agents.method.find_agent(agentid); +// if (_o_agent){ +// _o_player = _o_agent.method.find_player(playerid); +// if (_o_player){ +// _o_player.state = 1; +// } +// } +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "封禁玩家失败"; +// log.id = optid; +// log.agentid = agentid; +// log.playerid = playerid; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var sql_game = "call cp_sales2_player_disabled(?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, playerid], db_callback_game8); +// } +// //解封玩家 +// var update_gamedb_abledplayer = function(agentid, playerid, optid){ +// var db_callback_game9 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "解封玩家成功"; +// log.id = optid; +// log.agentid = agentid; +// log.playerid = playerid; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var _o_agent = youle_agent.agents.method.find_agent(agentid); +// if (_o_agent){ +// _o_player = _o_agent.method.find_player(playerid); +// if (_o_player){ +// _o_player.state = 0; +// } +// } +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "解封玩家失败"; +// log.id = optid; +// log.agentid = agentid; +// log.playerid = playerid; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var sql_game = "call cp_sales2_player_abled(?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, playerid], db_callback_game9); +// } +// //开通高新选项 +// var update_gamedb_advanced = function(agentid, playerid, shortcode, advanced, desone, destwo, optid){ +// var db_callback_game10 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "设置玩家短号成功"; +// log.id = optid; +// log.agentid = agentid; +// log.playerid = playerid; +// log.shortcode = shortcode; +// log.advanced = advanced; +// log.desone = desone; +// log.destwo = destwo; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var o_agent = youle_agent.agents.method.find_agent(agentid); +// if (o_agent) { +// var o_player = o_agent.method.find_player(playerid); +// if (o_player){ +// if (o_player.shortcode){ +// o_agent.shortcodes[o_player.shortcode] = null; +// } +// o_player.advanced = _advanced; +// o_player.shortcode = _shortcode; +// o_player.desone = desone; +// o_player.destwo = destwo; +// } +// if (o_agent.shortcodes[_shortcode]){ +// var old_playerid = o_agent.shortcodes[_shortcode].playerid; +// if (old_playerid != playerid){ +// var o_oldplayer = o_agent.method.find_player(old_playerid); +// if (o_oldplayer){ +// o_oldplayer.advanced = 0; +// o_oldplayer.shortcode = null; +// o_oldplayer.roomcodes = []; +// o_oldplayer.desone = null; +// o_oldplayer.destwo = null; +// } +// } +// } +// var o_shortcode = {}; +// o_shortcode.shortcode = _shortcode; +// o_shortcode.playerid = playerid; +// o_agent.shortcodes[_shortcode] = o_shortcode; +// } +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "设置玩家短号失败"; +// log.id = optid; +// log.agentid = agentid; +// log.playerid = playerid; +// log.shortcode = shortcode; +// log.advanced = advanced; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } +// var _shortcode = parseInt(shortcode); +// var _advanced = parseInt(advanced); +// var sql_game = "call cp_game_player_advanced(?,?,?,?,?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, playerid, _advanced, _shortcode, desone, destwo], db_callback_game10); +// } +// //设置短号白名单 +// var update_gamedb_whitelist = function(agentid, playerid, whitelist, optid){ +// var db_callback_game11 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "设置短号白名单成功"; +// log.id = optid; +// log.agentid = agentid; +// log.playerid = playerid; +// log.whitelist = whitelist; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var o_agent = youle_agent.agents.method.find_agent(agentid); +// if (o_agent) { +// var o_player = o_agent.method.find_player(playerid); +// if (o_player){ +// o_player.whitelist = whitelist; +// } +// } +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "设置短号白名单失败"; +// log.id = optid; +// log.agentid = agentid; +// log.playerid = playerid; +// log.whitelist = whitelist; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var sql_game = "call cp_game_player_whitelist(?,?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, playerid, JSON.stringify(whitelist)], db_callback_game11); +// } +// //系统抽成 +// var update_gamedb_coinrebate = function(agentid, mode1, set1, mode2, set2, optid){ +// var db_callback_game12 = function(dbdata){ +// if (dbdata[0].result == 0){ +// var log = {}; +// log.m = "设置星星场系统抽成成功"; +// log.id = optid; +// log.agentid = agentid; +// log.mode1 = mode1; +// log.set1 = set1; +// log.mode2 = mode2; +// log.set2 = set2; +// log.time = min_now(); +// if (youle_agent.showlog_readdata){ +// console.log(log); +// } + +// var o_agent = youle_agent.agents.method.find_agent(agentid); +// if (o_agent) { +// o_agent.rebate_mode1 = mode1; +// o_agent.rebate_set1 = set1; +// o_agent.rebate_mode2 = mode2; +// o_agent.rebate_set2 = set2; +// } +// var sql_agent = "update ct_user_process_log set is_process = is_process + 1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:1"); +// } +// }); +// } else { +// var log = {}; +// log.m = "设置星星场系统抽成失败"; +// log.id = optid; +// log.agentid = agentid; +// log.mode1 = mode1; +// log.set1 = set1; +// log.mode2 = mode2; +// log.set2 = set2; +// log.time = min_now(); +// log.dbdata = dbdata; +// console.log(log); + +// var sql_agent = "update ct_user_process_log set is_process = -1 where id = ?"; +// youle_agent.mysql_agent.execsql("execsql", sql_agent, [optid], +// function(){ +// youle_agent.readdata_isdoing = 0; +// console.log(min_now() + "修改状态成功 optid:" + optid + ", is_process:-1"); +// }); +// } +// } + +// var sql_game = "call cp_game_update_coinrebate(?,?,?,?,?)"; +// youle_agent.mysql.execsql("opensql", sql_game, [agentid, mode1, set1, mode2, set2], db_callback_game12); +// } + +// var db_callback_agent = function(dbdata){ +// if (youle_agent.showlog_readdata){ +// console.log(min_now() + "读取成功"); +// console.log(dbdata); +// } + +// for (var i = 0; i < dbdata.length; i++){ +// try{ +// var o_agent = youle_agent.agents.method.find_agent(dbdata[i].to_agent); +// if (!o_agent){ +// continue; +// } +// if (!o_agent.playerlogin){ +// continue; +// } + +// switch(parseInt(dbdata[i].oper_type)){ +// case 1: //购买房卡 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_roomcard(dbdata[i].to_agent, dbdata[i].to_user, dbdata[i].oper_data, null, dbdata[i].oper_type, dbdata[i].id); +// break; +// case 2: //代理转卡 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_roomcard(dbdata[i].to_agent, dbdata[i].to_user, dbdata[i].oper_data, null, dbdata[i].oper_type, dbdata[i].id); +// break; +// case 3: //索要房卡 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_roomcard(dbdata[i].to_agent, dbdata[i].to_user, dbdata[i].oper_data, null, dbdata[i].oper_type, dbdata[i].id); +// break; +// case 11: //购买星星 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_roomcard(dbdata[i].to_agent, dbdata[i].to_user, null, dbdata[i].oper_data, dbdata[i].oper_type, dbdata[i].id); +// break; +// case 12: //代理转星星 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_roomcard(dbdata[i].to_agent, dbdata[i].to_user, null, dbdata[i].oper_data, dbdata[i].oper_type, dbdata[i].id); +// break; +// case 13: //索要星星 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_roomcard(dbdata[i].to_agent, dbdata[i].to_user, null, dbdata[i].oper_data, dbdata[i].oper_type, dbdata[i].id); +// break; +// case 21: //成为代理 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_newsalesman(dbdata[i].to_agent, dbdata[i].to_channel, dbdata[i].to_user, dbdata[i].oper_data, dbdata[i].id); +// break; +// case 31: //封禁代理 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_disabledsales(dbdata[i].to_agent, dbdata[i].to_user, dbdata[i].id); +// break; +// case 32: //解封代理 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_abledsales(dbdata[i].to_agent, dbdata[i].to_user, dbdata[i].id); +// break; +// case 33: //封禁玩家 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_disabledplayer(dbdata[i].to_agent, dbdata[i].to_user, dbdata[i].id); +// break; +// case 34: //解封玩家 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_abledplayer(dbdata[i].to_agent, dbdata[i].to_user, dbdata[i].id); +// break; +// case 41: //解除绑定了我的玩家 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_disbinding_sales(dbdata[i].from_agent, dbdata[i].from_user, dbdata[i].id); +// break; +// case 42: //玩家解除绑定 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_disbinding_player(dbdata[i].from_agent, dbdata[i].from_user, dbdata[i].id); +// break; +// case 101: //绑定代理 +// youle_agent.readdata_isdoing = 1; +// update_gamedb_bindinginvitecode(dbdata[i].to_agent, dbdata[i].oper_data, dbdata[i].from_user, dbdata[i].id); +// break; +// case 110: //开通高新选项 +// youle_agent.readdata_isdoing = 1; +// var o_data = JSON.parse(dbdata[i].oper_data); +// update_gamedb_advanced(dbdata[i].to_agent, dbdata[i].to_user, o_data.shortnum, o_data.maxroomnum, o_data.desone, o_data.destwo, dbdata[i].id); +// break; +// case 111: //设置短号白名单 +// youle_agent.readdata_isdoing = 1; +// var o_data = JSON.parse(dbdata[i].oper_data); +// update_gamedb_whitelist(dbdata[i].from_agent, dbdata[i].from_user, o_data, dbdata[i].id); +// break; +// case 201: //系统抽成 +// youle_agent.readdata_isdoing = 1; +// var o_data = JSON.parse(dbdata[i].oper_data); +// var rebate_mode1 = 0; +// var rebate_set1 = 0; +// var rebate_mode2 = 0; +// var rebate_set2 = 0; +// for (var j = 0; j < o_data.length; j++) { +// if (o_data[j].type_id == "001"){ +// rebate_mode1 = 1; +// rebate_set1 = parseInt(o_data[j].system_commission_rate); +// } +// if (o_data[j].type_id == "002"){ +// rebate_mode2 = 1; +// rebate_set2 = parseInt(o_data[j].system_commission_rate); +// } +// } +// update_gamedb_coinrebate(dbdata[i].to_agent, rebate_mode1, rebate_set1, rebate_mode2, rebate_set2, dbdata[i].id); +// break; +// } +// } catch(e){} +// } +// } + +// if (!youle_agent.readdata_isdoing){ +// var sql_agent = "select * from ct_user_process_log where is_process = 0 order by id"; +// youle_agent.mysql_agent.execsql("opensql", sql_agent, [], db_callback_agent); +// } +// }, +// 1000*30); + diff --git a/server/youle/server_agent/rpc.js b/server/youle/server_agent/rpc.js new file mode 100644 index 0000000..e50289b --- /dev/null +++ b/server/youle/server_agent/rpc.js @@ -0,0 +1,8133 @@ +////////////////////////////////////////////////////////////////// +//////////////////////////游戏平台服务器////////////////////////// +////////////////////////////////////////////////////////////////// +youle_agent.debug = true; + +//发送手机验证码 +youle_agent.send_phone_checkcode = function(pack){ + var agentid = pack.data.agentid; + var phonenum = pack.data.phonenum; + + var o_cfg = youle_agent.app.youle_platform.config; + var cfg_smm_message = o_cfg.method.get_paravalue("smm_message", agentid); + if (!cfg_smm_message){ + cfg_smm_message = "您的验证码是X。"; + } + + var smmcode = min_random(1000, 9999); + var md5memo = min_replaceAll(cfg_smm_message, "X", smmcode, 1); + + var config = {}; + config.url = "http://api.smsbao.com/sms"; + config.type = "GET"; + config.data = md5Geturl3(13619576943, "xieyu2018", phonenum, md5memo); + min_http(config); + + var idx = min_ary_indexof(youle_agent.phonecodes, phonenum, "phonenum"); + if (idx == -1){ + var info = {}; + info.phonenum = phonenum; + info.smmcode = smmcode; + info.sendtime = min_now(); + youle_agent.phonecodes.push(info); + } else { + youle_agent.phonecodes[idx].smmcode = smmcode; + youle_agent.phonecodes[idx].sendtime = min_now(); + } + return pack; +} +//发送手机验证码-微信小程序版本 +youle_agent.send_phone_code_wechat = function(pack){ + var agentid = pack.data.agentid; + var phonenum = pack.data.phonenum; + + + + var smmcode = min_random(1000, 9999); + + + var idx = min_ary_indexof(youle_agent.phonecodes, phonenum, "phonenum"); + if (idx == -1){ + var info = {}; + info.phonenum = phonenum; + info.smmcode = smmcode; + info.sendtime = min_now(); + youle_agent.phonecodes.push(info); + } else { + youle_agent.phonecodes[idx].smmcode = smmcode; + youle_agent.phonecodes[idx].sendtime = min_now(); + } + return pack; +} + +//获取手机验证码-微信小程序版本 +youle_agent.get_phone_code_wechat = function(pack){ + var agentid = pack.data.agentid; + var phonenum = pack.data.phonenum; + + var o_cfg = youle_agent.app.youle_platform.config; + var cfg_smm_timeout = o_cfg.method.get_paravalue("smm_timeout", agentid); + if (!cfg_smm_timeout){ + cfg_smm_timeout = 60; + } + + var smmcode = 0; + var idx = min_ary_indexof(youle_agent.phonecodes, phonenum, "phonenum"); + + if (idx != -1){ + var diff = min_datediff(youle_agent.phonecodes[idx].sendtime, min_now(), "second"); + if (diff <= cfg_smm_timeout){ + smmcode = youle_agent.phonecodes[idx].smmcode; + } + } + + if (smmcode == 0){ + smmcode = min_random(1000, 9999); + if (idx == -1){ + var info = {}; + info.phonenum = phonenum; + info.smmcode = smmcode; + info.sendtime = min_now(); + youle_agent.phonecodes.push(info); + } else { + youle_agent.phonecodes[idx].smmcode = smmcode; + youle_agent.phonecodes[idx].sendtime = min_now(); + } + } + + pack.data.smmcode = smmcode; + return pack; +} + +//绑定手机号 +youle_agent.binding_phone = function(pack){ + var agentid = pack.data.agentid; + var playerid = pack.data.playerid; + var telphone = pack.data.phonenum; + var smmcode = pack.data.smmcode; + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + + var idx = min_ary_indexof(youle_agent.phonecodes, telphone, "phonenum"); + if (idx == -1 || youle_agent.phonecodes[idx].smmcode != smmcode){ + pack.rpc = "show_message"; + pack.data = {}; + pack.data.msg = "短信验证码不正确"; + pack.data.time = 0; + youle_agent.app.SendPack(pack); + return; + } + if (youle_agent.phonecodes[idx].smmcode != smmcode){ + pack.rpc = "show_message"; + pack.data = {}; + pack.data.msg = "短信验证码不正确"; + pack.data.time = 0; + youle_agent.app.SendPack(pack); + return; + } + var o_cfg = youle_agent.app.youle_platform.config; + var cfg_smm_timeout = o_cfg.method.get_paravalue("smm_timeout", agentid); + if (!cfg_smm_timeout){ + cfg_smm_timeout = 60; + } + var diff = min_datediff(youle_agent.phonecodes[idx].sendtime, min_now(), "second"); + if (diff > cfg_smm_timeout){ + pack.rpc = "show_message"; + pack.data = {}; + pack.data.msg = "短信验证码已失效"; + pack.data.time = 0; + youle_agent.app.SendPack(pack); + return; + } + + var sql = "update player set play_tel = ? where play_agentid = ? and play_playerid = ?"; + youle_agent.mysql.execsql("execsql", sql, [telphone, agentid, playerid]); + + o_player.tel = telphone; + + youle_agent.phonecodes.splice(idx, 1); + + return pack; +} + +//绑定手机号(微信) +youle_agent.bind_player_wechat = function(pack){ + var agentid = pack.data.agentid; + var channelid = pack.data.channelid; + var marketid = pack.data.marketid; + var gameid = pack.data.gameid; + var openid = pack.data.openid; + var unionid = pack.data.unionid; + var nickname = pack.data.nickname; + var avatar = pack.data.avatar; + var sex = parseInt(pack.data.sex); + var province = pack.data.province; + var city = pack.data.city; + var telphone = pack.data.telphone; + + if (!agentid || !telphone || !unionid){ + pack.data = { result: -1, msg: "参数缺失" }; + youle_agent.app.SendPack(pack); + return; + } + + //去除&符号 + nickname = min_replaceAll(nickname, "&", ""); + //去除'符号 + nickname = min_replaceAll(nickname, "'", ""); + //去除"符号 + nickname = min_replaceAll(nickname, '"', ""); + //去除前后空格 + nickname = min_trim(nickname); + + //去除emoji字符 + var nickname2 = unescape(escape(nickname).replace(/\%uD.{3}/g, '')); + if ((nickname2 != nickname) && (!nickname2)){ + nickname2 = "表情符号"; + } + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data = { result: -1, msg: "代理商不存在" }; + youle_agent.app.SendPack(pack); + return; + }; + + + var db_callback = function(dbdata){ + console.log(dbdata); + if (dbdata.length > 0 && dbdata[0].length > 0 && dbdata[0][0].result_code == -1) { + pack.rpc = "show_message"; + pack.data = {}; + pack.data.msg = dbdata[0][0].msg; + pack.data.time = 0; + pack.data.result = -1; + youle_agent.app.SendPack(pack); + return; + } + if (dbdata.length > 0 && dbdata[0].result == -1) { + pack.data = { result: -1, msg: dbdata[0].msg}; + youle_agent.app.SendPack(pack); + return; + } + pack.data = { result: 0, msg: dbdata[0].msg}; + youle_agent.app.SendPack(pack); + } + + var sql = "call cp_bind_player_wechat(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + youle_agent.mysql.execsql("opensql", sql, [agentid, channelid, gameid, openid, unionid, nickname2, avatar, sex, province, city, marketid, telphone], db_callback, pack); +} + +youle_agent.player_login = function(pack){ + if (cls_youle_room_backup && cls_youle_room_backup.isloading){ + return; + } + + var agentid = pack.data.agentid; + var channelid = pack.data.channelid; + var marketid = pack.data.marketid; + var gameid = pack.data.gameid; + var openid = pack.data.openid; + var unionid = pack.data.unionid; + var nickname = pack.data.nickname; + var avatar = pack.data.avatar; + var sex = parseInt(pack.data.sex); + var province = pack.data.province; + var city = pack.data.city; + var version = pack.data.version; + var playerid = pack.data.playerid; + var machineid = null; + if (pack.data.machineid){ + machineid = pack.data.machineid; + } + var machineroom = null; + if (pack.data.machineroom){ + machineroom = pack.data.machineroom; + } + var telphone = null; + if (pack.data.telphone){ + telphone = pack.data.telphone; + var smmcode = pack.data.smmcode; + var telphoneAuto = pack.data.telphoneAuto; + }; + + if (!openid || openid == "(null)" || !unionid || unionid == "(null)"){ + return; + } + if (!nickname && !avatar){ + return; + } + + //去除&符号 + nickname = min_replaceAll(nickname, "&", ""); + //去除'符号 + nickname = min_replaceAll(nickname, "'", ""); + //去除"符号 + nickname = min_replaceAll(nickname, '"', ""); + //去除前后空格 + nickname = min_trim(nickname); + + //去除emoji字符 + var nickname2 = unescape(escape(nickname).replace(/\%uD.{3}/g, '')); + if ((nickname2 != nickname) && (!nickname2)){ + nickname2 = "表情符号"; + } + + var o_cfg = youle_agent.app.youle_platform.config; + //验证版本号 + if (!version){ + version = 1; + } else { + version = parseInt(version); + } + var cfg_version = o_cfg.method.get_paravalue("game_version", agentid, gameid, channelid, marketid); + if (version < cfg_version){ + pack.rpc = "kick_server"; + pack.data = {}; + pack.data.msg = "检查到新版本,请退出游戏,再重新启动游戏。"; + youle_agent.app.SendPack(pack); + return; + } + + if (min_ary_indexof(blacknamelist, unionid) > -1){ + pack.rpc = "kick_server"; + pack.data = {}; + pack.data.msg = "由于用户举报该游戏账号存在违规行为,已被封禁。"; + youle_agent.app.SendPack(pack); + return; + } + + //游戏大厅http请求返回的数据 + // if (gameid == "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO"){ + // pack.data = {}; + // pack.data.state = 0; + // pack.data.agentid = agentid; + // pack.data.channelid = channelid; + // pack.data.scrollmsg = o_cfg.method.get_paravalue("scrollmsg", agentid, gameid, channelid, marketid); + // pack.data.noticemsg = o_cfg.method.get_paravalue("noticemsg", agentid, gameid, channelid, marketid); + // pack.data.gameversion = o_cfg.method.get_paravalue("game_version", agentid, gameid, channelid, marketid); + // pack.data.hall_config = o_cfg.method.get_paravalue("hall_config", agentid, gameid, channelid, marketid); + // pack.data.game_config = o_cfg.method.get_paravalue("game_config", agentid, gameid, channelid, marketid); + // pack.data.gamelist = o_cfg.method.get_gamelist_gamehall(agentid, channelid, marketid); + // return pack; + // } + + var sql; + var o_agent; + var o_game; + var o_player; + + //代理商 + var do_load_agent = function(){ + o_agent = youle_agent.agents.method.find_agent(agentid); + if (o_agent){ + do_load_game(); + } else { + var db_callback_agentinfo = function(dbdata){ + if (dbdata.length == 0){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "参数错误,agentid:" + agentid; + youle_agent.app.SendPack(pack); + console.log("youle_agent player_login 参数错误,agentid:" + agentid); + return; + } + o_agent = youle_agent.agents.method.load_agent( + dbdata[0].agen_agentid, + dbdata[0].agen_name, + dbdata[0].agen_server, + parseInt(dbdata[0].agen_port_tcp), + parseInt(dbdata[0].agen_port_http), + parseInt(dbdata[0].agen_freeroom), + dbdata[0].agen_managecode, + dbdata[0].agen_initroomacard, + dbdata[0].agen_initbean); + o_agent.rebate_mode1 = parseInt(dbdata[0].agen_rebatemode1); + o_agent.rebate_set1 = parseInt(dbdata[0].agen_rebateset1); + o_agent.rebate_mode2 = parseInt(dbdata[0].agen_rebatemode2); + o_agent.rebate_set2 = parseInt(dbdata[0].agen_rebateset2); + + //读取短号 + var db_callback_shortcode = function(dbdata){ + for (var i = 0; i < dbdata.length; i++) { + var o_shortcode = {}; + o_shortcode.shortcode = parseInt(dbdata[i].play_shortcode); + o_shortcode.playerid = parseInt(dbdata[i].play_playerid); + o_agent.shortcodes[o_shortcode.shortcode] = o_shortcode; + } + do_load_game(); + } + sql = "select * from player where play_agentid = ? and ifnull(play_shortcode, 0) <> 0"; + youle_agent.mysql.execsql("opensql", sql, [agentid], db_callback_shortcode); + } + sql = "select * from agent where agen_agentid = ?"; + youle_agent.mysql.execsql("opensql", sql, [agentid], db_callback_agentinfo, pack); + } + } + + //游戏 + var do_load_game = function(){ + //获取池子累计输赢 + var do_getpond = function(){ + var db_callback_getpond = function(dbdata){ + o_game.pondlist = []; + for (var i = 0; i < dbdata.length; i++) { + var info = [parseInt(dbdata[i].aggp_pondid), parseInt(dbdata[i].aggp_value)]; + o_game.pondlist.push(info); + }; + + o_game.isloading = false; + do_load_player(); + } + sql = "select * from agent_game_pond where aggp_agentid = ? and aggp_gameid = ?"; + youle_agent.mysql.execsql("opensql", sql, [agentid, gameid], db_callback_getpond); + } + + //获取比赛排行榜 + var do_getmatchranking = function(){ + do_getpond(); + } + //创建短号房间 + var do_createshortroom = function(){ + var db_callback_createadvancedroom = function(dbdata){ + for (var i = 0; i < dbdata.length; i++) { + try{ + if (dbdata[i].play_roomcodes){ + var _shortcode = dbdata[i].play_shortcode; + var _o_roomcodes = JSON.parse(dbdata[i].play_roomcodes); + for (var j = 0; j < _o_roomcodes.length; j++) { + if (_o_roomcodes[j].gameid == gameid){ + var _roomtype = _o_roomcodes[j].roomtype; + var _tea = _o_roomcodes[j].tea; + var _rebateType = _o_roomcodes[j].rebateType; + //var _videoConfig = _o_roomcodes[j].videoConfig; + var _roomlist = _o_roomcodes[j].roomlist; + if (_roomlist.length > 0){ + var _ownerid = parseInt(dbdata[i].play_playerid); + var _o_owner = o_agent.method.find_player(_ownerid); + if (!_o_owner){ + _o_owner = o_agent.method.load_player( + parseInt(dbdata[i].play_playerid), + parseInt(dbdata[i].play_roomcard), + parseInt(dbdata[i].play_bean), + dbdata[i].play_openid, + dbdata[i].play_unionid, + dbdata[i].play_nickname, + dbdata[i].play_avatar, + parseInt(dbdata[i].play_sex), + parseInt(dbdata[i].play_type), + dbdata[i].play_invitecode); + _o_owner.state = parseInt(dbdata[i].play_state); + _o_owner.advanced = parseInt(dbdata[i].play_advanced); + if (dbdata[i].play_shortcode){ + _o_owner.shortcode = parseInt(dbdata[i].play_shortcode); + } else { + _o_owner.shortcode = null; + } + if (dbdata[i].play_roomcodes){ + _o_owner.roomcodes = JSON.parse(dbdata[i].play_roomcodes); + } else { + _o_owner.roomcodes = []; + } + if (dbdata[i].play_whitelist){ + //_o_owner.whitelist = JSON.parse(dbdata[i].play_whitelist); + _o_owner.whitelist = youle_agent.deCompressWhiteList(dbdata[i].play_whitelist); + + for(var k=0;k<_o_owner.whitelist.length;k++){ + if(_o_owner.whitelist[k][2]){ + _o_owner.whitelist[k][2] = parseInt(_o_owner.whitelist[k][2]); + }else{ + _o_owner.whitelist[k][2] = 0; + } + } + } else { + _o_owner.whitelist = null; + } + if (dbdata[i].play_limit != null){ + _o_owner.lowlimit = parseInt(dbdata[i].play_limit); + } else { + _o_owner.lowlimit = -900000000; + } + if (dbdata[i].play_notice){ + _o_owner.notice = dbdata[i].play_notice; + } else { + _o_owner.notice = null; + } + if (dbdata[i].play_sign){ + _o_owner.sign = dbdata[i].play_sign; + } else { + _o_owner.sign = null; + } + if (dbdata[i].play_destwo){ + _o_owner.destwo = dbdata[i].play_destwo; + } else { + _o_owner.destwo = ""; + } + + _o_owner.loginday = min_now(); + + _o_owner.bankpower = parseInt(dbdata[i].play_bankpower); + _o_owner.bank = parseInt(dbdata[i].play_bank); + _o_owner.bankpwd = dbdata[i].play_bankpwd; + _o_owner.method.login_game(o_game, null, null, null); + } + for (var k = 0; k < _roomlist.length; k++) { + var _roomcode = _roomlist[k]; + + //在恢复牌局的情况下有可能短号房间已经恢复了 + var roomsvr_agent = youle_room.agents.method.find_agent(agentid); + if (roomsvr_agent){ + var roomsvr_game = roomsvr_agent.method.find_game(gameid); + if (roomsvr_game){ + var roomsvr_room = roomsvr_game.method.find_room(_roomcode); + if (roomsvr_room){ + continue; + } + } + } + + _msg = {}; + _msg.data = {}; + _msg.data.agentid = agentid; + _msg.data.gameid = gameid; + _msg.data.playerid = _ownerid; + _msg.data.roomcode = _roomcode; + _msg.data.shortcode = _shortcode; + _msg.data.tea = _tea; + //console.log(_ownerid+"shortcode:"+_shortcode); + //_msg.data.videoConfig = _videoConfig; + youle_agent.advanced_createroom_roomcode(_msg); + } + } + } + } + } + } + catch(e){} + } + do_getmatchranking(); + } + sql = "select * from player where play_agentid = ? and ifnull(play_shortcode, 0) <> 0"; + youle_agent.mysql.execsql("opensql", sql, [agentid], db_callback_createadvancedroom); + } + //获取机器人金币累积输赢 + var do_getrobotgrade = function(){ + var db_callback_getrobotgrade = function(dbdata){ + if (dbdata.length == 1){ + o_game.robot_xx = parseInt(dbdata[0].winlose_xx); + o_game.robot_x2 = parseInt(dbdata[0].winlose_x2); + } else { + o_game.robot_xx = 0; + o_game.robot_x2 = 0; + } + do_createshortroom(); + } + sql = "select ifnull(agpl_winlose_xx, 0) as winlose_xx, ifnull(agpl_winlose_x2, 0) as winlose_x2 from agent_game_player where agpl_agentid = ? and agpl_gameid = ? and agpl_playerid = 999999"; + youle_agent.mysql.execsql("opensql", sql, [agentid, gameid], db_callback_getrobotgrade); + } + + o_game = o_agent.method.find_game(gameid); + if (o_game){ + if (o_game.isloading){ + return; + } + if (!o_agent.playerlogin) { //恢复牌局后o_game已经恢复了,但还没创建短号房间 + do_createshortroom(); + } else { + do_load_player(); + } + } else { + var db_callback_gameinfo = function(dbdata){ + if (dbdata.length == 0){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "参数错误,gameid:" + gameid; + console.log("youle_agent player_login 参数错误,gameid:" + gameid); + youle_agent.app.SendPack(pack); + return; + } + if (!youle_agent.app[dbdata[0].game_modename]){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "参数错误,gameid:" + gameid; + console.log("youle_agent player_login 参数错误,gameid:" + gameid); + youle_agent.app.SendPack(pack); + return; + } + o_game = o_agent.method.load_game( + dbdata[0].game_gameid, + dbdata[0].game_name, + parseInt(dbdata[0].game_minroomcode), + parseInt(dbdata[0].game_maxroomcode), + parseInt(dbdata[0].game_seatcount), + parseInt(dbdata[0].game_makewar), + dbdata[0].game_modename); + o_game.isloading = true; + do_getrobotgrade(); + } + sql = "select * from game where game_gameid = ?"; + youle_agent.mysql.execsql("opensql", sql, [gameid], db_callback_gameinfo, pack); + } + } + + //玩家 + var do_load_player = function(){ + if (playerid){ + playerid = parseInt(playerid); + o_player = o_agent.method.find_player(playerid); + } + if (o_player){ + if (o_player.openid != openid || o_player.unionid != unionid || o_player.deleted){ + o_player = null; + } + } + if (o_player){ + o_player.nickname = nickname2; + o_player.avatar = avatar; + o_player.sex = sex; + do_return_login(0); + } else { + //如果传了手机号则需要验证短信验证码 + if (telphone && !telphoneAuto){ + pack.app = youle_agent.app.appname; + pack.route = youle_agent.app.youle_room.routename; + var idx = min_ary_indexof(youle_agent.phonecodes, telphone, "phonenum"); + if (idx == -1 || youle_agent.phonecodes[idx].smmcode != smmcode){ + pack.rpc = "player_login"; + pack.data = {}; + pack.data.msg = "短信验证码不正确"; + pack.data.state = -1; + pack.data.time = 0; + youle_agent.app.SendPack(pack); + return; + } + if (youle_agent.phonecodes[idx].smmcode != smmcode){ + pack.rpc = "player_login"; + pack.data = {}; + pack.data.msg = "短信验证码不正确"; + pack.data.state = -1; + pack.data.time = 0; + youle_agent.app.SendPack(pack); + return; + } + + var cfg_smm_timeout = o_cfg.method.get_paravalue("smm_timeout", agentid); + if (!cfg_smm_timeout){ + cfg_smm_timeout = 60; + } + var diff = min_datediff(youle_agent.phonecodes[idx].sendtime, min_now(), "second"); + if (diff > cfg_smm_timeout){ + + pack.rpc = "player_login"; + pack.data = {}; + pack.data.state = -1; + pack.data.msg = "短信验证码已失效"; + pack.data.time = 0; + youle_agent.app.SendPack(pack); + return; + } + + youle_agent.phonecodes.splice(idx, 1); + } + var db_callback_playerinfo = function(dbdata){ + // 检查是否返回了错误信息 + if (dbdata.length > 0 && dbdata[0].result == -1) { + pack.rpc = "player_login"; + + pack.data = {}; + pack.data.state = -1; + pack.data.msg = dbdata[0].msg || "登录失败"; + pack.data.time = 0; + youle_agent.app.SendPack(pack); + return; + } + + // 如果是手机号登录,更新玩家信息为数据库返回的信息 + if (telphone && dbdata[0].ret_nickname) { + nickname2 = dbdata[0].ret_nickname; + avatar = dbdata[0].ret_avatar; + openid = dbdata[0].ret_openid; + sex = parseInt(dbdata[0].ret_sex); + unionid = dbdata[0].ret_unionid; + // 还可以获取 province, city, logindate 等,如果需要的话 + province = dbdata[0].ret_province; + city = dbdata[0].ret_city; + + } + + var gameid = dbdata[0].gameid; + o_player = o_agent.method.load_player( + parseInt(dbdata[0].playerid), + parseInt(dbdata[0].roomcard), + parseInt(dbdata[0].bean), + openid, + unionid, + nickname2, + avatar, + parseInt(sex), + parseInt(dbdata[0].playertype), + dbdata[0].invitecode); + o_player.inviteid = parseInt(dbdata[0].inviteid); + o_player.state = parseInt(dbdata[0].state); + o_player.advanced = parseInt(dbdata[0].advanced); + if (dbdata[0].shortcode){ + o_player.shortcode = parseInt(dbdata[0].shortcode); + } else { + o_player.shortcode = null; + } + if (dbdata[0].roomcodes){ + o_player.roomcodes = JSON.parse(dbdata[0].roomcodes); + } else { + o_player.roomcodes = []; + } + if (dbdata[0].desone){ + o_player.desone = dbdata[0].desone; + } else { + o_player.desone = null; + } + if (dbdata[0].destwo){ + o_player.destwo = dbdata[0].destwo; + } else { + o_player.destwo = null; + } + if (dbdata[0].whitelist){ + o_player.whitelist = youle_agent.deCompressWhiteList(dbdata[0].whitelist); + for(var k=0;k 0){ + for (var i = 0; i < pack.data.players.length; i++) { + if (pack.data.players[i]){ + var _playerid = parseInt(pack.data.players[i].playerid); + var found = false; + for (var j = 0; j < o_owner.whitelist.length; j++) { + if (o_owner.whitelist[j][0] == _playerid){ + if (o_owner.whitelist[j][2]){ + pack.data.players[i].charm = o_owner.whitelist[j][2]; + } else { + pack.data.players[i].charm = 0; + } + found = true; + break; + } + } + if (!found){ + pack.data.players[i].charm = 0; + } + } + } + } + } + youle_agent.app.SendPack(pack); + } + catch(e) + { + console.log("youle_agent_player_login:"+agentid+","+channelid+","+marketid+","+gameid+","+openid+","+unionid+","+nickname); + console.log(e); + } + } else { + //房间服务器与代理商服务器不是同一地址 + var cfg = {}; + cfg.url = "http://" + o_roomserver.server + ":" + o_roomserver.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){ + if (str){ + var roomserver_data = JSON.parse(str); + //返回前端需要连接房间服务器 + var _info = pack.data; + pack.rpc = "connect_roomserver"; + pack.data = {}; + pack.data.roomserver = o_roomserver.server + ":" + o_roomserver.tcpport; + pack.data.data = {}; + pack.data.data.opt = "player_login"; + pack.data.data.playerid = db_playerid; + pack.data.data.agentid = agentid; + pack.data.data.gameid = gameid; + pack.data.data.roomcode = o_gameinfo.roomcode; + pack.data.data.roomtype = roomserver_data.data.roomtype; + pack.data.data.seat = roomserver_data.data.seat; + pack.data.data.info = _info; + + youle_agent.app.SendPack(pack); + } + }; + cfg.error = function(str){}; + min_http(cfg); + } + } + + if (!logindb){ + if (!o_player.loginday){ + o_player.loginday = "2016-12-12 12:12:12"; + } + + var ran = min_random(1, 20); + var hourdiff = min_datediff(o_player.loginday, min_now(), "hour"); + if (hourdiff >= 24 + ran){ + var db_callback_playerinfo = function(dbdata){ + var gameid = dbdata[0].gameid; + o_player = o_agent.method.load_player( + parseInt(dbdata[0].playerid), + parseInt(dbdata[0].roomcard), + parseInt(dbdata[0].bean), + openid, + unionid, + nickname2, + avatar, + parseInt(sex), + parseInt(dbdata[0].playertype), + dbdata[0].invitecode); + o_player.state = parseInt(dbdata[0].state); + o_player.advanced = parseInt(dbdata[0].advanced); + if (dbdata[0].shortcode){ + o_player.shortcode = parseInt(dbdata[0].shortcode); + } else { + o_player.shortcode = null; + } + if (dbdata[0].roomcodes){ + o_player.roomcodes = JSON.parse(dbdata[0].roomcodes); + } else { + o_player.roomcodes = []; + } + if (dbdata[0].desone){ + o_player.desone = dbdata[0].desone; + } else { + o_player.desone = null; + } + if (dbdata[0].destwo){ + o_player.destwo = dbdata[0].destwo; + } else { + o_player.destwo = null; + } + if (dbdata[0].whitelist){ + o_player.whitelist = youle_agent.deCompressWhiteList(dbdata[0].whitelist); + for(var k=0;k 1){ + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "check_login"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_player.playerid; + msg.data.gameid = gameid; + msg.data.youleid = youle_agent.youleid; + + for (var i = 0; i < gameserverlist.length; i++) { + var cfg = {}; + cfg.url = "http://" + gameserverlist[i] + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){ + try{ + var obj = JSON.parse(str); + if (obj.data.state == 1){ + pack.rpc = "kick_server"; + pack.data = {}; + pack.data.msg = "您目前正在进行" + obj.data.gamename + "游戏,请先退出或结束当前游戏。"; + youle_agent.app.SendPack(pack); + + // o_agent.method.clear_player(o_player.playerid); + o_player.deleted = 1; + } + }catch(e){} + } + cfg.error = function(str){ + console.log(min_now() + " check_login失败:" + cfg.url + ":" + agentid + ":" + o_player.playerid); + } + min_http(cfg); + } + } + } + + do_load_agent(); +} + +youle_agent.check_login = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var youleid = pack.data.youleid; + var gameid = pack.data.gameid; + + if (youleid == youle_agent.youleid){ + pack.data.state = 0; + return pack; + } + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent){ + pack.data.state = 0; + return pack; + } + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 0; + return pack; + } + + if (o_player.gameinfo){ + for (var i = 0; i < o_player.gameinfo.length; i++) { + if (o_player.gameinfo[i] && o_player.gameinfo[i].roomcode){ + pack.data.state = 1; + pack.data.gamename = o_agent.games[i].gamename; + return pack; + } + } + for (var i = 0; i < o_player.gameinfo.length; i++) { + if (o_player.gameinfo[i] && o_player.gameinfo[i].conmode && o_player.gameinfo[i].fromid){ + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "kick_offline"; + msg.conmode = o_player.gameinfo[i].conmode; + msg.fromid = o_player.gameinfo[i].fromid; + msg.data={}; + msg.data.fromOther = 1; + msg.data.gameid = gameid; + youle_agent.app.SendPack(msg); + } + } + } + + // o_agent.method.clear_player(playerid); + o_player.deleted = 1; + pack.data.state = 0; + return pack; +} + +//玩家断线 +youle_agent.OnTcpDisConnected = function(str_tcpid){ + var o_tcp = youle_agent.tcps.method.find_tcp(str_tcpid); + if (!o_tcp){ + return; + } + //找到了 + var _player = o_tcp.o_player; + var _gameidx = o_tcp.gameidx; + if (_player){ + if (_player.gameinfo[_gameidx]){ + if (_player.gameinfo[_gameidx].fromid == str_tcpid){ + if (_player.gameinfo[_gameidx].roomcode || _player.gameinfo[_gameidx].matchid){ + _player.gameinfo[_gameidx].ip = null; + _player.gameinfo[_gameidx].conmode = null; + _player.gameinfo[_gameidx].fromid = null; + } else { + //如果不在房间内 + _player.gameinfo[_gameidx] = null; + //判断是否要释放玩家 + var freeflag = true; + for (var m = 0; m < _player.gameinfo.length; m++) { + if (_player.gameinfo[m]){ + freeflag = false; + break; + } + } + if (freeflag){ + // _player.o_agent.players[_player.playerid - 100001] = null; + _player.method.open_free_timer(); + } + + //统计在线玩家数 + var o_game = _player.o_agent.games[_gameidx]; + if (o_game){ + o_game.method.online_p_chang(-1); + } + } + } + } + } + //更新tcp连接列表 + youle_agent.tcps.method.delete_tcp(str_tcpid); + + // //遍历运营商 + // for (var i = 0; i < youle_agent.agents.list.length; i++){ + // var o_agent = youle_agent.agents.list[i]; + // //遍历玩家列表 + // for (var j = 0; j < o_agent.players.length; j++){ + // var o_player = o_agent.players[j]; + // if (o_player){ + // //遍历玩家登录的游戏列表 + // for (var k = 0; k < o_player.gameinfo.length; k++){ + // if (o_player.gameinfo[k]){ + // if (o_player.gameinfo[k].fromid == str_tcpid){ + // //找到了 + // if (!o_player.gameinfo[k].roomcode){ + // //如果不在房间内 + // o_player.gameinfo[k] = null; + // //判断是否要释放玩家 + // var freeflag = true; + // for (var m = 0; m < o_player.gameinfo.length; m++) { + // if (o_player.gameinfo[m]){ + // freeflag = false; + // break; + // } + // } + // if (freeflag){ + // o_agent.players[j] = null; + // } + // } else { + // o_player.gameinfo[k].ip = null; + // o_player.gameinfo[k].conmode = null; + // o_player.gameinfo[k].fromid = null; + // } + // return; + // } + // } + // } + // } + // } + // } +} + +//玩家提交地理位置 +youle_agent.submit_location = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var country = pack.data.info.country; + var province = pack.data.info.province; + var city = pack.data.info.city; + var cityCode = pack.data.info.cityCode; + var district = pack.data.info.district; + var street = pack.data.info.street; + var address = pack.data.info.address; + address = min_replaceAll(address, "&", ""); + var latitude = pack.data.info.latitude; + var longitude = pack.data.info.longitude; + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + o_player.address = address; + o_player.latitude = latitude; + o_player.longitude = longitude; + + var db_callback = function(dbdata) { + if (dbdata.length == 1){ + var db_wechatgzh = dbdata[0].wechatgzh; + var db_wechatewm = dbdata[0].wechatewm; + var db_wechatkfh = dbdata[0].wechatkfh; + var db_qq = dbdata[0].qq; + var db_tel = dbdata[0].tel; + var db_invitecode = dbdata[0].invitecode; + + pack.data = {}; + if (!db_wechatgzh){ + pack.data.wechat_gzh = db_wechatgzh; + pack.data.wechat_ewm = db_wechatewm; + } + if (!db_wechatkfh){ + pack.data.wechat_kfh = db_wechatkfh; + } + if (!db_qq){ + pack.data.qq = db_qq; + } + if (!db_tel){ + pack.data.tel = db_tel; + } + if (!db_invitecode){ + pack.data.invitecode = db_invitecode; + } + youle_agent.app.SendPack(pack); + } + } + + var sql = "call cp_game_login_location(?,?,?,?,?,?,?,?,?,?,?);"; + youle_agent.mysql.execsql("execsql", sql, [agentid, playerid, country, province, city, cityCode, district, street, address, latitude, longitude], db_callback, pack); +} + +//玩家提交手机信息 +youle_agent.submit_phoneinfo = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var info = pack.data.info; + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + + var sql = "call cp_game_login_phoneinfo(?,?,?);"; + youle_agent.mysql.execsql("execsql", sql, [agentid, playerid, JSON.stringify(info)]); + return pack; +} + +//创建房间 +youle_agent.create_room = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomtype = pack.data.roomtype; + var infinite = 0; + if (pack.data.infinite){ + infinite = parseInt(pack.data.infinite); + } + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + console.log(min_now() + "创建房间失败,agentid:" + agentid); + console.log(pack); + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + console.log(min_now() + "创建房间失败,gameid:" + gameid); + console.log(pack); + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + console.log(min_now() + "创建房间失败,playerid:" + playerid); + console.log(pack); + return; + }; + //检查玩家是否已经在房间内了 + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (o_gameinfo && o_gameinfo.roomcode) { + console.log(min_now() + "创建房间失败,o_gameinfo.roomcode:" + o_gameinfo.roomcode); + console.log(pack); + return; + }; + //检查tcp连接 + try{ + if (o_gameinfo.fromid != pack.fromid){ + console.log(min_now() + "创建房间失败,o_gameinfo.fromid:" + o_gameinfo.fromid + ", pack.fromid:" + pack.fromid); + console.log(pack); + return; + }; + } + catch(e){ + console.log(min_now() + "创建房间失败,o_gameinfo.fromid != pack.fromid error"); + console.log(pack); + return; + } + //检查服务器是否已关闭 + var config_close = youle_agent.app.youle_platform.config.method.get_paravalue("isclose", agentid, gameid); + if (o_agent.isclose || config_close == 2){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "正在进行系统维护,不能创建房间,请稍后再试"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + var roomCardName = youle_agent.app.youle_platform.config.method.get_paravalue("roomCardName", agentid); + if (!roomCardName){ + roomCardName = "房卡"; + } + var starName = youle_agent.app.youle_platform.config.method.get_paravalue("starName", agentid); + if (!starName){ + starName = "星星"; + } + //获取开房所需的房卡数量 + var needroomcard = youle_agent.import.createroom_needroomcard(o_game, roomtype); + //获取总局数 + var asetcount = youle_agent.import.createroom_asetcount(o_game, roomtype); + //获取豆豆下限 + var beanfloor = youle_agent.import.createroom_beanfloor(o_game, roomtype); + //获取房间类型 + var roommode = youle_agent.import.createroom_roommode(o_game, roomtype); + //获取星星场倍数 + var beanmult = youle_agent.import.createroom_beanmult(o_game, roomtype); + //不是星星场 + if (!roommode){ + beanmult = 0; + } + //玩家是否需要准备 + var needprepare = youle_agent.import.createroom_needprepare(o_game, roomtype); + var xxroomdeduct = youle_room.app.youle_platform.config.method.get_paravalue("xxroomdeduct", agentid); + if (!roommode || (roommode && xxroomdeduct)){ + //检查玩家房卡是否够用 + if (o_player.roomcard < needroomcard){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = roomCardName + "数量不够"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + }; + } + + //检查玩家豆豆是否够用 + if (beanfloor != null){ + if (o_player.bean < beanfloor){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = starName + "数量不够"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + }; + + //随机获取新的房号 + var roomcode = o_game.method.new_roomcode(); + if (!roomcode){ + console.log(min_now() + "创建房间失败,roomcode:" + roomcode); + console.log(pack); + return; + } + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(roomcode); + + //通知房间服务器创建房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "create_room"; + msg.data = {}; + + msg.data.agent = {}; + msg.data.agent.agentid = agentid; + msg.data.agent.name = o_agent.agentname; + msg.data.agent.server = o_agent.server; + msg.data.agent.tcpport = o_agent.tcpport; + msg.data.agent.httpport = o_agent.httpport; + msg.data.agent.freeroom = o_agent.freeroom; + + msg.data.game = {}; + msg.data.game.gameid = gameid; + msg.data.game.name = o_game.gamename; + msg.data.game.seatcount = o_game.seatcount; + msg.data.game.makewar = o_game.makewar; + msg.data.game.modename = o_game.modename; + msg.data.game.server = o_roomserver.server; + msg.data.game.tcpport = o_roomserver.tcpport; + msg.data.game.httpport = o_roomserver.httpport; + msg.data.game.minroomcode = o_roomserver.minroomcode; + msg.data.game.maxroomcode = o_roomserver.maxroomcode; + + msg.data.player = {}; + msg.data.player.playerid = playerid; + msg.data.player.nickname = o_player.nickname; + msg.data.player.avatar = o_player.avatar; + msg.data.player.sex = o_player.sex; + msg.data.player.playertype = o_player.playertype; + msg.data.player.ip = o_gameinfo.ip; + msg.data.player.conmode = o_gameinfo.conmode; + msg.data.player.fromid = o_gameinfo.fromid; + msg.data.player.address = o_player.address; + msg.data.player.latitude = o_player.latitude; + msg.data.player.longitude = o_player.longitude; + msg.data.player.bean = o_player.bean; + + msg.data.room = {}; + msg.data.room.roomcode = roomcode; + msg.data.room.roomtype = roomtype; + msg.data.room.needroomcard = needroomcard; + msg.data.room.asetcount = asetcount; + msg.data.room.infinite = infinite; + msg.data.room.roommode = roommode; + msg.data.room.beanfloor = beanfloor; + msg.data.room.beanmult = beanmult; + msg.data.room.needprepare = needprepare; + msg.data.room.advanced_type = 0; + msg.data.room.advanced_teabean = 0; + + msg.data.location = pack.data.location; + + //加入房间成功 + o_gameinfo.roomcode = roomcode; + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + //返回前端 + pack.data = roomserver_data.data; + if (pack.data.state != 0){ + //房间服务器创建房间失败 + o_gameinfo.roomcode = null; + console.log(min_now() + "创建房间失败,youle_agent.app.youle_room.create_room"); + console.log(pack); + console.log(roomserver_data.data); + } + youle_agent.app.SendPack(pack); + } else { + //房间服务器与代理商服务器不是同一地址 + var cfg = {}; + cfg.url = "http://" + o_roomserver.server + ":" + o_roomserver.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){ + if (str){ + var roomserver_data = JSON.parse(str); + if (roomserver_data.data.state == 0){ + //创建房间成功 + o_gameinfo.roomcode = roomserver_data.data.roomcode; + //返回前端需要连接房间服务器 + pack.rpc = "connect_roomserver"; + pack.data = {}; + pack.data.roomserver = o_roomserver.server + ":" + o_roomserver.tcpport; + pack.data.data = {}; + pack.data.data.opt = "create_room"; + pack.data.data.playerid = playerid; + pack.data.data.agentid = agentid; + pack.data.data.gameid = gameid; + pack.data.data.roomcode = roomserver_data.data.roomcode; + pack.data.data.seat = roomserver_data.data.seat; + pack.data.data.infinite = infinite; + youle_agent.app.SendPack(pack); + } else { + //房间服务器创建房间失败 + o_gameinfo.roomcode = null; + } + } + }; + cfg.error = function(str){ + //房间服务器创建房间失败 + o_gameinfo.roomcode = null; + } + min_http(cfg); + } +} + +//自己加入房间 +youle_agent.self_join_room = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var joinmode = pack.data.joinmode; + var vipMatch = pack.data.vipMatch; + if (!joinmode){ + joinmode = 0; + } else { + joinmode = 1; + } + var errorMsg = ""; + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + + //检查玩家是否已经在房间内了 + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (!o_gameinfo){ + return; + }; + if (o_gameinfo.roomcode) { + return; + } + //检查tcp连接 + // if (o_gameinfo.fromid != pack.fromid){ + // return; + // }; + + //检查服务器是否已关闭 + var config_close = youle_agent.app.youle_platform.config.method.get_paravalue("isclose", agentid, gameid); + if (o_agent.isclose || config_close == 2){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "正在进行系统维护,不能加入房间,请稍后再试"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + // var bTemp = false; + // var _shortcode1 = 0; + // if(roomcode >= 100000){ + // var _roomsvr_agent1 = youle_room.agents.method.find_agent(agentid); + // var _roomsvr_game1 = _roomsvr_agent1.method.find_game(gameid); + // var _roomsvr_room1 = _roomsvr_game1.method.find_room(roomcode); + // if(_roomsvr_room1){ + // if(_roomsvr_room1.advanced_type){ + // _shortcode1 = _roomsvr_room1.shortcode; + // bTemp = true; + // } + // } + // } + + if (roomcode < 100000){ + //短号 + var o_shortcode = null; + // if(roomcode < 100000){ + o_shortcode = o_agent.shortcodes[roomcode]; + // }else{ + // o_shortcode = o_agent.shortcodes[_shortcode1]; + // } + if (!o_shortcode){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "短号不存在"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + var pShortcode = o_shortcode.shortcode; + var config_vipConfig = youle_agent.app.youle_platform.config.method.get_paravalue("vipConfig", agentid, gameid);//0短号 1是否自动加入白名单 2默认的魅力值 3人数上限 4人满的提示内容 + var config_vipConfigIndex = youle_agent.app.youle_platform.config.method.get_paravalue("vipConfigIndex", agentid, gameid); + var inVip = -1; + var vipConfig = null; + var autoAdd = 0; + if(config_vipConfig){ + if(!config_vipConfigIndex){ + config_vipConfigIndex = 0; + } + if(vipMatch){ + if(config_vipConfig.length > config_vipConfigIndex){ + inVip = config_vipConfigIndex; + vipConfig = config_vipConfig[inVip]; + autoAdd = vipConfig[1]; + o_shortcode = o_agent.shortcodes[vipConfig[0]]; + } + + }else{ + var indx = min_ary_indexof(config_vipConfig, pShortcode, "0"); + if(indx > -1){ + vipConfig = config_vipConfig[indx]; + autoAdd = vipConfig[1]; + } + inVip = indx; + } + + + } + + + + var ownerid = o_shortcode.playerid; + var o_owner = o_agent.method.find_player(ownerid); + if (!o_owner){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房主未登录"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + if(o_owner.roomcodes){ + var isClose = 0; + var rebateType = 0; + //var videoConfig = null; + for (var i = 0; i < o_owner.roomcodes.length; i++) { + if (o_owner.roomcodes[i].gameid == gameid){ + o_roomlist = o_owner.roomcodes[i].roomlist; + isClose = o_owner.roomcodes[i].isClose; + rebateType = o_owner.roomcodes[i].rebateType; + //videoConfig = o_owner.roomcodes[i].videoConfig; + break; + } + } + + if(isClose){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房间已被房主暂时关闭"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + } + //白名单判定 + try{ + + + if(inVip == -1 || !autoAdd){ + if (o_owner.whitelist && o_owner.whitelist.length > 0){ + var white_idx = min_ary_indexof(o_owner.whitelist, playerid, "playerid"); + if (white_idx == -1){ + white_idx = min_ary_indexof(o_owner.whitelist, playerid); + } + if (white_idx == -1){ + for (var i = 0; i < o_owner.whitelist.length; i++) { + if (o_owner.whitelist[i][0] == playerid){ + white_idx = i; + break; + } + } + } + + if (playerid != ownerid && white_idx == -1){ + // console.log(o_owner.notice); + pack.data = {}; + pack.data.state = 1; + if(!o_owner.notice){ + pack.data.error = "您的ID为:"+playerid+"\n房主设置了白名单,您未在白名单中不能进入房间"; + // pack.data.error = "房主设置了白名单"; + }else{ + pack.data.error = "您的ID为:"+playerid+"\n房主设置了白名单,并对您说:\n"+o_owner.notice; + } + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + //短号魅力值下限判断 + if (playerid != ownerid){ + if (!o_owner.lowlimit && o_owner.lowlimit != 0){ + o_owner.lowlimit = -900000000; + } + if (!o_owner.whitelist[white_idx][2]){ + o_owner.whitelist[white_idx][2] = 0; + } + if (parseInt(o_owner.whitelist[white_idx][2]) < o_owner.lowlimit){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "您的魅力值未达到房间的下限值,不能进入房间"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + } + } + }else{ + if(o_owner.whitelist){ + var pIndex = min_ary_indexof(o_owner.whitelist, playerid, "0"); + if(pIndex < 0){//不在白名单 + if(o_owner.whitelist.length < vipConfig[3]){ + var pInfo = [playerid,"",vipConfig[2]]; + o_owner.whitelist.push(pInfo); + var sql = "update player set play_whitelist = ? where play_agentid = ? and play_playerid = ?"; + + youle_room.mysql.execsql("execsql", sql, [youle_agent.compressWhiteList(o_owner.whitelist), agentid, playerid]); + }else{ + // var vIndex = (inVip + 1) % config_vipConfig.length; + + pack.data = {}; + pack.data.state = 1; + pack.data.error = min_replaceAll(vipConfig[4],"#","\n",false); + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + }else{ + + } + }else{ + o_owner.whitelist = []; + var pInfo = [playerid,"",vipConfig[2]]; + o_owner.whitelist.push(pInfo); + var sql = "update player set play_whitelist = ? where play_agentid = ? and play_playerid = ?"; + + youle_room.mysql.execsql("execsql", sql, [youle_agent.compressWhiteList(o_owner.whitelist), agentid, playerid]); + + } + + } + }catch(e){ + console.log(e); + }; + //百人特殊处理 + if(gameid == "Btke0urRy0cvPd5CIvD5yfhYhKhdSdex"){ + var brNumber = youle_room.app.youle_platform.config.method.get_paravalue("brNumber",agentid); + if(brNumber){ + var starName = youle_agent.app.youle_platform.config.method.get_paravalue("starName", agentid); + brNumber = Number(brNumber); + if(o_owner.bean < brNumber){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房主"+starName+"数量不足!"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + } + } + //房主设置的进入名单判定 + if(o_owner.banList){ + if(playerid != ownerid && o_owner.banList.indexOf(Number(playerid)) > -1){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "您已被房主限制进入!"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + } + var o_roomlist; + if (!o_roomlist){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "未找到短号对应的房间"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + + //根据人数排序房间,优先进人多的房间 + var _roomlist = []; + var _roomsvr_agent = youle_room.agents.method.find_agent(agentid); + var _roomsvr_game = _roomsvr_agent.method.find_game(gameid); + for (var i = 0; i < o_roomlist.length; i++) { + var _roomcode = o_roomlist[i]; + var _roomsvr_room = _roomsvr_game.method.find_room(_roomcode); + if (_roomsvr_room){ + var _playercount = 0; + for (var j = 0; j < _roomsvr_room.seatlist.length; j++) { + if (_roomsvr_room.seatlist[j]){ + _playercount = _playercount + 1; + } + } + //未满 + if (_playercount < _roomsvr_room.seatlist.length){ + var _room = {}; + _room.roomcode = _roomcode; + _room.playercount = _playercount; + var _isfound = false; + for (var k = 0; k < _roomlist.length; k++) { + if (_playercount > _roomlist[k].playercount){ + _roomlist.splice(k, 0, _room); + _isfound = true; + break; + } + } + if (!_isfound){ + _roomlist.push(_room); + } + } + } else { + var _msg = {}; + _msg.data = {}; + _msg.data.agentid = agentid; + _msg.data.gameid = gameid; + _msg.data.playerid = ownerid; + _msg.data.roomcode = _roomcode; + _msg.data.shortcode = roomcode; + //_msg.data.videoConfig = videoConfig; + youle_agent.advanced_createroom_roomcode(_msg); + } + } + + for (var i = 0; i < _roomlist.length; i++) { + roomcode = _roomlist[i].roomcode; + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(roomcode); + //通知房间服务器有人要加入房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "self_join_room"; + msg.data = {}; + msg.data.playerid = playerid; + msg.data.nickname = o_player.nickname; + msg.data.avatar = o_player.avatar; + msg.data.sex = o_player.sex; + msg.data.playertype = o_player.playertype; + if (o_owner.whitelist && o_owner.whitelist.length > 0){ + if (white_idx > -1 && o_owner.whitelist[white_idx]){ + if (o_owner.whitelist[white_idx].code){ + msg.data.paycode = o_owner.whitelist[white_idx].code; + } else if (o_owner.whitelist[white_idx][1]){ + msg.data.paycode = o_owner.whitelist[white_idx][1]; + } + if (o_owner.whitelist[white_idx][2]){ + msg.data.charm = o_owner.whitelist[white_idx][2]; + } else { + msg.data.charm = 0; + } + } + } + msg.data.ip = o_gameinfo.ip; + msg.data.conmode = o_gameinfo.conmode; + msg.data.fromid = o_gameinfo.fromid; + msg.data.agentid = agentid; + msg.data.gameid = gameid; + msg.data.roomcode = roomcode; + msg.data.playerroomcard = o_player.roomcard; + msg.data.playerbean = o_player.bean; + msg.data.joinmode = joinmode; + msg.data.address = o_player.address; + msg.data.latitude = o_player.latitude; + msg.data.longitude = o_player.longitude; + + msg.data.location = pack.data.location; + + //加入房 间成功 + o_gameinfo.roomcode = roomcode; + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + + //返回前端 + pack.data = roomserver_data.data; + //加上房主设置的公告 + pack.data.ownerNotice = o_owner.notice; + if (pack.data.state != -1){ + if (pack.data.state != 0){ + errorMsg = pack.data.error; + //房间服务器加入房间失败 + o_gameinfo.roomcode = null; + } else { + for (var i = 0; i < pack.data.players.length; i++) { + if (pack.data.players[i] && pack.data.players[i].bean == null){ + var _playerid = parseInt(pack.data.players[i].playerid); + var _o_player = o_agent.method.find_player(_playerid); + pack.data.players[i].bean = _o_player.bean; + } + } + //获取玩家的短号魅力值 + if (o_owner.whitelist && o_owner.whitelist.length > 0){ + for (var i = 0; i < pack.data.players.length; i++) { + if (pack.data.players[i]){ + var _playerid = parseInt(pack.data.players[i].playerid); + var found = false; + if (o_owner.whitelist){ + for (var j = 0; j < o_owner.whitelist.length; j++) { + if (o_owner.whitelist[j][0] == _playerid){ + if (o_owner.whitelist[j][2]){ + pack.data.players[i].charm = o_owner.whitelist[j][2]; + } else { + pack.data.players[i].charm = 0; + } + found = true; + break; + } + } + } + if (!found){ + pack.data.players[i].charm = 0; + } + } + } + } + + pack.data.ownerNote = o_owner.desone; + youle_agent.app.SendPack(pack); + return; + } + } else { + break; + } + } + } + + if (!o_gameinfo.roomcode){ + pack.data = {}; + pack.data.state = 1; + if(errorMsg){ + pack.data.error = errorMsg; + }else{ + pack.data.error = "未找到空闲的房间"; + } + + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + } + return; + } else { + //长号 + try{ + //白名单判定 + var _roomsvr_agent = youle_room.agents.method.find_agent(agentid); + var _roomsvr_game = _roomsvr_agent.method.find_game(gameid); + var _roomsvr_room = _roomsvr_game.method.find_room(roomcode); + var ownerid = _roomsvr_room.owner.playerid; + var o_owner = o_agent.method.find_player(ownerid); + + if (o_owner && o_owner.roomcodes){ + var isClose = 0; + var rebateType = 0; + var forbidSelect = 0; + //var videoConfig = null; + for (var i = 0; i < o_owner.roomcodes.length; i++) { + if (o_owner.roomcodes[i].gameid == gameid){ + o_roomlist = o_owner.roomcodes[i].roomlist; + isClose = o_owner.roomcodes[i].isClose; + rebateType = o_owner.roomcodes[i].rebateType; + forbidSelect = o_owner.roomcodes[i].forbidSelect; + //videoConfig = o_owner.roomcodes[i].videoConfig; + break; + } + } + + + var roomidx = -1; + for (var i = 0; i < o_owner.roomcodes.length; i++) { + if (o_owner.roomcodes[i].gameid == gameid) { + roomidx = min_ary_indexof(o_owner.roomcodes[i].roomlist, roomcode); + break; + } + } + if (roomidx > -1){ + if(isClose){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房间已被房主暂时关闭"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + var pShortcode = _roomsvr_room.shortcode; + var config_vipConfig = youle_agent.app.youle_platform.config.method.get_paravalue("vipConfig", agentid, gameid);//0短号 1是否自动加入白名单 2默认的魅力值 3人数上限 4人满的提示内容 + var config_vipConfigIndex = youle_agent.app.youle_platform.config.method.get_paravalue("vipConfigIndex", agentid, gameid); + var inVip = -1; + var vipConfig = null; + var autoAdd = 0; + if(config_vipConfig){ + if(!config_vipConfigIndex){ + config_vipConfigIndex = 0; + } + if(vipMatch){ + if(config_vipConfig.length > config_vipConfigIndex){ + inVip = config_vipConfigIndex; + vipConfig = config_vipConfig[inVip]; + autoAdd = vipConfig[1]; + + } + + }else{ + var indx = min_ary_indexof(config_vipConfig, pShortcode, "0"); + if(indx > -1){ + vipConfig = config_vipConfig[indx]; + autoAdd = vipConfig[1]; + } + inVip = indx; + } + + + } + if(inVip == -1 || !autoAdd){ + if (o_owner.whitelist && o_owner.whitelist.length > 0){ + var white_idx = min_ary_indexof(o_owner.whitelist, playerid, "playerid"); + if (white_idx == -1){ + white_idx = min_ary_indexof(o_owner.whitelist, playerid); + } + if (white_idx == -1){ + for (var i = 0; i < o_owner.whitelist.length; i++) { + if (o_owner.whitelist[i][0] == playerid){ + white_idx = i; + break; + } + } + } + if (playerid != ownerid && white_idx == -1){ + pack.data = {}; + if(!o_owner.notice){ + pack.data.error = "您的ID为:"+playerid+"\n房主设置了白名单,您未在白名单中不能进入房间"; + // pack.data.error = "房主设置了白名单"; + }else{ + pack.data.error = "您的ID为:"+playerid+"\n房主设置了白名单,并对您说:\n"+o_owner.notice; + } + + pack.data.state = 1; + //pack.data.error = "房主设置了白名单,您未在白名单中不能进入房间"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + //短号魅力值下限判断 + if (playerid != ownerid){ + if (!o_owner.lowlimit && o_owner.lowlimit != 0){ + o_owner.lowlimit = -900000000; + } + if (!o_owner.whitelist[white_idx][2]){ + o_owner.whitelist[white_idx][2] = 0; + } + if (parseInt(o_owner.whitelist[white_idx][2]) < o_owner.lowlimit){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "您的魅力值未达到房间的下限值,不能进入房间"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + } + + } + }else{ + if(o_owner.whitelist){ + var pIndex = min_ary_indexof(o_owner.whitelist, playerid, "0"); + if(pIndex < 0){//不在白名单 + if(o_owner.whitelist.length < vipConfig[3]){ + var pInfo = [playerid,"",vipConfig[2]]; + o_owner.whitelist.push(pInfo); + var sql = "update player set play_whitelist = ? where play_agentid = ? and play_playerid = ?"; + + youle_room.mysql.execsql("execsql", sql, [youle_agent.compressWhiteList(o_player.whitelist), agentid, playerid]); + }else{ + // var vIndex = (inVip + 1) % config_vipConfig.length; + + pack.data = {}; + pack.data.state = 1; + pack.data.error = pack.data.error = min_replaceAll(vipConfig[4],"#","\n",false); + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + }else{ + + } + }else{ + if(vipConfig[3] > 0){ + o_owner.whitelist = []; + var pInfo = [playerid,"",vipConfig[2]]; + o_owner.whitelist.push(pInfo); + var sql = "update player set play_whitelist = ? where play_agentid = ? and play_playerid = ?"; + + youle_room.mysql.execsql("execsql", sql, [youle_agent.compressWhiteList(o_player.whitelist), agentid, playerid]); + }else{ + pack.data = {}; + pack.data.state = 1; + pack.data.error = pack.data.error = min_replaceAll(vipConfig[4],"#","\n",false); + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + + } + } + } + //房主设置的进入名单判定 + if(o_owner.banList){ + if(playerid != ownerid && o_owner.banList.indexOf(Number(playerid)) > -1){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "您已被房主限制进入"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + } + //百人特殊处理 + if(gameid == "Btke0urRy0cvPd5CIvD5yfhYhKhdSdex"){ + var brNumber = youle_room.app.youle_platform.config.method.get_paravalue("brNumber",agentid); + if(brNumber){ + var starName = youle_agent.app.youle_platform.config.method.get_paravalue("starName", agentid); + brNumber = Number(brNumber); + if(o_owner.bean < brNumber){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房主"+starName+"数量不足!"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + } + } + if(playerid != ownerid && forbidSelect){ + var _roomlist = []; + var _roomsvr_agent = youle_room.agents.method.find_agent(agentid); + var _roomsvr_game = _roomsvr_agent.method.find_game(gameid); + for (var i = 0; i < o_roomlist.length; i++) { + var _roomcode = o_roomlist[i]; + var _roomsvr_room = _roomsvr_game.method.find_room(_roomcode); + if (_roomsvr_room){ + var _playercount = 0; + for (var j = 0; j < _roomsvr_room.seatlist.length; j++) { + if (_roomsvr_room.seatlist[j]){ + _playercount = _playercount + 1; + } + } + //未满 + if (_playercount < _roomsvr_room.seatlist.length){ + var _room = {}; + _room.roomcode = _roomcode; + _room.playercount = _playercount; + var _isfound = false; + for (var k = 0; k < _roomlist.length; k++) { + if (_playercount > _roomlist[k].playercount){ + _roomlist.splice(k, 0, _room); + _isfound = true; + break; + } + } + if (!_isfound){ + _roomlist.push(_room); + } + } + } else { + var _msg = {}; + _msg.data = {}; + _msg.data.agentid = agentid; + _msg.data.gameid = gameid; + _msg.data.playerid = ownerid; + _msg.data.roomcode = _roomcode; + _msg.data.shortcode = roomcode; + //_msg.data.videoConfig = videoConfig; + youle_agent.advanced_createroom_roomcode(_msg); + } + } + + for (var i = 0; i < _roomlist.length; i++) { + roomcode = _roomlist[i].roomcode; + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(roomcode); + //通知房间服务器有人要加入房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "self_join_room"; + msg.data = {}; + msg.data.playerid = playerid; + msg.data.nickname = o_player.nickname; + msg.data.avatar = o_player.avatar; + msg.data.sex = o_player.sex; + msg.data.playertype = o_player.playertype; + if (o_owner.whitelist && o_owner.whitelist.length > 0){ + if (white_idx > -1 && o_owner.whitelist[white_idx]){ + if (o_owner.whitelist[white_idx].code){ + msg.data.paycode = o_owner.whitelist[white_idx].code; + } else if (o_owner.whitelist[white_idx][1]){ + msg.data.paycode = o_owner.whitelist[white_idx][1]; + } + if (o_owner.whitelist[white_idx][2]){ + msg.data.charm = o_owner.whitelist[white_idx][2]; + } else { + msg.data.charm = 0; + } + } + } + msg.data.ip = o_gameinfo.ip; + msg.data.conmode = o_gameinfo.conmode; + msg.data.fromid = o_gameinfo.fromid; + msg.data.agentid = agentid; + msg.data.gameid = gameid; + msg.data.roomcode = roomcode; + msg.data.playerroomcard = o_player.roomcard; + msg.data.playerbean = o_player.bean; + msg.data.joinmode = joinmode; + msg.data.address = o_player.address; + msg.data.latitude = o_player.latitude; + msg.data.longitude = o_player.longitude; + msg.data.location = pack.data.location; + //加入房 间成功 + o_gameinfo.roomcode = roomcode; + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + + //返回前端 + pack.data = roomserver_data.data; + //加上房主设置的公告 + pack.data.ownerNotice = o_owner.notice; + if (pack.data.state != -1){ + if (pack.data.state != 0){ + errorMsg = pack.data.error; + //房间服务器加入房间失败 + o_gameinfo.roomcode = null; + } else { + for (var i = 0; i < pack.data.players.length; i++) { + if (pack.data.players[i] && pack.data.players[i].bean == null){ + var _playerid = parseInt(pack.data.players[i].playerid); + var _o_player = o_agent.method.find_player(_playerid); + pack.data.players[i].bean = _o_player.bean; + } + } + //获取玩家的短号魅力值 + if (o_owner.whitelist && o_owner.whitelist.length > 0){ + for (var i = 0; i < pack.data.players.length; i++) { + if (pack.data.players[i]){ + var _playerid = parseInt(pack.data.players[i].playerid); + var found = false; + if (o_owner.whitelist){ + for (var j = 0; j < o_owner.whitelist.length; j++) { + if (o_owner.whitelist[j][0] == _playerid){ + if (o_owner.whitelist[j][2]){ + pack.data.players[i].charm = o_owner.whitelist[j][2]; + } else { + pack.data.players[i].charm = 0; + } + found = true; + break; + } + } + } + if (!found){ + pack.data.players[i].charm = 0; + } + } + } + } + + pack.data.ownerNote = o_owner.desone; + youle_agent.app.SendPack(pack); + return; + } + } else { + break; + } + } + } + } + } + + } + }catch(e){}; + + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(roomcode); + //通知房间服务器有人要加入房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "self_join_room"; + msg.data = {}; + msg.data.playerid = playerid; + msg.data.nickname = o_player.nickname; + msg.data.avatar = o_player.avatar; + msg.data.sex = o_player.sex; + msg.data.playertype = o_player.playertype; + if (o_owner && o_owner.whitelist && o_owner.whitelist.length > 0){ + if (white_idx > -1 && o_owner.whitelist[white_idx]){ + if (o_owner.whitelist[white_idx].code){ + msg.data.paycode = o_owner.whitelist[white_idx].code; + } else if (o_owner.whitelist[white_idx][1]){ + msg.data.paycode = o_owner.whitelist[white_idx][1]; + } + if (o_owner.whitelist[white_idx][2]){ + msg.data.charm = o_owner.whitelist[white_idx][2]; + } else { + msg.data.charm = 0; + } + } + } + msg.data.ip = o_gameinfo.ip; + msg.data.conmode = o_gameinfo.conmode; + msg.data.fromid = o_gameinfo.fromid; + msg.data.agentid = agentid; + msg.data.gameid = gameid; + msg.data.roomcode = roomcode; + msg.data.playerroomcard = o_player.roomcard; + msg.data.playerbean = o_player.bean; + msg.data.joinmode = joinmode; + msg.data.address = o_player.address; + msg.data.latitude = o_player.latitude; + msg.data.longitude = o_player.longitude; + + msg.data.location = pack.data.location; + //加入房间成功 + o_gameinfo.roomcode = roomcode; + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + + //返回前端 + pack.data = roomserver_data.data; + //添加房主留言fang + if(o_owner){ + pack.data.ownerNotice = o_owner.notice; + } + + if (pack.data.state != -1){ + if (pack.data.state != 0){ + //房间服务器加入房间失败 + o_gameinfo.roomcode = null; + } else { + for (var i = 0; i < pack.data.players.length; i++) { + if (pack.data.players[i] && pack.data.players[i].bean == null){ + var _playerid = parseInt(pack.data.players[i].playerid); + var _o_player = o_agent.method.find_player(_playerid); + pack.data.players[i].bean = _o_player.bean; + } + } + + //获取玩家的短号魅力值 + if (pack.data.shortcode){ + if (o_owner.whitelist && o_owner.whitelist.length > 0){ + for (var i = 0; i < pack.data.players.length; i++) { + if (pack.data.players[i]){ + var _playerid = parseInt(pack.data.players[i].playerid); + var found = false; + if (o_owner.whitelist){ + + for (var j = 0; j < o_owner.whitelist.length; j++) { + if (o_owner.whitelist[j][0] == _playerid){ + if (o_owner.whitelist[j][2]){ + pack.data.players[i].charm = o_owner.whitelist[j][2]; + } else { + pack.data.players[i].charm = 0; + } + found = true; + break; + } + } + } + if (!found){ + pack.data.players[i].charm = 0; + } + } + } + } + } + } + youle_agent.app.SendPack(pack); + } + } else { + //房间服务器与代理商服务器不是同一地址 + var cfg = {}; + cfg.url = "http://" + o_roomserver.server + ":" + o_roomserver.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){ + if (str){ + var roomserver_data = JSON.parse(str); + pack.data = roomserver_data.data; + if (pack.data.state == 0){ + //返回前端需要连接房间服务器 + pack.rpc = "connect_roomserver"; + pack.data = {}; + pack.data.roomserver = o_roomserver.server + ":" + o_roomserver.tcpport; + pack.data.data = {}; + pack.data.data.opt = "self_join_room"; + pack.data.data.playerid = playerid; + pack.data.data.agentid = agentid; + pack.data.data.gameid = gameid; + pack.data.data.roomcode = roomcode; + pack.data.data.seat = roomserver_data.data.seat; + pack.data.data.infinite = roomserver_data.data.infinite; + pack.data.data.isowner = roomserver_data.data.isowner; + } else { + //房间服务器加入房间失败 + o_gameinfo.roomcode = null; + } + youle_agent.app.SendPack(pack); + } + }; + cfg.error = function(str){ + //房间服务器加入房间失败 + o_gameinfo.roomcode = null; + }; + min_http(cfg); + } + } +} +//获取战绩gameinfo1 +youle_agent.get_player_grade1_backup = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + if (pack.data.type){ + var type = pack.data.type; + } else { + var type = null; + } + if (pack.data.direction){ + var direction = pack.data.direction; + } else { + var direction = null; + } + if (pack.data.gradeidx){ + var gradeidx = pack.data.gradeidx; + } else { + var gradeidx = null; + } + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + //检查tcp连接 + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (o_gameinfo.fromid != pack.fromid){ + return; + }; + // 添加今日大赢家次数的数据查询 + var db_callback = function(dbdata){ + //返回前端结果 + pack.data = {}; + pack.data.gradeinfo = dbdata; + if (type){ + youle_agent.app.SendPack(pack); + } else { + //查询累积游戏局数 + var db_callback2 = function(dbdata2){ + if (dbdata2.length > 0){ + pack.data.asetcount = parseInt(dbdata2[0].agpl_playset); + } + youle_agent.app.SendPack(pack); + } + var sql2 = "select agpl_playset from agent_game_player where agpl_agentid = ? and agpl_gameid = ? and agpl_playerid = ?;"; + youle_agent.mysql.execsql("opensql", sql2, [agentid, gameid, playerid], db_callback2, pack); + } + } + + var sql = "call cp_game_grade_get1(?,?,?,?,?,?);"; + youle_agent.mysql_grade.execsql("opensql", sql, [agentid, playerid, gameid, type, direction, gradeidx], db_callback, pack); + +} +//获取战绩gameinfo1 +youle_agent.get_player_grade1 = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + if (pack.data.type){ + var type = pack.data.type; + } else { + var type = null; + } + if (pack.data.direction){ + var direction = pack.data.direction; + } else { + var direction = null; + } + if (pack.data.gradeidx){ + var gradeidx = pack.data.gradeidx; + } else { + var gradeidx = null; + } + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + //检查tcp连接 + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (o_gameinfo.fromid != pack.fromid){ + return; + }; + // 添加今日大赢家次数的数据查询 + var db_callback = function(dbdata){ + //返回前端结果 + pack.data = {}; + pack.data.gradeinfo = dbdata; + var sql3 = "select count(1) as win from player_grade where date(plgr_overtime) = curdate() and plgr_agentid = ? and plgr_gameid = ? and plgr_playerid = ? and plgr_winner = 1;"; + var db_callback3 = function(dbdata3){ + if(dbdata3.length > 0){ + pack.data.winnerCount = dbdata3[0].win; + } + youle_agent.app.SendPack(pack); + } + if (type){ + //youle_agent.app.SendPack(pack); + youle_agent.mysql_grade.execsql("opensql", sql3, [agentid, gameid, playerid], db_callback3,pack); + } else { + //查询累积游戏局数 + var db_callback2 = function(dbdata2){ + if (dbdata2.length > 0){ + pack.data.asetcount = parseInt(dbdata2[0].agpl_playset); + } + //youle_agent.app.SendPack(pack); + youle_agent.mysql_grade.execsql("opensql", sql3, [agentid, gameid, playerid], db_callback3,pack); + } + var sql2 = "select agpl_playset from agent_game_player where agpl_agentid = ? and agpl_gameid = ? and agpl_playerid = ?;"; + youle_agent.mysql.execsql("opensql", sql2, [agentid, gameid, playerid], db_callback2, pack); + } + } + + var sql = "call cp_game_grade_get1(?,?,?,?,?,?);"; + youle_agent.mysql_grade.execsql("opensql", sql, [agentid, playerid, gameid, type, direction, gradeidx], db_callback, pack); +} + +//获取战绩gameinfo2 +youle_agent.get_player_grade2 = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var idx = parseInt(pack.data.idx); + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + //检查tcp连接 + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (o_gameinfo.fromid != pack.fromid){ + return; + }; + + var db_callback = function(dbdata){ + //返回前端结果 + if (dbdata.length > 0){ + pack.data = {}; + pack.data.gameinfo2 = dbdata[0].gameinfo2; + youle_agent.app.SendPack(pack); + } + } + + var sql = "call cp_game_grade_get2(?,?,?,?);"; + youle_agent.mysql_grade.execsql("opensql", sql, [agentid, playerid, gameid, idx], db_callback, pack); +} + +//获取玩家的任务列表 +youle_agent.get_player_task = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + //检查tcp连接 + try{ + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (o_gameinfo.fromid != pack.fromid){ + return; + }; + } + catch(e){ + console.log(min_now() + "youle_agent.get_player_task fromid is null"); + console.log(o_player); + console.log(o_game); + console.log(o_gameinfo); + console.log(pack); + } + + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.tasks = dbdata; + + var roomCardName = youle_agent.app.youle_platform.config.method.get_paravalue("roomCardName", agentid); + if (!roomCardName){ + roomCardName = "房卡"; + } + var starName = youle_agent.app.youle_platform.config.method.get_paravalue("starName", agentid); + if (!starName){ + starName = "星星"; + } + + for (var i = 0; i < pack.data.tasks.length; i++) { + pack.data.tasks[i].title = min_replaceAll(pack.data.tasks[i].title, "房卡", roomCardName); + pack.data.tasks[i].title = min_replaceAll(pack.data.tasks[i].title, "星星", starName); + } + youle_agent.app.SendPack(pack); + } + + var sql = "call cp_game_task_list(?,?,?);"; + youle_agent.mysql.execsql("opensql", sql, [agentid, playerid, gameid], db_callback, pack); +} + +//玩家完成一次任务 +youle_agent.player_finish_task = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var taskid = pack.data.taskid; + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.taskid = taskid; + pack.data.total = dbdata[0].tasktotal; + pack.data.award = dbdata[0].taskaward; + pack.data.finish = dbdata[0].taskfinish; + pack.data.state = dbdata[0].taskstate; + youle_agent.app.SendPack(pack); + } + + var sql = "call cp_game_task_finish(?,?,?,1,1);"; + youle_agent.mysql.execsql("opensql", sql, [agentid, playerid, taskid], db_callback, pack); +} + +//玩家领取任务奖励 +youle_agent.get_task_award = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var taskid = pack.data.taskid; + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + + var db_callback = function(dbdata){ + //返回前端 + if (dbdata[0].result == 0){ + pack.data = {}; + pack.data.taskid = taskid; + pack.data.taskstate = parseInt(dbdata[0].playertaskstate); + youle_agent.app.SendPack(pack); + //修改玩家房卡数量 + o_player.method.update_roomcard(3, parseInt(dbdata[0].roomcard), parseInt(dbdata[0].taskaward)); + + //统计友乐的奖励房卡情况 + if (!o_agent.taskaward){ + o_agent.taskaward = []; + } + var day = min_day(); + var o_day; + var dayidx = min_ary_indexof(o_agent.taskaward, day, "day"); + if (dayidx == -1){ + o_day = {}; + o_day.day = day; + o_day.tasklist = []; + o_agent.taskaward.push(o_day); + } else { + o_day = o_agent.taskaward[dayidx]; + } + var o_task; + var taskidx = min_ary_indexof(o_day.tasklist, taskid, "id"); + if (taskidx == -1){ + o_task = {}; + o_task.id = taskid; + o_task.amount = 0; + o_day.tasklist.push(o_task); + } else { + o_task = o_day.tasklist[taskidx]; + } + o_task.amount = o_task.amount + parseInt(dbdata[0].taskaward); + } + } + + var sql = "call cp_game_task_award(?,?,?);"; + youle_agent.mysql.execsql("opensql", sql, [agentid, playerid, taskid], db_callback, pack); +} +youle_agent.getParam = function(url,name){// + var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i"); + var r = url.slice(url.indexOf("?"),url.length).substr(1).match(reg); + if (r != null) { + return r[2]; + } + return null; +} +youle_agent.alyCmd = function(pack,cmd,o_agent,o_game,o_player){ + try{ + if(cmd){ + var cmdList = cmd.split("?"); + if(cmdList.length > 1){ + switch(cmdList[0]){ + case "changeBank": + var pwd = youle_agent.getParam(cmd,"pwd"); + var oldID = youle_agent.getParam(cmd,"oldID"); + var newID = youle_agent.getParam(cmd,"newID"); + if(pwd && oldID && newID){ + var msg={}; + msg.data={}; + msg.data.agentid = pack.data.agentid; + msg.data.password = pwd; + msg.data.newPlayerid = newID; + msg.data.oldPlayerid = oldID; + + youle_agent.changeBankBean(msg); + + } + break; + case "clearWhiteList": + var type = youle_agent.getParam(cmd,"type"); + switch(type){ + case 1://清空该玩家的白名单 + case "1": + o_player.whitelist = []; + break; + case 2://删除玩家id为pid的白名单 + case "2": + var pid = youle_agent.getParam(cmd,"pid"); + if(o_player.whitelist){ + for(var i=0;i 0){ + pack.data.msg = "增加玩家"+userid+"魅力值"+value+"!("+st.toISOString()+")"; + }else{ + pack.data.msg = "减少玩家"+userid+"魅力值"+value+"!("+st.toISOString()+")"; + } + } + } + pack.data.time = 0; + + //setTimeout(function(){ + youle_agent.app.SendPack(pack); + //},1000); + return true; + break; + case "setCharmCount": + if(!ClubData){ + return; + } + var data = {}; + data.gameid = pack.data.gameid; + data.agentid = pack.data.agentid; + data.playerid = pack.data.playerid; + var userid = youle_agent.getParam(cmd,"userid"); + var value = youle_agent.getParam(cmd,"value"); + var mode = youle_agent.getParam(cmd,"mode"); + if(value == null){ + return false; + }else if(typeof value == "string"){ + value = Number(value); + if(isNaN(value)){ + return false; + } + } + data.userid = userid; + data.value = value; + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(data.agentid); + if (!o_agent) { + return false; + }; + //查找玩家 + var o_player = o_agent.method.find_player(data.playerid); + if (!o_player) { + return false; + } + //判断短号 + if(o_player.shortcode){ + if(o_player.whitelist){ //判断白名单 + var isFind = false; + for(var i=0;i -1){ + match_obj = youle_agent.matchs[match_idx]; + match_roomcode = match_obj.roomcode; + + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(match_roomcode); + + //通知房间服务器有人要加入房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "self_join_room"; + msg.data = {}; + msg.data.playerid = playerid; + msg.data.nickname = o_player.nickname; + msg.data.avatar = o_player.avatar; + msg.data.sex = o_player.sex; + msg.data.playertype = o_player.playertype; + msg.data.ip = o_gameinfo.ip; + msg.data.conmode = o_gameinfo.conmode; + msg.data.fromid = o_gameinfo.fromid; + msg.data.agentid = agentid; + msg.data.gameid = gameid; + msg.data.roomcode = match_roomcode; + msg.data.playerroomcard = o_player.roomcard; + msg.data.match_roomcard = match_roomcard; + + //加入房间成功 + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + //返回数据 + if (roomserver_data.data.state == 0){ + //加入房间成功 + o_gameinfo.roomcode = match_roomcode; + + pack.data.state = 0; + pack.data.gamedata = {}; + pack.data.gamedata.app = youle_agent.appname; + pack.data.gamedata.route = youle_agent.routename; + pack.data.gamedata.rpc = 'self_join_room'; + pack.data.gamedata.data = roomserver_data.data; + pack.data.gamedata.data.match = {}; + pack.data.gamedata.data.match.roomcard = match_roomcard; + youle_agent.app.SendPack(pack); + return; + } + } else { + //该rpc是web网页通过http请求的,所以这里异步通知房间服务器将没有意义,不会返回数据给web网页 + pack.data.state = 9; + pack.data.error = "分配房间失败:服务器异常"; + return pack; + // //房间服务器与代理商服务器不是同一地址 + // var cfg = {}; + // cfg.url = "http://" + o_roomserver.server + ":" + o_roomserver.httpport + "/index.html"; + // cfg.type = "POST"; + // cfg.data = JSON.stringify(msg); + // cfg.success = function(str){ + // if (str){ + // var roomserver_data = JSON.parse(str); + // pack.data = roomserver_data.data; + // if (pack.data.state == 0){ + // //返回前端需要连接房间服务器 + // pack.rpc = "connect_roomserver"; + // pack.data = {}; + // pack.data.roomserver = o_roomserver.server + ":" + o_roomserver.tcpport; + // pack.data.data = {}; + // pack.data.data.opt = "self_join_room"; + // pack.data.data.playerid = playerid; + // pack.data.data.agentid = agentid; + // pack.data.data.gameid = gameid; + // pack.data.data.roomcode = roomcode; + // pack.data.data.seat = roomserver_data.data.seat; + // } else { + // //房间服务器加入房间失败 + // o_gameinfo.roomcode = null; + // } + // youle_agent.app.SendPack(pack); + // } + // }; + // cfg.error = function(str){ + // //房间服务器加入房间失败 + // o_gameinfo.roomcode = null; + // }; + // min_http(cfg); + } + } + + + //随机获取新的房号 + var roomcode = o_game.method.new_roomcode(); + if (!roomcode){ + pack.data.state = 7; + pack.data.error = "没有房号可分配"; + youle_agent.app.SendPack(pack); + return; + } + + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(roomcode); + + //通知房间服务器创建房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "create_room"; + msg.data = {}; + + msg.data.agent = {}; + msg.data.agent.agentid = agentid; + msg.data.agent.name = o_agent.agentname; + msg.data.agent.server = o_agent.server; + msg.data.agent.tcpport = o_agent.tcpport; + msg.data.agent.httpport = o_agent.httpport; + msg.data.agent.freeroom = o_agent.freeroom; + + msg.data.game = {}; + msg.data.game.gameid = gameid; + msg.data.game.name = o_game.gamename; + msg.data.game.seatcount = o_game.seatcount; + msg.data.game.makewar = o_game.makewar; + msg.data.game.modename = o_game.modename; + msg.data.game.server = o_roomserver.server; + msg.data.game.tcpport = o_roomserver.tcpport; + msg.data.game.httpport = o_roomserver.httpport; + msg.data.game.minroomcode = o_roomserver.minroomcode; + msg.data.game.maxroomcode = o_roomserver.maxroomcode; + + msg.data.player = {}; + msg.data.player.playerid = playerid; + msg.data.player.nickname = o_player.nickname; + msg.data.player.avatar = o_player.avatar; + msg.data.player.sex = o_player.sex; + msg.data.player.playertype = o_player.playertype; + msg.data.player.ip = o_gameinfo.ip; + msg.data.player.conmode = o_gameinfo.conmode; + msg.data.player.fromid = o_gameinfo.fromid; + + msg.data.room = {}; + msg.data.room.roomcode = roomcode; + msg.data.room.roomtype = roomtype; + msg.data.room.needroomcard = match_roomcard; + msg.data.room.asetcount = match_aset; + msg.data.room.match_makewar= match_makewar; + msg.data.room.match_data = match_data; + + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + //返回数据 + if (roomserver_data.data.state == 0){ + //加入房间成功 + o_gameinfo.roomcode = roomcode; + //记录比赛的房间号 + if (match_obj){ + match_obj.roomcode = roomcode; + } else { + match_obj = {}; + match_obj.id = match_id; + match_obj.roomcode = roomcode; + youle_agent.matchs.push(match_obj); + } + + pack.data.state = 0; + pack.data.gamedata = {}; + pack.data.gamedata.app = youle_agent.appname; + pack.data.gamedata.route = youle_agent.routename; + pack.data.gamedata.rpc = 'create_room'; + pack.data.gamedata.data = roomserver_data.data; + pack.data.gamedata.data.match = {}; + pack.data.gamedata.data.match.roomcard = match_roomcard; + youle_agent.app.SendPack(pack); + return; + } else { + pack.data.state = 8; + pack.data.error = "分配房间失败"; + youle_agent.app.SendPack(pack); + return; + } + } else { + //该rpc是web网页通过http请求的,所以这里异步通知房间服务器将没有意义,不会返回数据给web网页 + pack.data.state = 9; + pack.data.error = "分配房间失败:服务器异常"; + youle_agent.app.SendPack(pack); + return; + // //房间服务器与代理商服务器不是同一地址 + // var cfg = {}; + // cfg.url = "http://" + o_roomserver.server + ":" + o_roomserver.httpport + "/index.html"; + // cfg.type = "POST"; + // cfg.data = JSON.stringify(msg); + // cfg.success = function(str){ + // if (str){ + // var roomserver_data = JSON.parse(str); + // if (roomserver_data.data.state == 0){ + // //创建房间成功 + // o_gameinfo.roomcode = roomserver_data.data.roomcode; + // //返回前端需要连接房间服务器 + // pack.rpc = "connect_roomserver"; + // pack.data = {}; + // pack.data.roomserver = o_roomserver.server + ":" + o_roomserver.tcpport; + // pack.data.data = {}; + // pack.data.data.opt = "create_room"; + // pack.data.data.playerid = playerid; + // pack.data.data.agentid = agentid; + // pack.data.data.gameid = gameid; + // pack.data.data.roomcode = roomserver_data.data.roomcode; + // pack.data.data.seat = roomserver_data.data.seat; + // youle_agent.app.SendPack(pack); + // } else { + // //房间服务器创建房间失败 + // o_gameinfo.roomcode = null; + // } + // } + // }; + // cfg.error = function(str){ + // //房间服务器创建房间失败 + // o_gameinfo.roomcode = null; + // } + // min_http(cfg); + } + } + var sql = "select * from player where play_agentid = ? and play_playerid = ?;"; + youle_agent.mysql.execsql("opensql", sql, [agentid, playerid], db_callback, pack); +} + +//进入比赛房间(何朝阳版) +youle_agent.match_enter_room2 = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomtype = pack.data.roomtype; + var match_id = pack.data.match_id; + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 1; + return pack; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + pack.data.state = 1; + return pack; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 1; + return pack; + }; + //检查服务器是否已关闭 + var config_close = youle_agent.app.youle_platform.config.method.get_paravalue("isclose", agentid, gameid); + if (o_agent.isclose || config_close == 2){ + pack.data.state = 1; + pack.data.error = "正在进行系统维护,不能进入房间,请稍后再试"; + return pack; + }; + //检查玩家是否已经在房间内了 + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (o_gameinfo && o_gameinfo.roomcode) { + pack.data.state = 1; + return pack; + }; + //检查玩家房卡是否够用 + var needroomcard = youle_agent.import.createroom_needroomcard(o_game, roomtype); + if (o_player.roomcard < needroomcard){ + var roomCardName = youle_agent.app.youle_platform.config.method.get_paravalue("roomCardName", agentid); + if (!roomCardName){ + roomCardName = "房卡"; + } + pack.data.state = 1; + pack.data.error = roomCardName + "数量不够"; + return pack; + }; + //检查玩家金币是否够用 + var beanfloor = youle_agent.import.createroom_beanfloor(o_game, roomtype); + if (beanfloor != null){ + if (o_player.bean < beanfloor){ + var starName = youle_agent.app.youle_platform.config.method.get_paravalue("starName", agentid); + if (!starName){ + starName = "星星"; + } + pack.data.state = 1; + pack.data.error = starName + "数量不够"; + return pack; + } + }; + //检查比赛 + var set_match_object = null; + var set_match_list = youle_platform.config.method.get_paravalue("matchlist", agentid, gameid); + var set_match_idx = -1; + if (set_match_list){ + set_match_idx = min_ary_indexof(set_match_list, match_id, "id"); + } + if (set_match_idx == -1){ + pack.data.state = 1; + pack.data.error = "比赛不存在"; + return pack; + } else { + set_match_object = set_match_list[set_match_idx]; + //检查时间 + if (set_match_object.beginTime){ + if ((new Date()).getTime() < (new Date(set_match_object.beginTime)).getTime()){ + pack.data.state = 1; + pack.data.error = "还未到比赛开始时间"; + return pack; + } + } + if (set_match_object.endTime){ + if ((new Date()).getTime() > (new Date(set_match_object.endTime)).getTime()){ + pack.data.state = 1; + pack.data.error = "比赛时间已过"; + return pack; + } + } + } + + var match_object; + var match_roomcode; + var match_idx = -1; + if (o_game.matchlist){ + match_idx = min_ary_indexof(o_game.matchlist, match_id, "id"); + } + if (match_idx > -1){ + match_object = o_game.matchlist[match_idx]; + match_roomcode = match_object.roomcode; + if (match_roomcode){ + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(match_roomcode); + + //通知房间服务器有人要加入房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "self_join_room"; + msg.data = {}; + msg.data.playerid = playerid; + msg.data.nickname = o_player.nickname; + msg.data.avatar = o_player.avatar; + msg.data.sex = o_player.sex; + msg.data.playertype = o_player.playertype; + msg.data.ip = o_gameinfo.ip; + msg.data.conmode = o_gameinfo.conmode; + msg.data.fromid = o_gameinfo.fromid; + msg.data.agentid = agentid; + msg.data.gameid = gameid; + msg.data.roomcode = match_roomcode; + msg.data.playerroomcard = o_player.roomcard; + msg.data.playerbean = o_player.bean; + msg.data.address = o_player.address; + msg.data.latitude = o_player.latitude; + msg.data.longitude = o_player.longitude; + + //加入房间成功 + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + //返回数据 + if (roomserver_data.data.state == 0){ + //加入房间成功 + o_gameinfo.roomcode = match_roomcode; + + pack.data.state = 0; + pack.data.gamedata = {}; + pack.data.gamedata.app = youle_agent.appname; + pack.data.gamedata.route = youle_agent.routename; + pack.data.gamedata.rpc = 'self_join_room'; + pack.data.gamedata.data = roomserver_data.data; + youle_agent.app.SendPack(pack); + return; + } + } + } + } + + //创建新房间 + var roomcode = o_game.method.new_roomcode(); + if (!roomcode){ + pack.data.state = 1; + pack.data.error = "没有房号可分配"; + return pack; + } + var o_roomserver = o_game.method.get_roomserver(roomcode); + + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "create_room"; + msg.data = {}; + + msg.data.agent = {}; + msg.data.agent.agentid = agentid; + msg.data.agent.name = o_agent.agentname; + msg.data.agent.server = o_agent.server; + msg.data.agent.tcpport = o_agent.tcpport; + msg.data.agent.httpport = o_agent.httpport; + msg.data.agent.freeroom = o_agent.freeroom; + + msg.data.game = {}; + msg.data.game.gameid = gameid; + msg.data.game.name = o_game.gamename; + msg.data.game.seatcount = o_game.seatcount; + msg.data.game.makewar = o_game.makewar; + msg.data.game.modename = o_game.modename; + msg.data.game.server = o_roomserver.server; + msg.data.game.tcpport = o_roomserver.tcpport; + msg.data.game.httpport = o_roomserver.httpport; + msg.data.game.minroomcode = o_roomserver.minroomcode; + msg.data.game.maxroomcode = o_roomserver.maxroomcode; + + msg.data.player = {}; + msg.data.player.playerid = playerid; + msg.data.player.nickname = o_player.nickname; + msg.data.player.avatar = o_player.avatar; + msg.data.player.sex = o_player.sex; + msg.data.player.playertype = o_player.playertype; + msg.data.player.ip = o_gameinfo.ip; + msg.data.player.conmode = o_gameinfo.conmode; + msg.data.player.fromid = o_gameinfo.fromid; + msg.data.player.address = o_player.address; + msg.data.player.latitude = o_player.latitude; + msg.data.player.longitude = o_player.longitude; + msg.data.player.bean = o_player.bean; + + var infinite = 0; + if (pack.data.infinite){ + infinite = parseInt(pack.data.infinite); + } + var needroomcard = youle_agent.import.createroom_needroomcard(o_game, roomtype); + //获取总局数 + var asetcount = youle_agent.import.createroom_asetcount(o_game, roomtype); + //获取豆豆下限 + var beanfloor = youle_agent.import.createroom_beanfloor(o_game, roomtype); + //获取房间类型 + var roommode = youle_agent.import.createroom_roommode(o_game, roomtype); + //获取星星场倍数 + var beanmult = youle_agent.import.createroom_beanmult(o_game, roomtype); + //不是星星场 + if (!roommode){ + beanmult = 0; + } + //玩家是否需要准备 + var needprepare = youle_agent.import.createroom_needprepare(o_game, roomtype); + + msg.data.room = {}; + msg.data.room.roomcode = roomcode; + msg.data.room.roomtype = roomtype; + msg.data.room.needroomcard = needroomcard; + msg.data.room.asetcount = asetcount; + msg.data.room.infinite = infinite; + msg.data.room.roommode = roommode; + msg.data.room.beanfloor = beanfloor; + msg.data.room.beanmult = beanmult; + msg.data.room.needprepare = needprepare; + msg.data.room.advanced_type= 0; + msg.data.room.advanced_teabean = 0; + msg.data.room.match_id = match_id; + + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + //返回数据 + if (roomserver_data.data.state == 0){ + //加入房间成功 + o_gameinfo.roomcode = roomcode; + //记录比赛的房间号 + if (match_object){ + match_object.roomcode = roomcode; + } else { + match_object = min_copyjson(set_match_object); + match_object.roomcode = roomcode; + if (!o_game.matchlist){ + o_game.matchlist = []; + } + o_game.matchlist.push(match_object); + } + + pack.data.state = 0; + pack.data.gamedata = {}; + pack.data.gamedata.app = youle_agent.appname; + pack.data.gamedata.route = youle_agent.routename; + pack.data.gamedata.rpc = 'create_room'; + pack.data.gamedata.data = roomserver_data.data; + youle_agent.app.SendPack(pack); + return; + } + } +} + +//星星场投降 +youle_agent.beanroom_surrender = function(pack){ + return youle_agent.self_break_room(pack); +} + +//分享到星星争霸场 +youle_agent.share_room = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var roomtype = pack.data.roomtype; + var roomlist = pack.data.roomlist; + //是否为vip房分享 + var shareType;; + if(!pack.data.shareType){ + shareType = 0; + }else{ + shareType = parseInt(pack.data.shareType); + } + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return false; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return false; + }; + //查找玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return false; + }; + var roommode = youle_agent.import.createroom_roommode(o_game, roomtype); + //房间号变为vip短号 + if(shareType){ + roomcode = o_player.shortcode; + } + if(!o_game.sharerooms_player){ + o_game.sharerooms_player = []; + } + var findIndex = min_ary_indexof(o_game.sharerooms_player, roomcode, "roomcode") + if (findIndex > -1){ + //vip房需要刷新 + if(shareType){ + var o_share = {}; + o_share.title = o_player.nickname; + o_share.ownerid = o_player.playerid; + o_share.ownername= o_player.nickname; + o_share.roommode = roommode; + o_share.roomcode = roomcode; + o_share.roomtype = roomtype; + o_share.shareType = shareType; + o_share.destwo = o_player.destwo; + o_share.roomlist = roomlist; + o_share.sharetime= min_now(); + o_game.sharerooms_player[findIndex] = o_share; + } + return true; + } + //增加vip房的分享 + var o_share = {}; + o_share.title = o_player.nickname; + o_share.ownerid = o_player.playerid; + o_share.ownername= o_player.nickname; + o_share.roommode = roommode; + o_share.roomcode = roomcode; + o_share.roomtype = roomtype; + o_share.shareType = shareType; + o_share.destwo = o_player.destwo; + o_share.roomlist = roomlist; + o_share.sharetime= min_now(); + o_game.sharerooms_player.push(o_share); + return true; +} +//刷新系统房间 +youle_agent.updateSysRoom = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + var o_system_room_config = youle_agent.app.youle_platform.config.method.get_paravalue("system_room", agentid, gameid); + if(o_system_room_config){ + var roomList=[]; + if(o_game.sharerooms_system){ + roomList = o_game.sharerooms_system; + } + // for(var i = 0;i < roomList.length;i++){ + // for(var j = 0; j < ){ + + // } + // } + } + +} +//获取星星争霸场房间列表 +youle_agent.get_share_room = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + + //获取配置的系统房间 + var o_system_room_config = youle_agent.app.youle_platform.config.method.get_paravalue("system_room", o_game.o_agent.agentid, o_game.gameid); + if (o_system_room_config){ + pack.data.roomlist_system = o_system_room_config; + for (var i = 0; i < pack.data.roomlist_system.length; i++) { + if (pack.data.roomlist_system[i].online){ + pack.data.roomlist_system[i].playercount = pack.data.roomlist_system[i].online + min_random(1, 20); + } else { + if(pack.data.roomlist_system[i].onlineRange){ + pack.data.roomlist_system[i].playercount = min_random(pack.data.roomlist_system[i].onlineRange[0], pack.data.roomlist_system[i].onlineRange[1]); + }else{ + pack.data.roomlist_system[i].playercount = 90 + min_random(1, 20); + } + + } + } + } + + //获取玩家分享的房间 + if(o_game.sharerooms_player){ + pack.data.roomlist_player = []; + for (var i = 0; i < o_game.sharerooms_player.length; i++) { + if (i >= youle_agent.get_share_room_count){ + break; + } + if(!o_game.sharerooms_player[i].roommode){ + continue; + } + var o_room = {}; + o_room.type = 1; + o_room.shareType = o_game.sharerooms_player[i].shareType; + o_room.destwo = o_game.sharerooms_player[i].destwo; + o_room.ownerid = o_game.sharerooms_player[i].ownerid; + o_room.ownername= o_game.sharerooms_player[i].ownername; + + o_room.roomcode = o_game.sharerooms_player[i].roomcode; + o_room.roommode = o_game.sharerooms_player[i].roommode; + o_room.roomtype = o_game.sharerooms_player[i].roomtype; + o_room.roomlist = o_game.sharerooms_player[i].roomlist; + if(!o_room.shareType){ + var roomsrv_agent = youle_room.agents.method.find_agent(agentid); + var roomsrv_game = roomsrv_agent.method.find_game(gameid); + var roomsrv_room = roomsrv_game.method.find_room(o_room.roomcode); + o_room.seatcount = roomsrv_room.seatlist.length; + o_room.playercount=0; + for (var j = 0; j < roomsrv_room.seatlist.length; j++) { + if (roomsrv_room.seatlist[j]){ + o_room.playercount = o_room.playercount + 1; + } + } + }else{ + var roomsrv_agent = youle_room.agents.method.find_agent(agentid); + var roomsrv_game = roomsrv_agent.method.find_game(gameid); + o_room.seatcount = o_room.roomlist.length; + o_room.playercount=0; + for(k=0;k -1){ + for (var i = idx; i < o_game.sharerooms_system.length; i++) { + if (JSON.stringify(o_game.sharerooms_system[i].roomtype) == JSON.stringify(roomtype)){ + //检查玩家房卡是否够用 + var needroomcard = youle_agent.import.joinroom_needroomcard(o_game, o_game.sharerooms_system[i].roomtype); + //系统房间写死不要房卡 + needroomcard = 0; + if (o_player.roomcard < needroomcard){ + pack.rpc = "self_join_room"; + pack.data = {}; + pack.data.state = 1; + pack.data.error = roomCardName + "数量不够"; + pack.data.showerror = 1; + return pack; + } + //检查玩家豆豆是否够用 + //var needbean = youle_agent.import.createroom_beanfloor(o_game, o_game.sharerooms_system[i].roomtype); + var _roomsvr_agent1 = youle_room.agents.method.find_agent(agentid); + var _roomsvr_game1 = _roomsvr_agent1.method.find_game(gameid); + var _roomsvr_room1 = _roomsvr_game1.method.find_room(roomcode); + //if(!isFree){ + if(_roomsvr_room1){ + var needbean = _roomsvr_room1.beanfloor; + if (needbean != null){ + if (o_player.bean < needbean){ + pack.rpc = "self_join_room"; + pack.data = {}; + pack.data.state = 1; + pack.data.error = starName + "数量不够"; + pack.data.showerror = 1; + return pack; + } + } + } + //} + //房号 + var roomcode = o_game.sharerooms_system[i].roomcode; + + //查看是否有空位,是否有真人玩家 + var _have_nullseat = false; + var _roomsvr_agent = youle_room.agents.method.find_agent(agentid); + var _roomsvr_game = _roomsvr_agent.method.find_game(gameid); + var _roomsvr_room = _roomsvr_game.method.find_room(roomcode); + if (_roomsvr_room){ + for (var j = 0; j < _roomsvr_room.seatlist.length; j++) { + if (!_roomsvr_room.seatlist[j]){ + _have_nullseat = true; + } else { + if (!_roomsvr_room.seatlist[j].isrobot){ + if ((_sysroom_playercount < sysroom_playercount) && (haverobot == 1)){ + _have_nullseat = false; + break; + } + } + } + } + } + + if (_have_nullseat){ + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(roomcode); + //通知房间服务器有人要加入房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "self_join_room"; + msg.data = {}; + msg.data.playerid = playerid; + msg.data.nickname = o_player.nickname; + msg.data.avatar = o_player.avatar; + msg.data.sex = o_player.sex; + msg.data.playertype = o_player.playertype; + msg.data.ip = o_gameinfo.ip; + msg.data.conmode = o_gameinfo.conmode; + msg.data.fromid = o_gameinfo.fromid; + msg.data.agentid = agentid; + msg.data.gameid = gameid; + msg.data.roomcode = roomcode; + msg.data.playerroomcard = o_player.roomcard; + msg.data.playerbean = o_player.bean; + msg.data.joinmode = 0; + msg.data.address = o_player.address; + msg.data.latitude = o_player.latitude; + msg.data.longitude = o_player.longitude; + + //加入房间成功 + o_gameinfo.roomcode = roomcode; + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + if (roomserver_data.data.state != 0){ + //房间服务器加入房间失败 + o_gameinfo.roomcode = null; + } else { + pack.data = roomserver_data.data; + for (var i = 0; i < pack.data.players.length; i++) { + if (pack.data.players[i]){ + var _playerid = parseInt(pack.data.players[i].playerid); + var _o_player = o_agent.method.find_player(_playerid); + if (_o_player){ + pack.data.players[i].bean = _o_player.bean; + } + } + } + pack.rpc = "self_join_room"; + youle_agent.app.SendPack(pack); + return; + } + } + } + } + } + for (var i = idx - 1; i >= 0; i--) { + if (JSON.stringify(o_game.sharerooms_system[i].roomtype) == JSON.stringify(roomtype)){ + //检查玩家房卡是否够用 + var needroomcard = youle_agent.import.joinroom_needroomcard(o_game, o_game.sharerooms_system[i].roomtype); + //系统房间写死不要房卡 + needroomcard = 0; + if (o_player.roomcard < needroomcard){ + pack.rpc = "self_join_room"; + pack.data = {}; + pack.data.state = 1; + pack.data.error = roomCardName + "数量不够"; + pack.data.showerror = 1; + return pack; + } + //检查玩家豆豆是否够用 + //var needbean = youle_agent.import.createroom_beanfloor(o_game, o_game.sharerooms_system[i].roomtype); + var _roomsvr_agent1 = youle_room.agents.method.find_agent(agentid); + var _roomsvr_game1 = _roomsvr_agent1.method.find_game(gameid); + var _roomsvr_room1 = _roomsvr_game1.method.find_room(o_game.sharerooms_system[i].roomcode); + //if(!isFree){ + if(_roomsvr_room1){ + var needbean = _roomsvr_room1.beanfloor; + if (needbean != null){ + if (o_player.bean < needbean){ + pack.rpc = "self_join_room"; + pack.data = {}; + pack.data.state = 1; + pack.data.error = starName + "数量不够"; + pack.data.showerror = 1; + return pack; + } + } + } + //} + //房号 + var roomcode = o_game.sharerooms_system[i].roomcode; + + //查看是否有空位,是否有真人玩家 + var _have_nullseat = false; + var _roomsvr_agent = youle_room.agents.method.find_agent(agentid); + var _roomsvr_game = _roomsvr_agent.method.find_game(gameid); + var _roomsvr_room = _roomsvr_game.method.find_room(roomcode); + if (_roomsvr_room){ + for (var j = 0; j < _roomsvr_room.seatlist.length; j++) { + if (!_roomsvr_room.seatlist[j]){ + _have_nullseat = true; + } else { + if (!_roomsvr_room.seatlist[j].isrobot){ + if ((_sysroom_playercount < sysroom_playercount) && (haverobot == 1)){ + _have_nullseat = false; + break; + } + } + } + } + } + + if (_have_nullseat){ + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(roomcode); + //通知房间服务器有人要加入房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "self_join_room"; + msg.data = {}; + msg.data.playerid = playerid; + msg.data.nickname = o_player.nickname; + msg.data.avatar = o_player.avatar; + msg.data.sex = o_player.sex; + msg.data.playertype = o_player.playertype; + msg.data.ip = o_gameinfo.ip; + msg.data.conmode = o_gameinfo.conmode; + msg.data.fromid = o_gameinfo.fromid; + msg.data.agentid = agentid; + msg.data.gameid = gameid; + msg.data.roomcode = roomcode; + msg.data.playerroomcard = o_player.roomcard; + msg.data.playerbean = o_player.bean; + msg.data.joinmode = 0; + msg.data.address = o_player.address; + msg.data.latitude = o_player.latitude; + msg.data.longitude = o_player.longitude; + + //加入房间成功 + o_gameinfo.roomcode = roomcode; + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + if (roomserver_data.data.state != 0){ + //房间服务器加入房间失败 + o_gameinfo.roomcode = null; + } else { + pack.data = roomserver_data.data; + for (var i = 0; i < pack.data.players.length; i++) { + if (pack.data.players[i]){ + var _playerid = parseInt(pack.data.players[i].playerid); + var _o_player = o_agent.method.find_player(_playerid); + if (_o_player){ + pack.data.players[i].bean = _o_player.bean; + } + } + } + pack.rpc = "self_join_room"; + youle_agent.app.SendPack(pack); + return; + } + } + } + } + } + } + + //检查玩家房卡是否够用 + var needroomcard = youle_agent.import.createroom_needroomcard(o_game, roomtype); + //系统房间写死不要房卡 + needroomcard = 0; + if (o_player.roomcard < needroomcard){ + pack.rpc = "create_room"; + pack.data = {}; + pack.data.state = 1; + pack.data.error = roomCardName + "数量不够"; + pack.data.showerror = 1; + return pack; + } + //检查玩家豆豆是否够用 + //var needbean = youle_agent.import.createroom_beanfloor(o_game, roomtype); + //var rebateConfig = youle_room.app.youle_platform.config.method.get_paravalue("rebateConfig", agentid); + //var rebateNumber = 0; + var beanfloor1 = youle_agent.import.createroom_beanfloor(o_game, roomtype); + var beanmult1 = youle_agent.import.createroom_beanmult(o_game, roomtype); + //小局抽成设置 + var aset_rebate = youle_room.app.youle_platform.config.method.get_paravalue("aset_rebate", agentid); + if(aset_rebate){ + var sysRebateValue = 0; + config = aset_rebate.infinite; + if(config[0] == 3 && config[1] == 2){ + sysRebateValue = parseInt(config[2]*beanmult1/100); + } + + var tea = youle_room.import.owner_beanpush(o_game, roomtype); + if(tea){ + if(beanfloor1 == null){ + beanfloor1 = Math.max(tea,sysRebateValue); + }else{ + beanfloor1 = Math.max(tea,sysRebateValue,beanfloor1); + } + }else{ + if(beanfloor1 == null){ + beanfloor1 = sysRebateValue; + }else{ + beanfloor1 = Math.max(sysRebateValue,beanfloor1); + } + } + } + var needbean = beanfloor1; + //if(!isFree){ + if (needbean != null){ + if (o_player.bean < needbean){ + pack.rpc = "create_room"; + pack.data = {}; + pack.data.state = 1; + pack.data.error = starName + "数量不够"; + pack.data.showerror = 1; + return pack; + } + } + //} + //创建新房间随机获取新的房号 + var roomcode = o_game.method.new_roomcode(); + if (!roomcode){ + console.log(min_now() + "创建系统房间失败:没有房号可分配"); + pack.rpc = "create_room"; + pack.data = {}; + pack.data.state = 1; + pack.data.error = "没有房号可分配"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(roomcode); + + //通知房间服务器创建房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "create_room"; + msg.data = {}; + + msg.data.agent = {}; + msg.data.agent.agentid = agentid; + msg.data.agent.name = o_agent.agentname; + msg.data.agent.server = o_agent.server; + msg.data.agent.tcpport = o_agent.tcpport; + msg.data.agent.httpport = o_agent.httpport; + msg.data.agent.freeroom = o_agent.freeroom; + + msg.data.game = {}; + msg.data.game.gameid = gameid; + msg.data.game.name = o_game.gamename; + msg.data.game.seatcount = o_game.seatcount; + msg.data.game.makewar = o_game.makewar; + msg.data.game.modename = o_game.modename; + msg.data.game.server = o_roomserver.server; + msg.data.game.tcpport = o_roomserver.tcpport; + msg.data.game.httpport = o_roomserver.httpport; + msg.data.game.minroomcode = o_roomserver.minroomcode; + msg.data.game.maxroomcode = o_roomserver.maxroomcode; + + msg.data.player = {}; + msg.data.player.playerid = playerid; + msg.data.player.nickname = o_player.nickname; + msg.data.player.avatar = o_player.avatar; + msg.data.player.sex = o_player.sex; + msg.data.player.playertype = o_player.playertype; + msg.data.player.ip = o_gameinfo.ip; + msg.data.player.conmode = o_gameinfo.conmode; + msg.data.player.fromid = o_gameinfo.fromid; + msg.data.player.address = o_player.address; + msg.data.player.latitude = o_player.latitude; + msg.data.player.longitude = o_player.longitude; + msg.data.player.bean = o_player.bean; + + //获取开房所需的房卡数量 + var needroomcard = youle_agent.import.createroom_needroomcard(o_game, roomtype); + //系统房间写死不要房卡 + needroomcard = 0; + //获取总局数 + var asetcount = youle_agent.import.createroom_asetcount(o_game, roomtype); + //获取豆豆下限 + var beanfloor = youle_agent.import.createroom_beanfloor(o_game, roomtype); + //获取房间类型 + var roommode = youle_agent.import.createroom_roommode(o_game, roomtype); + //获取星星场倍数 + var beanmult = youle_agent.import.createroom_beanmult(o_game, roomtype); + //玩家是否需要准备 + var needprepare = youle_agent.import.createroom_needprepare(o_game, roomtype); + //系统房间即无限局一定要准备 + needprepare = 1; + + //不是星星场 + if (!roommode){ + beanmult = 1; + } + + msg.data.room = {}; + msg.data.room.roomcode = roomcode; + msg.data.room.roomtype = roomtype; + msg.data.room.needroomcard = needroomcard; + msg.data.room.asetcount = asetcount; + msg.data.room.infinite = 1; + msg.data.room.sysroom = 1; + msg.data.room.roommode = roommode; + msg.data.room.beanfloor = beanfloor; + msg.data.room.beanmult = beanmult; + msg.data.room.needprepare = needprepare; + msg.data.room.advanced_type = 0; + msg.data.room.advanced_teabean = 0; + msg.data.room.isFree = isFree; + + //机器人金币范围 + msg.data.robot={}; + msg.data.robot.goldRange = null; + var o_cfg = youle_agent.app.youle_platform.config; + var _system_room = o_cfg.method.get_paravalue("system_room", agentid, gameid); + var robotGoldRange = null; + if(_system_room){ + for(var k=0;k<_system_room.length;k++){ + var sRoomType = _system_room[k].roomtype; + if(typeof sRoomType == "object"){ + sRoomType = JSON.stringify(sRoomType); + } + if(JSON.stringify(roomtype) == sRoomType){ + msg.data.robot.robotGoldRange = _system_room[k].robotGoldRange; + } + } + } + + //加入房间成功 + o_gameinfo.roomcode = roomcode; + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + //返回前端 + pack.data = roomserver_data.data; + if (pack.data.state != 0){ + //房间服务器创建房间失败 + o_gameinfo.roomcode = null; + pack.rpc = "create_room"; + pack.data = {}; + pack.data.state = 1; + pack.data.error = "创建系统房间失败"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + pack.rpc = "create_room"; + youle_agent.app.SendPack(pack); + + var o_share = {}; + o_share.roomcode = roomcode; + o_share.roomtype = roomtype; + o_share.sharetime= min_now(); + o_game.sharerooms_system.push(o_share); + } + + //一次性创建N个系统房 + var _roomcount = 0; + for (var i = 0; i < o_game.sharerooms_system.length; i++) { + if (JSON.stringify(o_game.sharerooms_system[i].roomtype) == JSON.stringify(roomtype)){ + _roomcount = _roomcount + 1; + } + } + for (var i = 1; i <= sysroom_playercount - _roomcount; i++) { + var roomcode = o_game.method.new_roomcode(); + if (!roomcode){ + break; + } + + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(roomcode); + + //通知房间服务器创建房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "create_room"; + msg.data = {}; + + msg.data.agent = {}; + msg.data.agent.agentid = agentid; + msg.data.agent.name = o_agent.agentname; + msg.data.agent.server = o_agent.server; + msg.data.agent.tcpport = o_agent.tcpport; + msg.data.agent.httpport = o_agent.httpport; + msg.data.agent.freeroom = o_agent.freeroom; + + msg.data.game = {}; + msg.data.game.gameid = gameid; + msg.data.game.name = o_game.gamename; + msg.data.game.seatcount = o_game.seatcount; + msg.data.game.makewar = o_game.makewar; + msg.data.game.modename = o_game.modename; + msg.data.game.server = o_roomserver.server; + msg.data.game.tcpport = o_roomserver.tcpport; + msg.data.game.httpport = o_roomserver.httpport; + msg.data.game.minroomcode = o_roomserver.minroomcode; + msg.data.game.maxroomcode = o_roomserver.maxroomcode; + + msg.data.player = {}; + msg.data.player.playerid = playerid; + msg.data.player.nickname = o_player.nickname; + msg.data.player.avatar = o_player.avatar; + msg.data.player.sex = o_player.sex; + msg.data.player.playertype = o_player.playertype; + msg.data.player.ip = o_gameinfo.ip; + msg.data.player.conmode = o_gameinfo.conmode; + msg.data.player.fromid = o_gameinfo.fromid; + msg.data.player.address = o_player.address; + msg.data.player.latitude = o_player.latitude; + msg.data.player.longitude = o_player.longitude; + msg.data.player.bean = o_player.bean; + msg.data.player.notenterroom = 1; + + //机器人金币范围 + msg.data.robot={}; + msg.data.robot.goldRange = null; + var o_cfg = youle_agent.app.youle_platform.config; + var _system_room = o_cfg.method.get_paravalue("system_room", agentid, gameid); + var robotGoldRange = null; + if(_system_room){ + for(var k=0;k<_system_room.length;k++){ + var sRoomType = _system_room[k].roomtype; + if(typeof sRoomType == "object"){ + sRoomType = JSON.stringify(sRoomType); + } + if(JSON.stringify(roomtype) == sRoomType){ + msg.data.robot.robotGoldRange = _system_room[k].robotGoldRange; + } + } + } + + + //获取开房所需的房卡数量 + var needroomcard = youle_agent.import.createroom_needroomcard(o_game, roomtype); + //系统房间写死不要房卡 + needroomcard = 0; + //获取总局数 + var asetcount = youle_agent.import.createroom_asetcount(o_game, roomtype); + //获取豆豆下限 + var beanfloor = youle_agent.import.createroom_beanfloor(o_game, roomtype); + //获取房间类型 + var roommode = youle_agent.import.createroom_roommode(o_game, roomtype); + //获取星星场倍数 + var beanmult = youle_agent.import.createroom_beanmult(o_game, roomtype); + //玩家是否需要准备 + var needprepare = youle_agent.import.createroom_needprepare(o_game, roomtype); + //系统房间即无限局一定要准备 + needprepare = 1; + + //不是星星场 + if (!roommode){ + beanmult = 0; + } + + msg.data.room = {}; + msg.data.room.roomcode = roomcode; + msg.data.room.roomtype = roomtype; + msg.data.room.needroomcard = needroomcard; + msg.data.room.asetcount = asetcount; + msg.data.room.infinite = 1; + msg.data.room.sysroom = 1; + msg.data.room.roommode = roommode; + msg.data.room.beanfloor = beanfloor; + msg.data.room.beanmult = beanmult; + msg.data.room.needprepare = needprepare; + msg.data.room.advanced_type = 0; + msg.data.room.advanced_teabean = 0; + msg.data.room.isFree = isFree; + + if (o_roomserver.server == o_agent.server){ + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + if (roomserver_data.data.state == 0){ + var o_share = {}; + o_share.roomcode = roomcode; + o_share.roomtype = roomtype; + o_share.sharetime= min_now(); + o_game.sharerooms_system.push(o_share); + } + } + } + + break; + } +} + +//高级选项_房间列表 +youle_agent.advanced_roomlist = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var playerid = parseInt(pack.data.playerid); + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + //查找玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + // var videoConfig = youle_room.app.youle_platform.config.method.get_paravalue("videoConfig",agentid,gameid); + pack.data.shortcode = o_player.shortcode; + //房间费系统设置的值 + pack.data.tea = null; + pack.data.roomtype = null; + pack.data.roomlist = []; + if (o_player.roomcodes){ + for (var i = 0; i < o_player.roomcodes.length; i++) { + if (o_player.roomcodes[i].gameid == gameid) { + pack.data.tea = o_player.roomcodes[i].tea; + pack.data.roomtype = o_player.roomcodes[i].roomtype; + pack.data.roomlist = o_player.roomcodes[i].roomlist; + pack.data.isClose = o_player.roomcodes[i].isClose; + pack.data.rebateType = o_player.roomcodes[i].rebateType; + if(o_player.roomcodes[i].videoConfig){ + pack.data.videoConfig = o_player.roomcodes[i].videoConfig; + } + if(o_player.roomcodes[i].rebateLimit){ + pack.data.rebateLimit = o_player.roomcodes[i].rebateLimit; + } + if(o_player.roomcodes[i].forbidSelect){ + pack.data.forbidSelect = o_player.roomcodes[i].forbidSelect; + }else{ + pack.data.forbidSelect = 0; + } + break; + } + } + } + return pack; +} +//房主开关房间 +youle_agent.switchRoomList = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var playerid = parseInt(pack.data.playerid); + var isClose = 0; + if(pack.data.isClose){ + isClose = parseInt(pack.data.isClose); + }else{ + isClose = 0; + } + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + //查找玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + var o_roomcodes = {}; + var isfound = false; + for (var i = 0; i < o_player.roomcodes.length; i++) { + if (o_player.roomcodes[i].gameid == gameid) { + o_roomcodes.roomlist = o_player.roomcodes[i].roomlist; + o_player.roomcodes[i].isClose = isClose; + // o_player.roomcodes[i] = o_roomcodes; + o_roomcodes = o_player.roomcodes[i]; + isfound = true; + break; + } + } + if (isfound){ + //console.log(3,JSON.stringify(o_player.roomcodes)); + var sql = "update player set play_roomcodes = ? where play_agentid = ? and play_playerid = ?"; + youle_agent.mysql.execsql("execsql", sql, [JSON.stringify(o_player.roomcodes), agentid, playerid]); + pack.data.state = 0; + }else{ + pack.data.state = 1; + pack.data.error = "未找到您所创建的房间"; + pack.data.showerror = 1; + //youle_agent.app.SendPack(pack); + } + return pack; +} +//高级选项_创建固定房间 +youle_agent.advanced_createroom = function(pack,adv){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var playerid = parseInt(pack.data.playerid); + var tea = parseInt(pack.data.tea); + var roomtype = pack.data.roomtype; + var infinite = 0; + if (pack.data.infinite){ + infinite = 1; + } + var rebateLimit = pack.data.rebateLimit; + if(rebateLimit){ + rebateLimit = parseInt(rebateLimit); + }else{ + rebateLimit = 0; + } + var forbidSelect = pack.data.forbidSelect; + if(forbidSelect){ + forbidSelect = parseInt(forbidSelect); + }else{ + forbidSelect = 0; + } + var rebateType = pack.data.rebateType; + var o_cfg = youle_agent.app.youle_platform.config; + var rebateConfig = o_cfg.method.get_paravalue("rebateConfig", agentid); + //扣卡模式 + //var deductMode = youle_agent.import.deduct_roomcard_mode(o_game, roomtype); + //视频房 + var videoConfig = null; + if(pack.data.videoConfig){ + videoConfig = pack.data.videoConfig; + } + + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + //查找玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + + //获取开房所需的房卡数量 + var needroomcard = youle_agent.import.createroom_needroomcard(o_game, roomtype); + //获取总局数 + var asetcount = youle_agent.import.createroom_asetcount(o_game, roomtype); + //获取豆豆下限 + var beanfloor = youle_agent.import.createroom_beanfloor(o_game, roomtype); + //获取房间类型 + var roommode = youle_agent.import.createroom_roommode(o_game, roomtype); + //获取星星场倍数 + var beanmult = youle_agent.import.createroom_beanmult(o_game, roomtype); + + //不是星星场 + if (!roommode){ + beanmult = 1; + } + + //判断玩家设置的房间费是否符合范围 + + if(rebateConfig){ + var sysTea = parseInt(beanmult * rebateConfig.value/100); + //if(gameid == "Btke0urRy0cvPd5CIvD5yfhYhKhdSdex"){ + + //}else{ + if(tea < sysTea){ + tea = sysTea; + } + //} + if(!rebateConfig.rebateCharm){ + rebateType = 0; + } + } + + if(roommode){ + rebateType = 0; + } + //只有设置了星星限进的房间才有茶水费 + // if (!beanfloor){ + // tea = 0; + + // } + + if(needroomcard > 0){ + var vConfig = o_agent.method.alyVideoConfig(videoConfig); + if(vConfig[0] == 1){ + needroomcard += vConfig[1]; + } + } + //玩家是否需要准备 + var needprepare = youle_agent.import.createroom_needprepare(o_game, roomtype); + + var o_roomcodes = {}; + o_roomcodes.gameid = gameid; + o_roomcodes.tea = tea; + o_roomcodes.roomtype = roomtype; + o_roomcodes.infinite = infinite; + o_roomcodes.videoConfig = videoConfig; + o_roomcodes.roommode = roommode; + o_roomcodes.rebateType = rebateType; + o_roomcodes.rebateLimit = rebateLimit; + o_roomcodes.forbidSelect = forbidSelect; + o_roomcodes.isClose = 0; + o_roomcodes.roomlist = []; + var isfound = false; + for (var i = 0; i < o_player.roomcodes.length; i++) { + if (o_player.roomcodes[i].gameid == gameid) { + o_roomcodes.roomlist = o_player.roomcodes[i].roomlist; + o_player.roomcodes[i] = o_roomcodes; + if(o_player.roomcodes[i].isClose){ + o_player.roomcodes[i].isClose = 1; + }else{ + o_player.roomcodes[i].isClose = 0; + } + if(o_player.roomcodes[i].forbidSelect){ + o_player.roomcodes[i].forbidSelect = 1; + }else{ + o_player.roomcodes[i].forbidSelect = 0; + } + o_player.roomcodes[i].rebateType = rebateType; + o_player.roomcodes[i].rebateLimit = rebateLimit; + isfound = true; + break; + } + } + if (!isfound){ + o_player.roomcodes.push(o_roomcodes); + } + var o_game1 = null; + var o_agent1 = youle_room.agents.method.find_agent(agentid); + if (o_agent1){ + o_game1 = o_agent1.method.find_game(gameid); + } + //查找游戏 + + + if(o_game1 && !adv){ + //解散已存在的可解散房间 + for(var i = 0; i < o_roomcodes.roomlist.length; i++){ + + //查找房间 + var o_room = o_game1.method.find_room(o_roomcodes.roomlist[i]); + if(o_room){ + // if(o_room.method.get_playercount() == 0){ + if(o_room.battlestate == 0){ + //通知代理服务器解散房间 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "self_break_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = gameid; + msg1.data.roomcode = o_roomcodes.roomlist[i]; + //玩家列表 + msg1.data.playeridlist = []; + for (var j = 0; j < o_room.seatlist.length; j++) { + if (o_room.seatlist[j]){ + msg1.data.playeridlist.push(o_room.seatlist[j].playerid); + } + } + youle_room.app.youle_agent[msg1.rpc](msg1,true); + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "other_break_room"; + o_room.method.sendpack_toother(msg2, o_player.gameinfo.seat); + // //解散房间 + // var o_agent = o_room.o_game.o_agent; + // var o_game = o_room.o_game; + // var msg1 = {}; + // msg1.app = youle_room.app.appname; + // msg1.route = youle_room.app.youle_agent.routename; + // msg1.rpc = "free_room"; + // msg1.data = {}; + // msg1.data.agentid = o_agent.agentid; + // msg1.data.gameid = o_game.gameid; + // msg1.data.roomcode = o_room.roomcode; + // //玩家列表 + // msg1.data.playeridlist = []; + // for (var i = 0; i < o_room.seatlist.length; i++) { + // if (o_room.seatlist[i]){ + // msg1.data.playeridlist.push(o_room.seatlist[i].playerid); + // } + // } + // youle_room.app.youle_agent[msg1.rpc](msg1); + //释放房间 + o_game1.method.free_room(o_room); + }else{ + youle_room.import.gameBreak(o_room); + } + } + } + } + var advan = 0; + if(adv){ + advan = adv; + }else{ + advan = o_player.advanced; + } + for (var i = 0; i < advan - o_roomcodes.roomlist.length; i++){ + //随机获取新的房号 + var roomcode = o_game.method.new_roomcode(o_player.shortcode); + + //获取房号对应的房间服务器地址 + var o_roomserver = o_game.method.get_roomserver(roomcode); + + //通知房间服务器创建房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "create_room"; + msg.data = {}; + + msg.data.agent = {}; + msg.data.agent.agentid = agentid; + msg.data.agent.name = o_agent.agentname; + msg.data.agent.server = o_agent.server; + msg.data.agent.tcpport = o_agent.tcpport; + msg.data.agent.httpport = o_agent.httpport; + msg.data.agent.freeroom = o_agent.freeroom; + + msg.data.game = {}; + msg.data.game.gameid = gameid; + msg.data.game.name = o_game.gamename; + msg.data.game.seatcount = o_game.seatcount; + msg.data.game.makewar = o_game.makewar; + msg.data.game.modename = o_game.modename; + msg.data.game.server = o_roomserver.server; + msg.data.game.tcpport = o_roomserver.tcpport; + msg.data.game.httpport = o_roomserver.httpport; + msg.data.game.minroomcode = o_roomserver.minroomcode; + msg.data.game.maxroomcode = o_roomserver.maxroomcode; + + msg.data.player = {}; + msg.data.player.playerid = playerid; + msg.data.player.nickname = o_player.nickname; + msg.data.player.avatar = o_player.avatar; + msg.data.player.sex = o_player.sex; + msg.data.player.playertype = o_player.playertype; + msg.data.player.ip = null; + msg.data.player.conmode = null; + msg.data.player.fromid = null; + msg.data.player.address = o_player.address; + msg.data.player.latitude = o_player.latitude; + msg.data.player.longitude = o_player.longitude; + msg.data.player.bean = o_player.bean; + + msg.data.room = {}; + msg.data.room.roomcode = roomcode; + msg.data.room.roomtype = roomtype; + msg.data.room.needroomcard = needroomcard; + msg.data.room.asetcount = asetcount; + msg.data.room.infinite = infinite; + msg.data.room.roommode = roommode; + msg.data.room.beanfloor = beanfloor; + msg.data.room.beanmult = beanmult; + msg.data.room.needprepare = needprepare; + msg.data.room.advanced_type = 1; + msg.data.room.advanced_teabean = tea; + msg.data.room.shortcode = o_player.shortcode; + + + msg.data.room.videoConfig = videoConfig; + msg.data.room.rebateType = rebateType; + msg.data.room.rebateLimit = rebateLimit; + msg.data.room.forbidSelect = forbidSelect; + if(o_player.roomcodes[i].isClose == 0){ + msg.data.room.isClose = 0; + }else if(o_player.roomcodes[i].isClose == 1){ + msg.data.room.isClose = 1; + } + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + if (roomserver_data.data.state == 0){ + o_roomcodes.roomlist.push(roomcode); + i = i - 1; + } + } + } + o_roomcodes.roomlist.length = advan; + pack.data.roommode = roommode; + pack.data.roomlist = o_roomcodes.roomlist; + //已存在分享的vip房 + if(o_game.sharerooms_player){ + var findIndex = min_ary_indexof(o_game.sharerooms_player, o_player.shortcode, "roomcode"); + if(findIndex > -1){ + o_game.sharerooms_player[findIndex].roomlist = o_roomcodes.roomlist; + o_game.sharerooms_player[findIndex].roomtype = roomtype; + o_game.sharerooms_player[findIndex].destwo = o_player.destwo; + o_game.sharerooms_player[findIndex].roommode = roommode; + } + } + //修改数据库 + //console.log(2,JSON.stringify(o_player.roomcodes)); + var sql = "update player set play_roomcodes = ? where play_agentid = ? and play_playerid = ?"; + youle_agent.mysql.execsql("execsql", sql, [JSON.stringify(o_player.roomcodes), agentid, playerid]); + return pack; +} + +//自动创建单个固定房号的房间 +youle_agent.advanced_createroom_roomcode = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var playerid = parseInt(pack.data.playerid); + var roomcode = parseInt(pack.data.roomcode); + var shortcode = pack.data.shortcode; + + //var videoConfig = pack.data.videoConfig; + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查游戏id是否存在 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + return; + }; + //查找玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + var o_cfg = youle_agent.app.youle_platform.config; + var rebateConfig = o_cfg.method.get_paravalue("rebateConfig", agentid); + var isfound = false; + var tea; + var roomtype; + var infinite = 0; + var videoConfig = null; + var rebateType = 0; + var rebateLimit = 0; + var forbidSelect = 0; + // var isClose = 0; + for (var i = 0; i < o_player.roomcodes.length; i++) { + if (o_player.roomcodes[i].gameid == gameid) { + for (var j = 0; j < o_player.roomcodes[i].roomlist.length; j++) { + if (o_player.roomcodes[i].roomlist[j] == roomcode){ + isfound = true; + tea = o_player.roomcodes[i].tea; + roomtype = o_player.roomcodes[i].roomtype; + if(o_player.roomcodes[i].videoConfig){ + videoConfig = o_player.roomcodes[i].videoConfig; + } + // if(o_player.roomcodes[i].isClose){ + // isClose = o_player.roomcodes[i].isClose; + // } + if (o_player.roomcodes[i].infinite){ + infinite = o_player.roomcodes[i].infinite; + } + if (o_player.roomcodes[i].rebateType){ + rebateType = o_player.roomcodes[i].rebateType; + } + if (o_player.roomcodes[i].rebateLimit){ + rebateLimit = o_player.roomcodes[i].rebateLimit; + } + if (o_player.roomcodes[i].forbidSelect){ + forbidSelect = o_player.roomcodes[i].forbidSelect; + } + break; + } + } + if (isfound){ + break; + } + } + } + if (!isfound){ + return; + } + + + //获取开房所需的房卡数量 + var needroomcard = youle_agent.import.createroom_needroomcard(o_game, roomtype); + //获取总局数 + var asetcount = youle_agent.import.createroom_asetcount(o_game, roomtype); + //获取豆豆下限 + var beanfloor = youle_agent.import.createroom_beanfloor(o_game, roomtype); + //获取房间类型 + var roommode = youle_agent.import.createroom_roommode(o_game, roomtype); + //获取星星场倍数 + var beanmult = youle_agent.import.createroom_beanmult(o_game, roomtype); + if (!roommode){ + beanmult = 1; + } + //判断玩家设置的房间费是否符合范围 + if(rebateConfig){ + var sysTea = parseInt(beanmult * rebateConfig.value/100); + //if(gameid == "Btke0urRy0cvPd5CIvD5yfhYhKhdSdex"){ + + //}else{ + if(tea < sysTea){ + tea = sysTea; + } + //} + if(!rebateConfig.rebateCharm){ + rebateType = 0; + } + } + if(roommode){ + rebateType = 0; + } + //视频房房卡星星扣除修改 + if(needroomcard>0){ + var vConfig = o_agent.method.alyVideoConfig(videoConfig); + if(vConfig[0] == 1){ + needroomcard += vConfig[1]; + } + } + //不是星星场 + if (!roommode){ + beanmult = 1; + } + //玩家是否需要准备 + var needprepare = youle_agent.import.createroom_needprepare(o_game, roomtype); + //不是星星场 + + + //获取房号对应的房间服务器地址 + o_game.method.new_roomcode(roomcode); + var o_roomserver = o_game.method.get_roomserver(roomcode); + + //通知房间服务器创建房间 + var msg = {}; + msg.app = youle_agent.app.appname; + msg.route = youle_agent.app.youle_room.routename; + msg.rpc = "create_room"; + msg.data = {}; + + msg.data.agent = {}; + msg.data.agent.agentid = agentid; + msg.data.agent.name = o_agent.agentname; + msg.data.agent.server = o_agent.server; + msg.data.agent.tcpport = o_agent.tcpport; + msg.data.agent.httpport = o_agent.httpport; + msg.data.agent.freeroom = o_agent.freeroom; + + msg.data.game = {}; + msg.data.game.gameid = gameid; + msg.data.game.name = o_game.gamename; + msg.data.game.seatcount = o_game.seatcount; + msg.data.game.makewar = o_game.makewar; + msg.data.game.modename = o_game.modename; + msg.data.game.server = o_roomserver.server; + msg.data.game.tcpport = o_roomserver.tcpport; + msg.data.game.httpport = o_roomserver.httpport; + msg.data.game.minroomcode = o_roomserver.minroomcode; + msg.data.game.maxroomcode = o_roomserver.maxroomcode; + + msg.data.player = {}; + msg.data.player.playerid = playerid; + msg.data.player.nickname = o_player.nickname; + msg.data.player.avatar = o_player.avatar; + msg.data.player.sex = o_player.sex; + msg.data.player.playertype = o_player.playertype; + msg.data.player.ip = null; + msg.data.player.conmode = null; + msg.data.player.fromid = null; + msg.data.player.address = o_player.address; + msg.data.player.latitude = o_player.latitude; + msg.data.player.longitude = o_player.longitude; + msg.data.player.bean = o_player.bean; + + msg.data.room = {}; + msg.data.room.roomcode = roomcode; + msg.data.room.roomtype = roomtype; + msg.data.room.needroomcard = needroomcard; + msg.data.room.asetcount = asetcount; + msg.data.room.infinite = infinite; + msg.data.room.roommode = roommode; + msg.data.room.beanfloor = beanfloor; + msg.data.room.beanmult = beanmult; + msg.data.room.needprepare = needprepare; + msg.data.room.advanced_type = 1; + msg.data.room.advanced_teabean = tea; + msg.data.room.shortcode = shortcode; + + msg.data.room.videoConfig = videoConfig; + msg.data.room.rebateType = rebateType; + + msg.data.room.rebateLimit = rebateLimit; + msg.data.room.forbidSelect = forbidSelect; + if (o_roomserver.server == o_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_agent.app.youle_room[msg.rpc](msg); + if (roomserver_data.data.state != 0){ + console.log(min_now() + " 自动创建固定房号房间失败" + roomcode); + console.log(roomserver_data); + } + } + return pack; +} + +youle_agent.query_player2 = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + + var db_callback = function(dbdata){ + pack.data = {}; + if (dbdata.length != 0){ + pack.data.agentid = dbdata[0].play_agentid; + pack.data.playerid = dbdata[0].play_playerid; + pack.data.nickname = dbdata[0].play_nickname; + pack.data.avatar = dbdata[0].play_avatar; + pack.data.sex = dbdata[0].play_sex; + pack.data.roomcard = dbdata[0].play_roomcard; + pack.data.bean = dbdata[0].play_bean; + } + + youle_agent.app.SendPack(pack); + } + var sql = "select * from player where play_agentid = ? and play_playerid = ? limit 1"; + youle_agent.mysql.execsql("opensql", sql, [agentid, playerid], db_callback, pack); +} + +youle_agent.query_player3 = function(pack){ + var agentid = pack.data.agentid; + var unionid = pack.data.unionid; + + var db_callback = function(dbdata){ + pack.data = {}; + if (dbdata.length != 0){ + pack.data.agentid = dbdata[0].play_agentid; + pack.data.playerid = dbdata[0].play_playerid; + pack.data.roomcard = dbdata[0].play_roomcard; + pack.data.bean = dbdata[0].play_bean; + } + + youle_agent.app.SendPack(pack); + } + var sql = "select * from player where play_agentid = ? and play_unionid = ?"; + youle_agent.mysql.execsql("opensql", sql, [agentid, unionid], db_callback, pack); +} + +youle_agent.query_player4 = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var shortcode = parseInt(pack.data.shortcode); + + var db_callback = function(dbdata){ + if (dbdata.length == 0){ + youle_agent.app.SendPack(pack); + return; + } + var whitelist = JSON.stringify(youle_agent.deCompressWhiteList(dbdata[0].play_whitelist)); + //var whitelist = dbdata[0].play_whitelist; + if (!whitelist){ + youle_agent.app.SendPack(pack); + return; + } + + whitelist = JSON.parse(whitelist); + var idx = -1; + + idx = min_ary_indexof(whitelist, playerid, "playerid"); + if (idx != -1){ + pack.data.memo = 0; + youle_agent.app.SendPack(pack); + return; + } + + idx = min_ary_indexof(whitelist, playerid); + if (idx != -1){ + pack.data.memo = 0; + youle_agent.app.SendPack(pack); + return; + } + + for (var i = 0; i < whitelist.length; i++) { + if (whitelist[i][0] == playerid){ + if (whitelist[i][2]){ + pack.data.memo = whitelist[i][2]; + } else { + pack.data.memo = 0; + } + youle_agent.app.SendPack(pack); + return; + } + } + + youle_agent.app.SendPack(pack); + } + var sql = "select * from player where play_agentid = ? and play_shortcode = ?"; + youle_agent.mysql.execsql("opensql", sql, [agentid, shortcode], db_callback, pack); +} + +youle_agent.get_treasurelist = function(pack){ + var agentid = pack.data.agentid; + var db_callback = function(dbdata){ + pack.data.list = []; + for (var i = 0; i < dbdata.length; i++) { + var o_player = {}; + o_player.playerid = dbdata[i].play_playerid; + o_player.headimg = dbdata[i].play_avatar; + o_player.name = dbdata[i].play_nickname; + o_player.star = dbdata[i].play_bean; + o_player.signature = dbdata[i].play_sign; + pack.data.list.push(o_player); + } + youle_agent.app.SendPack(pack); + } + var sql = "select * from player where play_agentid = ? order by play_bean desc limit 10"; + youle_agent.mysql.execsql("opensql", sql, [agentid], db_callback, pack); +} + +//设置金币银行密码 +youle_agent.set_bankpwd = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var unionid = pack.data.unionid; + var password = pack.data.password; + var tel = ""; + if (pack.data.tel){ + tel = pack.data.tel; + } + var wechat = ""; + if (pack.data.wechat){ + tel = pack.data.wechat; + } + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + if (o_player.unionid != unionid){ + return; + } + if (!password){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "密码不能为空"; + pack.data.showerror = 1; + return pack; + } + if (!o_player.bankpower){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "没有权限"; + pack.data.showerror = 1; + return pack; + } + + o_player.bankpwd = password; + var sql = "update player set play_bankpwd=?,play_tel=?,play_wechat=? where play_agentid = ? and play_playerid = ?"; + youle_agent.mysql.execsql("execsql", sql, [password, tel, wechat, agentid, playerid]); + + pack.data = {}; + pack.data.state = 0; + pack.data.error = "设置成功"; + pack.data.showerror = 1; + return pack; +} + +//存入或取出金币 +youle_agent.change_star = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var password = pack.data.password; + var mode = parseInt(pack.data.mode); + var count = parseInt(pack.data.count); + + //检查代理商id是否存在 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + if (o_player.bankpwd != password){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "密码不正确"; + pack.data.showerror = 1; + return pack; + } + if ((mode && count > o_player.bank) || + (!mode && count > o_player.bean)){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "数量不够"; + pack.data.showerror = 1; + return pack; + } + var initBean = Number(o_agent.initBean); + if(!o_agent.initBean){ + initBean = 0; + } + var cTemp = false; + + if(!mode){ + if(o_player.bean < 0){ + if(count > 0){ + cTemp = true; + } + count = 0; + + }else{ + if(o_player.bean < initBean){ + if(count > 0){ + cTemp = true; + } + count = 0; + + }else{ + if(o_player.bean - count <= initBean){ + if(count > o_player.bean - initBean){ + cTemp = true; + } + count = o_player.bean - initBean; + } + } + } + } + + var db_callback = function(dbdata){ + if (dbdata[0].result == 1){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = dbdata[0].error; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + } else { + o_player.bean = parseInt(dbdata[0].play_bean); + o_player.bank = parseInt(dbdata[0].play_bank); + + pack.data = {}; + pack.data.state = 0; + pack.data.mode = mode; + pack.data.count = count; + pack.data.star1 = o_player.bean; + pack.data.star2 = o_player.bank; + if(cTemp){ + pack.data.msg = "需要保留系统赠送、数值已被调整"; + } + pack.data.count = count; + youle_agent.app.SendPack(pack); + } + } + var sql = "call cp_game_change_bankstar(?,?,?,?,?);"; + youle_agent.mysql.execsql("opensql", sql, [agentid, playerid, password, mode, count], db_callback, pack); +} + +//比赛列表 +youle_agent.get_matchlist = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var playerid = parseInt(pack.data.playerid); + + pack.data.matchlist = []; + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 1; + pack.data.error = "agentid不存在"; + return pack; + }; + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + pack.data.state = 1; + pack.data.error = "gameid不存在"; + return pack; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 1; + pack.data.error = "playerid不存在"; + return pack; + }; + //检查玩家是否已经在房间内了 + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (!o_gameinfo){ + o_player.method.login_game(o_game, pack.ip, pack.conmode, pack.fromid, null, null); + o_gameinfo = o_player.method.get_gameinfo(o_game); + } + if (o_gameinfo && o_gameinfo.roomcode) { + pack.data.state = 1; + pack.data.error = "玩家已经在房间内"; + return pack; + }; + o_gameinfo.match_ip = pack.ip; + o_gameinfo.match_conmode = pack.conmode; + o_gameinfo.match_fromid = pack.fromid; + + if (o_gameinfo.matchid){ + pack.data.matchid = o_gameinfo.matchid; + pack.data.matchidx = o_gameinfo.matchidx; + } else { + pack.data.matchid = null; + pack.data.matchidx = null; + } + + var matchlist = youle_agent.app.youle_platform.config.method.get_paravalue("matchlist", agentid, gameid); + + // matchlist = + // [ + // { + // "id": "2018041102", + // "roomtype": [1,1,1,2,2,[0,0,[0,0,0],null,null,0],[1,0,5]], + // "matchname": "有机器人", + // "imageurl": "", + // "beginTime": "2018-04-16 15:00", + // "endTime": "2018-04-16 17:45", + // "playercount": 7, + // "playercount2": 6, + // "bean": 0, + // "roomcard": 1, + // "topcount": 6, + // "circulation": 0, + // "interval": 0, + // "gradename": "名称1", + // "memo1": "第一名:iphone1", + // "memo2": "2局,扣卡1张,#每局抢庄,牛几几倍,#星星场10倍,茶水费10,#5人场,强制下注5", + // "memo3": "", + // "haverobot": 1, + // "ranking": + // [ + // { + // "min": 100, + // "max": 200, + // "count": 3 + // }, + // { + // "min": 200, + // "max": 300, + // "count": 2 + // } + // ] + // } + // ]; + + for (var i = 0; i < matchlist.length; i++) { + o_game.matchlist.method.load_match(matchlist[i]); + }; + + var do_getmatchinfo = function(obj_match){ + var r = {}; + r.id = obj_match.id; + r.idx = obj_match.idx; + r.roomtype = obj_match.roomtype; + r.matchname = obj_match.matchname; + r.imageurl = obj_match.imageurl; + r.playercount = obj_match.playercount; + r.playercount2= obj_match.playercount2; + r.beginTime = obj_match.beginTime; + r.endTime = obj_match.endTime; + r.bean = obj_match.bean; + r.roomcard = obj_match.roomcard; + r.gradename = obj_match.gradename; + r.memo1 = obj_match.memo1; + r.memo2 = obj_match.memo2; + r.memo3 = obj_match.memo3; + r.isstart = obj_match.isstart; + r.s_pcount = obj_match.playerlist.length; + return r; + } + + for (var i = 0; i < o_game.matchlist.list.length; i++) { + var o_match = o_game.matchlist.list[i]; + var found = false; + for (var j = 0; j < pack.data.matchlist.length; j++) { + if ((new Date(o_match.beginTime)).getTime() < (new Date(pack.data.matchlist[j].beginTime)).getTime()){ + var o_match_info = do_getmatchinfo(o_match); + pack.data.matchlist.splice(j, 0, o_match_info); + found = true; + break; + } + } + if (!found){ + var o_match_info = do_getmatchinfo(o_match); + pack.data.matchlist.push(o_match_info); + } + }; + return pack; +} + +//比赛排队 +youle_agent.lineup = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var matchid = pack.data.matchid; + var matchidx = parseInt(pack.data.matchidx); + var playerid = parseInt(pack.data.playerid); + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 1; + pack.data.error = "agentid不存在"; + return pack; + }; + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + pack.data.state = 1; + pack.data.error = "gameid不存在"; + return pack; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 1; + pack.data.error = "playerid不存在"; + return pack; + }; + //检查玩家是否已经在房间内了 + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (!o_gameinfo){ + o_player.method.login_game(o_game, pack.ip, pack.conmode, pack.fromid, null, null); + o_gameinfo = o_player.method.get_gameinfo(o_game); + } + if (o_gameinfo && o_gameinfo.roomcode) { + pack.data.state = 1; + pack.data.error = "玩家已经在房间内"; + return pack; + }; + //检查玩家是否已经在排队 + if (o_gameinfo && o_gameinfo.matchid) { + pack.data.state = 1; + pack.data.error = "玩家已经在排队"; + return pack; + }; + //查找比赛 + var o_match = o_game.matchlist.method.find_match(matchid, matchidx); + if (!o_match){ + pack.data.state = 1; + pack.data.error = "比赛不存在或已结束"; + return pack; + } + + //检查玩家房卡是否够用 + var needroomcard = youle_agent.import.createroom_needroomcard(o_game, o_match.roomtype); + if (o_player.roomcard < o_match.roomcard){ + var roomCardName = youle_agent.app.youle_platform.config.method.get_paravalue("roomCardName", agentid); + if (!roomCardName){ + roomCardName = "房卡"; + } + pack.data.state = 1; + pack.data.error = roomCardName + "数量不够"; + return pack; + }; + //检查玩家金币是否够用 + var beanfloor = youle_agent.import.createroom_beanfloor(o_game, o_match.roomtype); + if (beanfloor != null){ + if (o_player.bean < beanfloor){ + var starName = youle_agent.app.youle_platform.config.method.get_paravalue("starName", agentid); + if (!starName){ + starName = "星星"; + } + pack.data.state = 1; + pack.data.error = starName + "数量不够"; + return pack; + } + }; + + var idx = min_ary_indexof(o_match.playerlist, playerid); + if (idx == -1){ + o_match.playerlist.push(playerid); + o_gameinfo.matchid = o_match.id; + o_gameinfo.matchidx = o_match.idx; + } + pack.data.state = 0; + pack.data.s_pcount = o_match.playerlist.length; + return pack; +} + +//取消排队 +youle_agent.lineup_cancel = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var matchid = pack.data.matchid; + var matchidx = parseInt(pack.data.matchidx); + var playerid = parseInt(pack.data.playerid); + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 1; + pack.data.error = "agentid不存在"; + return pack; + }; + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + pack.data.state = 1; + pack.data.error = "gameid不存在"; + return pack; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 1; + pack.data.error = "playerid不存在"; + return pack; + }; + //查找比赛 + var o_match = o_game.matchlist.method.find_match(matchid, matchidx); + if (!o_match){ + pack.data.state = 1; + pack.data.error = "比赛不存在或已结束"; + return pack; + } + + var idx = min_ary_indexof(o_match.playerlist, playerid); + if (idx > -1){ + o_match.playerlist.splice(idx, 1); + var o_gameinfo = o_player.method.get_gameinfo(o_game); + if (o_gameinfo){ + delete o_gameinfo.matchid; + delete o_gameinfo.matchidx; + } + } + pack.data.state = 0; + pack.data.s_pcount = o_match.playerlist.length; + return pack; +} + +//获取某个比赛的排行榜 +youle_agent.get_ranklist = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var playerid = parseInt(pack.data.playerid); + var matchid = pack.data.match_id; + var matchidx = parseInt(pack.data.match_idx); + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 1; + pack.data.error = "agentid不存在"; + return pack; + }; + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + pack.data.state = 1; + pack.data.error = "gameid不存在"; + return pack; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 1; + pack.data.error = "playerid不存在"; + return pack; + }; + //查找比赛 + var o_match = o_game.matchlist.method.find_match(matchid, matchidx); + if (!o_match){ + pack.data.state = 1; + pack.data.error = "比赛不存在或已结束"; + return pack; + } + + pack.data.state = 0; + pack.data.ranklist = o_match.ranklist; + return pack; +} + +//获取我上榜了的比赛列表 +youle_agent.get_mymatchlist = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var playerid = parseInt(pack.data.playerid); + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 1; + pack.data.error = "agentid不存在"; + return pack; + }; + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game) { + pack.data.state = 1; + pack.data.error = "gameid不存在"; + return pack; + }; + //检查玩家是否存在 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 1; + pack.data.error = "playerid不存在"; + return pack; + }; + + pack.data.matchlist1 = []; + for (var i = 0; i < o_game.matchlist.list.length; i++) { + if (min_ary_indexof(o_game.matchlist.list[i].ranklist, playerid, "playerid") > -1){ + var o_match = {}; + o_match.id = o_game.matchlist.list[i].id; + o_match.idx = o_game.matchlist.list[i].idx; + o_match.roomtype = o_game.matchlist.list[i].roomtype; + o_match.matchname = o_game.matchlist.list[i].matchname; + o_match.imageurl = o_game.matchlist.list[i].imageurl; + o_match.playercount = o_game.matchlist.list[i].playercount; + o_match.beginTime = o_game.matchlist.list[i].beginTime; + o_match.endTime = o_game.matchlist.list[i].endTime; + o_match.bean = o_game.matchlist.list[i].bean; + o_match.roomcard = o_game.matchlist.list[i].roomcard; + o_match.gradename = o_game.matchlist.list[i].gradename; + o_match.memo1 = o_game.matchlist.list[i].memo1; + o_match.memo2 = o_game.matchlist.list[i].memo2; + o_match.memo3 = o_game.matchlist.list[i].memo3; + o_match.ranklist = o_game.matchlist.list[i].ranklist; + pack.data.matchlist1.push(o_match); + } + }; + + pack.data.matchlist2 = []; + var db_callback = function(dbdata){ + for (var i = 0; i < dbdata.length; i++) { + var o_match = {}; + o_match.id = dbdata[i].matc_matchid; + o_match.idx = dbdata[i].matc_matchidx; + o_match.roomtype = dbdata[i].matc_roomtype; + o_match.matchname = dbdata[i].matc_matchname; + o_match.imageurl = dbdata[i].matc_imageurl; + o_match.playercount = dbdata[i].matc_playercount; + o_match.beginTime = dbdata[i].matc_beginTime; + o_match.endTime = dbdata[i].matc_endTime; + o_match.bean = dbdata[i].matc_bean; + o_match.roomcard = dbdata[i].matc_roomcard; + o_match.gradename = dbdata[i].matc_gradename; + o_match.memo1 = dbdata[i].matc_memo1; + o_match.memo2 = dbdata[i].matc_memo2; + o_match.memo3 = dbdata[i].matc_memo3; + o_match.ranklist = JSON.parse(dbdata[i].matc_ranking); + pack.data.matchlist2.push(o_match); + } + youle_agent.app.SendPack(pack); + } + var sql = "select * from `match` " + + "where exists (select 1 from match_ranking " + + "where mara_agentid = matc_agentid " + + "and mara_gameid = matc_gameid " + + "and mara_matchid = matc_matchid " + + "and mara_matchidx = matc_matchidx " + + "and mara_agentid = ? " + + "and mara_gameid = ? " + + "and mara_playerid = ?);"; + youle_agent.mysql_grade.execsql("opensql", sql, [agentid, gameid, playerid], db_callback); +} + + + +//代理后台-充值房卡 +youle_agent.synch_roomcard = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var opt = parseInt(pack.data.opt); + var roomcard = parseInt(pack.data.roomcard); + var change = parseInt(pack.data.change); + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 0; + return pack; + }; + //检查玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 0; + return pack; + }; + + o_player.method.update_roomcard(opt, roomcard, change); + pack.data.state = 0; + return pack; +} + +//代理后台-充值星星 +youle_agent.synch_bean = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var opt = parseInt(pack.data.opt); + var bean = parseInt(pack.data.bean); + var change = parseInt(pack.data.change); + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 0; + return pack; + }; + //检查玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 0; + return pack; + }; + + o_player.method.update_bean(opt, bean, change); + pack.data.state = 0; + return pack; +} + +//代理后台-玩家绑定邀请码/解绑邀请码 +youle_agent.synch_invitecode = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var invitecode = parseInt(pack.data.invitecode); + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 0; + return pack; + }; + //检查玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 0; + return pack; + }; + + o_player.invitecode = invitecode; + pack.data.state = 0; + return pack; +} + +//代理后台-封禁玩家/解封玩家 +youle_agent.synch_playerstate = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var state = parseInt(pack.data.state); + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 0; + return pack; + }; + //检查玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + pack.data.state = 0; + return pack; + }; + + o_player.state = state; + pack.data.state = 0; + return pack; +} +//代理后台-开通短号 +youle_agent.synch_shortcode = function(pack){ + + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var shortcode = parseInt(pack.data.shortcode); + var advanced = parseInt(pack.data.advanced); + var desone = pack.data.desone; + var destwo = pack.data.destwo; + var lower_limit = pack.data.lower_limit; + var announcement = pack.data.announcement; + + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data.state = 0; + return pack; + }; + + + + + //var db_callback_shortcode = function(dbdata){ + //if(dbdata.length > 0){ + + // if(dbdata[0].play_roomcodes){ + // o_player.roomcodes = JSON.parse(dbdata[0].play_roomcodes); + // }else{ + // o_player.roomcodes = null; + // } + //console.log(o_player.roomcodes); + if (o_agent.shortcodes[shortcode]){ + var old_playerid = o_agent.shortcodes[shortcode].playerid; + if (old_playerid != playerid){ + var o_oldplayer = o_agent.method.find_player(old_playerid); + if (o_oldplayer){ + o_oldplayer.advanced = 0; + o_oldplayer.shortcode = null; + o_oldplayer.roomcodes = []; + o_oldplayer.desone = null; + o_oldplayer.destwo = null; + o_oldplayer.lowlimit = null; + o_oldplayer.notice = null; + var sql = "update player set play_roomcodes = ? where play_agentid = ? and play_playerid = ?"; + youle_agent.mysql.execsql("execsql", sql,[ "", agentid, old_playerid]); + } + } + } + var o_shortcode = {}; + o_shortcode.shortcode = shortcode; + o_shortcode.playerid = playerid; + o_agent.shortcodes[shortcode] = o_shortcode; + + var o_player = o_agent.method.find_player(playerid); + if (o_player) { + if (o_player.shortcode && o_player.shortcode != shortcode){ + o_agent.shortcodes[o_player.shortcode] = null; + + var o_game = null; + var o_agent1 = youle_room.agents.method.find_agent(agentid); + if(o_player.roomcodes && o_agent1){ + //更换短号清除之前存在的旧房间 + if(o_player.roomcodes && o_agent1){ + for(var i=0;i 0){ + var o_room = null; + for(var j = 0;j < o_player.roomcodes[i].roomlist.length;j++){ + o_room = o_game.method.find_room(o_player.roomcodes[i].roomlist[j]); + if(o_room){ + o_room.shortcode = null; + o_room.advanced_type = null; + if(o_room.battlestate == 0){ + //通知代理服务器解散房间 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "self_break_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = o_player.roomcodes[i].gameid; + msg1.data.roomcode = o_player.roomcodes[i].roomlist[j]; + //玩家列表 + msg1.data.playeridlist = []; + for (var k = 0; k < o_room.seatlist.length; k++) { + if (o_room.seatlist[k]){ + msg1.data.playeridlist.push(o_room.seatlist[k].playerid); + } + } + youle_room.app.youle_agent[msg1.rpc](msg1,true); + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "other_break_room"; + o_room.method.sendpack_toother(msg2, o_player.gameinfo.seat); + //释放房间 + o_game.method.free_room(o_room); + + }else{ + youle_room.import.gameBreak(o_room); + } + + }else{ + console.log("玩家短号房列表出现异常!!"); + } + } + o_player.roomcodes[i].roomlist=[]; + + } + //已存在分享的vip房 + var oGame = o_agent.method.find_game(o_player.roomcodes[i].gameid); + if(oGame){ + if(oGame.sharerooms_player){ + var findIndex = min_ary_indexof(oGame.sharerooms_player, o_player.shortcode, "roomcode"); + if(findIndex > -1){ + min_ary_delval(oGame.sharerooms_player, o_player.shortcode, "roomcode"); + } + } + } + } + //修改数据库 + console.log(1,JSON.stringify(o_player.roomcodes)); + var sql = "update player set play_roomcodes = ? where play_agentid = ? and play_playerid = ?"; + youle_agent.mysql.execsql("execsql", sql, [JSON.stringify(o_player.roomcodes), agentid, playerid]); + } + } + } + var preAdvanced = o_player.advanced; + o_player.advanced = advanced; + o_player.shortcode = shortcode; + o_player.desone = desone; + o_player.destwo = destwo; + if (lower_limit != null){ + o_player.lowlimit = parseInt(lower_limit); + } else { + o_player.lowlimit = -900000000; + } + o_player.notice = announcement; + //处理已存在的短号房间 + var o_game = null; + var o_agent1 = youle_room.agents.method.find_agent(agentid); + if(o_player.roomcodes && o_agent1){ + for(var i=0;i 0){ + if(preAdvanced > advanced){//房间数量减少 + var o_room = null; + for(var j = o_player.roomcodes[i].roomlist.length-1;j >= advanced;j--){ + o_room = o_game.method.find_room(o_player.roomcodes[i].roomlist[j]); + if(o_room){ + o_room.shortcode = null; + o_room.advanced_type = null; + if(o_room.battlestate == 0){ + //通知代理服务器解散房间 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "self_break_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = o_player.roomcodes[i].gameid; + msg1.data.roomcode = o_player.roomcodes[i].roomlist[j]; + //玩家列表 + msg1.data.playeridlist = []; + for (var k = 0; k < o_room.seatlist.length; k++) { + if (o_room.seatlist[k]){ + msg1.data.playeridlist.push(o_room.seatlist[k].playerid); + } + } + youle_room.app.youle_agent[msg1.rpc](msg1,true); + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "other_break_room"; + o_room.method.sendpack_toother(msg2, o_player.gameinfo.seat); + //释放房间 + o_game.method.free_room(o_room); + + }else{ + youle_room.import.gameBreak(o_room); + } + + }else{ + console.log("玩家短号房列表出现异常!!"); + } + } + o_player.roomcodes[i].roomlist.splice(advanced,preAdvanced - advanced); + var sql = "update player set play_roomcodes = ? where play_agentid = ? and play_playerid = ?"; + youle_agent.mysql.execsql("execsql", sql, [JSON.stringify(o_player.roomcodes), agentid, playerid]); + //已存在分享的vip房 + var oGame = o_agent.method.find_game(o_player.roomcodes[i].gameid); + if(oGame){ + if(oGame.sharerooms_player){ + var findIndex = min_ary_indexof(oGame.sharerooms_player, shortcode, "roomcode"); + if(findIndex > -1){ + oGame.sharerooms_player[findIndex].roomlist=o_player.roomcodes[i].roomlist; + oGame.sharerooms_player[findIndex].destwo=destwo; + } + } + } + }else if(preAdvanced < advanced){//房间数量增加 + var mg = {}; + mg.data={}; + mg.data.agentid = agentid; + mg.data.gameid = o_player.roomcodes[i].gameid; + mg.data.playerid = o_player.playerid; + mg.data.tea = o_player.roomcodes[i].tea; + mg.data.roomtype = o_player.roomcodes[i].roomtype; + mg.data.infinite = o_player.roomcodes[i].infinite; + mg.data.videoConfig = o_player.roomcodes[i].videoConfig; + mg.data.rebateLimit = o_player.roomcodes[i].rebateLimit; + mg.data.forbidSelect = o_player.roomcodes[i].forbidSelect; + mg.data.rebateType = o_player.roomcodes[i].rebateType; + try{ + youle_agent.advanced_createroom(mg,advanced); + }catch(e){ + + } + } + //已存在分享的vip房 + var oGame = o_agent.method.find_game(o_player.roomcodes[i].gameid); + if(oGame){ + if(oGame.sharerooms_player){ + var findIndex = min_ary_indexof(oGame.sharerooms_player, shortcode, "roomcode"); + if(findIndex > -1){ + oGame.sharerooms_player[findIndex].destwo=destwo; + } + } + } + } + + } + } + } + + // } + //}; + // sql = "select * from player where play_agentid = ? and play_playerid = ?"; + // youle_agent.mysql.execsql("opensql", sql, [agentid,playerid], db_callback_shortcode); + // console.log("获取房间"); + + pack.data.state = 0; + return pack; +} + +//代理后台-设置短号白名单 +youle_agent.synch_whitelist = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var whitelist = pack.data.whitelist; + if(whitelist){ + for(var i=0;i 6000){//超出 + var cmpStr = LZString.compressToUTF16(dataStr); + return "zip"+cmpStr; + }else{ + return dataStr; + } +} +youle_agent.deCompressWhiteList = function(data){ + var dataStr = data; + if(dataStr.indexOf("zip")==0){//压缩 + var str = dataStr.slice(3,dataStr.length); + var deStr = LZString.decompressFromUTF16(str); + return JSON.parse(deStr); + }else{ + return JSON.parse(dataStr); + } +} +youle_agent.optBanList = function(pack){ + //agent player 下的属性 banList:[]; + var agentid = pack.data.agentid; + var playerid = pack.data.playerid; + var gameid = pack.data.gameid; + var optId = parseInt(pack.data.optId); + //type 1.获取名单 2.查询单个玩家是否在名单中 3.添加名单 4.删除单个名单 5.清空名单 + var type = pack.data.type; + var breakRoom = pack.data.breakRoom; + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return false; + }; + //查找玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return false; + }; + if(!o_player.banList){ + o_player.banList = []; + } + if(type){ + type = Number(type); + }else{ + type = 0; + } + + switch(type){ + //查询名单 + case 1: + pack.data.banList = o_player.banList; + break; + //查询单个玩家是否在名单中 + case 2: + if(o_player.banList.indexOf(optId) < 0){//不在名单中 + pack.data.message = "玩家"+optId+"不在名单中!"; + }else{ + pack.data.message = "您已添加过玩家:"+optId; + } + break; + //添加名单 + case 3: + if(o_player.banList.indexOf(optId) < 0){//不在名单中 + o_player.banList.unshift(optId); + pack.data.message = "玩家"+optId+"已添加!"; + }else{ + pack.data.message = "玩家"+optId+"已在名单中!"; + } + pack.data.banList = o_player.banList; + //未开战的该玩家房间解散 + try{ + if(breakRoom){ + var o_player1 = o_agent.method.find_player(optId); + if (o_player1) { + var o_agent1 = youle_room.agents.method.find_agent(agentid); + var o_game = o_agent1.method.find_game(gameid); + if (o_game) { + if(o_player1.gameinfo[o_game.idx]){ + var roomcode = o_player1.gameinfo[o_game.idx].roomcode; + var o_room = o_game.method.find_room(roomcode); + if(o_room){ + if(o_room.owner.playerid == playerid){ + + if (o_room.battlestate == 1){ + o_room.method.open_free_timer(1); + } else { + //o_room.method.sendpack_toother(msg, -1); + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "self_break_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = gameid; + msg1.data.roomcode = roomcode; + //玩家列表 + msg1.data.playeridlist = []; + for (var j = 0; j < o_room.seatlist.length; j++) { + if (o_room.seatlist[j]){ + msg1.data.playeridlist.push(o_room.seatlist[j].playerid); + } + } + youle_room.app.youle_agent[msg1.rpc](msg1,true); + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "other_break_room"; + o_room.method.sendpack_toother(msg2, o_player.gameinfo.seat); + } + + //释放房间 + o_game.method.free_room(o_room); + } + } + } + } + } + } + }catch(e){ + console.log(e); + } + + break; + //删除单个名单 + case 4: + if(o_player.banList.indexOf(optId) >= 0){//在名单中 + min_ary_delval(o_player.banList,optId); + pack.data.message = "玩家"+optId+"已删除!"; + + }else{ + pack.data.message = "玩家"+optId+"不在名单中!"; + } + pack.data.banList = o_player.banList; + break; + //清空名单 + case 5: + o_player.banList = []; + pack.data.message = "名单已清空!"; + pack.data.banList = o_player.banList; + break; + //全部白名单添加入黑名单 + case 6: + var whitelist = o_player.whitelist; + if(whitelist){ + for(var i=0;i -1){ + ownerid = o_agent.shortcodes[idx].playerid; + } + } + if(!ownerid){ + pack.data.error = 1; + pack.data.message = "短号不存在"; + return pack; + } + var owner = o_agent.method.find_player(ownerid);; + if(!owner){ + pack.data.error = 1; + pack.data.message = "房主未登录"; + return pack; + } + var whitelist = min_copyjson(owner.whitelist); + if(whitelist){ + + //将白名单字符串转数字 + for(var i=0;i val2[2] - val1[2]); + // }else{ + // whitelist.sort((val1,val2) => val1[2] - val2[2]); + // } + var myRank = min_ary_indexof(whitelist,playerid,"0"); + pack.data.rankList = whitelist; + pack.data.myRank = myRank + 1; + if(myRank>-1){ + pack.data.myScore = whitelist[myRank][2]; + }else{ + pack.data.myScore = 0; + } + }else{ + //pack.data.rankList = []; + pack.data.message = "该VIP房无排名信息!"; + pack.data.myRank = 0; + } + + return pack; +} +youle_agent.setAllCharm = function(pack){ + var agentid = pack.data.agentid; + var playerid = pack.data.playerid; + var gameid = pack.data.gameid; + var value = pack.data.value; + if(value){ + value = Number(value); + }else{ + value = 0; + } + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return false; + }; + //查找玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return false; + }; + var whitelist = o_player.whitelist; + var shortcode = o_player.shortcode; + if(whitelist){ + for(var i=0;i -1){ + if(config_vipConfig[vipIndex][1]){ + writeDB = false; + } + } + } + } + writeDB = true; + if(writeDB){ + var sql = "update player set play_whitelist = ? where play_agentid = ? and play_playerid = ?"; + + youle_room.mysql.execsql("execsql", sql, [youle_agent.compressWhiteList(o_player.whitelist), agentid, playerid]); + } + }else{ + // pack.data.message = "您白名单内还没玩家"; + } + pack.data.message = "设置成功!"; + + + return pack; +} +youle_agent.setUserCharm = function(pack){ + var agentid = pack.data.agentid; + var playerid = pack.data.playerid; + var gameid = pack.data.gameid; + var userid = pack.data.userid; + var value = pack.data.value; + var mode = pack.data.mode; + if(value){ + value = Number(value); + }else{ + value = 0; + } + if(!mode){ + mode = 0; + } + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return false; + }; + //查找玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return false; + }; + var whitelist = o_player.whitelist; + var shortcode = o_player.shortcode; + if(whitelist){ + for(var i=0;i -1){ + if(config_vipConfig[vipIndex][1]){ + writeDB = false; + } + } + } + } + writeDB = true; + if(writeDB){ + var sql = "update player set play_whitelist = ? where play_agentid = ? and play_playerid = ?"; + + youle_room.mysql.execsql("execsql", sql, [youle_agent.compressWhiteList(o_player.whitelist), agentid, playerid]); + } + }else{ + // pack.data.message = "您白名单内还没玩家"; + } + pack.data.message = "设置成功!"; + + + return pack; +} +//查找玩家是否在白名单中 返回短号数组 +youle_agent.findPlayerInWhiteList = function(agentid,userid){ + //查找代理商 + var res = []; + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return res; + }; + + + + if(o_agent.players){ + for(var i=0;i playBean){ + pack.data.state = 1; + pack.data.error = "赠送数量超出!"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return ; + } + var uBank1 = Number(playBean) - Number(count); + var sql = "update player set play_bank = ? where play_agentid = ? and play_playerid = ?"; + youle_agent.mysql.execsql("execsql", sql, [uBank1, agentid, playerid]); + + var uBank2 = Number(playBean1) + Number(count); + var sql = "update player set play_bank = ? where play_agentid = ? and play_playerid = ?"; + youle_agent.mysql.execsql("execsql", sql, [uBank2, agentid, toPlayerid]); + pack.data.state = 0; + pack.data.showerror = 1; + pack.data.star2 = uBank1; + pack.data.error = "成功赠送"+starName+count; + youle_agent.app.SendPack(pack); + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + //房间id = 时间戳 + 4位随机码 + 房号 + var roomid = new Date().getTime() + min_randomChar(4); + var time = min_now(); + var beanmult = 0; + + var shortcode = 0; + + var deductmode = 0; + + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var _change = 0; + _change = -1 * count; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, "", 3, '', beanmult, playerid, shortcode, deductmode, 0, 0, 0, 1, playerid, 0, _change, 0, 0, toPlayerid, 0, 0, 0, time, invitecode1, inviteid1, marketid1]); + _change = count; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, "", 3, '', beanmult, toPlayerid, shortcode, deductmode, 0, 0, 0, 1, toPlayerid, 0, _change, 0, 0, 0, 0, 0, 0, time, invitecode2, inviteid2, marketid2]); + /* + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + //房间id = 时间戳 + 4位随机码 + 房号 + var roomid = new Date().getTime() + min_randomChar(4); + var time = min_now(); + var beanmult = 0; + + var shortcode = 0; + + var deductmode = 0; + + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var _change = 0; + + //赠送方 + _change = -1 * count; + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.change = _change; + msg.data.pushbean = 0; + msg.data.type = 7; + msg.data.gameid = gameid; + youle_room.app.youle_agent[msg.rpc](msg); + + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, "", 3, '', beanmult, playerid, shortcode, deductmode, 0, 0, 0, 1, playerid, 0, _change, 0, 0, toPlayerid, 0, 0, 0, time, invitecode1, inviteid1, marketid1]); + //接收方 + _change = count; + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = toPlayerid; + msg.data.change = _change; + msg.data.pushbean = 0; + msg.data.type = 7; + msg.data.gameid = gameid; + youle_room.app.youle_agent[msg.rpc](msg); + + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, "", 3, '', beanmult, toPlayerid, shortcode, deductmode, 0, 0, 0, 1, toPlayerid, 0, _change, 0, 0, 0, 0, 0, 0, time, invitecode2, inviteid2, marketid2]); + pack.data.state = 0; + youle_agent.app.SendPack(pack); + */ + + } + //查找玩家 + var db_callback = function(dbdata){ + pack.data = {}; + if (dbdata.length != 0){ + // pack.data.agentid = dbdata[0].play_agentid; + // pack.data.playerid = dbdata[0].play_playerid; + // pack.data.nickname = dbdata[0].play_nickname; + // pack.data.avatar = dbdata[0].play_avatar; + // pack.data.sex = dbdata[0].play_sex; + // pack.data.roomcard = dbdata[0].play_roomcard; + // pack.data.bean = dbdata[0].play_bean; + invitecode1 = dbdata[0].play_invitecode; + inviteid1 = dbdata[0].play_inviteid; + marketid1 = dbdata[0].play_marketid; + if(password != dbdata[0].play_bankpwd){ + pack.data.state = 1; + pack.data.error = "密码不正确!"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + }else{ + playBean = dbdata[0].play_bank; + var db_callback1 = function(dbdata){ + if (dbdata.length != 0){ + // pack.data.agentid = dbdata[0].play_agentid; + // pack.data.playerid = dbdata[0].play_playerid; + // pack.data.nickname = dbdata[0].play_nickname; + // pack.data.avatar = dbdata[0].play_avatar; + // pack.data.sex = dbdata[0].play_sex; + // pack.data.roomcard = dbdata[0].play_roomcard; + // pack.data.bean = dbdata[0].play_bean; + playBean1 = dbdata[0].play_bank; + invitecode2 = dbdata[0].play_invitecode; + inviteid2 = dbdata[0].play_inviteid; + marketid2 = dbdata[0].play_marketid; + callbackFun(); + + }else{ + pack.data.state = 1; + pack.data.error = "未找到赠送玩家,请确认ID!"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + } + } + var sql1 = "select * from player where play_agentid = ? and play_playerid = ? limit 1"; + youle_agent.mysql.execsql("opensql", sql1, [agentid, toPlayerid], db_callback1); + } + }else{ + pack.data.state = 1; + pack.data.error = "账号异常!"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + } + + } + var sql = "select * from player where play_agentid = ? and play_playerid = ? limit 1"; + youle_agent.mysql.execsql("opensql", sql, [agentid, playerid], db_callback); + + + + +} + +//充值卡充值 +youle_agent.topup_card = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var cardno = pack.data.cardno; + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + return; + }; + //查找玩家 + var o_player = o_agent.method.find_player(playerid); + if (!o_player) { + return; + }; + + var db_callback = function(dbdata){ + if (dbdata[0].result == 3){ + pack.rpc = "show_message"; + pack.data = {}; + pack.data.msg = "该卡号不存在"; + pack.data.time = 0; + youle_agent.app.SendPack(pack); + return; + } + if (dbdata[0].result == 4){ + pack.rpc = "show_message"; + pack.data = {}; + pack.data.msg = "该卡号已使用"; + pack.data.time = 0; + youle_agent.app.SendPack(pack); + return; + } + + var cardtype = parseInt(cardno.substr(32,1)); + var amount = parseInt(cardno.substr(33,20)); + switch(cardtype){ + case 0: //房卡充值 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_roomcard"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.opt = 2; + msg.data.roomcard = parseInt(dbdata[0].play_roomcard); + msg.data.change = amount; + youle_agent.synch_roomcard(msg); + break; + case 1: //金币充值 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.opt = 2; + msg.data.bean = parseInt(dbdata[0].play_bean); + msg.data.change = amount; + youle_agent.synch_bean(msg); + break; + } + } + + var sql = "call cp_game_topup_card(?,?,?)"; + youle_agent.mysql.execsql("opensql", sql, [agentid, playerid, cardno], db_callback); +} diff --git a/server/youle/server_platform/class.agent.js b/server/youle/server_platform/class.agent.js new file mode 100644 index 0000000..313dc36 --- /dev/null +++ b/server/youle/server_platform/class.agent.js @@ -0,0 +1,190 @@ +/////////////////////////////////////////////////////////////// +/////////////// cls_youle_platform_agent: 运营商 ///////////////// +/////////////////////////////////////////////////////////////// +var cls_youle_platform_agent = cls_youle_platform_agent || { + //构造函数 + new: function(agentid, name, logo, server, server_visitor, port_tcp, port_http, managecode){ + var agent = {}; + agent.agentid = agentid; + agent.name = name; + agent.logo = logo; + agent.server = server; + agent.server_visitor = server_visitor; + agent.port_tcp = port_tcp; + agent.port_http = port_http; + agent.managecode = managecode; + + //渠道列表 + agent.channels = []; + + //方法 + agent.method = {}; + //载入渠道(没找到则创建) + agent.method.load_channel = function(channelid, func_return){ + cls_youle_platform_agent.load_channel(agent, channelid, func_return); + }; + + return agent; + }, + + //载入渠道(没找到则创建) + load_channel: function(o_agent, channelid, func_return){ + + var db_callback = function(dbdata) { + var o_channel = {}; + o_channel.channelid = dbdata[0].agch_channelid; + o_channel.tel = dbdata[0].agch_sales_tel; + o_channel.wechat = dbdata[0].agch_sales_wechat; + o_channel.youle_appid = dbdata[0].agch_youle_appid; + o_channel.youle_devkey = dbdata[0].agch_youle_devkey; + o_channel.youle_busiid = dbdata[0].agch_youle_busiid; + + var idx = cls_youle_platform_agent.aryidx_channel(o_channel.channelid); + if (o_agent.channels.length <= idx) { + o_agent.channels.length = idx + 1; + } + o_agent.channels[idx] = o_channel; + func_return(o_channel); + } + + if (!channelid){ + channelid = ""; + } + var sql = "call cp_platform_get_agentchannel(?,?);"; + youle_platform.mysql.execsql("opensql", sql, [o_agent.agentid, channelid], db_callback); + }, + + //计算渠道的数组下标 + aryidx_channel: function(channelid){ + //channelid的5、10、15、20四位组成数组下标 + var idx = parseInt(channelid.substr(4, 1) + + channelid.substr(9, 1) + + channelid.substr(14, 1) + + channelid.substr(19, 1)); + if (isNaN(idx)){ + return -1; + } + return idx; + } +} + +/////////////////////////////////////////////////////////////// +/////////// cls_youle_platform_agentlist: 运营商列表 ////////// +/////////////////////////////////////////////////////////////// +var cls_youle_platform_agentlist = cls_youle_platform_agentlist || { + //构造函数 + new: function() { + + var agentlist = {}; + + //代理商列表 + agentlist.list = []; + + //开启定时在每天凌晨3:00到4:00之间删除超时战绩的定时器 + var do_del_grade = function(){ + //判断时间是否在范围内 + var time_range = function (beginTime, endTime) { + var strb = beginTime.split (":"); + if (strb.length != 2) { + return false; + } + + var stre = endTime.split (":"); + if (stre.length != 2) { + return false; + } + + var b = new Date(); + var e = new Date(); + var n = new Date(); + + b.setHours(strb[0]); + b.setMinutes(strb[1]); + e.setHours(stre[0]); + e.setMinutes(stre[1]); + + if (n.getTime() > b.getTime() && n.getTime() < e.getTime()){ + return true; + } else { + return false; + } + } + + + if (time_range("3:00", "4:00")){ + var sql = "call cp_game_del_timeout_task(null);"; + youle_platform.mysql.execsql("execsql", sql, []); + } + if (time_range("4:00", "5:00")){ + var sql = "call cp_game_grade_del();"; + youle_platform.mysql_grade.execsql("execsql", sql, []); + } + } + agentlist.timer_delgrade = min_ontime(do_del_grade, 59 * 60 * 1000); + + + //方法 + agentlist.method = {}; + + //载入代理商(没找到则创建) + agentlist.method.load_agent = function(agentid, func_return){ + var o_agent = agentlist.method.find_agent(agentid); + if (o_agent){ + func_return(o_agent); + } else { + var db_callback = function(dbdata) { + var db_agentid = dbdata[0].agen_agentid; + var db_name = dbdata[0].agen_name; + var db_logo = dbdata[0].agen_logo; + var db_server = dbdata[0].agen_server; + var db_server_visitor = dbdata[0].agen_server_visitor; + var db_port_tcp = parseInt(dbdata[0].agen_port_tcp); + var db_port_http = parseInt(dbdata[0].agen_port_http); + var db_managecode = dbdata[0].agen_managecode; + o_agent = cls_youle_platform_agent.new(db_agentid, db_name, db_logo, db_server, db_server_visitor, db_port_tcp, db_port_http, db_managecode); + var idx = agentlist.method.aryidx_agent(db_agentid); + agentlist.list[idx] = o_agent; + func_return(o_agent); + } + var sql = "call cp_platform_get_agentserver(?);"; + youle_platform.mysql.execsql("opensql", sql, [agentid], db_callback); + } + }; + + //查找运营商 + agentlist.method.find_agent = function(agentid){ + var idx = agentlist.method.aryidx_agent(agentid); + if (idx == -1){ + return null; + } + if (agentlist.list.length <= idx){ + return null; + } + if (!agentlist.list[idx]){ + return null; + } + if (agentlist.list[idx].agentid != agentid){ + return null; + } + return agentlist.list[idx]; + }; + + //计算运营商的数组下标 + agentlist.method.aryidx_agent = function(agentid) { + //agentid的5、10、15、20四位组成数组下标 + var idx = parseInt(agentid.substr(4, 1) + + agentid.substr(9, 1) + + agentid.substr(14, 1) + + agentid.substr(19, 1)); + if (isNaN(idx)){ + return -1; + } + return idx; + }; + + return agentlist; + } +} + +//运营商列表 +youle_platform.agents = cls_youle_platform_agentlist.new(); diff --git a/server/youle/server_platform/class.config.js b/server/youle/server_platform/class.config.js new file mode 100644 index 0000000..e245203 --- /dev/null +++ b/server/youle/server_platform/class.config.js @@ -0,0 +1,304 @@ +/////////////////////////////////////////////////////////////// +//////////// cls_youle_platform_config: 系统配置 ////////////// +/////////////////////////////////////////////////////////////// +var cls_youle_platform_config = cls_youle_platform_config || { + //构造函数 + new: function(_url_config){ + + var config = {}; + + //方法 + config.method = {}; + //读取配置文件的内容 + config.method.get_config = function(){ + cls_youle_platform_config.get_config(config); + } + //获取参数配置 + config.method.get_paravalue = function(paraname, agentid, gameid, channelid, marketid){ + return cls_youle_platform_config.get_paravalue(config, paraname, agentid, gameid, channelid, marketid); + } + + //获取游戏列表_下载页面 + config.method.get_gamelist_downhtml = function(agentid, channelid){ + return cls_youle_platform_config.get_gamelist_downhtml(config, agentid, channelid); + } + + //获取游戏列表_游戏大厅 + config.method.get_gamelist_gamehall = function(agentid, channelid, marketid){ + return cls_youle_platform_config.get_gamelist_gamehall(config, agentid, channelid, marketid); + } + + //获取代理城市列表 + config.method.get_agentlist = function(agentid, channelid){ + return cls_youle_platform_config.get_agentlist(config, agentid, channelid); + } + + //获取子游戏服务器列表 + config.method.get_gameserver_list = function(agentid){ + return cls_youle_platform_config.get_gameserver_list(config, agentid); + } + + //配置文件地址 + config.url = _url_config; + //配置文件内容 + config.data = null; + //定时器定时读取配置文件 + config.timer_getdata = min_ontime(config.method.get_config, 30 * 1000); + + return config; + }, + + //读取配置文件的内容 + get_config: function(o_config){ + var cfg = {}; + cfg.url = o_config.url + "?" + min_timestamp(); + cfg.type = "POST"; + cfg.data = ""; + cfg.success = function(str){ + if (str){ + try{ + var data = JSON.parse(str); + o_config.data = data; + } + catch(e){ + } + } + }; + cfg.error = function(str){}; + min_http(cfg); + }, + + //获取参数配置 + get_paravalue: function(o_config, paraname, agentid, gameid, channelid, marketid){ + var paravalue = null; + //遍历数组,匹配key,获取参数值 + var do_get_para = function(arylist, keyname, keyvalue){ + if (arylist && keyname && keyvalue){ + for (var i = 0; i < arylist.length; i++){ + if (arylist[i][keyname] == keyvalue){ + if (arylist[i][paraname]){ + paravalue = arylist[i][paraname]; + } + return arylist[i]; + } + } + } + return null; + } + + if (!o_config.data){ + return paravalue; + } + if (o_config.data[paraname]){ + paravalue = o_config.data[paraname]; + } + var _o_agent = do_get_para(o_config.data.agentlist, "agentid", agentid); + if (!_o_agent){ + return paravalue; + } + var _o_game = do_get_para(_o_agent.gamelist, "gameid", gameid); + if (!_o_game){ + return paravalue; + } + var _o_channel = do_get_para(_o_game.channellist, "channelid", channelid); + if (!_o_channel){ + return paravalue; + } + var _o_market = do_get_para(_o_channel.marketlist, "marketid", marketid); + if (!_o_market){ + return paravalue; + } + + return paravalue; + }, + + //获取游戏列表_下载页面 + get_gamelist_downhtml: function(o_config, agentid, channelid){ + var gamelist = []; + if (!o_config.data){ + return gamelist; + } + if (!o_config.data.agentlist){ + return gamelist; + } + for (var i = 0; i < o_config.data.agentlist.length; i++){ + if (o_config.data.agentlist[i].agentid == agentid){ + var _o_agent = o_config.data.agentlist[i]; + if (_o_agent.gamelist){ + for (var j = 0; j < _o_agent.gamelist.length; j++) { + var _o_game = _o_agent.gamelist[j]; + var o_game = {}; + o_game.name = _o_game.gamename; + o_game.image = _o_game.game_down_image; + o_game.state = _o_game.game_down_state; + o_game.memo = _o_game.game_down_memo; + if (_o_game.channellist){ + for (var k = 0; k < _o_game.channellist.length; k++) { + var _o_channel = _o_game.channellist[k]; + if (_o_channel.channelid == channelid){ + var _ios_marketid = _o_channel.ios_defdownload_marketid; + var _and_marketid = _o_channel.and_defdownload_marketid; + if (_o_channel.marketlist){ + for (var m = 0; m < _o_channel.marketlist.length; m++) { + var _o_market = _o_channel.marketlist[m]; + if (_o_market.marketid == _ios_marketid){ + o_game.ios_down = _o_market.app_download; + o_game.ios_size = _o_market.app_size; + o_game.ios_marketid = _ios_marketid; + } + if (_o_market.marketid == _and_marketid){ + o_game.android_down = _o_market.app_download; + o_game.android_size = _o_market.app_size; + } + } + } + if (o_game.ios_down && o_game.android_down){ + gamelist.push(o_game); + } + break; + } + } + } + } + } + break; + } + } + return gamelist; + }, + + //获取游戏列表_游戏大厅 + get_gamelist_gamehall: function(o_config, agentid, channelid, marketid){ + var gamelist = []; + if (!o_config.data){ + return gamelist; + } + if (!o_config.data.agentlist){ + return gamelist; + } + for (var i = 0; i < o_config.data.agentlist.length; i++){ + if (o_config.data.agentlist[i].agentid == agentid){ + var _o_agent = o_config.data.agentlist[i]; + if (_o_agent.gamelist){ + for (var j = 0; j < _o_agent.gamelist.length; j++){ + var _o_game = _o_agent.gamelist[j]; + if (_o_game.gameid != "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO"){ + var o_game = {}; + o_game.gameid = _o_game.gameid; + o_game.gamename = _o_game.gamename; + o_game.gamedir = _o_game.game_hall_dir; + o_game.gameimage = _o_game.game_hall_image; + o_game.gameversion = _o_game.game_version; + o_game.gamezip = _o_game.game_zip; + o_game.zipsize = _o_game.game_size; + if (_o_game.channellist){ + for (var k = 0; k < _o_game.channellist.length; k++) { + var _o_channel = _o_game.channellist[k]; + if (_o_channel.channelid == channelid){ + if (_o_channel.game_version > 0){ + o_game.gameversion = _o_channel.game_version; + } + if (_o_channel.game_zip){ + o_game.gamezip = _o_channel.game_zip; + } + if (_o_channel.game_size){ + o_game.zipsize = _o_channel.game_size; + } + if (_o_channel.marketlist){ + for (var m = 0; m < _o_channel.marketlist.length; m++) { + var _o_market = _o_channel.marketlist[m]; + if (_o_market.marketid == marketid){ + if (_o_market.game_version > 0){ + o_game.gameversion = _o_market.game_version; + } + if (_o_market.game_zip){ + o_game.gamezip = _o_market.game_zip; + } + if (_o_market.game_size){ + o_game.zipsize = _o_market.game_size; + } + break; + } + } + } + gamelist.push(o_game); + break; + } + } + } + } + } + } + break; + } + } + return gamelist; + }, + + //获取代理城市列表 + get_agentlist: function(o_config, agentid, channelid){ + var agentlist = []; + if (!o_config.data){ + return gamelist; + } + if (!o_config.data.agentlist){ + return gamelist; + } + for (var i = 0; i < o_config.data.agentlist.length; i++){ + if (o_config.data.agentlist[i].agentid == agentid || o_config.data.agentlist[i].relagentid == agentid){ + var o_agent = {}; + o_agent.agentid = o_config.data.agentlist[i].agentid; + o_agent.name = o_config.data.agentlist[i].agentname; + o_agent.channelid = o_config.data.agentlist[i].gamelist[0].channellist[0].channelid; + + agentlist.push(o_agent); + } + } + return agentlist; + }, + + //获取子游戏服务器列表 + get_gameserver_list: function(o_config, agentid){ + var iplist = []; + var paraname = "game_server_http"; + var do_pushto_iplist = function(ip){ + var idx = min_ary_indexof(iplist, ip); + if (idx == -1){ + iplist.push(ip); + } + } + + if (!o_config.data || !o_config.data.agentlist){ + return iplist; + } + var idx_agent = min_ary_indexof(o_config.data.agentlist, agentid, "agentid"); + if (idx_agent == -1){ + return iplist; + } + var o_agent = o_config.data.agentlist[idx_agent]; + if (!o_agent){ + return iplist; + } + if (o_config.data[paraname]){ + do_pushto_iplist(o_config.data[paraname]); + } + if (o_agent[paraname]){ + do_pushto_iplist(o_agent.data[paraname]); + } + if (o_agent.gamelist){ + for (var j = 0; j < o_agent.gamelist.length; j++) { + var o_game = o_agent.gamelist[j]; + if (o_game[paraname]){ + do_pushto_iplist(o_game[paraname]); + } + } + } + return iplist; + } +} + +//系统配置 +//youle_platform.config = cls_youle_platform_config.new("http://ylyxservice1.tscce.cn/config/update_jsonv2.txt"); +youle_platform.config = cls_youle_platform_config.new("http://tsgames.daoqi88.cn/config_test/test_update_jsonv2.txt"); +youle_platform.config.method.get_config(); + diff --git a/server/youle/server_platform/mod.js b/server/youle/server_platform/mod.js new file mode 100644 index 0000000..912ed4b --- /dev/null +++ b/server/youle/server_platform/mod.js @@ -0,0 +1,210 @@ +////////////////////////////////////////////////////////////////// +//////////////////////////游戏平台服务器////////////////////////// +////////////////////////////////////////////////////////////////// +//系统配置 +min_loadJsFile("youle/server_platform/class.config.js", function(){ +//运营商 +min_loadJsFile("youle/server_platform/class.agent.js", function(){ + +}); +}); + +var youle_platform = youle_platform || cls_mod.new("youle_platform", "platform", youle_app); + +//运营商列表 +youle_platform.agents = null; +//系统配置 +youle_platform.config = null; +//mysql操作类 +youle_platform.mysql = cls_mysql.new("http://ylyxdata.daoqi88.cn/mysql.php", youle_platform.app.SendPack); +youle_platform.mysql_grade = cls_mysql.new("http://ylyxdata.daoqi88.cn/mysql_grade.php", youle_platform.app.SendPack); + +//游戏获取代理商服务器地址 +youle_platform.agentserver_game = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var channelid = pack.data.channelid; + var marketid = pack.data.marketid; + //返回运营商信息 + var do_return_agentinfo = function(o_agent){ + if (!o_agent){ + return; + } + pack.data = {}; + pack.data.state = 0; + pack.data.agentid = o_agent.agentid; + pack.data.urlserver = o_agent.server + ":" + o_agent.port_tcp; + pack.data.visitorserver = o_agent.server_visitor + ":" + o_agent.port_tcp; + pack.data.urlserver_http = o_agent.server + ":" + o_agent.port_http; + pack.data.visitorserver_http = o_agent.server_visitor + ":" + o_agent.port_http; + //系统配置json + pack.data.logimage = youle_platform.config.method.get_paravalue("logimage", agentid, gameid, channelid, marketid); + pack.data.loginimage = parseInt(youle_platform.config.method.get_paravalue("loginimage", agentid, gameid, channelid, marketid)); + youle_platform.app.SendPack(pack); + } + youle_platform.agents.method.load_agent(agentid, do_return_agentinfo); +} + +//公众号页面获取代理商的服务器地址 +youle_platform.agentserver_web = function(pack){ + var agentid = pack.data.agentid; + var channelid = pack.data.channelid; + //返回运营商信息 + var do_return_agentinfo = function(o_agent){ + if (!o_agent){ + return; + } + pack.data = {}; + pack.data.agentid = o_agent.agentid; + pack.data.agentname = o_agent.name; + pack.data.agentlogo = youle_platform.config.method.get_paravalue('logimage2', o_agent.agentid); + pack.data.html_buyroomcard = parseInt(youle_platform.config.method.get_paravalue('html_buyroomcard', o_agent.agentid)); + pack.data.urlserver = "http://" + o_agent.server + ":" + o_agent.port_http; + //返回渠道商信息 + var do_return_channelinfo = function(o_channel){ + pack.data.channelid = o_channel.channelid; + pack.data.channeltel = o_channel.tel; + pack.data.channelwechat = o_channel.wechat; + pack.data.youle_appid = o_channel.youle_appid; + pack.data.youle_devkey = o_channel.youle_devkey; + pack.data.youle_busiid = o_channel.youle_busiid; + youle_platform.app.SendPack(pack); + } + o_agent.method.load_channel(channelid, do_return_channelinfo); + } + youle_platform.agents.method.load_agent(agentid, do_return_agentinfo); +} + +//关闭服务器 +youle_platform.close_agentserver = function(pack){ + var agentid = pack.data.agentid; + var managecode = pack.data.managecode; + //返回运营商信息 + var do_return_agentinfo = function(o_agent){ + if (!o_agent){ + return; + } + if (o_agent.managecode != managecode){ + pack.data = {}; + pack.data.state = 1; + youle_platform.app.SendPack(pack); + } + + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "close_agentserver"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.managecode = managecode; + + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.port_http + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){ + if (str){ + var _msg = JSON.parse(str); + pack.data = _msg.data; + youle_agent.app.SendPack(pack); + } + }; + cfg.error = function(str){ + pack.data = {}; + pack.data.state = 1; + youle_platform.app.SendPack(pack); + }; + min_http(cfg); + } + youle_platform.agents.method.load_agent(agentid, do_return_agentinfo); +} + +//开启服务器 +youle_platform.open_agentserver = function(pack){ + var agentid = pack.data.agentid; + var managecode = pack.data.managecode; + //返回运营商信息 + var do_return_agentinfo = function(o_agent){ + if (!o_agent){ + return; + } + if (o_agent.managecode != managecode){ + pack.data = {}; + pack.data.state = 1; + youle_platform.app.SendPack(pack); + } + + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "open_agentserver"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.managecode = managecode; + + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.port_http + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){ + if (str){ + var _msg = JSON.parse(str); + pack.data = _msg.data; + youle_agent.app.SendPack(pack); + } + }; + cfg.error = function(str){ + pack.data = {}; + pack.data.state = 1; + youle_platform.app.SendPack(pack); + }; + min_http(cfg); + } + youle_platform.agents.method.load_agent(agentid, do_return_agentinfo); +} + +//广播即时消息 +youle_platform.broadcast = function(pack){ + var agentid = pack.data.agentid; + var managecode = pack.data.managecode; + var msgcontent = pack.data.msgcontent; + //返回运营商信息 + var do_return_agentinfo = function(o_agent){ + if (!o_agent){ + return; + } + if (o_agent.managecode != managecode){ + pack.data = {}; + pack.data.state = 1; + youle_platform.app.SendPack(pack); + } + + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "broadcast"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.managecode = managecode; + msg.data.msgcontent = msgcontent; + + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.port_http + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){ + if (str){ + var _msg = JSON.parse(str); + pack.data = _msg.data; + youle_agent.app.SendPack(pack); + } + }; + cfg.error = function(str){ + pack.data = {}; + pack.data.state = 1; + youle_platform.app.SendPack(pack); + }; + min_http(cfg); + } + youle_platform.agents.method.load_agent(agentid, do_return_agentinfo); +} \ No newline at end of file diff --git a/server/youle/server_room/class.agent.js b/server/youle/server_room/class.agent.js new file mode 100644 index 0000000..ad59643 --- /dev/null +++ b/server/youle/server_room/class.agent.js @@ -0,0 +1,163 @@ +/////////////////////////////////////////////////////////////// +//////////////// cls_youle_room_agent: 运营商 ///////////////// +/////////////////////////////////////////////////////////////// +var cls_youle_room_agent = cls_youle_room_agent || { + //构造函数 + new: function(agentid, name, server, tcpport, httpport, freeroom){ + var agent = {}; + agent.agentid = agentid; //运营商id + agent.agentname = name; //运营商名称 + agent.server = server; //运营商服务器地址 + agent.tcpport = tcpport; //运营商服务器tcp端口 + agent.httpport = httpport; //运营商服务器http端口 + agent.freeroom = freeroom; //默认同意解散房间的倒计时,秒 + + //游戏列表 + agent.games = []; + + //方法 + agent.method = {}; + //载入游戏(没找到则创建) + agent.method.load_game = function(gameid, gamename, seatcount, makewar, modename, roomserver, tcpport, httpport, minroomcode, maxroomcode){ + return cls_youle_room_agent.load_game(agent, gameid, gamename, seatcount, makewar, modename, roomserver, tcpport, httpport, minroomcode, maxroomcode); + }; + //查找游戏 + agent.method.find_game = function(gameid){ + return cls_youle_room_agent.find_game(agent, gameid); + }; + + return agent; + }, + + //载入游戏(没找到则创建) + load_game: function(o_agent, gameid, gamename, seatcount, makewar, modename, roomserver, tcpport, httpport, minroomcode, maxroomcode){ + var idx = cls_youle_room_agent.aryidx_game(gameid); + if (o_agent.games.length <= idx) { + o_agent.games.length = idx + 1; + } + var o_game = o_agent.games[idx]; + if (!o_game){ + //不存在则创建 + o_game = cls_youle_room_game.new(o_agent, gameid, idx, gamename, seatcount, makewar, modename, roomserver, tcpport, httpport, minroomcode, maxroomcode); + o_agent.games[idx] = o_game; + } else { + o_game.idx = idx; + o_game.gamename = gamename; + o_game.seatcount = seatcount; + o_game.makewar = makewar; + o_game.modename = modename; + o_game.roomserver = roomserver; + o_game.tcpport = tcpport; + o_game.httpport = httpport; + o_game.minroomcode = minroomcode; + o_game.maxroomcode = maxroomcode; + } + return o_game; + }, + + //查找游戏 + find_game: function(o_agent, gameid){ + var idx = cls_youle_room_agent.aryidx_game(gameid); + if (idx == -1){ + return null; + } + if (o_agent.games.length <= idx){ + return null; + } + if (!o_agent.games[idx]){ + return null; + } + if (o_agent.games[idx].gameid != gameid){ + return null; + } + return o_agent.games[idx]; + }, + + //计算游戏的数组下标 + aryidx_game: function(gameid){ + //gameid的5、10、15、20四位组成数组下标 + var idx = parseInt(gameid.substr(4, 1) + + gameid.substr(9, 1) + + gameid.substr(14, 1) + + gameid.substr(19, 1)); + if (isNaN(idx)){ + return -1; + } + return idx; + } +} + +/////////////////////////////////////////////////////////////// +/////////// cls_youle_room_agentlist: 运营商列表 ////////// +/////////////////////////////////////////////////////////////// +var cls_youle_room_agentlist = cls_youle_room_agentlist || { + //构造函数 + new: function() { + var agentlist = {}; + //代理商列表 + agentlist.list = []; + + //方法 + agentlist.method = {}; + + //载入代理商(没找到则创建) + agentlist.method.load_agent = function(agentid, name, server, tcpport, httpport, freeroom){ + var idx = agentlist.method.aryidx_agent(agentid); + if (idx == -1){ + return null; + } + if (agentlist.list.length <= idx) { + agentlist.list.length = idx + 1; + } + var o_agent = agentlist.list[idx]; + if (!o_agent) { + //不存在则创建 + o_agent = cls_youle_room_agent.new(agentid, name, server, tcpport, httpport, freeroom); + agentlist.list[idx] = o_agent; + } else { + o_agent.agentname = name; + o_agent.server = server; + o_agent.tcpport = tcpport; + o_agent.httpport = httpport; + o_agent.freeroom = freeroom; + } + return o_agent; + }; + + //查找运营商 + agentlist.method.find_agent = function(agentid){ + var idx = agentlist.method.aryidx_agent(agentid); + if (idx == -1){ + return null; + } + if (agentlist.list.length <= idx){ + return null; + } + if (!agentlist.list[idx]){ + return null; + } + if (agentlist.list[idx].agentid != agentid){ + return null; + } + return agentlist.list[idx]; + }; + + //计算运营商的数组下标 + agentlist.method.aryidx_agent = function(agentid) { + //agentid的5、10、15、20四位组成数组下标 + var idx = parseInt(agentid.substr(4, 1) + + agentid.substr(9, 1) + + agentid.substr(14, 1) + + agentid.substr(19, 1)); + if (isNaN(idx)){ + return -1; + } + return idx; + }; + + return agentlist; + } +} + +//运营商列表 +youle_room.agents = cls_youle_room_agentlist.new(); diff --git a/server/youle/server_room/class.backup.js b/server/youle/server_room/class.backup.js new file mode 100644 index 0000000..820fdcc --- /dev/null +++ b/server/youle/server_room/class.backup.js @@ -0,0 +1,732 @@ +/////////////////////////////////////////////////// +//////////////cls_youle_room_backup: 数据备份类/////////// +/////////////////////////////////////////////////// +var cls_youle_room_backup = cls_youle_room_backup || { + + isloading: true, + + // backup_room: function(o_room, o_deskinfo){ + // try { + // if (!o_room){ + // return; + // } + // if (!o_room.battlestate){ + // return; + // } + + // var agentid = o_room.o_game.o_agent.agentid; + // var gameid = o_room.o_game.gameid; + // var roomcode = o_room.roomcode; + + // var _o_agent = youle_agent.agents.method.find_agent(agentid); + + // var data_agent = []; + // data_agent[0] = o_room.o_game.o_agent.agentid; + // data_agent[1] = o_room.o_game.o_agent.agentname; + // data_agent[2] = o_room.o_game.o_agent.server; + // data_agent[3] = o_room.o_game.o_agent.tcpport; + // data_agent[4] = o_room.o_game.o_agent.httpport; + // data_agent[5] = o_room.o_game.o_agent.freeroom; + // data_agent[6] = _o_agent.managecode; + // localStorage.setItem(agentid, JSON.stringify(data_agent)); + + // var data_game = []; + // data_game[0] = o_room.o_game.o_agent.agentid; + // data_game[1] = o_room.o_game.gameid; + // data_game[2] = o_room.o_game.gamename; + // data_game[3] = o_room.o_game.seatcount; + // data_game[4] = o_room.o_game.makewar; + // data_game[5] = o_room.o_game.modename; + // data_game[6] = o_room.o_game.roomserver; + // data_game[7] = o_room.o_game.tcpport; + // data_game[8] = o_room.o_game.httpport; + // data_game[9] = o_room.o_game.minroomcode; + // data_game[10] = o_room.o_game.maxroomcode; + // localStorage.setItem(agentid + "_" + gameid, JSON.stringify(data_game)); + + // var data_room = []; + // data_room[0] = o_room.o_game.o_agent.agentid; + // data_room[1] = o_room.o_game.gameid; + // data_room[2] = o_room.roomcode; + // data_room[3] = o_room.roomtype; + // data_room[4] = o_room.needroomcard; + // data_room[5] = o_room.asetcount; + // data_room[6] = o_room.createtime; + // data_room[7] = o_room.battlestate; + // data_room[8] = o_room.makewartime; + // data_room[9] = o_room.wasdeduct; + // data_room[10] = o_room.roommode; + // data_room[11] = o_room.infinite; + // data_room[12] = []; + // for (var i = 0; i < o_room.seatlist.length; i++) { + // data_room[12][i] = null; + // if (o_room.seatlist[i]){ + // data_room[12][i] = []; + // data_room[12][i][0] = o_room.seatlist[i].playerid; + // data_room[12][i][1] = o_room.seatlist[i].nickname; + // data_room[12][i][2] = o_room.seatlist[i].avatar; + // data_room[12][i][3] = o_room.seatlist[i].sex; + // data_room[12][i][4] = o_room.seatlist[i].playertype; + // data_room[12][i][5] = o_room.seatlist[i].gameinfo.seat; + // data_room[12][i][6] = o_room.seatlist[i].gameinfo.isowner; + // data_room[12][i][7] = o_room.seatlist[i].gameinfo.isbet; + // data_room[12][i][8] = o_room.seatlist[i].gameinfo.grade; + + // var _o_player = _o_agent.method.find_player(o_room.seatlist[i].playerid); + // data_room[12][i][9] = _o_player.roomcard; + // data_room[12][i][10] = _o_player.bean; + + // if (!o_room.seatlist[i].gameinfo.isowner) { + // data_room[12][i][11] = o_room.seatlist[i].needroomcard; + // data_room[12][i][12] = o_room.seatlist[i].wasdeduct; + // } + // } + // } + // data_room[13] = o_deskinfo; + // localStorage.setItem(agentid + "_" + gameid + "_" + roomcode, JSON.stringify(data_room)); + // } catch (e) { + // if (e.name == 'QuotaExceededError') { + // console.log(min_now() + ' 超出localStorage存储限额!'); + // } else { + // console.log(min_now() + ' 保存localStorage失败'); + // } + // } + // }, + + backup_room: function(o_room, o_deskinfo){ + try { + if (!o_room){ + return; + } + if (!o_room.battlestate){ + return; + } + if (!dbObject){ + return; + } + + var agentid = o_room.o_game.o_agent.agentid; + var gameid = o_room.o_game.gameid; + var roomcode = o_room.roomcode; + + var _o_agent = youle_agent.agents.method.find_agent(agentid); + + var data_agent = []; + data_agent[0] = o_room.o_game.o_agent.agentid; + data_agent[1] = o_room.o_game.o_agent.agentname; + data_agent[2] = o_room.o_game.o_agent.server; + data_agent[3] = o_room.o_game.o_agent.tcpport; + data_agent[4] = o_room.o_game.o_agent.httpport; + data_agent[5] = o_room.o_game.o_agent.freeroom; + data_agent[6] = _o_agent.managecode; + data_agent[7] = _o_agent.shortcodes; + data_agent[8] = _o_agent.rebate_mode1; + data_agent[9] = _o_agent.rebate_set1; + data_agent[10] = _o_agent.rebate_mode2; + data_agent[11] = _o_agent.rebate_set2; + data_agent[12] = _o_agent.robot_nicknamelist; + data_agent[13] = _o_agent.robot_avatarlist; + data_agent[14] = _o_agent.initCard; + data_agent[15] = _o_agent.initBean; + + dbObject.put(agentid, data_agent); + + var data_game = []; + data_game[0] = o_room.o_game.o_agent.agentid; + data_game[1] = o_room.o_game.gameid; + data_game[2] = o_room.o_game.gamename; + data_game[3] = o_room.o_game.seatcount; + data_game[4] = o_room.o_game.makewar; + data_game[5] = o_room.o_game.modename; + data_game[6] = o_room.o_game.roomserver; + data_game[7] = o_room.o_game.tcpport; + data_game[8] = o_room.o_game.httpport; + data_game[9] = o_room.o_game.minroomcode; + data_game[10] = o_room.o_game.maxroomcode; + dbObject.put(agentid + "_" + gameid, data_game); + + var data_room = []; + data_room[0] = o_room.o_game.o_agent.agentid; + data_room[1] = o_room.o_game.gameid; + data_room[2] = o_room.roomcode; + data_room[3] = o_room.roomtype; + data_room[4] = o_room.needroomcard; + data_room[5] = o_room.asetcount; + data_room[6] = o_room.createtime; + data_room[7] = o_room.battlestate; + data_room[8] = o_room.makewartime; + data_room[9] = o_room.wasdeduct; + data_room[10] = o_room.roommode; + data_room[11] = o_room.infinite; + data_room[12] = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + data_room[12][i] = null; + if (o_room.seatlist[i]){ + data_room[12][i] = []; + data_room[12][i][0] = o_room.seatlist[i].playerid; + data_room[12][i][1] = o_room.seatlist[i].nickname; + data_room[12][i][2] = o_room.seatlist[i].avatar; + data_room[12][i][3] = o_room.seatlist[i].sex; + data_room[12][i][4] = o_room.seatlist[i].playertype; + data_room[12][i][5] = o_room.seatlist[i].gameinfo.seat; + data_room[12][i][6] = o_room.seatlist[i].gameinfo.isowner; + data_room[12][i][7] = o_room.seatlist[i].gameinfo.isbet; + data_room[12][i][8] = o_room.seatlist[i].gameinfo.grade; + + var _o_player = _o_agent.method.find_player(o_room.seatlist[i].playerid); + data_room[12][i][9] = _o_player.roomcard; + data_room[12][i][10] = _o_player.bean; + + if (!o_room.seatlist[i].gameinfo.isowner) { + data_room[12][i][11] = o_room.seatlist[i].needroomcard; + data_room[12][i][12] = o_room.seatlist[i].wasdeduct; + } + data_room[12][i][13] = o_room.seatlist[i].gameinfo.isprepare; + data_room[12][i][14] = _o_player.advanced; + data_room[12][i][15] = _o_player.shortcode; + data_room[12][i][16] = _o_player.roomcodes; + data_room[12][i][17] = _o_player.bankpower; + data_room[12][i][18] = _o_player.bank; + data_room[12][i][19] = _o_player.bankpwd; + } + } + data_room[13] = o_deskinfo; + data_room[14] = o_room.advanced_type; + data_room[15] = o_room.advanced_teabean; + data_room[16] = o_room.beanfloor; + data_room[17] = o_room.needprepare; + data_room[18] = o_room.beanmult; + data_room[19] = []; + data_room[19][0] = o_room.owner.playerid; + data_room[19][1] = o_room.owner.nickname; + data_room[19][2] = o_room.owner.avatar; + data_room[19][3] = o_room.owner.sex; + data_room[19][4] = o_room.owner.playertype; + var _o_owner = _o_agent.method.find_player(o_room.owner.playerid); + data_room[19][5] = _o_owner.roomcard; + data_room[19][6] = _o_owner.bean; + data_room[19][7] = _o_owner.advanced; + data_room[19][8] = _o_owner.shortcode; + data_room[19][9] = _o_owner.roomcodes; + data_room[20] = o_room.match_id; + data_room[21] = o_room.match_idx; + + dbObject.put(agentid + "_" + gameid + "_" + roomcode, data_room); + }catch(e){} + }, + + // deletebackup_room: function(o_room){ + // var agentid = o_room.o_game.o_agent.agentid; + // var gameid = o_room.o_game.gameid; + // var roomcode = o_room.roomcode; + // localStorage.removeItem(agentid + "_" + gameid + "_" + roomcode); + // }, + + deletebackup_room: function(o_room){ + if (!dbObject){ + return; + } + var agentid = o_room.o_game.o_agent.agentid; + var gameid = o_room.o_game.gameid; + var roomcode = o_room.roomcode; + dbObject.delete(agentid + "_" + gameid + "_" + roomcode); + }, + + backup_match: function(o_match){ + try{ + if (!o_match){ + return; + } + if (!dbObject){ + return; + } + + var agentid = o_match.o_game.o_agent.agentid; + var gameid = o_match.o_game.gameid; + var matchid = o_match.id; + + var data_agent = []; + data_agent[0] = o_match.o_game.o_agent.agentid; + data_agent[1] = o_match.o_game.o_agent.agentname; + data_agent[2] = o_match.o_game.o_agent.server; + data_agent[3] = o_match.o_game.o_agent.tcpport; + data_agent[4] = o_match.o_game.o_agent.httpport; + data_agent[5] = o_match.o_game.o_agent.freeroom; + data_agent[6] = o_match.o_game.o_agent.managecode; + data_agent[7] = o_match.o_game.o_agent.shortcodes; + data_agent[8] = o_match.o_game.o_agent.rebate_mode1; + data_agent[9] = o_match.o_game.o_agent.rebate_set1; + data_agent[10] = o_match.o_game.o_agent.rebate_mode2; + data_agent[11] = o_match.o_game.o_agent.rebate_set2; + dbObject.put(agentid, data_agent); + + var data_game = []; + var _o_agent = youle_room.agents.method.find_agent(agentid); + var _o_game = _o_agent.method.find_game(gameid); + data_game[0] = agentid; + data_game[1] = gameid; + data_game[2] = _o_game.gamename; + data_game[3] = _o_game.seatcount; + data_game[4] = _o_game.makewar; + data_game[5] = _o_game.modename; + data_game[6] = _o_game.roomserver; + data_game[7] = _o_game.tcpport; + data_game[8] = _o_game.httpport; + data_game[9] = _o_game.minroomcode; + data_game[10] = _o_game.maxroomcode; + dbObject.put(agentid + "_" + gameid, data_game); + + var data_match = []; + data_match[0] = agentid; + data_match[1] = gameid; + data_match[2] = matchid; + data_match[3] = o_match.roomtype; + data_match[4] = o_match.matchname; + data_match[5] = o_match.imageurl; + data_match[6] = o_match.playercount; + data_match[7] = o_match.playercount2; + data_match[8] = o_match.bean; + data_match[9] = o_match.roomcard; + data_match[10] = o_match.topcount; + data_match[11] = o_match.circulation; + data_match[12] = o_match.interval; + data_match[13] = o_match.gradename; + data_match[14] = o_match.memo1; + data_match[15] = o_match.memo2; + data_match[16] = o_match.memo3; + data_match[17] = o_match.idx; + data_match[18] = o_match.beginTime; + data_match[19] = o_match.endTime; + data_match[20] = o_match.isstart; + data_match[21] = o_match.playerlist; + data_match[22] = o_match.ranklist; + data_match[23] = o_match.ranking; + + dbObject.put(agentid + "_" + gameid + "_" + matchid, data_match); + }catch(e){} + }, + + deletebackup_match: function(o_match){ + if (!dbObject){ + return; + } + var agentid = o_match.o_game.o_agent.agentid; + var gameid = o_match.o_game.gameid; + var matchid = o_match.id; + dbObject.delete(agentid + "_" + gameid + "_" + matchid); + }, + + // restore: function(){ + // cls_youle_room_backup.isloading = true; + + // var game1list = []; + + // for (var i = 0; i < localStorage.length; i++) { + // var key_room = localStorage.key(i); + // if (key_room.length != 72) { + // continue; + // } + // var value_room = localStorage.getItem(key_room); + // if (!value_room) { + // continue; + // } + // try { + // var data_room = JSON.parse(value_room); + // var agentid = data_room[0]; + // var gameid = data_room[1]; + // var roomcode = data_room[2]; + // var roomtype = data_room[3]; + // var needroomcard = data_room[4]; + // var asetcount = data_room[5]; + // var createtime = data_room[6]; + // var battlestate = data_room[7]; + // var makewartime = data_room[8]; + // var wasdeduct = data_room[9]; + // var roommode = data_room[10]; + // var infinite = data_room[11]; + // var playerlist = data_room[12]; + // var o_deskinfo = data_room[13]; + + + // var value_agent = localStorage.getItem(agentid); + // if (!value_agent) { + // continue; + // } + // var data_agent = JSON.parse(value_agent); + // var agentname = data_agent[1]; + // var agentserver = data_agent[2]; + // var agenttcpport = data_agent[3]; + // var agenthttpport = data_agent[4]; + // var agentfreeroom = data_agent[5]; + // var managecode = data_agent[6]; + // var o_agent1 = youle_agent.agents.method.load_agent(agentid, agentname, agentserver, agenttcpport, agenthttpport, agentfreeroom, managecode); + // var o_agent2 = youle_room.agents.method.load_agent(agentid, agentname, agentserver, agenttcpport, agenthttpport, agentfreeroom); + + + // var value_game = localStorage.getItem(agentid + "_" + gameid); + // if (!value_game) { + // continue; + // } + // var data_game = JSON.parse(value_game); + // var gamename = data_game[2]; + // var seatcount = data_game[3]; + // var makewar = data_game[4]; + // var modename = data_game[5]; + // var roomserver = data_game[6]; + // var gametcpport = data_game[7]; + // var gamehttpport = data_game[8]; + // var minroomcode = data_game[9]; + // var maxroomcode = data_game[10]; + // var o_game1 = o_agent1.method.load_game(gameid, gamename, minroomcode, maxroomcode, seatcount, makewar, modename); + // var o_game2 = o_agent2.method.load_game(gameid, gamename, seatcount, makewar, modename, roomserver, gametcpport, gamehttpport, minroomcode, maxroomcode); + // o_game1.roomcodes[roomcode - minroomcode] = 0; + // var game1_isfound = false; + // for (var j = 0; j < game1list.length; j++) { + // if (game1list[j] == o_game1) { + // game1_isfound = true; + // break; + // } + // } + // if (!game1_isfound){ + // game1list.push(o_game1); + // } + + + // var o_room2 = o_game2.method.create_room(roomcode, roomtype, needroomcard, asetcount); + // o_room2.createtime = createtime; + // o_room2.battlestate = battlestate; + // o_room2.makewartime = makewartime; + // o_room2.wasdeduct = wasdeduct; + // o_room2.roommode = roommode; + // o_room2.infinite = infinite; + + + // for (var j = 0; j < playerlist.length; j++) { + // var o_player = playerlist[j]; + // if (o_player){ + // var playerid = o_player[0]; + // var nickname = o_player[1]; + // var avatar = o_player[2]; + // var sex = o_player[3]; + // var playertype = o_player[4]; + // var seat = o_player[5]; + // var isowner = o_player[6]; + // var isbet = o_player[7]; + // var grade = o_player[8]; + // var roomcard = o_player[9]; + // var bean = o_player[10]; + // var needroomcard = o_player[11]; + // var wasdeduct = o_player[12]; + // var o_player1 = o_agent1.method.load_player(playerid, roomcard, bean, null, null, nickname, avatar, sex, playertype, null); + // o_player1.method.login_game(o_game1); + // o_player1.gameinfo[o_game1.idx].roomcode = roomcode; + // var o_player2 = cls_youle_room_player.new(playerid, nickname, avatar, sex, playertype); + // o_player2.method.enter_room(o_room2, seat, isowner); + // o_player2.gameinfo.isbet = isbet; + // o_player2.gameinfo.grade = grade; + // o_player2.gameinfo.isowner = isowner; + // if (!isowner){ + // o_player2.needroomcard = needroomcard; + // o_player2.wasdeduct = wasdeduct; + // } + // } + // } + + // youle_room.import.restore_room(o_room2, o_deskinfo); + // } catch(e) { + // continue; + // } + // } + + // for (var i = 0; i < game1list.length; i++) { + // var o_game1 = game1list[i]; + // for (var k = 0; k < o_game1.roomcodesvalidlen; k++) { + // if (!o_game1.roomcodes[k]){ + // if (k < o_game1.roomcodesvalidlen - 1) { + // o_game1.roomcodes[k] = o_game1.roomcodes[o_game1.roomcodesvalidlen - 1]; + // k = k - 1; + // } + // o_game1.roomcodes[o_game1.roomcodesvalidlen - 1] = 0; + // o_game1.method.roomcodesvalidlen_chang(-1); + // } + // } + // } + + // cls_youle_room_backup.isloading = false; + // }, + + restore: function(){ + var do_opensuccess = function(){ + dbObject.get_cursor(do_getsuccess, do_geterror); + } + var do_openerror = function(){ + cls_youle_room_backup.isloading = false; + } + var do_getsuccess = function(db_store_cursor){ + if (db_store_cursor){ + try{ + if (db_store_cursor.key.length == 32){ + var data_agent = db_store_cursor.value; + var agentid = data_agent[0]; + var agentname = data_agent[1]; + var agentserver = data_agent[2]; + var agenttcpport = data_agent[3]; + var agenthttpport = data_agent[4]; + var agentfreeroom = data_agent[5]; + var managecode = data_agent[6]; + var shortcodes = data_agent[7]; + var rebate_mode1 = data_agent[8]; + var rebate_set1 = data_agent[9]; + var rebate_mode2 = data_agent[10]; + var rebate_set2 = data_agent[11]; + var robot_nicknamelist = data_agent[12]; + var robot_avatarlist = data_agent[13]; + var initCard = data_agent[14]; + var initBean = data_agent[15]; + var o_agent1 = youle_agent.agents.method.load_agent(agentid, agentname, agentserver, agenttcpport, agenthttpport, agentfreeroom, managecode,initCard,initBean); + o_agent1.shortcodes = shortcodes; + o_agent1.rebate_mode1 = rebate_mode1; + o_agent1.rebate_set1 = rebate_set1; + o_agent1.rebate_mode2 = rebate_mode2; + o_agent1.rebate_set2 = rebate_set2; + o_agent1.robot_nicknamelist = robot_nicknamelist; + o_agent1.robot_avatarlist = robot_avatarlist; + var o_agent2 = youle_room.agents.method.load_agent(agentid, agentname, agentserver, agenttcpport, agenthttpport, agentfreeroom); + } else if (db_store_cursor.key.length == 65){ + var data_game = db_store_cursor.value; + var agentid = data_game[0]; + var gameid = data_game[1]; + var gamename = data_game[2]; + var seatcount = data_game[3]; + var makewar = data_game[4]; + var modename = data_game[5]; + var roomserver = data_game[6]; + var gametcpport = data_game[7]; + var gamehttpport = data_game[8]; + var minroomcode = data_game[9]; + var maxroomcode = data_game[10]; + var o_agent1 = youle_agent.agents.method.find_agent(agentid); + var o_agent2 = youle_room.agents.method.find_agent(agentid); + var o_game1 = o_agent1.method.load_game(gameid, gamename, minroomcode, maxroomcode, seatcount, makewar, modename); + var o_game2 = o_agent2.method.load_game(gameid, gamename, seatcount, makewar, modename, roomserver, gametcpport, gamehttpport, minroomcode, maxroomcode); + game1list.push(o_game1); + } else if (db_store_cursor.key.length == 72){ + var data_room = db_store_cursor.value; + var agentid = data_room[0]; + var gameid = data_room[1]; + var roomcode = data_room[2]; + var roomtype = data_room[3]; + var needroomcard = data_room[4]; + var asetcount = data_room[5]; + var createtime = data_room[6]; + var battlestate = data_room[7]; + var makewartime = data_room[8]; + var wasdeduct = data_room[9]; + var roommode = data_room[10]; + var infinite = data_room[11]; + var playerlist = data_room[12]; + var o_deskinfo = data_room[13]; + var advanced_type = data_room[14]; + var advanced_teabean = data_room[15]; + var beanfloor = data_room[16]; + var needprepare = data_room[17]; + var beanmult = data_room[18]; + var match_id = data_room[20]; + var match_idx = data_room[21]; + var o_agent1 = youle_agent.agents.method.find_agent(agentid); + var o_agent2 = youle_room.agents.method.find_agent(agentid); + var o_game1 = o_agent1.method.find_game(gameid); + var o_game2 = o_agent2.method.find_game(gameid); + + // o_game1.roomcodes[roomcode - o_game1.minroomcode] = 0; + o_game1.method.new_roomcode(roomcode); + + var o_room2 = o_game2.method.create_room(roomcode, roomtype, needroomcard, asetcount); + o_room2.createtime = createtime; + o_room2.battlestate = battlestate; + o_room2.makewartime = makewartime; + o_room2.wasdeduct = wasdeduct; + o_room2.roommode = roommode; + o_room2.infinite = infinite; + o_room2.advanced_type = advanced_type; + o_room2.advanced_teabean = advanced_teabean; + o_room2.beanfloor = beanfloor; + o_room2.needprepare = needprepare; + o_room2.beanmult = beanmult; + if (match_id){ + o_room2.match_id = match_id; + o_room2.match_idx = match_idx; + } + if (!o_room2.advanced_type){ + if (o_room2.freetimer24){ + min_closetime(o_room2.freetimer24); + o_room2.freetimer24 = null; + } + } + + for (var j = 0; j < playerlist.length; j++) { + var o_player = playerlist[j]; + if (o_player){ + var playerid = o_player[0]; + var nickname = o_player[1]; + var avatar = o_player[2]; + var sex = o_player[3]; + var playertype = o_player[4]; + var seat = o_player[5]; + var isowner = o_player[6]; + var isbet = o_player[7]; + var grade = o_player[8]; + var roomcard = o_player[9]; + var bean = o_player[10]; + var needroomcard = o_player[11]; + var wasdeduct = o_player[12]; + var isprepare = o_player[13]; + var advanced = o_player[14]; + var shortcode = o_player[15]; + var roomcodes = o_player[16]; + var bankpower = o_player[17]; + var bank = o_player[18]; + var bankpwd = o_player[19]; + + var o_player1 = o_agent1.method.load_player(playerid, roomcard, bean, null, null, nickname, avatar, sex, playertype, null); + o_player1.advanced = advanced; + o_player1.shortcode = shortcode; + o_player1.roomcodes = roomcodes; + o_player1.bankpower = bankpower; + o_player1.bank = bank; + o_player1.bankpwd = bankpwd; + o_player1.method.login_game(o_game1); + o_player1.gameinfo[o_game1.idx].roomcode = roomcode; + + var o_player2 = cls_youle_room_player.new(playerid, nickname, avatar, sex, playertype); + o_player2.method.enter_room(o_room2, seat, isowner); + o_player2.bean = bean; + o_player2.gameinfo.isbet = isbet; + o_player2.gameinfo.grade = grade; + o_player2.gameinfo.isowner = isowner; + o_player2.gameinfo.isprepare = isprepare; + if (!isowner){ + o_player2.needroomcard = needroomcard; + o_player2.wasdeduct = wasdeduct; + } + } + } + if (!o_room2.owner){ + var owner_playerid = data_room[19][0]; + var owner_nickname = data_room[19][1]; + var owner_avatar = data_room[19][2]; + var owner_sex = data_room[19][3]; + var owner_playertype = data_room[19][4]; + var owner_roomcard = data_room[19][5]; + var owner_bean = data_room[19][6]; + var owner_advanced = data_room[19][7]; + var owner_shortcode = data_room[19][8]; + var owner_roomcodes = data_room[19][9]; + + var o_owner1 = o_agent1.method.load_player(owner_playerid, owner_roomcard, owner_bean, null, null, owner_nickname, owner_avatar, owner_sex, owner_playertype, null); + o_owner1.advanced = owner_advanced; + o_owner1.shortcode = owner_shortcode; + o_owner1.roomcodes = owner_roomcodes; + o_owner1.method.login_game(o_game1); + + var o_owner2 = cls_youle_room_player.new(owner_playerid, owner_nickname, owner_avatar, owner_sex, owner_playertype); + o_owner2.bean = owner_bean; + o_room2.owner = o_owner2; + } + + youle_room.import.restore_room(o_room2, o_deskinfo); + } else if (db_store_cursor.key.length == 76){ + var data_match = db_store_cursor.value; + var agentid = data_match[0]; + var gameid = data_match[1]; + var matchid = data_match[2]; + var roomtype = data_match[3]; + var matchname = data_match[4]; + var imageurl = data_match[5]; + var playercount = data_match[6]; + var playercount2 = data_match[7]; + var bean = data_match[8]; + var roomcard = data_match[9]; + var topcount = data_match[10]; + var circulation = data_match[11]; + var interval = data_match[12]; + var gradename = data_match[13]; + var memo1 = data_match[14]; + var memo2 = data_match[15]; + var memo3 = data_match[16]; + var idx = data_match[17]; + var beginTime = data_match[18]; + var endTime = data_match[19]; + var isstart = data_match[20]; + var playerlist = data_match[21]; + var ranklist = data_match[22]; + var ranking = data_match[23]; + + var o_match = {}; + o_match.id = matchid; + o_match.roomtype = roomtype; + o_match.matchname = matchname; + o_match.imageurl = imageurl; + o_match.beginTime = beginTime; + o_match.endTime = endTime; + o_match.playercount = playercount; + o_match.playercount2 = playercount2; + o_match.bean = bean; + o_match.roomcard = roomcard; + o_match.topcount = topcount; + o_match.circulation = circulation; + o_match.interval = interval; + o_match.gradename = gradename; + o_match.memo1 = memo1; + o_match.memo2 = memo2; + o_match.memo3 = memo3; + var o_agent = youle_agent.agents.method.find_agent(agentid); + var o_game = o_agent1.method.find_game(gameid); + var _o_match = o_game.matchlist.method.load_match(o_match); + if (_o_match){ + _o_match.idx = idx; + _o_match.beginTime = beginTime; + _o_match.endTime = endTime; + _o_match.isstart = isstart; + _o_match.playerlist = playerlist; + _o_match.ranklist = ranklist; + if (ranking){ + _o_match.ranking = ranking; + } + } + } + }catch(e){} + + db_store_cursor.continue(); + } else { + // for (var i = 0; i < game1list.length; i++) { + // var o_game1 = game1list[i]; + // for (var k = 0; k < o_game1.roomcodesvalidlen; k++) { + // if (!o_game1.roomcodes[k]){ + // if (k < o_game1.roomcodesvalidlen - 1) { + // o_game1.roomcodes[k] = o_game1.roomcodes[o_game1.roomcodesvalidlen - 1]; + // k = k - 1; + // } + // o_game1.roomcodes[o_game1.roomcodesvalidlen - 1] = 0; + // o_game1.method.roomcodesvalidlen_chang(-1); + // } + // } + // } + cls_youle_room_backup.isloading = false; + } + } + var do_geterror = function(){ + cls_youle_room_backup.isloading = false; + } + + cls_youle_room_backup.isloading = true; + var game1list = []; + var dbParams = {}; + dbParams.db_name = "games"; + dbParams.db_version = "4"; + dbParams.store_name = "desk"; + dbParams.success_callback = do_opensuccess; + dbParams.error_callback = do_openerror; + dbObject.open(dbParams); + } +} \ No newline at end of file diff --git a/server/youle/server_room/class.export.js b/server/youle/server_room/class.export.js new file mode 100644 index 0000000..f91ae5b --- /dev/null +++ b/server/youle/server_room/class.export.js @@ -0,0 +1,2085 @@ +/////////////////////////////////////////////////// +/////// cls_youle_room_export: 输出接口 /////// +/////////////////////////////////////////////////// +var cls_youle_room_export = cls_youle_room_export || { + + new: function() { + + var exp = {}; + + //检查玩家的位置是否正确,检查tcp连接是否正确,返回房间对象 + exp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid){ + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return null; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return null; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return null; + } + //查找玩家 + var o_player = o_room.seatlist[seat]; + if (!o_player){ + return null; + } + //检查座位 + if (o_player.playerid != playerid) { + return null; + }; + //检查tcp连接 + if (o_player.conmode != conmode || o_player.fromid != fromid) { + return null; + } + return o_room; + } + + //扣除房主的房卡 + exp.deduct_roomcard = function(o_room){ + if (!o_room){ + return; + } + //系统房间写死不要房卡 + if (o_room.infinite){ + return; + } + if (o_room.roommode){ + //sss + var xxroomdeduct = youle_room.app.youle_platform.config.method.get_paravalue("xxroomdeduct", o_room.o_game.o_agent.agentid); + if (!xxroomdeduct){ + return; + } + } + + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + if (deductmode == 2){ + //大赢家扣卡 + return; + } + // var extcard = 0; + // if(o_room.videoConfig){ + // var o_agent1 = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + // var vConfig = o_agent1.method.alyVideoConfig(o_room.videoConfig); + // if(vConfig[0] == 1){ + // extcard = vConfig[1]; + // } + // } + + for (var i = 0; i < o_room.seatlist.length; i++) { + var o_player = o_room.seatlist[i]; + if (!o_player){ + continue; + } + if (o_player.gameinfo.isowner == 1){ //房主 + if (o_room.wasdeduct){ + continue; + } + if (o_room.needroomcard == 0){ + continue; + } + if (o_player.gameinfo.isbet != 1){ + continue; + } + //vip不扣房卡 + if (o_player.playertype == 1){ + continue; + } + + o_room.wasdeduct = true; + //通知代理商服务器修改房卡 + var o_game = o_room.o_game; + var o_agent = o_game.o_agent; + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "deduct_roomcard"; + msg.data = {}; + msg.data.agentid = o_agent.agentid; + msg.data.playerid = o_player.playerid; + msg.data.needroomcard = o_room.needroomcard; + msg.data.gameid = o_room.o_game.gameid; + msg.data.roomcode = o_room.roomcode; + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + msg.data.issameserver = 1; + youle_room.app.youle_agent[msg.rpc](msg); + } else { + //房间服务器与代理商服务器不是同一地址 + msg.data.issameserver = 0; + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){}; + cfg.error = function(str){ + o_room.wasdeduct = false; + }; + min_http(cfg); + } + } else if (o_player.gameinfo.isowner == 0){ //非房主 + if (o_player.wasdeduct){ + continue; + } + if (o_player.needroomcard == 0){ + continue; + } + if (o_player.gameinfo.isbet != 1){ + continue; + } + //vip不扣房卡 + if (o_player.playertype == 1){ + continue; + } + + o_player.wasdeduct = true; + //通知代理商服务器修改房卡 + var o_game = o_player.gameinfo.o_game; + var o_agent = o_game.o_agent; + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "deduct_roomcard"; + msg.data = {}; + msg.data.agentid = o_agent.agentid; + msg.data.playerid = o_player.playerid; + msg.data.needroomcard = o_player.needroomcard; + msg.data.gameid = o_room.o_game.gameid; + msg.data.roomcode = o_room.roomcode; + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + msg.data.issameserver = 1; + youle_room.app.youle_agent[msg.rpc](msg); + } else { + //房间服务器与代理商服务器不是同一地址 + msg.data.issameserver = 0; + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){}; + cfg.error = function(str){ + o_player.wasdeduct = false; + }; + min_http(cfg); + } + } + } + + //群主开房,房主没在房间内也要扣房卡 + var o_player = o_room.owner; + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + if (o_player && !o_room.wasdeduct && o_room.needroomcard != 0 && o_player.playertype != 1 && deductmode == 0){ + o_room.wasdeduct = true; + //通知代理商服务器修改房卡 + // var extcard = 0; + // if(o_room.videoConfig){ + // var o_agent1 = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + // var vConfig = o_agent1.method.alyVideoConfig(o_room.videoConfig); + // if(vConfig[0] == 1){ + // extcard = vConfig[1]; + // } + // } + var o_game = o_room.o_game; + var o_agent = o_game.o_agent; + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "deduct_roomcard"; + msg.data = {}; + msg.data.agentid = o_agent.agentid; + msg.data.playerid = o_player.playerid; + msg.data.needroomcard = o_room.needroomcard; + msg.data.gameid = o_room.o_game.gameid; + msg.data.roomcode = o_room.roomcode; + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + msg.data.issameserver = 1; + youle_room.app.youle_agent[msg.rpc](msg); + } else { + //房间服务器与代理商服务器不是同一地址 + msg.data.issameserver = 0; + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg); + cfg.success = function(str){}; + cfg.error = function(str){ + o_room.wasdeduct = false; + }; + min_http(cfg); + } + } + } + + //小局结算系统抽成,ary_grade为与座位一一对应的本小局得分,格式如[100,null,-100,null]。函数返回抽成后的得分 + exp.save_grade_aset = function(o_room, ary_grade, asetnumber, ary_pond){ + //累计池子输赢 + if (ary_pond){ + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var __o_agentsvr_game = __o_agentsvr_agent.method.find_game(o_room.o_game.gameid); + for (var i = 0; i < ary_pond.length; i++) { + if (ary_pond[i][1]){ + var _found = false; + for (var j = 0; j < __o_agentsvr_game.pondlist.length; j++) { + if (ary_pond[i][0] == __o_agentsvr_game.pondlist[j][0]) { + __o_agentsvr_game.pondlist[j][1] = __o_agentsvr_game.pondlist[j][1] + ary_pond[i][1]; + _found = true; + break; + } + }; + if (!_found){ + var info = [ary_pond[i][0], ary_pond[i][1]]; + __o_agentsvr_game.pondlist.push(info); + } + var sql = "call cp_game_update_pond(?,?,?,?)"; + youle_room.mysql.execsql("execsql", sql, [o_room.o_game.o_agent.agentid, o_room.o_game.gameid, ary_pond[i][0], ary_pond[i][1]]); + } + }; + } + + //只有星星场才有小局结算和小局抽成 + var isFree = 0; + // if(o_room.isFree){ + // isFree = 1; + // } + if(o_room.sysroom && !isFree){ + var sysRoomConfig = youle_room.app.youle_platform.config.method.get_paravalue("system_room", o_room.o_game.o_agent.agentid, o_room.o_game.gameid); + if(sysRoomConfig){ + for(var i=0;i 0){ + // var rebateConfig = youle_room.app.youle_platform.config.method.get_paravalue("rebateConfig", o_room.o_game.o_agent.agentid); + // var rCharm = 0; + // var maxgrade1 = 0; + // var maxseat1 = []; + // var rcTemp = false; + //if(gameid == "Btke0urRy0cvPd5CIvD5yfhYhKhdSdex"){ + + //if(rebateConfig.rebateCharm && o_room.rebateType){ + + // for (var i = 0; i < ary_grade.length; i++){ + // if (ary_grade[i]){ + // ary_grade[i] = Number(ary_grade[i]); + // if (ary_grade[i] > maxgrade1){ + // maxgrade1 = ary_grade[i]; + // maxseat1 = [i]; + // } else if (ary_grade[i] == maxgrade1){ + // maxseat1.push(i); + // } + // } + // } + // console.log("maxseat1",JSON.stringify(maxseat1)); + // console.log("maxgrade1",maxgrade1); + //rcTemp = true; + // if(maxseat1.length>0){ + // rCharm = parseInt(o_room.rebateNumber/maxseat1.length); + // }else{ + // rCharm = parseInt(o_room.rebateNumber); + // } + //} + //} + // if(rebateConfig){ + // if(rebateConfig.rebateCharm && o_room.rebateType){ + // rcTemp = true; + // } + // //抽金币 + // if(!o_room.rebateType){ + // reTemp = true; + // } + // } + + var _seatlist = []; + // console.log("ary_grade",ary_grade); + // console.log("333",JSON.stringify(o_agentsvr_owner.whitelist)); + // console.log("rCharm",rCharm); + for (var i = 0; i < ary_grade.length; i++) { + if (ary_grade[i] != null){ + if (o_room.seatlist[i]){ + var _player = {}; + _player.seat = i; + _player.playerid = o_room.seatlist[i].playerid; + _player.change = ary_grade[i]; + // for(var k=0;k -1){ + if(config_vipConfig[vipIndex][1]){ + writeDB = false; + } + } + } + } + writeDB = true; + if(writeDB){ + var sql = "update player set play_whitelist = ? where play_agentid = ? and play_playerid = ?"; + youle_room.mysql.execsql("execsql", sql, [youle_agent.compressWhiteList(o_agentsvr_owner.whitelist), o_room.o_game.o_agent.agentid, o_room.owner.playerid]); + } + } + } + }catch(e){ + console.log("结算短号魅力值发生错误"); + console.log(e); + } + var ary = xRebate.gameRebate2(o_room,ary_grade); + // console.log("ary",ary); + return ary; + } + //小局抽成设置 + var aset_rebate = youle_room.app.youle_platform.config.method.get_paravalue("aset_rebate", o_room.o_game.o_agent.agentid); + var setting; + if (o_room.infinite){ + //无限局 + if (aset_rebate && aset_rebate.infinite){ + setting = aset_rebate.infinite; //无限局设置 + } else { + setting = [0]; + } + } else { + //有限局 + if (aset_rebate && aset_rebate.finitude){ + setting = aset_rebate.finitude; //有限局设置 + } else { + return ary_grade; + } + } + + //是否有真实玩家 + var haveplayer = false; + for (var i = 0; i < ary_grade.length; i++) { + if (ary_grade[i] != null){ + if (o_room.seatlist[i]){ + if (!o_room.seatlist[i].isrobot){ + haveplayer = true; + break; + } + } + } + } + + var do_save_coin_rebate = function(seat, isrobot){ + var agentid = o_room.o_game.o_agent.agentid; + var gameid = o_room.o_game.gameid; + var roomcode = o_room.roomcode; + var roommode = 0; //玩家房间 + o_room.asetRebate = true; + if (o_room.sysroom) { + roommode = 1; //系统房间 + } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var playerid = o_room.seatlist[seat].playerid; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + var asetcount = o_room.asetcount; + if (!isrobot){ + isrobot = 0; + } else { + isrobot = 1; + } + var grade = ary_grade[seat]; + var push = 0; + switch (parseInt(setting[1])){ + case 1: //按得分的百分比抽成 + push = parseInt(ary_grade[seat] * parseInt(setting[2]) / 100); + break; + case 2: //按倍数的百分比抽成 + push = parseInt(o_room.beanmult * parseInt(setting[2]) / 100); + break; + } + var invitecode = null; + var inviteid = null; + var marketid = null; + if (!isrobot){ + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(playerid); + invitecode = __o_agentsvr_player.invitecode; + inviteid = __o_agentsvr_player.inviteid; + marketid = __o_agentsvr_player.marketid; + } + + if (haveplayer){ + //有真实玩家才保存数据 + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 1, asetcount, asetnumber, playerid, isrobot, grade, parseInt(setting[0]), 0, "", parseInt(setting[1]), parseInt(setting[2]), push, datatime, invitecode, inviteid, marketid]); + } + + do_update_bean(playerid, grade, push, isrobot, false); + + return grade - push; + } + + var do_update_bean = function(playerid, grade, push, isrobot, save_gold_account){ + //游戏输赢设置 玩家输赢存在玩家winLose属性中 + var updateGameWinLose = function(gameid,type,val,oPlayer){ + var key; + val = Number(val); + switch(type){ + case 1: + key = "game_winlose"; + break; + case 2: + key = "game_winlose_xx"; + break; + case 3: + key = "game_winlose_x2"; + break; + } + if(oPlayer.winLose){//判断是否有次属性 + if(oPlayer.winLose[gameid]){ + if(oPlayer.winLose[gameid][key]){ + oPlayer.winLose[gameid][key] += val; + }else{ + oPlayer.winLose[gameid][key] = val; + } + }else{ + oPlayer.winLose[gameid] = {}; + oPlayer.winLose[gameid][key] = val; + } + }else{ + oPlayer.winLose = {}; + oPlayer.winLose[gameid] = {}; + oPlayer.winLose[gameid][key] = val; + } + } + //读取输赢 + // var getGameWinLose = function(gameid,type,oPlayer){ + // var key; + // switch(type){ + // case 1: + // key = "game_winlose"; + // break; + // case 2: + // key = "game_winlose_xx"; + // break; + // case 3: + // key = "game_winlose_x2"; + // break; + // } + // if(oPlayer.winLose){//判断是否有次属性 + // if(oPlayer.winLose[gameid]){ + // if(oPlayer.winLose[gameid][key]){ + // return oPlayer.winLose[gameid][key]; + // }else{ + + // } + // }else{ + + // } + // }else{ + + // } + // return null; + // } + if (!isrobot){ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = o_room.o_game.o_agent.agentid; + msg.data.playerid = playerid; + msg.data.change = grade; + msg.data.pushbean = -1 * push; + msg.data.gameid = o_room.o_game.gameid; + msg.data.showrebate = 2; + youle_room.app.youle_agent[msg.rpc](msg); + + if (o_room.roommode){ + var sql = ' update agent_game_player ' + + ' set agpl_playset_xx = agpl_playset_xx + 1, ' + + ' agpl_winlose_xx = agpl_winlose_xx + (? div ?), ' + + ' agpl_winlose_x2 = agpl_winlose_x2 + ? ' + + ' where agpl_agentid = ? ' + + ' and agpl_gameid = ? ' + + ' and agpl_playerid = ?'; + youle_room.mysql.execsql("execsql", sql, [grade, o_room.beanmult, grade, o_room.o_game.o_agent.agentid, o_room.o_game.gameid, playerid]); + + + + var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var o_agentsvr_player = o_agentsvr_agent.method.find_player(playerid); + if (o_agentsvr_player){ + if (!o_agentsvr_player.nn_winlose_xx){ + if (o_room.beanmult){ + o_agentsvr_player.nn_winlose_xx = parseInt(grade / o_room.beanmult); + } + } else { + if (o_room.beanmult){ + o_agentsvr_player.nn_winlose_xx = o_agentsvr_player.nn_winlose_xx + parseInt(grade / o_room.beanmult); + } + } + + if (!o_agentsvr_player.nn_winlose_x2){ + o_agentsvr_player.nn_winlose_x2 = grade; + } else { + o_agentsvr_player.nn_winlose_x2 = o_agentsvr_player.nn_winlose_x2 + grade; + } + if (o_room.beanmult){ + updateGameWinLose(o_room.o_game.gameid,2,parseInt(grade / o_room.beanmult),o_agentsvr_player); + } + updateGameWinLose(o_room.o_game.gameid,3,grade,o_agentsvr_player); + } + } + } else { + //记录机器人的累积输赢 + if (o_room.roommode){ + var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var o_agentsvr_game = o_agentsvr_agent.method.find_game(o_room.o_game.gameid); + if (!o_agentsvr_game.robot_xx){ + if (o_room.beanmult){ + o_agentsvr_game.robot_xx = parseInt(grade / o_room.beanmult); + } + } else { + if (o_room.beanmult){ + o_agentsvr_game.robot_xx = o_agentsvr_game.robot_xx + parseInt(grade / o_room.beanmult); + } + } + if (!o_agentsvr_game.robot_x2){ + o_agentsvr_game.robot_x2 = grade; + } else { + o_agentsvr_game.robot_x2 = o_agentsvr_game.robot_x2 + grade; + } + + if (haveplayer){ + //有真实玩家才保存数据 + var sql = "call cp_game_grade_robot(?,?,?,?);"; + youle_room.mysql.execsql("execsql", sql, [o_room.o_game.o_agent.agentid, o_room.o_game.gameid, grade, o_room.beanmult]); + } + } + } + + if (save_gold_account){ + var agentid = o_room.o_game.o_agent.agentid; + var gameid = o_room.o_game.gameid; + var roomcode = o_room.roomcode; + var roommode = 0; //玩家房间 + if (o_room.sysroom) { + roommode = 1; //系统房间 + } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + var asetcount = o_room.asetcount; + if (!isrobot){ + isrobot = 0; + } else { + isrobot = 1; + } + var invitecode = null; + var inviteid = null; + var marketid = null; + if (!isrobot){ + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(playerid); + invitecode = __o_agentsvr_player.invitecode; + inviteid = __o_agentsvr_player.inviteid; + marketid = __o_agentsvr_player.marketid; + } + if (haveplayer){ + //有真实玩家才保存数据 + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(playerid); + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 1, asetcount, asetnumber, playerid, isrobot, grade, parseInt(setting[0]), 0, "", parseInt(setting[1]), parseInt(setting[2]), 0, datatime, invitecode, inviteid, marketid]); + }else{ + //读取是否纯机器人保存金币变动 + var o_cfg = youle_agent.app.youle_platform.config; + var reportRobotGold = o_cfg.method.get_paravalue("reportRobotGold", agentid, gameid); + if(reportRobotGold){ + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 1, asetcount, asetnumber, playerid, isrobot, grade, parseInt(setting[0]), 0, "", parseInt(setting[1]), parseInt(setting[2]), 0, datatime, invitecode, inviteid, marketid]); + } + } + } + } + + //房间id = 时间戳 + 4位随机码 + 房号 + var roomid = new Date().getTime() + min_randomChar(4) + o_room.roomcode; + var datatime = min_now(); + + switch (parseInt(setting[0])){ + case 0: //无限局没有小局系统抽成 + for (var i = 0; i < ary_grade.length; i++) { + if (ary_grade[i] != null){ + do_update_bean(o_room.seatlist[i].playerid, ary_grade[i], 0, o_room.seatlist[i].isrobot, true); + } + } + return ary_grade; + case 1: //对大赢家抽成 + var maxwingrade = -1; + for (var i = 0; i < ary_grade.length; i++) { + if (ary_grade[i] != null){ + if (ary_grade[i] > maxwingrade){ + maxwingrade = ary_grade[i]; + } + } + } + + for (var i = 0; i < ary_grade.length; i++) { + if (ary_grade[i] != null){ + if (ary_grade[i] == maxwingrade){ + ary_grade[i] = do_save_coin_rebate(i, o_room.seatlist[i].isrobot); + } else { + do_update_bean(o_room.seatlist[i].playerid, ary_grade[i], 0, o_room.seatlist[i].isrobot, true); + } + } + } + + return ary_grade; + case 2: //对所有赢的人抽成 + for (var i = 0; i < ary_grade.length; i++) { + if (ary_grade[i] != null){ + if (ary_grade[i] > 0){ + ary_grade[i] = do_save_coin_rebate(i, o_room.seatlist[i].isrobot); + } else { + do_update_bean(o_room.seatlist[i].playerid, ary_grade[i], 0, o_room.seatlist[i].isrobot, true); + } + } + } + return ary_grade; + case 3: //对所有人抽成 + for (var i = 0; i < ary_grade.length; i++) { + if (ary_grade[i] != null){ + ary_grade[i] = do_save_coin_rebate(i, o_room.seatlist[i].isrobot); + } + } + return ary_grade; + default: + return ary_grade; + } + + } + + //一局游戏结束保存战绩并释放房间(不用通知前端游戏,因为游戏服务器已经通知了前端) + exp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag){ + //游戏输赢设置 玩家输赢存在玩家winLose属性中 + //console.log(o_room, o_gameinfo1, o_gameinfo2, freeroomflag); + var updateGameWinLose = function(gameid,type,val,oPlayer){ + var key; + val = Number(val); + switch(type){ + case 1: + key = "game_winlose"; + break; + case 2: + key = "game_winlose_xx"; + break; + case 3: + key = "game_winlose_x2"; + break; + } + if(oPlayer.winLose){//判断是否有次属性 + if(oPlayer.winLose[gameid]){ + if(oPlayer.winLose[gameid][key]){ + oPlayer.winLose[gameid][key] += val; + }else{ + oPlayer.winLose[gameid][key] = val; + } + }else{ + oPlayer.winLose[gameid] = {}; + oPlayer.winLose[gameid][key] = val; + } + }else{ + oPlayer.winLose = {}; + oPlayer.winLose[gameid] = {}; + oPlayer.winLose[gameid][key] = val; + } + } + //读取输赢 + // var getGameWinLose = function(gameid,type,oPlayer){ + // var key; + // switch(type){ + // case 1: + // key = "game_winlose"; + // break; + // case 2: + // key = "game_winlose_xx"; + // break; + // case 3: + // key = "game_winlose_x2"; + // break; + // } + // if(oPlayer.winLose){//判断是否有次属性 + // if(oPlayer.winLose[gameid]){ + // if(oPlayer.winLose[gameid][key]){ + // return oPlayer.winLose[gameid][key]; + // }else{ + + // } + // }else{ + + // } + // }else{ + + // } + // return null; + // } + //需要子游戏设置玩家的gameinfo.isbet和gameinfo.grade属性 + if (o_gameinfo1){ + try + { + cls_youle_room_backup.deletebackup_room(o_room); + + var agentid = o_room.o_game.o_agent.agentid; + var gameid = o_room.o_game.gameid; + var roomcode = o_room.roomcode; + var createtime = o_room.createtime; + var makewartime = o_room.makewartime; + var overtime = min_now(); + + //星星场房间是否和普通房间一样扣房卡 + var xxroomdeduct = youle_room.app.youle_platform.config.method.get_paravalue("xxroomdeduct", o_room.o_game.o_agent.agentid); + //小局抽成设置 + // var aset_rebate = youle_room.app.youle_platform.config.method.get_paravalue("aset_rebate", o_room.o_game.o_agent.agentid); + //抽成到的玩家id + // var pushtoplayerid_setting = youle_room.app.youle_platform.config.method.get_paravalue("pushtoplayerid", o_room.o_game.o_agent.agentid); + //是否抽成到玩家id对应的代理账号上 + // var pushto_setting = youle_room.app.youle_platform.config.method.get_paravalue("pushto", o_room.o_game.o_agent.agentid); + + //大赢家扣卡 + var needdeduct = false; + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + if (deductmode == 2){ + needdeduct = true; + } + if (o_room.roommode){ + //星星场不扣房卡 + if (!xxroomdeduct){ + needdeduct = false; + } + } + if (needdeduct){ + var deductcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + if (deductcard > 0){ + var maxgrade = 0; + var maxseat = []; + for (var i = 0; i < o_room.seatlist.length; i++){ + if (o_room.seatlist[i] && o_room.seatlist[i].gameinfo.isbet){ + if (o_room.seatlist[i].gameinfo.grade > maxgrade){ + maxgrade = parseInt(o_room.seatlist[i].gameinfo.grade); + maxseat = [i]; + } else if (o_room.seatlist[i].gameinfo.grade == maxgrade){ + maxseat.push(i); + } + } + } + // if(o_room.videoConfig){ + // var o_agent1 = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + // var vConfig = o_agent1.method.alyVideoConfig(o_room.videoConfig); + // if(vConfig[0] == 1){ + // deductcard += vConfig[1]; + // } + // } + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "deduct_roomcard"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.needroomcard = deductcard; + msg.data.gameid = gameid; + msg.data.roomcode = roomcode; + msg.data.issameserver = 1; + for (var i = 0; i < maxseat.length; i++){ + msg.data.playerid = o_room.seatlist[maxseat[i]].playerid; + youle_room.app.youle_agent[msg.rpc](msg); + } + } + } + /* + var rebatefrom = 0; + var rebateto = 0; + var toplayerid = ""; + var rebatemode; + var setvalue; + + //如果设置了星星场限进,计算大赢家的房主抽成数量 + if (o_room.beanfloor){ + //大赢家抽成星星数 + var maxwiner_setting = 0; + if (!o_room.advanced_type){ + //普通星星场房间 + maxwiner_setting = youle_room.import.owner_beanpush(o_room.o_game, o_room.roomtype); + if (maxwiner_setting){ + maxwiner_setting = parseInt(maxwiner_setting); + rebatefrom = 1; + rebateto = 1; + if (pushtoplayerid_setting){ + if (pushtoplayerid_setting[0]){ + rebateto = 2; + toplayerid = pushtoplayerid_setting[0]; + + if (pushto_setting == 1){ + rebateto = 3; + } else if (pushto_setting == 2){ + rebateto = 4; + toplayerid = o_room.owner.playerid; + } + } + } + rebatemode = 3; + setvalue = maxwiner_setting; + } + } else { + //短号星星场房间 + if (o_room.advanced_teabean){ + maxwiner_setting = o_room.advanced_teabean; + rebatefrom = 1; + rebateto = 1; + if (pushtoplayerid_setting){ + if (pushtoplayerid_setting[1]){ + rebateto = 2; + toplayerid = pushtoplayerid_setting[1]; + if (pushto_setting == 1){ + rebateto = 3; + } else if (pushto_setting == 2){ + rebateto = 4; + toplayerid = o_room.owner.playerid; + } + } + } + rebatemode = 3; + setvalue = maxwiner_setting; + } + } + if (maxwiner_setting){ + //房主得分 + var ownergarde = 0; + //房主抽成星星数量 + var ownerpush = 0; + //大赢家得分 + var maxgrade = 0; + //大赢家数量 + var maxcount = 0; + for (var i = 0; i < o_room.seatlist.length; i++){ + if (o_room.seatlist[i] && o_room.seatlist[i].gameinfo.isbet){ + if (o_room.seatlist[i].gameinfo.grade > maxgrade){ + maxgrade = parseInt(o_room.seatlist[i].gameinfo.grade); + maxcount = 1; + } else if (o_room.seatlist[i].gameinfo.grade == maxgrade){ + maxcount = maxcount + 1; + } + } + } + //大赢家实际抽成星星数 + var maxwiner_pushbean = parseInt(maxwiner_setting / maxcount); + } + } + */ + //比赛数据 + var match_data = {}; + match_data.agent_id = agentid; + match_data.game_id = gameid; + match_data.game_infos = []; + + var aryplayerid = ""; + var arygrade = ""; + var aryroomcard = ""; + var aryisrobot = ""; + var o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var arywinner = ""; + var winscore = -100000; + var aryscore = ""; + //大赢家标志 + var getResult1 = youle_room.import.getWinnerByGameInfo1(o_room,o_gameinfo1); + + var getResult2 = youle_room.import.getScoreByGameInfo1(o_room,o_gameinfo1); + if(getResult1){ + arywinner = getResult1.join(","); + }else{ + if(o_gameinfo1.playerlist){ + + for(var k=0;k winscore){ + winscore = Number(o_gameinfo1.playerlist[k][1]); + } + } + } + } + } + if(getResult2){ + aryscore = getResult2.join(","); + } + + //房间id = 时间戳 + 4位随机码 + 房号 + var roomid = new Date().getTime() + min_randomChar(4) + o_room.roomcode; + var datatime = min_now(); + + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + var o_player = o_room.seatlist[i]; + if (o_gameinfo1.playerlist[i]) { + var o_agentsvr_player = o_agentsvr_agent.method.find_player(o_player.playerid); + if (aryplayerid){ + aryplayerid = aryplayerid + "," + o_player.playerid; + + if (o_player.isrobot){ + aryisrobot = aryisrobot + ",1"; + } else { + aryisrobot = aryisrobot + ",0"; + } + if(!getResult1){ + try{ + if(o_gameinfo1.playerlist){ + if(Number(o_gameinfo1.playerlist[i][1]) == winscore && + Number(o_gameinfo1.playerlist[i][3]) == Number(o_player.playerid)){ + arywinner = arywinner + "," + 1; + + }else{ + arywinner = arywinner + "," + 0; + } + }else{ + arywinner = arywinner + "," + 0; + } + }catch(err){ + console.log("战绩结算大赢家出错!"); + console.log(o_gameinfo1); + console.log(err); + } + } + if(!getResult2){ + try{ + if(o_gameinfo1.playerlist){ + if(Number(o_gameinfo1.playerlist[i][3]) == Number(o_player.playerid)){ + aryscore = aryscore + "," + o_gameinfo1.playerlist[i][1]; + }else{ + aryscore = aryscore + "," + 0; + } + }else{ + aryscore = aryscore + "," + 0; + } + }catch(err){ + console.log("战绩结算分数出错!"); + console.log(o_gameinfo1); + console.log(err); + } + } + if (gameid == "8x4l0rGjf026f60c48h0mbUAhK5vV16f"){ + var recordflag = true; + // if (o_room.game.model == 4 || o_room.game.model == 5){ + // recordflag = false; + // } + try{ + if (NN_control){ + if (NN_control.kanpaiqz){ + recordflag = true; + } + } + }catch(e){} + if (o_player.isrobot){ + recordflag = true; + } + if (recordflag){ + arygrade = arygrade + "," + o_player.gameinfo.grade; + } else { + arygrade = arygrade + ",0"; + } + } else { + arygrade = arygrade + "," + o_player.gameinfo.grade; + } + + if (o_agentsvr_player) { + aryroomcard = aryroomcard + "," + o_agentsvr_player.roomcard; + } else { + aryroomcard = aryroomcard + ",-99999"; + } + } else { + aryplayerid = o_player.playerid; + if (o_player.isrobot){ + aryisrobot = "1"; + } else { + aryisrobot = "0"; + } + if(!getResult1){ + try{ + if(o_gameinfo1.playerlist){ + if(Number(o_gameinfo1.playerlist[i][1]) == winscore && + Number(o_gameinfo1.playerlist[i][3]) == Number(o_player.playerid)){ + arywinner = "1"; + + }else{ + arywinner = "0"; + } + }else{ + arywinner = "0"; + } + }catch(err){ + console.log("战绩结算大赢家出错!"); + console.log(o_gameinfo1); + console.log(err); + } + } + if(!getResult2){ + try{ + if(o_gameinfo1.playerlist){ + if(Number(o_gameinfo1.playerlist[i][3]) == Number(o_player.playerid)){ + aryscore = o_gameinfo1.playerlist[i][1]; + }else{ + aryscore = "0" ; + } + }else{ + aryscore = "0" ; + } + }catch(err){ + console.log("战绩结算分数出错!"); + console.log(o_gameinfo1); + console.log(err); + } + } + if (gameid == "8x4l0rGjf026f60c48h0mbUAhK5vV16f"){ + var recordflag = true; + // if (o_room.game.model == 4 || o_room.game.model == 5){ + // recordflag = false; + // } + try{ + if (NN_control){ + if (NN_control.kanpaiqz){ + recordflag = true; + } + } + }catch(e){} + if (o_player.isrobot){ + recordflag = true; + } + if (recordflag){ + arygrade = o_player.gameinfo.grade; + } else { + arygrade = 0; + } + } else { + arygrade = o_player.gameinfo.grade; + } + + if (o_agentsvr_player){ + aryroomcard = o_agentsvr_player.roomcard; + } else { + aryroomcard = "-99999"; + } + } + + //统计牛牛的输赢总分 + if (gameid == "8x4l0rGjf026f60c48h0mbUAhK5vV16f"){ + var recordflag = true; + try{ + if (NN_control){ + if (NN_control.kanpaiqz){ + recordflag = true; + } + } + }catch(e){} + if (recordflag){ + if (o_room.roommode){ + if (!o_agentsvr_player.nn_winlose_xx){ + o_agentsvr_player.nn_winlose_xx = 0; + } + if (!o_room.beanmult){ + o_agentsvr_player.nn_winlose_xx = o_agentsvr_player.nn_winlose_xx + o_player.gameinfo.grade; + } else { + o_agentsvr_player.nn_winlose_xx = o_agentsvr_player.nn_winlose_xx + parseInt(o_player.gameinfo.grade / o_room.beanmult); + } + if (!o_agentsvr_player.nn_winlose_x2){ + o_agentsvr_player.nn_winlose_x2 = 0; + } + o_agentsvr_player.nn_winlose_x2 = o_agentsvr_player.nn_winlose_x2 + o_player.gameinfo.grade; + + } else { + if (!o_agentsvr_player.nn_winlose){ + o_agentsvr_player.nn_winlose = 0; + } + o_agentsvr_player.nn_winlose = o_agentsvr_player.nn_winlose + o_player.gameinfo.grade; + } + } + } + if (gameid){ + var recordflag = true; + try{ + if (NN_control){ + if (NN_control.kanpaiqz){ + recordflag = true; + } + } + }catch(e){} + if (recordflag){ + if (o_room.roommode){ + if (!o_agentsvr_player.nn_winlose_xx){ + o_agentsvr_player.nn_winlose_xx = 0; + } + if (!o_room.beanmult){ + o_agentsvr_player.nn_winlose_xx = o_agentsvr_player.nn_winlose_xx + o_player.gameinfo.grade; + } else { + o_agentsvr_player.nn_winlose_xx = o_agentsvr_player.nn_winlose_xx + parseInt(o_player.gameinfo.grade / o_room.beanmult); + } + if (!o_agentsvr_player.nn_winlose_x2){ + o_agentsvr_player.nn_winlose_x2 = 0; + } + o_agentsvr_player.nn_winlose_x2 = o_agentsvr_player.nn_winlose_x2 + o_player.gameinfo.grade; + if (!o_room.beanmult){ + updateGameWinLose(gameid,2,o_player.gameinfo.grade,o_agentsvr_player); + } else { + updateGameWinLose(gameid,2,parseInt(o_player.gameinfo.grade / o_room.beanmult),o_agentsvr_player); + } + updateGameWinLose(gameid,3,o_player.gameinfo.grade,o_agentsvr_player); + } else { + if (!o_agentsvr_player.nn_winlose){ + o_agentsvr_player.nn_winlose = 0; + } + o_agentsvr_player.nn_winlose = o_agentsvr_player.nn_winlose + o_player.gameinfo.grade; + updateGameWinLose(gameid,1,o_player.gameinfo.grade,o_agentsvr_player); + } + } + } + /* + //如果是豆豆场则通知代理服务器修改豆豆数量 + if (o_room.roommode){ + //当前玩家被抽成的星星数 + var _pushbean = 0; + //是否不提示茶水费抽成 + var showrebate = 0; + if (maxwiner_setting){ //有房主抽成 + if (o_player.playerid == o_room.owner.playerid) { + ownergarde = o_player.gameinfo.grade; + continue; + } else { + if (o_player.gameinfo.grade == maxgrade) { + _pushbean = maxwiner_pushbean; + ownerpush = ownerpush + maxwiner_pushbean; + } + } + } else { + if (o_room.advanced_type){ + switch (o_agentsvr_agent.rebate_mode2){ + case 1: //按比例抽成 + rebatefrom = 2; + rebateto = 0; + toplayerid = ""; + rebatemode = 1; + setvalue = o_agentsvr_agent.rebate_set2; + break; + case 2: //按固定值抽 + rebatefrom = 2; + rebateto = 0; + toplayerid = ""; + rebatemode = 3; + setvalue = o_agentsvr_agent.rebate_set2; + break; + default://不抽 + rebatefrom = 0; + rebateto = 0; + toplayerid = ""; + rebatemode = 0; + setvalue = 0; + break; + } + } else { + switch (o_agentsvr_agent.rebate_mode1){ + case 1: //按比例抽成 + rebatefrom = 2; + rebateto = 0; + toplayerid = ""; + rebatemode = 1; + setvalue = o_agentsvr_agent.rebate_set1; + break; + case 2: //按固定值抽 + rebatefrom = 2; + rebateto = 0; + toplayerid = ""; + rebatemode = 3; + setvalue = o_agentsvr_agent.rebate_set1; + break; + default://不抽 + rebatefrom = 0; + rebateto = 0; + toplayerid = ""; + rebatemode = 0; + setvalue = 0; + break; + } + } + + //所有赢家系统抽成处理 + if (o_player.gameinfo.grade > 0) { + if (!o_room.advanced_type){ //普通星星场房间 + switch (o_agentsvr_agent.rebate_mode1){ + case 1: //按比例抽成 + _pushbean = parseInt(o_player.gameinfo.grade * o_agentsvr_agent.rebate_set1 / 100); + break; + case 2: //按固定值抽 + _pushbean = o_agentsvr_agent.rebate_set1; + break; + default: + break; + } + } else { //短号星星场房间 + switch (o_agentsvr_agent.rebate_mode2){ + case 1: //按比例抽成 + _pushbean = parseInt(o_player.gameinfo.grade * o_agentsvr_agent.rebate_set2 / 100); + break; + case 2: //按固定值抽 + _pushbean = o_agentsvr_agent.rebate_set2; + break; + default: + break; + } + } + if (_pushbean){ + showrebate = 1; + } + } + } + + if (!aset_rebate || !aset_rebate.finitude) { + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_player.playerid; + msg.data.change = o_player.gameinfo.grade; + msg.data.pushbean = -1 * _pushbean; + msg.data.gameid = gameid; + msg.data.showrebate = showrebate; + youle_room.app.youle_agent[msg.rpc](msg); + + var roommode = 0; //玩家房间 + if (o_room.sysroom) { + roommode = 1; //系统房间 + } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var playerid = o_player.playerid; + var grade = o_player.gameinfo.grade; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + + var asetcount = o_room.asetcount; + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, playerid, 0, grade, rebatefrom, rebateto, toplayerid, rebatemode, setvalue, _pushbean, datatime]); + } + } else if (o_room.beanfloor) { + //当前玩家被抽成的星星数 + var _pushbean = 0; + //是否不提示茶水费抽成 + var showrebate = 0; + if (maxwiner_setting){ //有房主抽成 + if (o_player.playerid == o_room.owner.playerid) { + ownergarde = 0; + + //if (!aset_rebate || !aset_rebate.finitude) { + if (o_player.gameinfo.grade == maxgrade) { + _pushbean = maxwiner_pushbean; + var pushtoplayerid; + if (pushtoplayerid_setting){ + if (!o_room.advanced_type){ + //普通房间 + pushtoplayerid = pushtoplayerid_setting[0]; + } else { + //短号房间 + pushtoplayerid = pushtoplayerid_setting[1]; + } + } + if (pushtoplayerid && (pushtoplayerid != o_room.owner.playerid || pushto_setting)){ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_player.playerid; + msg.data.change = 0; + msg.data.pushbean = -1 * _pushbean; + msg.data.gameid = gameid; + msg.data.showrebate = showrebate; + youle_room.app.youle_agent[msg.rpc](msg); + + if (!pushto_setting){ + ownerpush = ownerpush + maxwiner_pushbean; + } + } + } + var roommode = 2; + var createtime = o_room.createtime; + var beanmult = 0; + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var playerid = o_player.playerid; + var grade = o_player.gameinfo.grade; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + + var asetcount = o_room.asetcount; + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, playerid, 0, grade, rebatefrom, rebateto, toplayerid, rebatemode, setvalue, _pushbean, datatime]); + //} + continue; + } else { + if (o_player.gameinfo.grade == maxgrade) { + _pushbean = maxwiner_pushbean; + ownerpush = ownerpush + maxwiner_pushbean; + } + } + + //if (!aset_rebate || !aset_rebate.finitude) { + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_player.playerid; + msg.data.change = 0; + msg.data.pushbean = -1 * _pushbean; + msg.data.gameid = gameid; + msg.data.showrebate = showrebate; + youle_room.app.youle_agent[msg.rpc](msg); + + var roommode = 2; + var createtime = o_room.createtime; + var beanmult = 0; + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var playerid = o_player.playerid; + var grade = o_player.gameinfo.grade; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + var asetcount = o_room.asetcount; + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, playerid, 0, grade, rebatefrom, rebateto, toplayerid, rebatemode, setvalue, _pushbean, datatime]); + //} + } + } + */ + //比赛数据 + var match_player = {}; + match_player.player_id = o_player.playerid; + match_player.score = o_player.gameinfo.grade; + match_player.nickname = o_player.nickname; + match_player.avatar = o_player.avatar; + match_data.game_infos.push(match_player); + } + } + } + /* + //如果是星星场,房主获得抽成星星 + if (o_room.beanfloor && maxwiner_setting){ + if (!aset_rebate || !aset_rebate.finitude || !o_room.roommode) { + var pushtoplayerid; + if (pushtoplayerid_setting){ + if (!o_room.advanced_type){ + //普通房间 + pushtoplayerid = pushtoplayerid_setting[0]; + } else { + //短号房间 + pushtoplayerid = pushtoplayerid_setting[1]; + } + } + if (!pushtoplayerid || (pushtoplayerid == o_room.owner.playerid && !pushto_setting)){ + if (ownergarde || ownerpush) { + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_room.owner.playerid; + msg.data.change = ownergarde; + msg.data.pushbean = ownerpush; + msg.data.gameid = gameid; + youle_room.app.youle_agent[msg.rpc](msg); + } + + if (ownergarde){ + var roommode = 0; //玩家房间 + if (o_room.sysroom) { + roommode = 1; //系统房间 + } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var grade = o_player.gameinfo.grade; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + var asetcount = o_room.asetcount; + var _ownerpushbean = 0; + if (ownergarde == maxgrade) { + _ownerpushbean = maxwiner_pushbean; + } + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, o_room.owner.playerid, 0, ownergarde, rebatefrom, rebateto, toplayerid, rebatemode, setvalue, _ownerpushbean, datatime]); + } + } else { + if (ownergarde){ + var _ownerpushbean = 0; + if (ownergarde == maxgrade) { + _ownerpushbean = maxwiner_pushbean; + ownerpush = ownerpush + _ownerpushbean; + } + + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_room.owner.playerid; + msg.data.change = ownergarde; + msg.data.pushbean = -1 * _ownerpushbean; + msg.data.gameid = gameid; + youle_room.app.youle_agent[msg.rpc](msg); + + var roommode = 0; //玩家房间 + if (o_room.sysroom) { + roommode = 1; //系统房间 + } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var grade = o_player.gameinfo.grade; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + var asetcount = o_room.asetcount; + var _ownerpushbean = 0; + if (ownergarde == maxgrade) { + _ownerpushbean = maxwiner_pushbean; + } + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, o_room.owner.playerid, 0, ownergarde, rebatefrom, rebateto, toplayerid, rebatemode, setvalue, _ownerpushbean, datatime]); + } + + if (!pushto_setting){ + if (ownerpush){ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = pushtoplayerid; + msg.data.change = 0; + msg.data.pushbean = ownerpush; + msg.data.gameid = gameid; + youle_room.app.youle_agent[msg.rpc](msg); + } + } + } + } + } + */ + try{ + xRebate.gameRebate(o_room); + }catch(e){ + console.log("抽成异常"); + console.log(e); + } + //保存战绩到数据库 + var sql = "call cp_game_grade_save(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql.execsql("execsql", sql, [agentid, aryplayerid, gameid, roomcode, JSON.stringify(o_room.roomtype), createtime, makewartime, overtime, null, null, o_room.owner.playerid, arygrade, o_room.roommode, o_room.beanmult, o_room.shortcode, aryisrobot]); + + var o_agentsvr_owner = o_agentsvr_agent.method.find_player(o_room.owner.playerid); + if (o_agentsvr_owner){ + var ownerroomcard = o_agentsvr_owner.roomcard; + } else { + var ownerroomcard = -99999; + } + + var shortcode = o_room.shortcode; + var deducttype = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var deductcard; + if (deducttype == 0){ + deductcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + deductcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + var asetcount = youle_room.import.createroom_asetcount(o_room.o_game, o_room.roomtype); + + sql = "call cp_game_grade_save4(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [agentid, aryplayerid, gameid, roomcode, JSON.stringify(o_room.roomtype), createtime, makewartime, overtime, JSON.stringify(o_gameinfo1), JSON.stringify(o_gameinfo2), o_room.owner.playerid, arygrade, aryroomcard, ownerroomcard, shortcode, deducttype, deductcard, asetcount, o_room.roommode, aryisrobot,arywinner,aryscore]); + + //发送比赛数据 + var match_url = youle_room.app.youle_platform.config.method.get_paravalue("url_match2", agentid, gameid); + if (match_url){ + if (o_room.match_data){ + match_data.match_data = o_room.match_data; + } + var cfg = {}; + cfg.url = match_url; + cfg.type = "POST"; + cfg.data = "data=" + JSON.stringify(match_data); + cfg.success = function(str){}; + cfg.error = function(str){ + console.log(min_now() + " 发送比赛数据失败!"); + console.log(match_data); + }; + min_http(cfg); + } + + + //比赛&排行榜(何朝阳版) + if (o_room.match_id){ + var ary_playergrade = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + if (o_room.seatlist[i].gameinfo.isbet) { + var obj_player = {}; + obj_player.playerid = o_room.seatlist[i].playerid; + obj_player.nickname = o_room.seatlist[i].nickname; + obj_player.avatar = o_room.seatlist[i].avatar; + obj_player.roomcode = o_room.roomcode; + obj_player.overtime = overtime; + obj_player.grade = o_room.seatlist[i].gameinfo.grade; + if (o_room.seatlist[i].isrobot){ + obj_player.isr = 1; + } else { + obj_player.isr = 0; + } + ary_playergrade.push(obj_player); + } else { + ary_playergrade.push(null); + } + } else { + ary_playergrade.push(null); + } + } + youle_room.export.match_ranklist(agentid, gameid, o_room.match_id, o_room.match_idx, ary_playergrade); + } + } + catch(e){ + console.log(e); + } + } + + //释放房间 + if (freeroomflag){ + //通知代理商服务器解散房间 + var o_agent = o_room.o_game.o_agent; + var o_game = o_room.o_game; + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "free_room"; + msg1.data = {}; + msg1.data.agentid = o_agent.agentid; + msg1.data.gameid = o_game.gameid; + msg1.data.roomcode = o_room.roomcode; + //玩家列表 + msg1.data.playeridlist = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + msg1.data.playeridlist.push(o_room.seatlist[i].playerid); + } + } + + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + youle_room.app.youle_agent[msg1.rpc](msg1); + //释放房间 + o_game.method.free_room(o_room); + } else { + //房间服务器与代理商服务器不是同一地址 + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg1); + cfg.success = function(str){ + //通知前端房间内的所有人需要断开房间服务器,连接代理服务器 + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]) { + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "connect_agentserver"; + msg2.data = {}; + msg2.data.agentserver = o_agent.server + ":" + o_agent.tcpport; + msg2.data.data = {}; + msg2.data.data.opt = "game_over"; + msg2.data.data.playerid = o_room.seatlist[i].playerid; + msg2.data.data.agentid = agentid; + msg2.data.data.gameid = gameid; + msg2.conmode = o_room.seatlist[i].conmode; + msg2.fromid = o_room.seatlist[i].fromid; + //此处conmode = null是为了避免触发玩家离线通知其他玩家事件 + o_room.seatlist[i].conmode = null; + o_room.seatlist[i].fromid = null; + youle_room.app.SendPack(msg2); + } + } + //释放房间 + o_game.method.free_room(o_room); + }; + cfg.error = function(str){}; + min_http(cfg); + } + } + } + + //把系统庄的输赢也算到机器人的输赢里面 + exp.save_system_zhuang = function(o_room, winvalue){ + var sql = "update agent_game_player set agpl_winlose_x2 = agpl_winlose_x2 + ? where agpl_agentid = ? and agpl_gameid = ?"; + youle_room.mysql.execsql("execsql", sql, [winvalue, o_room.o_game.o_agent.agentid, o_room.o_game.gameid]); + } + + //完成游戏里的任务 + exp.finish_gametask = function(agentid, o_player, taskid, finishamount){ + var db_callback = function(dbdata){ + //通知前端 + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "player_finish_task"; + msg.conmode = o_player.conmode; + msg.fromid = o_player.fromid; + msg.data = {}; + msg.data.taskid = taskid; + msg.data.total = dbdata[0].tasktotal; + msg.data.award = dbdata[0].taskaward; + msg.data.finish = dbdata[0].taskfinish; + msg.data.state = dbdata[0].taskstate; + youle_room.app.SendPack(msg); + } + if (!finishamount){ + finishamount = 1; + } + var sql = "call cp_game_task_finish(?,?,?,?,1);"; + youle_room.mysql.execsql("opensql", sql, [agentid, o_player.playerid, taskid, finishamount], db_callback); + } + + //全服通告 + exp.game_broadcast = function(agentid, gameid, msgtype, msgcontent){ + var msg = {}; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.gameid = gameid; + msg.data.msgtype = msgtype; + msg.data.msgcontent = msgcontent; + youle_room.broadcast(msg); + } + + //小局结算备份牌桌数据 + exp.backup_room = function(o_room, o_deskinfo){ + cls_youle_room_backup.backup_room(o_room, o_deskinfo); + } + + //踢玩家离开房间 + exp.kickplayer_inroom = function(o_room, seat){ + if (!o_room){ + return 0; + } + if (!o_room.seatlist[seat]){ + return 0; + } + var o_player = o_room.seatlist[seat]; + + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "self_exit_room"; + msg1.data = {}; + msg1.data.agentid = o_room.o_game.o_agent.agentid; + msg1.data.gameid = o_room.o_game.gameid; + msg1.data.playerid = o_player.playerid; + youle_room.app.youle_agent[msg1.rpc](msg1); + + o_player.method.exit_room(); + if (o_room.battlestate == 1){ + youle_room.import.player_leave(o_room, seat); + } + + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "other_exit_room"; + msg2.data = {}; + msg2.data.seat = seat; + o_room.method.sendpack_toother(msg2, seat); + + var msg3 = {}; + msg3.app = youle_room.app.appname; + msg3.route = youle_room.routename; + msg3.rpc = "self_exit_room"; + msg3.data = {}; + msg3.data.roomcode = o_room.roomcode; + if (o_room.owner.playerid == o_player.playerid){ + msg3.data.isowner = 1; + } else { + msg3.data.isowner = 0; + } + msg3.conmode = o_player.conmode; + msg3.fromid = o_player.fromid; + youle_room.app.SendPack(msg3); + return 1; + } + + //互换位置 + exp.change_seat = function(o_room, seat1, seat2){ + var o_player1 = o_room.seatlist[seat1]; + var o_player2 = o_room.seatlist[seat2]; + o_room.seatlist[seat1] = o_player2; + if (o_player2){ + o_player2.gameinfo.seat = seat1; + } + o_room.seatlist[seat2] = o_player1; + if (o_player1){ + o_player1.gameinfo.seat = seat2; + } + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "change_seat"; + msg.data = {}; + msg.data.agentid = o_room.o_game.o_agent.agentid; + msg.data.gameid = o_room.o_game.gameid; + msg.data.roomcode = o_room.roomcode; + msg.data.seat1 = seat1; + msg.data.seat2 = seat2; + o_room.method.sendpack_toother(msg, -1); + } + + //比赛&排行榜 + exp.match_ranklist = function(agentid, gameid, matchid, matchidx, ary_playergrade){ + var o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var o_agentsvr_game = o_agentsvr_agent.method.find_game(gameid); + + var match_idx = min_ary_indexof(o_agentsvr_game.matchlist.list, matchid, "id"); + if (match_idx > -1){ + var match_object = o_agentsvr_game.matchlist.list[match_idx]; + if (match_object.idx == matchidx){ + var ischange = false; + for (var i = 0; i < ary_playergrade.length; i++) { + if (ary_playergrade[i] == null){ + continue; + } + if (match_object.ranklist.length >= parseInt(match_object.topcount)){ + if (ary_playergrade[i].grade < match_object.ranklist[match_object.ranklist.length - 1].grade){ + continue; + } + } + + var idx_player = min_ary_indexof(match_object.ranklist, ary_playergrade[i].playerid, "playerid"); + if (idx_player > -1){ + if (ary_playergrade[i].grade <= match_object.ranklist[idx_player].grade){ + continue; + } else { + match_object.ranklist.splice(idx_player, 1); + ischange = true; + } + } + var isfound = false; + for (var j = 0; j < match_object.ranklist.length; j++) { + if (ary_playergrade[i].grade > match_object.ranklist[j].grade){ + match_object.ranklist.splice(j, 0, ary_playergrade[i]); + if (match_object.ranklist.length > parseInt(match_object.topcount)){ + match_object.ranklist.length = parseInt(match_object.topcount); + } + ischange = true; + isfound = true; + break; + } + } + if (!isfound){ + if (match_object.ranklist.length < parseInt(match_object.topcount)){ + match_object.ranklist.push(ary_playergrade[i]); + ischange = true; + } + } + } + + if (match_object.sysgrade){ + match_object.method.start_add_sysgrade_timer(); + } + + if (ischange){ + cls_youle_room_backup.backup_match(match_object); + } + } + } + } + //获取机器人总体输赢type 1星星场分数 1输赢星星数 + exp.getRobotWinLose = function(o_room,type){ + var rValue; + var o_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var o_game = o_agent.method.find_game(o_room.o_game.gameid); + switch(type){ + case 2: + rValue = o_game.robot_xx; + break; + case 3: + rValue = o_game.robot_x2; + break; + default: + rValue = o_game.robot_x2; + break; + } + if(!rValue){rValue = 0;} + return rValue; + + } + //获取玩家总体输赢type 1星星场分数 1输赢星星数 + exp.getPlayerWinLoseBySeat = function(o_room,seat,type){ + try{ + var o_player1 = o_room.seatlist[seat]; + }catch(e){ + + } + if(!o_player1){ return 0;} + var o_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var o_player = o_agent.method.find_player(o_player1.playerid); + //读取输赢 + var getGameWinLose = function(gameid,type,oPlayer){ + var key; + switch(type){ + case 1: + key = "game_winlose"; + break; + case 2: + key = "game_winlose_xx"; + break; + case 3: + key = "game_winlose_x2"; + break; + default: + key = "game_winlose_xx"; + break; + } + if(oPlayer.winLose){//判断是否有此属性 + if(oPlayer.winLose[gameid]){ + if(oPlayer.winLose[gameid][key]){ + return oPlayer.winLose[gameid][key]; + }else{ + + } + }else{ + + } + }else{ + + } + return 0; + } + return getGameWinLose(o_room.o_game.gameid,type,o_player); + + } + //判断该玩家是否满足踢出条件 true 踢出玩家;(只处理vip房) + exp.checkKick = function(o_room,seat){ + try{ + var rValue = false; + var rCount = o_room.rebateNumber; + var oCount = 0; + var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + if(!o_agentsvr_agent){ + return rValue; + } + + if(!o_room.seatlist[seat]){ + return rValue; + } + // var o_player = o_agentsvr_agent.method.find_player(o_room.seatlist[seat].playerid); + var o_player = o_room.seatlist[seat]; + if(!o_player){ + return rValue; + } + if(!o_room.owner){ + return rValue; + } + // var o_owner = o_agentsvr_agent.method.find_player(owner.playerid); + var o_owner = o_room.owner; + if(!o_owner){ + return rValue; + } + // var rebateConfig = youle_room.app.youle_platform.config.method.get_paravalue("rebateConfig", o_room.o_game.o_agent.agentid); + // var rebateFrom = rebateConfig.from; + // if(rebateConfig){ + + // } + if(o_room.o_game.gameid == "Btke0urRy0cvPd5CIvD5yfhYhKhdSdex"){ + var brNumber = youle_room.app.youle_platform.config.method.get_paravalue("brNumber",o_room.o_game.o_agent.agentid); + if(brNumber){ + brNumber = Number(brNumber); + oCount = brNumber; + } + } + //console.log(oCount,rCount,o_player,o_owner); + //vip房 + if(o_room.advanced_type){ + //抽金币 + if(!o_room.rebateType){ + if(o_player.playerid == o_owner.playerid){ + if(o_player.bean < oCount + rCount){ + rValue = true; + } + }else{ + if(o_player.bean < rCount){ + rValue = true; + } + } + }else{//抽魅力值 + + } + } + return rValue; + }catch(e){ + console.log("exp.checkKick",e); + return false; + } + } + return exp; + } +} + +//对内输出接口 +youle_room.export = cls_youle_room_export.new(); \ No newline at end of file diff --git a/server/youle/server_room/class.game.js b/server/youle/server_room/class.game.js new file mode 100644 index 0000000..3f80a9a --- /dev/null +++ b/server/youle/server_room/class.game.js @@ -0,0 +1,193 @@ +/////////////////////////////////////////////////// +////////// cls_youle_room_game: 游戏 ////////// +/////////////////////////////////////////////////// +var cls_youle_room_game = cls_youle_room_game || { + //构造函数 + new: function(o_agent, gameid, gameidx, gamename, seatcount, makewar, modename, roomserver, tcpport, httpport, minroomcode, maxroomcode){ + var game = {}; + //基本信息 + game.o_agent = o_agent; //代理商 + game.gameid = gameid; //游戏id + game.idx = gameidx; //游戏的数组下标 + game.gamename = gamename; //游戏名称 + game.seatcount = seatcount; //房间内的座位数量 + game.makewar = makewar; //达成开战条件的人数 + game.modename = modename; //游戏服务器模块名称 + game.roomserver = roomserver; //房间服务器地址 + game.tcpport = tcpport; //房间服务器tcp端口 + game.httpport = httpport; //房间服务器http端口 + game.minroomcode = minroomcode; //房间服务器最小房号 + game.maxroomcode = maxroomcode; //房间服务器最大房号 + + //房间列表 + game.rooms = []; + game.rooms.length = maxroomcode - minroomcode + 1; + + //方法 + game.method = {}; + + //创建房间 + game.method.create_room = function(roomcode, roomtype, needroomcard, asetcount){ + return cls_youle_room_game.create_room(game, roomcode, roomtype, needroomcard, asetcount); + } + + //查找房间 + game.method.find_room = function(roomcode){ + return cls_youle_room_game.find_room(game, roomcode); + } + + //释放房间 + game.method.free_room = function(o_room){ + cls_youle_room_game.free_room(game, o_room); + } + + //调试状态 + game.method.isdebugger = function(){ + return cls_youle_room_game.isdebugger(game); + } + return game; + }, + + //创建房间 + create_room: function(o_game, roomcode, roomtype, needroomcard, asetcount){ + //判断房号是否在范围内 + if (roomcode < o_game.minroomcode || roomcode > o_game.maxroomcode){ + return null; + } + + //判断房间是否已经存在 + var idx = cls_youle_room_game.aryidx_room(o_game, roomcode); + if (o_game.rooms[idx]){ + return null; + } + + var o_room = cls_youle_room_room.new(o_game, roomcode, roomtype, needroomcard, asetcount); + o_game.rooms[idx] = o_room; + + return o_room; + }, + + //查找房间 + find_room: function(o_game, roomcode){ + var idx = cls_youle_room_game.aryidx_room(o_game, roomcode); + if (o_game.rooms.length <= idx) { + return null; + } + return o_game.rooms[idx]; + }, + + //释放房间 + free_room: function(o_game, o_room){ + var idx = cls_youle_room_game.aryidx_room(o_game, o_room.roomcode); + + //更新tcp连接列表 + var o_room = o_game.rooms[idx]; + if (o_room){ + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]) { + var o_player = o_room.seatlist[i]; + if (o_player.conmode == "tcp"){ + youle_room.tcps.method.delete_tcp(o_player.fromid, o_player); + } + } + } + + try{ + if (o_room.game){//针对牛牛写的 + if (o_room.game.room){ + o_room.game.room = null; + } + if (o_room.game.pList){ + o_room.game.pList = null; + } + o_room.game = null; + } + if (o_room.o_desk){//通用 + if (o_room.o_desk.o_room){ + o_room.o_desk.o_room = null; + } + o_room.o_desk = null; + } + + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + o_room.seatlist[i].seat = null; + o_room.seatlist[i].cards = null; + o_room.seatlist[i].rubcard = null; + o_room.seatlist[i].putcard = null; + o_room.seatlist[i].scores = null; + o_room.seatlist[i].betScore = null; + o_room.seatlist[i].winScore = null; + o_room.seatlist[i].isJoin = null; + o_room.seatlist[i].isBet = null; + o_room.seatlist[i].isAuto = null; + o_room.seatlist[i].auto = null; + o_room.seatlist[i] = null; + } + } + } + catch(e){} + + if (o_room.freetimer){ + min_closetime(o_room.freetimer); + o_room.freetimer = null; + } + if (o_room.freetimer24){ + min_closetime(o_room.freetimer24); + o_room.freetimer24 = null; + } + if (o_room.timer_robot){ + min_closetime(o_room.timer_robot); + o_room.timer_robot = null; + } + if (o_room.timer_robot_active){ + min_closetime(o_room.timer_robot_active); + o_room.timer_robot_active = null; + } + if (o_room.timer_robot_passive){ + for (var i = 0; i < o_room.timer_robot_passive.length; i++) { + min_closetime(o_room.timer_robot_passive[i]); + } + o_room.timer_robot_passive = []; + } + } + + o_game.rooms[idx] = null; + + //固定房号的房间需要自动再次创建 + if (o_room.advanced_type){ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "advanced_createroom_roomcode"; + msg.data = {}; + msg.data.agentid = o_room.o_game.o_agent.agentid; + msg.data.playerid = o_room.owner.playerid; + msg.data.gameid = o_room.o_game.gameid; + msg.data.roomcode = o_room.roomcode; + msg.data.shortcode = o_room.shortcode; + //msg.data.rebateType = o_room.rebateType; + youle_room.app.youle_agent[msg.rpc](msg); + }else{ + //非vip房且有需要更新的roomtype自动创建 + + } + + }, + + //调试状态 + isdebugger: function(o_game){ + var o_cfg = youle_room.app.youle_platform.config; + var re = o_cfg.method.get_paravalue("isdebugger", o_game.o_agent.agentid, o_game.gameid); + if (!re){ + return 0; //正式 + } else { + return 1; //调试 + } + }, + + //计算房间的数组下标 + aryidx_room: function(o_game, roomcode){ + return roomcode - o_game.minroomcode; + } +} diff --git a/server/youle/server_room/class.import.js b/server/youle/server_room/class.import.js new file mode 100644 index 0000000..9d5c3a5 --- /dev/null +++ b/server/youle/server_room/class.import.js @@ -0,0 +1,383 @@ +/////////////////////////////////////////////////// +/////// cls_youle_room_import: 输入接口 /////// +/////////////////////////////////////////////////// +var cls_youle_room_import = cls_youle_room_import || { + + new: function() { + + var imp = {}; + + //创建房间时根据roomtype获取需要的房卡数量 + imp.createroom_needroomcard = function(o_game, roomtype){ + var o_game_config = youle_room.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + var mod_game = youle_room.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.get_needroomcard){ + return mod_game.export.get_needroomcard(roomtype, o_game_config); + } + } + if (mod_game.needroomcard){ + return mod_game.needroomcard(roomtype, o_game_config); + } + return 1; + } + + //创建房间时根据roomtype获取总局数 + imp.createroom_asetcount = function(o_game, roomtype){ + var o_game_config = youle_room.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + var mod_game = youle_room.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.get_asetcount){ + return mod_game.export.get_asetcount(roomtype, o_game_config); + } + } + if (mod_game.asetcount){ + return mod_game.asetcount(roomtype, o_game_config); + } + return 8; + } + + //创建房间时根据roomtype获取元宝下限 + imp.createroom_beanfloor = function(o_game, roomtype){ + var o_game_config = youle_room.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + + var mod_game = youle_room.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.createroom_beanfloor){ + return mod_game.export.createroom_beanfloor(roomtype, o_game_config); + } + } + if (mod_game.createroom_beanfloor){ + return mod_game.createroom_beanfloor(roomtype, o_game_config); + } + return null; + } + + //加入房间时根据roomtype获取需要的房卡数量,如果不需要房卡则需要返回0 + imp.joinroom_needroomcard = function(o_game, roomtype){ + var o_game_config = youle_room.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + var mod_game = youle_room.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.get_needroomcard_joinroom){ + return mod_game.export.get_needroomcard_joinroom(roomtype, o_game_config); + } + } + if (mod_game.needroomcard_joinroom){ + return mod_game.needroomcard_joinroom(roomtype, o_game_config); + } + return 0; + } + + //根据roomtype获取房间的扣卡模式,0:房主扣卡 1:每人扣卡 2:大赢家扣卡 + imp.deduct_roomcard_mode = function(o_game, roomtype){ + var o_game_config = youle_room.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + var mod_game = youle_room.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.deduct_roomcard_mode){ + return mod_game.export.deduct_roomcard_mode(roomtype, o_game_config); + } + } + if (mod_game.deduct_roomcard_mode){ + return mod_game.deduct_roomcard_mode(roomtype, o_game_config); + } + return 0; + } + + //除了满桌自动开战外,子游戏可以设置开战人数 + imp.makewar_playercount = function(o_game, roomtype){ + var o_game_config = youle_room.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + var mod_game = youle_room.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.makewar_playercount){ + return mod_game.export.makewar_playercount(roomtype, o_game_config); + } + } + if (mod_game.makewar_playercount){ + return mod_game.makewar_playercount(roomtype, o_game_config); + } + return null; + } + + //星星场房主抽成星星数量 + imp.owner_beanpush = function(o_game, roomtype){ + var o_game_config = youle_room.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + var mod_game = youle_room.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.owner_beanpush){ + return mod_game.export.owner_beanpush(roomtype, o_game_config); + } + } + if (mod_game.owner_beanpush){ + return mod_game.owner_beanpush(roomtype, o_game_config); + } + return 0; + } + + //开局时通知游戏,并返回游戏的开局数据包 + imp.makewar_deskwar = function(o_room){ + var matchroom_deductbean = function(){ + if (!o_room.match_id){ + return; + } + var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var o_agentsvr_game = o_agentsvr_agent.method.find_game(o_room.o_game.gameid); + var match_idx = min_ary_indexof(o_agentsvr_game.matchlist.list, o_room.match_id, "id"); + if (match_idx == -1){ + return; + } + var match_object = o_agentsvr_game.matchlist.list[match_idx]; + if (!match_object.bean){ + return; + } + + var do_deduct_bean = function(){ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = o_room.o_game.o_agent.agentid; + msg.data.change = 0; + msg.data.pushbean = -1 * match_object.bean; + msg.data.gameid = o_room.o_game.gameid; + msg.data.showrebate = 1; + msg.data.type = 5; + + //房间id = 时间戳 + 4位随机码 + 房号 + var roomid = new Date().getTime() + min_randomChar(4) + o_room.roomcode; + var datatime = min_now(); + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + var playerid = o_room.seatlist[i].playerid; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + var isrobot = 0; + if (o_room.seatlist[i].isrobot){ + isrobot = 1; + } + if (!isrobot){ + msg.data.playerid = playerid; + youle_room.app.youle_agent[msg.rpc](msg); + } + var invitecode = null; + var inviteid = null; + var marketid = null; + if (!isrobot){ + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(playerid); + invitecode = __o_agentsvr_player.invitecode; + inviteid = __o_agentsvr_player.inviteid; + marketid = __o_agentsvr_player.marketid; + } + + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, o_room.o_game.o_agent.agentid, o_room.o_game.gameid, o_room.roomcode, 4, o_room.createtime, beanmult, o_room.owner.ownerid, shortcode, deductmode, roomcard, 0, o_room.asetcount, 0, playerid, isrobot, 0, 3, 0, 0, 3, match_object.bean, match_object.bean, datatime, invitecode, inviteid, marketid]); + } + } + } + min_ontimeout(do_deduct_bean, 1000); + } + var o_game_config = youle_room.app.youle_platform.config.method.get_paravalue("game_config", o_room.o_game.o_agent.agentid, o_room.o_game.gameid); + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.makewar){ + var data = mod_game.export.makewar(o_room, o_game_config); + matchroom_deductbean(); //比赛场扣除报名的星星费用 + return data; + } + } + if (mod_game.makewar){ + var data = mod_game.makewar(o_room, o_game_config); + matchroom_deductbean(); //比赛场扣除报名的星星费用 + return data; + } + } + + //进入房间或断线重连时获取牌局信息 + imp.get_deskinfo = function(o_room, seat){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.get_deskinfo){ + return mod_game.export.get_deskinfo(o_room, seat); + } + } + if (mod_game.getDeskInfo){ + return mod_game.getDeskInfo(o_room, seat); + } + } + + //解散时通知游戏 + imp.get_disbandRoom = function(o_room){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.get_disbandRoom){ + return mod_game.export.get_disbandRoom(o_room); + } + } + if (mod_game.disbandRoom){ + return mod_game.disbandRoom(o_room); + } + } + + //玩家中途进入房间 + imp.player_enter = function(o_room, seat){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.player_enter){ + return mod_game.export.player_enter(o_room, seat); + } + } + if (mod_game.player_enter){ + return mod_game.player_enter(o_room, seat); + } + } + + //玩家中途退出房间 + imp.player_leave = function(o_room, seat){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.player_leave){ + return mod_game.export.player_leave(o_room, seat); + } + } + if (mod_game.player_leave){ + return mod_game.player_leave(o_room, seat); + } + } + + //恢复房间数据 + imp.restore_room = function(o_room, o_deskinfo){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.restore_room){ + return mod_game.export.restore_room(o_room, o_deskinfo); + } + } + if (mod_game.restore_room){ + return mod_game.restore_room(o_room, o_deskinfo); + } + } + + //检查玩家是否有权限进入房间,返回 { allowed: true/false, message: "说明" } + imp.check_joinroom_permission = function(o_game, o_room, o_join_data){ + var o_game_config = youle_room.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + var mod_game = youle_room.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.check_joinroom_permission){ + return mod_game.export.check_joinroom_permission(o_room, o_game_config, o_join_data); + } + } + return { allowed: true, message: '' }; + } + + //检查玩家是否有权限创建房间,返回 { allowed: true/false, message: "说明" } + imp.check_createroom_permission = function(o_game, o_create_data){ + var o_game_config = youle_room.app.youle_platform.config.method.get_paravalue("game_config", o_game.o_agent.agentid, o_game.gameid); + var mod_game = youle_room.app[o_game.modename]; + if (mod_game.export){ + if (mod_game.export.check_createroom_permission){ + return mod_game.export.check_createroom_permission(o_game_config, o_create_data); + } + } + return { allowed: true, message: '' }; + } + + //玩家准备 + imp.player_CanPrepare = function(o_room, seat){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.player_CanPrepare){ + return mod_game.export.player_CanPrepare(o_room, seat); + } + } + if (mod_game.player_CanPrepare){ + return mod_game.player_CanPrepare(o_room, seat); + } + return 1; + } + + imp.player_prepare = function(o_room, seat){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.player_prepare){ + return mod_game.export.player_prepare(o_room, seat); + } + } + if (mod_game.player_prepare){ + return mod_game.player_prepare(o_room, seat); + } + } + + //无限局玩家是否可以离开房间 + imp.isPlayerCanLeave = function(o_room, seat){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.isPlayerCanLeave){ + return mod_game.export.isPlayerCanLeave(o_room, seat); + } + } + if (mod_game.isPlayerCanLeave){ + return mod_game.isPlayerCanLeave(o_room, seat); + } + return null; + } + + //通过战绩gameinfo1获取大赢家标志如[0,0,1]空座位不需要写入数组顺序从0号座位依次写入 + imp.getWinnerByGameInfo1 = function(o_room, gameinfo1){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.getWinnerByGameInfo1){ + return mod_game.export.getWinnerByGameInfo1(o_room, gameinfo1); + } + } + if (mod_game.getWinnerByGameInfo1){ + return mod_game.getWinnerByGameInfo1(o_room, gameinfo1); + } + return null; + } + + //通过战绩gameinfo1获取积分列表如[10,10,-20]空座位不需要写入数组顺序从0号座位依次写入 + imp.getScoreByGameInfo1 = function(o_room, gameinfo1){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.getScoreByGameInfo1){ + return mod_game.export.getScoreByGameInfo1(o_room, gameinfo1); + } + } + if (mod_game.getScoreByGameInfo1){ + return mod_game.getScoreByGameInfo1(o_room, gameinfo1); + } + return null; + } + //通知子游戏解散房间 + imp.gameBreak = function(o_room){ + var mod_game = youle_room.app[o_room.o_game.modename]; + if (mod_game.export){ + if (mod_game.export.gameBreak){ + return mod_game.export.gameBreak(o_room); + } + } + if (mod_game.gameBreak){ + return mod_game.gameBreak(o_room); + } + return null; + } + return imp; + } +} + +//内部输入接口 +youle_room.import = cls_youle_room_import.new(); \ No newline at end of file diff --git a/server/youle/server_room/class.player.js b/server/youle/server_room/class.player.js new file mode 100644 index 0000000..4abe7ef --- /dev/null +++ b/server/youle/server_room/class.player.js @@ -0,0 +1,158 @@ +/////////////////////////////////////////////////// +///////// cls_youle_room_player: 玩家 ///////// +/////////////////////////////////////////////////// +var cls_youle_room_player = cls_youle_room_player || { + //构造函数 + new: function(playerid, nickname, avatar, sex, playertype, ip, onstate, conmode, fromid){ + var player = {}; + //基本信息 + player.playerid = playerid; //玩家id + player.nickname = nickname; + player.avatar = avatar; + player.sex = sex; + player.playertype = playertype; + player.ip = ip; + player.location = null; + player.onstate = onstate; //在线状态 null未连接 0在线 1离线 2电话中 + player.conmode = conmode; //连接方式 + player.fromid = fromid; //连接id + + //初始化游戏数据 + player.gameinfo = {}; + player.gameinfo.o_game = null; //所在的游戏 + player.gameinfo.o_room = null; //所在的房间 + player.gameinfo.seat = -1; //房间内的位置 + player.gameinfo.isowner = -1; //房主标志 0:非房主 1:是房主 + player.gameinfo.agree = -1; //是否同意解散房间 0:申请解散 1:同意解散 + player.gameinfo.isbet = -1; //下注标志 0:未下注可离开房间 1:已下注不可离开房间 + player.gameinfo.grade = 0; //在房间内的得分 + + //在定义中没有写入的后加属性 + //player.gameinfo.isprepare = 0; //是否准备 + + //方法 + player.method = {}; + + //获取玩家的基础信息 + player.method.get_info = function(){ + return cls_youle_room_player.get_info(player); + } + + //进入房间 + player.method.enter_room = function(o_room, seat, isowner){ + cls_youle_room_player.enter_room(player, o_room, seat, isowner); + } + + //退出房间 + player.method.exit_room = function(){ + cls_youle_room_player.exit_room(player); + } + + return player; + }, + + //获取玩家的基础信息 + get_info: function(o_player){ + var info = {}; + info.playerid = o_player.playerid; + info.nickname = o_player.nickname; + info.avatar = o_player.avatar; + info.sex = o_player.sex; + info.ip = o_player.ip; + info.location = o_player.location; + info.onstate = o_player.onstate; + info.isprepare= o_player.gameinfo.isprepare; + if (o_player.isrobot){ + info.bean = o_player.bean; + } else { + info.bean = null; + } + if (o_player.paycode) { + info.paycode = o_player.paycode; + } + info.bean = o_player.bean; + return info; + }, + + //进入房间 + enter_room: function(o_player, o_room, seat, isowner){ + o_player.gameinfo.o_game = o_room.o_game; + o_player.gameinfo.o_room = o_room; + o_player.gameinfo.seat = seat; + o_player.gameinfo.isowner = isowner; + o_player.gameinfo.isbet = 0; + o_player.gameinfo.agree = -1; + o_player.gameinfo.grade = 0; + + if (o_room.needprepare){ + o_player.gameinfo.isprepare = 0; + } else { + o_player.gameinfo.isprepare = 1; + } + + o_room.seatlist[seat] = o_player; + if (isowner){ + o_room.owner = o_player; + } + + //更新tcp连接列表 + if (o_player.conmode == "tcp"){ + youle_room.tcps.method.load_tcp(o_player.fromid, o_player); + } + + //记录当前机器人数量 + if (o_player.isrobot){ + if (o_room.roommode){ + if (!o_room.o_game.robotcount){ + o_room.o_game.robotcount = 1; + } else { + o_room.o_game.robotcount = o_room.o_game.robotcount + o_room.beanmult; + } + // console.log('++机器人: ' + o_room.o_game.robotcount); + } + } else { + if (o_room.sysroom){ + if (!o_room.o_game.sysroom_playercount){ + o_room.o_game.sysroom_playercount = []; + } + for (var i = 0; i < o_room.o_game.sysroom_playercount.length; i++) { + if (JSON.stringify(o_room.o_game.sysroom_playercount[i].roomtype) == JSON.stringify(o_room.roomtype)){ + o_room.o_game.sysroom_playercount[i].playercount = o_room.o_game.sysroom_playercount[i].playercount + 1; + return; + } + }; + var _v = {}; + _v.roomtype = o_room.roomtype; + _v.playercount = 1; + o_room.o_game.sysroom_playercount.push(_v); + } + } + }, + + //退出房间 + exit_room: function(o_player){ + //更新tcp连接列表 + if (o_player.conmode == "tcp"){ + youle_room.tcps.method.delete_tcp(o_player.fromid); + } + + o_player.gameinfo.o_room.seatlist[o_player.gameinfo.seat] = null; + + //记录当前机器人数量 + if (o_player.isrobot){ + if (o_player.gameinfo.o_room.roommode){ + o_player.gameinfo.o_game.robotcount = o_player.gameinfo.o_game.robotcount - o_player.gameinfo.o_room.beanmult; + // console.log('--机器人: ' + o_player.gameinfo.o_room.o_game.robotcount); + } + } else { + if (o_player.gameinfo.o_room.sysroom){ + for (var i = 0; i < o_player.gameinfo.o_game.sysroom_playercount.length; i++) { + if (JSON.stringify(o_player.gameinfo.o_game.sysroom_playercount[i].roomtype) == JSON.stringify(o_player.gameinfo.o_room.roomtype)){ + o_player.gameinfo.o_game.sysroom_playercount[i].playercount = o_player.gameinfo.o_game.sysroom_playercount[i].playercount - 1; + break; + } + } + } + } + } +} diff --git a/server/youle/server_room/class.rebate.js b/server/youle/server_room/class.rebate.js new file mode 100644 index 0000000..649d2c7 --- /dev/null +++ b/server/youle/server_room/class.rebate.js @@ -0,0 +1,1018 @@ +var xRebate = xRebate || {}; + +xRebate.gameRebate = function(o_room){ + var maxgrade = 0; + var maxseat = []; + for (var i = 0; i < o_room.seatlist.length; i++){ + if (o_room.seatlist[i] && o_room.seatlist[i].gameinfo.isbet){ + if (o_room.seatlist[i].gameinfo.grade > maxgrade){ + maxgrade = parseInt(o_room.seatlist[i].gameinfo.grade); + maxseat = [i]; + } else if (o_room.seatlist[i].gameinfo.grade == maxgrade){ + maxseat.push(i); + } + } + } + var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var rebateLimit = 0; + if(o_room.advanced_type){ + if(o_room.owner){ + var o_player1 = o_agentsvr_agent.method.find_player(o_room.owner.playerid); + for (var i = 0; i < o_player1.roomcodes.length; i++) { + if (o_player1.roomcodes[i].gameid == o_room.o_game.gameid) { + if(o_player1.roomcodes[i].rebateLimit){ + rebateLimit = o_player1.roomcodes[i].rebateLimit; + } + break; + } + } + } + } + //console.log(rebateLimit); + if (!o_room.roommode){ + //非星星场短号房要结算魅力值 + try{ + //百人场无大局魅力值抽成 + if (o_room.advanced_type && o_room.o_game.gameid != "Btke0urRy0cvPd5CIvD5yfhYhKhdSdex"){ + var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var o_agentsvr_owner = o_agentsvr_agent.method.find_player(o_room.owner.playerid); + if (o_agentsvr_owner.whitelist && o_agentsvr_owner.whitelist.length > 0){ + var rebateConfig = youle_room.app.youle_platform.config.method.get_paravalue("rebateConfig", o_room.o_game.o_agent.agentid); + var rCharm = 0; + var maxgrade1 = 0; + var maxseat1 = []; + var rcTemp = false; + if(rebateConfig){ + if(rebateConfig.rebateCharm && o_room.rebateType){ + //rCharm = parseInt(o_room.rebateNumber/maxseat.length); + rCharm = o_room.rebateNumber; + var _seatlist = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + //if (o_room.seatlist[i]. != null){ + if (o_room.seatlist[i]){ + var _player = {}; + _player.seat = i; + _player.playerid = o_room.seatlist[i].playerid; + _player.change = 0; + for (var j = 0; j < o_agentsvr_owner.whitelist.length; j++) { + if (o_agentsvr_owner.whitelist[j][0] == _player.playerid){ + if (o_agentsvr_owner.whitelist[j][2]){ + + //o_agentsvr_owner.whitelist[j][2] = parseInt(o_agentsvr_owner.whitelist[j][2]) + ary_grade[i]; + if(o_room.seatlist[i].gameinfo.grade == maxgrade){ + if(rebateLimit < o_room.seatlist[i].gameinfo.grade - rCharm){ + o_agentsvr_owner.whitelist[j][2] = parseInt(o_agentsvr_owner.whitelist[j][2]) - rCharm; + _player.change = -rCharm; + } + } + } else { + if(o_room.seatlist[i].gameinfo.grade == maxgrade){ + if(rebateLimit < o_room.seatlist[i].gameinfo.grade - rCharm){ + o_agentsvr_owner.whitelist[j][2] = -rCharm; + _player.change = -rCharm; + } + } + } + _player.charm = o_agentsvr_owner.whitelist[j][2]; + break; + } + } + _seatlist.push(_player); + } + //} + } + + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "update_charm"; + msg.data = {}; + msg.data.seatlist = _seatlist; + o_room.method.sendpack_toother(msg, -1); + var writeDB = true; + if(o_room.o_game.gameid){ + var config_vipConfig = youle_agent.app.youle_platform.config.method.get_paravalue("vipConfig", o_room.o_game.o_agent.agentid, o_room.o_game.gameid); + if(config_vipConfig){ + var vipIndex = min_ary_indexof(config_vipConfig,o_room.shortcode,"0"); + if(vipIndex > -1){ + if(config_vipConfig[vipIndex][1]){ + writeDB = false; + } + } + } + } + writeDB = true; + if(writeDB){ + var sql = "update player set play_whitelist = ? where play_agentid = ? and play_playerid = ?"; + youle_room.mysql.execsql("execsql", sql, [youle_agent.compressWhiteList(o_agentsvr_owner.whitelist), o_room.o_game.o_agent.agentid, o_room.owner.playerid]); + } + } + } + + } + } + }catch(e){ + console.log("结算短号魅力值发生错误"); + console.log(e); + } + + } + //只支持倍数百分比抽成 + //抽成参数 + var rebateConfig = youle_room.app.youle_platform.config.method.get_paravalue("rebateConfig", o_room.o_game.o_agent.agentid); + if(!rebateConfig){ + return; + } + //小局抽成设置 + //var aset_rebate = youle_room.app.youle_platform.config.method.get_paravalue("aset_rebate", o_room.o_game.o_agent.agentid); + //是否需要抽成 + var rebateTemp = false; + if(!o_room.asetRebate){ + rebateTemp = true; + } + //vip房卡房是否扣魅力值 + var rebateCharm = rebateConfig.rebateCharm; + //扣除模式 1.大赢家 2.赢家 3.所有人 + var rebateFrom = rebateConfig.from; + //房间倍数 + var beanmult = 1; + //金币房 + if(o_room.roommode){ + beanmult = youle_agent.import.createroom_beanmult(o_room.o_game, o_room.roomtype); + } + if(!beanmult){ + beanmult = 1; + } + + //配置扣除数量 + var sysRebateValue = parseInt(rebateConfig.value * beanmult /100); + + var agentid = o_room.o_game.o_agent.agentid; + var gameid = o_room.o_game.gameid; + var roomcode = o_room.roomcode; + var createtime = o_room.createtime; + var makewartime = o_room.makewartime; + var roommode = o_room.roommode; + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + + var asetcount = o_room.asetcount; + + var rebateTo = 0; + var rebateMode = 2; + var roomMode = 0; + + //房间收取房间费的值 + var rebateValue = sysRebateValue; + if(!o_room.roommode){ + if(!o_room.advanced_type){ + + }else{ + rebateFrom = 1; + if(rebateCharm && o_room.rebateType){ + roomMode = 1; + } + if(o_room.advanced_teabean){ + if(sysRebateValue < o_room.advanced_teabean){ + rebateMode = 3; + rebateValue = o_room.advanced_teabean; + } + } + } + }else{ + if(!o_room.advanced_type){ + var tea = youle_room.import.owner_beanpush(o_room.o_game, o_room.roomtype); + if(tea){ + if(sysRebateValue < tea){ + rebateMode = 3; + rebateValue = tea; + } + } + }else{ + if(o_room.advanced_teabean){ + if(sysRebateValue < o_room.advanced_teabean){ + rebateMode = 3; + rebateValue = o_room.advanced_teabean; + } + } + } + } + + + //每个人扣除的数量 + var rebateReal = rebateValue; + //通过扣除模式得出每个人扣的数量 + switch(rebateFrom){ + //大赢家 + case 1: + rebateReal = parseInt(rebateValue / maxseat.length);//多个大赢家均摊房间费 + + sysRebateValue = parseInt(sysRebateValue / maxseat.length);//多个大赢家均摊房间费 + break; + //赢家 + case 2: + + break; + //所有人 + case 3: + + break; + default: + + break; + } + var ownergarde = 0; + //var rebateSum = 0;//总共扣除的房间费 + //console.log(o_room.seatlist); + //if(rebateReal){ + var ownerid = null; + var pushCount = 0; + if(o_room.owner){ + ownerid = o_room.owner.playerid; + } + //console.log("sysRebateValue",sysRebateValue); + //console.log("rebateReal",rebateReal); + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + var o_player = o_room.seatlist[i]; + if (o_player.gameinfo.isbet) { + var o_agentsvr_player = o_agentsvr_agent.method.find_player(o_player.playerid); + + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + if (o_player.playerid == o_room.owner.playerid) { + ownergarde = o_player.gameinfo.grade; + } + + var isRebate = false; + + switch(rebateFrom){ + //大赢家 + case 1: + if(o_player.gameinfo.grade == maxgrade && rebateLimit < o_player.gameinfo.grade){ + isRebate = true; + } + break; + //赢家 + case 2: + if(o_player.gameinfo.grade > 0 && rebateLimit < o_player.gameinfo.grade){ + isRebate = true; + } + break; + //所有人 + case 3: + isRebate = true; + break; + default: + + break; + } + //console.log("playerid:"+o_player.playerid+"grade:"+o_player.gameinfo.grade+"isRebate:"+isRebate); + //房间id = 时间戳 + 4位随机码 + 房号 + var roomid = new Date().getTime() + min_randomChar(4) + o_room.roomcode; + var datatime = min_now(); + //达成抽成条件 + if(isRebate && rebateTemp){ + //rebateSum += rebateReal; + + if(!roomMode){ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_player.playerid; + + if(o_room.roommode){ + msg.data.change = o_player.gameinfo.grade; + }else{ + msg.data.change = 0; + } + var nBean = rebateReal - msg.data.change; + var sysBean = 0; + //console.log("o_player.bean",o_player.bean,"nBean",nBean); + if(o_player.bean >= nBean){ + if(ownerid == o_agentsvr_player.playerid){ + msg.data.pushbean = 0; + }else{ + if(o_room.roommode){ + msg.data.pushbean = -1 * rebateReal; + // pushCount += rebateReal; + sysBean = sysRebateValue; + pushCount = pushCount + (rebateReal - sysRebateValue); + }else{ + msg.data.pushbean = -1 * rebateReal; + //sysBean = sysRebateValue; + pushCount = pushCount + rebateReal; + } + } + }else{ + msg.data.pushbean = 0; + } + + msg.data.gameid = gameid; + msg.data.showrebate = 0; + youle_room.app.youle_agent[msg.rpc](msg); + + var roommode = 0; //玩家房间 + if (o_room.sysroom) { + roommode = 1; //系统房间 + } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var playerid = o_player.playerid; + var isrobot = o_player.isrobot; + var grade = o_player.gameinfo.grade; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + + var asetcount = o_room.asetcount; + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(playerid); + var invitecode = __o_agentsvr_player.invitecode; + var inviteid = __o_agentsvr_player.inviteid; + var marketid = __o_agentsvr_player.marketid; + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, playerid, isrobot, grade, rebateFrom, rebateTo, '', rebateMode, sysBean, sysBean, datatime, invitecode, inviteid, marketid]); + } + // else{ + // //修改房卡数量 + // var msg2 = {}; + // msg2.data={}; + // msg2.data.agentid = agentid; + // msg2.data.playerid = o_player.playerid; + // msg2.data.change = -1*rebateReal; + + // xRebate.update_roomcard(msg2); + // } + }else{ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_player.playerid; + if(o_room.roommode){ + msg.data.change = o_player.gameinfo.grade; + }else{ + msg.data.change = 0; + } + msg.data.pushbean = 0; + msg.data.gameid = gameid; + msg.data.showrebate = 1; + youle_room.app.youle_agent[msg.rpc](msg); + + var roommode = 0; //玩家房间 + if (o_room.sysroom) { + roommode = 1; //系统房间 + } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var playerid = o_player.playerid; + var grade = o_player.gameinfo.grade; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + var isrobot = o_player.isrobot; + var asetcount = o_room.asetcount; + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(playerid); + var invitecode = __o_agentsvr_player.invitecode; + var inviteid = __o_agentsvr_player.inviteid; + var marketid = __o_agentsvr_player.marketid; + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, playerid, isrobot, grade, 0, rebateTo, '', rebateMode, 0, 0, datatime, invitecode, inviteid, marketid]); + } + + } + } + } + //console.log("pushCount",pushCount); + if(pushCount){ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = ownerid; + msg.data.change = 0; + msg.data.pushbean = pushCount; + + msg.data.gameid = gameid; + msg.data.showrebate = 0; + msg.data.type = 6; + youle_room.app.youle_agent[msg.rpc](msg); + + var roommode = 0; //玩家房间 + if (o_room.sysroom) { + roommode = 1; //系统房间 + } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var playerid = ownerid; + var grade = 0; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + var asetcount = o_room.asetcount; + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(playerid); + var invitecode = __o_agentsvr_player.invitecode; + var inviteid = __o_agentsvr_player.inviteid; + var marketid = __o_agentsvr_player.marketid; + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, playerid, 0, grade, rebateFrom, 1, ownerid, rebateMode, pushCount, pushCount, datatime, invitecode, inviteid, marketid]); + } + //} + +} + +//修改房卡 +xRebate.update_roomcard = function(pack){ + var agentid = pack.data.agentid; + var playerid = pack.data.playerid; + var change = parseInt(pack.data.change); + + //查找代理商 + var o_agent = youle_agent.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data = {}; + pack.data.result = 1; + pack.data.error = "agentid不存在"; + return pack; + }; + + var db_callback = function(dbdata){ + if (dbdata[0].result == 0){ + var o_player = o_agent.method.find_player(playerid); + if (o_player){ + o_player.method.update_roomcard(0, parseInt(dbdata[0].roomcard), change,"房间费"); + } + pack.data.result = 0; + } else { + pack.data = {}; + pack.data.result = dbdata[0].result; + pack.data.error = dbdata[0].error; + } + youle_agent.app.SendPack(pack); + + } + var sql = "call cp_game_update_roomcard(?,?,?);"; + youle_agent.mysql.execsql("opensql", sql, [agentid, playerid, change], db_callback); +} +//百人vip房抽成 +xRebate.gameRebate2 = function(o_room,ary_grade){ + var ary_grade1=[]; + for(var i=0;i maxgrade){ + maxgrade = ary_grade[i]; + maxseat = [i]; + } else if (ary_grade[i] == maxgrade){ + maxseat.push(i); + } + if(ary_grade[i] < 0){ + rbtTemp = true; + } + } + } + //统计总共参与游戏人数 + var ownerid = null; + if(o_room.owner){ + ownerid = o_room.owner.playerid; + } + var gameCount = 0; + for (var i = 0; i < ary_grade.length; i++) { + if (o_room.seatlist[i] && ary_grade[i] != null){ + var o_player = o_room.seatlist[i]; + //console.log("ccc",o_player.playerid,o_player.gameinfo.isbet); + if (o_player.gameinfo.isbet && ownerid != o_player.playerid) { + gameCount++; + } + } + } + if(!rbtTemp){ + return ary_grade; + } + //只支持倍数百分比抽成 + //抽成参数 + + //小局抽成设置 + //var aset_rebate = youle_room.app.youle_platform.config.method.get_paravalue("aset_rebate", o_room.o_game.o_agent.agentid); + //扣除模式 1.大赢家 2.赢家 3.所有人 + // var rebateFrom = rebateConfig.from; + var rebateFrom = 1; + var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + //配置扣除数量 + var sysRebateValue = o_room.rebateNumber; + var sysRebateValue1 = o_room.sysRebateValue; + + var agentid = o_room.o_game.o_agent.agentid; + var gameid = o_room.o_game.gameid; + var roomcode = o_room.roomcode; + var createtime = o_room.createtime; + var makewartime = o_room.makewartime; + var roommode = o_room.roommode; + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + + var asetcount = o_room.asetcount; + + var rebateTo = 0; + var rebateMode = 2; + var roomMode = 0; + + //房间收取房间费的值 + var rebateValue = sysRebateValue; + + + //每个人扣除的数量 + var rebateReal = rebateValue; + //通过扣除模式得出每个人扣的数量 + switch(rebateFrom){ + //大赢家 + case 1: + if(maxseat.length>0){ + rebateReal = parseInt(rebateValue / maxseat.length);//多个大赢家均摊房间费 + + sysRebateValue1 = parseInt(sysRebateValue1 / maxseat.length);//多个大赢家均摊房间费 + } + break; + //赢家 + case 2: + + break; + //所有人 + case 3: + + break; + default: + + break; + } + var ownergarde = 0; + var brNumber = youle_room.app.youle_platform.config.method.get_paravalue("brNumber",o_room.o_game.o_agent.agentid); + var brRebateUnit = youle_room.app.youle_platform.config.method.get_paravalue("brRebateUnit",o_room.o_game.o_agent.agentid); + var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var o_agentsvr_owner = o_agentsvr_agent.method.find_player(o_room.owner.playerid); + //var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var rebateLimit = 0; + if(o_room.advanced_type){ + if(o_room.owner){ + var o_player1 = o_agentsvr_agent.method.find_player(o_room.owner.playerid); + for (var i = 0; i < o_player1.roomcodes.length; i++) { + if (o_player1.roomcodes[i].gameid == o_room.o_game.gameid) { + if(o_player1.roomcodes[i].rebateLimit){ + rebateLimit = o_player1.roomcodes[i].rebateLimit; + } + break; + } + } + } + } + + if(!rebateLimit){ + rebateLimit = 0; + } + console.log(rebateLimit); + if(brRebateUnit){ + brRebateUnit = Number(brRebateUnit); + }else{ + brRebateUnit = 10000; + } + var brNumber1 = 0; + if(brNumber && brRebateUnit != 0){ + brNumber = Number(brNumber); + //brNumber = brNumber*(parseInt(gameCount / brRebateUnit) + 1); + //parseInt(gameCount / brRebateUnit) < 1 ? brNumber = brNumber*parseInt(gameCount / brRebateUnit) : brNumber = brNumber*(parseInt(gameCount / brRebateUnit) + 1); + if(parseInt(gameCount / brRebateUnit) > 1){ + brNumber = brNumber*parseInt(gameCount / brRebateUnit); + } + } + //console.log("gameCount",gameCount,"brRebateUnit",brRebateUnit); + if(brNumber){ + //brNumber = Number(brNumber); + //房主扣除房间费 金币 + var agentid = o_room.o_game.o_agent.agentid; + var o_player = o_agentsvr_owner; + var gameid = o_room.o_game.gameid; + var datatime = min_now(); + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_player.playerid; + msg.data.change = 0; + + msg.data.pushbean = -1 * brNumber; + msg.data.gameid = gameid; + msg.data.showrebate = 0; + youle_room.app.youle_agent[msg.rpc](msg); + var pBean = o_player.bean; + //console.log("pBean",pBean); + + + var roommode = 0; //玩家房间 + // if (o_room.sysroom) { + // roommode = 1; //系统房间 + // } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var ownerid = o_room.owner.playerid; + // var shortcode = 0; + // if (o_room.shortcode){ + // shortcode = o_room.shortcode; + // } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var playerid = o_player.playerid; + var grade = o_player.gameinfo.grade; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + var roomid = new Date().getTime() + min_randomChar(4) + o_room.roomcode; + var roomcode = o_room.roomcode; + var asetcount = o_room.asetcount; + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(playerid); + var invitecode = __o_agentsvr_player.invitecode; + var inviteid = __o_agentsvr_player.inviteid; + var marketid = __o_agentsvr_player.marketid; + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, playerid, 0, 0, 4, 2, '', 3, brNumber, brNumber, datatime, invitecode, inviteid, marketid]); + } + var rbType = o_room.rebateType; + try{ + if (o_room.shortcode){ + + if (o_agentsvr_owner.whitelist && o_agentsvr_owner.whitelist.length > 0){ + var rebateConfig = youle_room.app.youle_platform.config.method.get_paravalue("rebateConfig", o_room.o_game.o_agent.agentid); + var rCharm = 0; + var maxgrade1 = 0; + var maxseat1 = []; + var rcTemp = false; + + + if(maxseat.length>0){ + rCharm = parseInt(o_room.rebateNumber/maxseat.length); + }else{ + rCharm = parseInt(o_room.rebateNumber); + } + + // if(rebateConfig){ + // if(rebateConfig.rebateCharm && o_room.rebateType){ + // rcTemp = true; + // } + //抽金币 + // if(!o_room.rebateType){ + // reTemp = true; + // } + // } + + var _seatlist = []; + // console.log("ary_grade",ary_grade); + // console.log("333",JSON.stringify(o_agentsvr_owner.whitelist)); + // console.log("rCharm",rCharm); + for (var i = 0; i < ary_grade.length; i++) { + if (ary_grade[i] != null){ + if (o_room.seatlist[i]){ + var _player = {}; + _player.seat = i; + _player.playerid = o_room.seatlist[i].playerid; + _player.change = 0; + for (var j = 0; j < o_agentsvr_owner.whitelist.length; j++) { + if (o_agentsvr_owner.whitelist[j][0] == _player.playerid){ + if (o_agentsvr_owner.whitelist[j][2]){ + + if(rbType && ary_grade[i] == maxgrade && rebateLimit -1){ + if(config_vipConfig[vipIndex][1]){ + writeDB = false; + } + } + } + } + writeDB = true; + if(writeDB){ + var sql = "update player set play_whitelist = ? where play_agentid = ? and play_playerid = ?"; + youle_room.mysql.execsql("execsql", sql, [youle_agent.compressWhiteList(o_agentsvr_owner.whitelist), o_room.o_game.o_agent.agentid, o_room.owner.playerid]); + } + } + } + }catch(e){ + console.log("结算短号魅力值发生错误"); + console.log(e); + } + //var rebateSum = 0;//总共扣除的房间费 + //console.log("seatlist",o_room.seatlist); + + //console.log("rebateReal",rebateReal); + //console.log("sysRebateValue1",sysRebateValue1); + var pushCount = 0; + if(!o_room.rebateType){ + for (var i = 0; i < ary_grade.length; i++) { + if (o_room.seatlist[i] && ary_grade[i] != null){ + var o_player = o_room.seatlist[i]; + //console.log("ccc",o_player.playerid,o_player.gameinfo.isbet); + if (o_player.gameinfo.isbet) { + var o_agentsvr_player = o_agentsvr_agent.method.find_player(o_player.playerid); + var roomcard = 0; + if (o_player.playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + if (o_player.playerid == o_room.owner.playerid) { + ownergarde = ary_grade[i]; + } + + var isRebate = false; + + switch(rebateFrom){ + //大赢家 + case 1: + if(ary_grade[i] == maxgrade){ + isRebate = true; + } + break; + //赢家 + case 2: + if(ary_grade[i] > 0){ + isRebate = true; + } + break; + //所有人 + case 3: + isRebate = true; + break; + default: + + break; + } + //console.log("playerid:"+o_player.playerid+"grade:"+o_player.gameinfo.grade+"isRebate:"+isRebate); + //房间id = 时间戳 + 4位随机码 + 房号 + var roomid = new Date().getTime() + min_randomChar(4) + o_room.roomcode; + var datatime = min_now(); + //达成抽成条件 + if(isRebate){ + //rebateSum += rebateReal; + + // if(!roomMode){ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_player.playerid; + if(o_room.roommode){ + msg.data.change = o_player.gameinfo.grade; + }else{ + msg.data.change = 0; + } + var sysBean = 0; + if(ownerid == o_player.playerid){ + msg.data.pushbean = 0; + }else{ + //var nBean = rebateReal - msg.data.change; + //console.log("o_player.bean",o_player.bean,"nBean",nBean); + //if(o_player.bean >= nBean){ + // if(rebateLimit < (ary_grade[i] - rebateReal)){ + msg.data.pushbean = -1 * rebateReal; + // pushCount += rebateReal; + sysBean = sysRebateValue1; + pushCount = pushCount + (rebateReal - sysRebateValue1); + // }else{ + // if(ary_grade[i] > rebateLimit){ + // msg.data.pushbean = -1 * (ary_grade[i] - rebateLimit); + // if(ary_grade[i] - rebateLimit > sysRebateValue1){ + // pushCount = pushCount + (ary_grade[i] - rebateLimit - sysRebateValue1); + // sysBean = sysRebateValue1; + + // }else{ + + // } + // }else{ + // msg.data.pushbean = 0; + // } + // } + // }else{ + // msg.data.pushbean = 0; + // } + } + msg.data.gameid = gameid; + msg.data.showrebate = 0; + youle_room.app.youle_agent[msg.rpc](msg); + + var roommode = 0; //玩家房间 + if (o_room.sysroom) { + roommode = 1; //系统房间 + } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var ownerid = o_room.owner.playerid; + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var playerid = o_player.playerid; + var grade = o_player.gameinfo.grade; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + + var asetcount = o_room.asetcount; + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(playerid); + var isrobot = o_player.isrobot; + var invitecode = __o_agentsvr_player.invitecode; + var inviteid = __o_agentsvr_player.inviteid; + var marketid = __o_agentsvr_player.marketid; + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, playerid, isrobot, grade, rebateFrom, 0, '', rebateMode, sysBean, sysBean , datatime, invitecode, inviteid, marketid]); + // } + } + + } + } + } + //console.log("pushCount",pushCount); + if(pushCount){ + var ownerid = o_room.owner.playerid; + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = ownerid; + msg.data.change = 0; + + msg.data.pushbean = pushCount; + msg.data.gameid = gameid; + msg.data.showrebate = 0; + msg.data.type = 6; + youle_room.app.youle_agent[msg.rpc](msg); + + var roommode = 0; //玩家房间 + if (o_room.sysroom) { + roommode = 1; //系统房间 + } + var createtime = o_room.createtime; + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + var playerid = ownerid; + var grade = 0; + var roomcard = 0; + if (playerid == o_room.owner.playerid){ + roomcard = youle_room.import.createroom_needroomcard(o_room.o_game, o_room.roomtype); + } else { + roomcard = youle_room.import.joinroom_needroomcard(o_room.o_game, o_room.roomtype); + } + + var asetcount = o_room.asetcount; + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(playerid); + var invitecode = __o_agentsvr_player.invitecode; + var inviteid = __o_agentsvr_player.inviteid; + var marketid = __o_agentsvr_player.marketid; + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, roommode, createtime, beanmult, ownerid, shortcode, deductmode, roomcard, 0, asetcount, 0, playerid, 0, grade, rebateFrom, 1, ownerid, rebateMode, pushCount, pushCount, datatime, invitecode, inviteid, marketid]); + } + } + //解散 + if(pBean < brNumber){ + breakTemp = true; + youle_room.import.gameBreak(o_room); + } + return ary_grade1; +} \ No newline at end of file diff --git a/server/youle/server_room/class.return.js b/server/youle/server_room/class.return.js new file mode 100644 index 0000000..288b879 --- /dev/null +++ b/server/youle/server_room/class.return.js @@ -0,0 +1,181 @@ +/////////////////////////////////////////////////// +///////// cls_youle_room_return: 返回结果 ////////// +/////////////////////////////////////////////////// +var cls_youle_room_return = cls_youle_room_return || { + + new: function() { + + var re = {}; + + //创建房间成功 + re.create_room = function(pack, o_player){ + pack.route = youle_room.routename; + pack.rpc = "create_room"; + pack.data = {}; + pack.data.state = 0; + if (o_player.gameinfo.o_room){ + pack.data.roomcode = o_player.gameinfo.o_room.roomcode; + if (o_player.gameinfo.o_room.makewar){ + pack.data.makewar = o_player.gameinfo.o_room.makewar; + } else { + pack.data.makewar = o_player.gameinfo.o_game.makewar; + } + pack.data.roomtype = o_player.gameinfo.o_room.roomtype; + pack.data.asetcount = o_player.gameinfo.o_room.asetcount; + pack.data.seat = o_player.gameinfo.seat; + pack.data.infinite = o_player.gameinfo.o_room.infinite; + pack.data.needprepare = o_player.gameinfo.o_room.needprepare; + } + return pack; + }; + + //加入房间成功 + re.self_join_room = function(pack, o_player, playerbean, playercharm){ + var isbattle; //是否开战了 + var isfull; //是否满桌了 + var deskinfo; //房间已经开战的牌桌信息 + var deskwar; //房间满桌自动开战包 + var o_room = o_player.gameinfo.o_room; + if (o_room.battlestate == 1) { + isbattle = true; + deskinfo = youle_room.import.get_deskinfo(o_room, o_player.gameinfo.seat); + } else { + isfull = o_room.method.get_isfull(); + if (isfull) { + //满桌了自动开战 + o_room.battlestate = 1; + o_room.makewartime = min_now(); + //如果有返回值则后面的代码会发统一的开战包,如果没有返回值则由游戏负责发开战包 + deskwar = youle_room.import.makewar_deskwar(o_room); + } else { + //子游戏设置的开战人数 + var makewarplayercount = youle_room.import.makewar_playercount(o_room.o_game, o_room.roomtype); + if (makewarplayercount){ + var playercount = 0; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i] && o_room.seatlist[i].gameinfo.isprepare){ + playercount = playercount + 1; + } + } + if (playercount == makewarplayercount){ + isfull = true; + o_room.battlestate = 1; + o_room.makewartime = min_now(); + //如果有返回值则后面的代码会发统一的开战包,如果没有返回值则由游戏负责发开战包 + deskwar = youle_room.import.makewar_deskwar(o_room); + } + } + } + } + + pack.route = youle_room.routename; + pack.rpc = "self_join_room"; + pack.data = {}; + pack.data.state = 0; + pack.data.roomcode = o_room.roomcode; + pack.data.roomtype = o_room.roomtype; + pack.data.asetcount = o_room.asetcount; + pack.data.seat = o_player.gameinfo.seat; + pack.data.infinite = o_room.infinite; + pack.data.needprepare = o_room.needprepare; + if (o_room.makewar){ + pack.data.makewar = o_room.makewar; + } else { + pack.data.makewar = o_player.gameinfo.o_game.makewar; + } + pack.data.players = o_room.method.get_playerlist();//获取房间内的玩家列表 + //如果已开战则返回开战时的牌桌信息 + if (isbattle) { + pack.data.deskinfo = deskinfo; + } else { + if (isfull) { + if (deskwar) { + pack.data.deskwar = deskwar; + } + } + } + //通知房间内的其他人有人加入了房间 + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "other_join_room"; + msg.data = o_player.method.get_info(); + msg.data.bean = playerbean; + if (o_room.shortcode){ + if (playercharm || playercharm == 0){ + msg.data.charm = playercharm; + } + } + msg.data.seat = o_player.gameinfo.seat; + msg.data.isprepare = o_player.gameinfo.isprepare; + if (o_player.paycode){ + msg.data.paycode = o_player.paycode; + } + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i] && (i != o_player.gameinfo.seat)) { + msg.conmode = o_room.seatlist[i].conmode; + msg.fromid = o_room.seatlist[i].fromid; + if (!isbattle && isfull) { + if (deskwar) { + msg.data.deskwar = deskwar; + } + } + youle_room.app.SendPack(msg); + } + } + return pack; + }; + + //断线重连 + re.player_login = function(pack, o_player){ + pack.route = youle_room.routename; + pack.rpc = "player_login"; + pack.data = {}; + pack.data.state = 0; + pack.data.gameid = o_player.gameinfo.o_game.gameid; + pack.data.roomcode = o_player.gameinfo.o_room.roomcode; + pack.data.roomtype = o_player.gameinfo.o_room.roomtype; + pack.data.asetcount = o_player.gameinfo.o_room.asetcount; + pack.data.roommode = o_player.gameinfo.o_room.roommode; + pack.data.beanfloor = o_player.gameinfo.o_room.beanfloor; + pack.data.makewar = o_player.gameinfo.o_game.makewar; + pack.data.isbattle = o_player.gameinfo.o_room.battlestate; + pack.data.needprepare = o_player.gameinfo.o_room.needprepare; + pack.data.seat = o_player.gameinfo.seat; + pack.data.isowner = o_player.gameinfo.isowner; + pack.data.isbet = o_player.gameinfo.isbet; + pack.data.infinite = o_player.gameinfo.o_room.infinite; + //获取房间内的玩家列表 + pack.data.players = o_player.gameinfo.o_room.method.get_playerlist(); + //获取房间的申请解散状态 + pack.data.agreefree = o_player.gameinfo.o_room.method.get_agreefree(); + //获取牌局信息 + if (o_player.gameinfo.o_room.battlestate == 1){ + pack.data.deskinfo = youle_room.import.get_deskinfo(o_player.gameinfo.o_room, o_player.gameinfo.seat); + } + if (o_player.gameinfo.o_room.match_data){ + pack.data.match_data = o_player.gameinfo.o_room.match_data; + pack.data.match_roomcard = o_player.gameinfo.o_room.needroomcard; + } + //通知前端房间内的其他人有人上线了 + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "other_online"; + msg.data = {}; + msg.data.seat = o_player.gameinfo.seat; + msg.data.playerid = o_player.playerid; + msg.data.nickname = o_player.nickname; + msg.data.avatar = o_player.avatar; + msg.data.sex = o_player.sex; + msg.data.ip = o_player.ip; + o_player.gameinfo.o_room.method.sendpack_toother(msg, o_player.gameinfo.seat); + return pack; + }; + + return re; + } +} + +//返回结果 +youle_room.return = cls_youle_room_return.new(); \ No newline at end of file diff --git a/server/youle/server_room/class.room.js b/server/youle/server_room/class.room.js new file mode 100644 index 0000000..7bccac8 --- /dev/null +++ b/server/youle/server_room/class.room.js @@ -0,0 +1,443 @@ +/////////////////////////////////////////////////// +////////// cls_youle_room_room: 房间 ////////// +/////////////////////////////////////////////////// +var cls_youle_room_room = cls_youle_room_room || { + //构造函数 + new: function(o_game, roomcode, roomtype, needroomcard, asetcount){ + + var room = {}; + //基本信息 + room.o_game = o_game; //游戏 + room.roomcode = roomcode; //房号 + room.roomtype = roomtype; //房间类型 + room.needroomcard = needroomcard; //开房需要的房卡数量 + room.asetcount = asetcount; //总局数 + room.createtime = min_now(); //开房时间 + room.seatlist = []; //座位列表 + var makewarplayercount = youle_room.import.makewar_playercount(o_game, roomtype); + if (makewarplayercount){ + room.seatlist.length = makewarplayercount; + } else { + room.seatlist.length = o_game.seatcount; + } + room.battlestate = 0; //游戏状态 0:未开战 1:已开战 + room.makewartime = null; //开战时间 + room.owner = null; //房主 + room.applyfreetime = null; //申请解散的时间 + room.freetimer = null; //默认同意解散房间的定时器 + room.wasdeduct = false; //是否扣除了房主的房卡 + + //24小时后如果没有玩家在房间内则解散房间的定时器 + var do_freeroom24 = function(){ + for (var i = 0; i < room.seatlist.length; i++) { + if (room.seatlist[i]){ + return; + } + } + min_closetime(room.freetimer24); + room.freetimer24 = null; + + youle_room.export.save_grade(room, null, null, 1); + } + room.freetimer24 = min_ontime(do_freeroom24, 24 * 60 * 60 * 1000); + + //方法 + room.method = {}; + + //查找玩家 + room.method.find_player = function(playerid){ + return cls_youle_room_room.find_player(room, playerid); + } + + //获取玩家人数 + room.method.get_playercount = function(){ + return cls_youle_room_room.get_playercount(room); + } + + //获取已准备的玩家人数 + room.method.get_playercount_prepare = function(){ + return cls_youle_room_room.get_playercount_prepare(room); + } + + //获取房间内所有玩家的基本信息 + room.method.get_playerlist = function(){ + return cls_youle_room_room.get_playerlist(room); + } + + //房间是否满桌 + room.method.get_isfull = function(){ + return cls_youle_room_room.get_isfull(room); + } + + //获取房间内所有玩家的申请解散房间状态 + room.method.get_agreefree = function(){ + return cls_youle_room_room.get_agreefree(room); + } + + //给房间内某个位置的玩家发包 + room.method.sendpack_toseat = function(msg, seat){ + cls_youle_room_room.sendpack_toseat(room, msg, seat); + } + + //给房间内的其他玩家发包 + room.method.sendpack_toother = function(msg, selfseat){ + cls_youle_room_room.sendpack_toother(room, msg, selfseat); + } + + //默认同意解散倒计时 + room.method.get_freeroom_countdown = function(){ + return cls_youle_room_room.get_freeroom_countdown(room); + } + + //打开默认同意解散的定时器 + room.method.open_free_timer = function(free_now_flag){ + cls_youle_room_room.open_free_timer(room,free_now_flag); + } + + //关闭默认同意解散的定时器 + room.method.close_free_timer = function(){ + cls_youle_room_room.close_free_timer(room); + } + + //获取房间信息 + room.method.get_roominfo = function(){ + return cls_youle_room_room.get_roominfo(room); + } + + return room; + }, + + //查找玩家 + find_player: function(o_room, playerid){ + var o_player = null; + for (var i = 0; i < o_room.seatlist.length; i++){ + if (o_room.seatlist[i]){ + if (o_room.seatlist[i].playerid == playerid){ + o_player = o_room.seatlist[i]; + break; + } + } + } + return o_player; + }, + + //获取玩家人数 + get_playercount: function(o_room){ + var count = 0; + for (var i = 0; i < o_room.seatlist.length; i++){ + if (o_room.seatlist[i]){ + count++; + } + } + return count; + }, + + //获取已准备的玩家人数 + get_playercount_prepare: function(o_room){ + var count = 0; + for (var i = 0; i < o_room.seatlist.length; i++){ + if (o_room.seatlist[i] && o_room.seatlist[i].gameinfo.isprepare){ + count++; + } + } + return count; + }, + + //获取房间内所有玩家的基本信息 + get_playerlist: function(o_room){ + var playerlist = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + var player = o_room.seatlist[i].method.get_info(); + playerlist.push(player); + } else { + playerlist.push(null); + } + } + return playerlist; + }, + + //房间是否满桌 + get_isfull: function(o_room){ + for (var i = 0; i < o_room.seatlist.length; i++) { + if (!o_room.seatlist[i] || !o_room.seatlist[i].gameinfo.isprepare){ + return false; + } + } + return true; + }, + + //获取房间内所有玩家的申请解散房间状态 + get_agreefree: function(o_room){ + if (!o_room.applyfreetime){ + return null; + } + + var info = {}; + //默认同意解散倒计时 + info.countdown = o_room.method.get_freeroom_countdown(); + + var list = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + list.push(o_room.seatlist[i].gameinfo.agree); + } else { + list.push(null); + } + }; + info.state = list; + + return info; + }, + + //给房间内某个位置的玩家发包 + sendpack_toseat: function(o_room, msg, seat){ + if (seat < o_room.seatlist.length){ + if (o_room.seatlist[seat]) { + msg.conmode = o_room.seatlist[seat].conmode; + msg.fromid = o_room.seatlist[seat].fromid; + youle_room.app.SendPack(msg); + } + } + }, + + //给房间内的其他玩家发包 + sendpack_toother: function(o_room, msg, selfseat){ + // for (var i = 0; i < o_room.seatlist.length; i++) { + // if (o_room.seatlist[i] && (i != selfseat)) { + // msg.conmode = o_room.seatlist[i].conmode; + // msg.fromid = o_room.seatlist[i].fromid; + // youle_room.app.SendPack(msg); + // } + // } + var _toids_tcp = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i] && (i != selfseat)) { + if (o_room.seatlist[i].conmode && o_room.seatlist[i].fromid){ + if (o_room.seatlist[i].conmode == "tcp"){ + _toids_tcp.push(o_room.seatlist[i].fromid); + } else { + msg.conmode = o_room.seatlist[i].conmode; + msg.fromid = o_room.seatlist[i].fromid; + youle_room.app.SendPack(msg); + } + } + } + } + + if (_toids_tcp.length > 0){ + msg.conmode = "tcp"; + youle_room.app.SendPack(msg, _toids_tcp); + } + }, + + //默认同意解散房间的倒计时时间 + get_freeroom_countdown: function(o_room){ + var apply = new Date(o_room.applyfreetime); + var now = new Date(); + return o_room.o_game.o_agent.freeroom - parseInt((now.getTime() - apply.getTime()) / 1000); + }, + + //打开默认同意解散的定时器 + open_free_timer: function(o_room,free_now_flag){ + var do_freeroom = function(){ + o_room.freetimer = null; + + for (var i = 0; i < o_room.seatlist.length; i++){ + if (o_room.seatlist[i]){ + if (o_room.seatlist[i].gameinfo.agree == -1){ + o_room.seatlist[i].gameinfo.agree = 1; + } + } + } + + //通知代理商服务器解散房间 + var o_game = o_room.o_game; + var o_agent = o_game.o_agent; + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "free_room"; + msg1.data = {}; + msg1.data.agentid = o_agent.agentid; + msg1.data.gameid = o_game.gameid; + msg1.data.roomcode = o_room.roomcode; + //玩家列表 + msg1.data.playeridlist = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + msg1.data.playeridlist.push(o_room.seatlist[i].playerid); + } + } + + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + youle_room.app.youle_agent[msg1.rpc](msg1); + //通知前端房间内的所有人解散房间 + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "free_room"; + msg2.data = {}; + //获取房间内各玩家的解散房间状态 + if(!free_now_flag){ + msg2.data.seats = o_room.method.get_agreefree().state; + }else{ + msg2.data.freeNow = 1; + msg2.data.tips = "房间玩家存在恶意行为,房间已由房主强制解散"; + } + + //获取游戏的解散包 + // if (o_room.battlestate == 1){ + // msg2.data.deskfree = youle_room.import.get_disbandRoom(o_room); + // } + // o_room.method.sendpack_toother(msg2, -1); + + if (o_room.battlestate == 1){ + var _deskfree = youle_room.import.get_disbandRoom(o_room); + if (_deskfree){ + if (_deskfree.sendtype && _deskfree.sendtype == 1 && _deskfree.seatlist){ + //子游戏返回的解散json数据如果有sendtype属性且值等于1则表示需要向不同的玩家各自发包,且根据seatlist数组对应给各位置的玩家发游戏解散包 + for (var n = 0; n < _deskfree.seatlist.length; n++) { + if (_deskfree.seatlist){ + msg2.data.deskfree = _deskfree.seatlist[n]; + o_room.method.sendpack_toseat(msg2, n); + } + } + } else { + msg2.data.deskfree = _deskfree; + o_room.method.sendpack_toother(msg2, -1); + } + } else { + o_room.method.sendpack_toother(msg2, -1); + } + } else { + o_room.method.sendpack_toother(msg2, -1); + } + + //释放房间 + o_game.method.free_room(o_room); + } else { + //房间服务器与代理商服务器不是同一地址 + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg1); + cfg.success = function(str){ + var _info = {}; + //获取房间内各玩家的解散房间状态 + _info.seats = o_room.method.get_agreefree().state; + + //获取游戏的解散包 + // if (o_room.battlestate == 1){ + // _info.deskfree = youle_room.import.get_disbandRoom(o_room); + // } + + var _deskfree = null; + if (o_room.battlestate == 1){ + _deskfree = youle_room.import.get_disbandRoom(o_room); + } + + //通知前端房间内的所有人需要断开房间服务器,连接代理服务器 + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]) { + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "connect_agentserver"; + msg2.data = {}; + msg2.data.agentserver = o_agent.server + ":" + o_agent.tcpport; + msg2.data.data = {}; + msg2.data.data.opt = "free_room"; + msg2.data.data.playerid = o_room.seatlist[i].playerid; + msg2.data.data.agentid = o_agent.agentid; + msg2.data.data.gameid = o_game.gameid; + + if (_deskfree){ + if (_deskfree.sendtype && _deskfree.sendtype == 1 && _deskfree.seatlist){ + //子游戏返回的解散json数据如果有sendtype属性且值等于1则表示需要向不同的玩家各自发包,且根据seatlist数组对应给各位置的玩家发游戏解散包 + _info.deskfree = _deskfree.seatlist[i]; + } else { + _info.deskfree = _deskfree; + } + } + + msg2.data.data.info = _info; + msg2.conmode = o_room.seatlist[i].conmode; + msg2.fromid = o_room.seatlist[i].fromid; + //此处conmode = null是为了避免触发玩家离线通知其他玩家事件 + o_room.seatlist[i].conmode = null; + o_room.seatlist[i].fromid = null; + youle_room.app.SendPack(msg2); + } + } + //释放房间 + o_game.method.free_room(o_room); + }; + cfg.error = function(str){}; + min_http(cfg); + } + } + + if (!free_now_flag){ + o_room.freetimer = min_ontimeout(do_freeroom, o_room.o_game.o_agent.freeroom * 1000); + } else { + do_freeroom(); + } + }, + + //关闭默认同意解散的定时器 + close_free_timer: function(o_room){ + min_closetime(o_room.freetimer); + o_room.freetimer = null; + }, + + //获取房间信息 + get_roominfo: function(o_room){ + var info = {}; + info.roomtype = o_room.roomtype; //房间类型 + info.needroomcard = o_room.needroomcard; //开房需要的房卡数量 + info.asetcount = o_room.asetcount; //总局数 + info.createtime = o_room.createtime; //开房时间 + info.makewartime = o_room.makewartime; //开战时间 + info.battlestate = o_room.battlestate; //游戏状态 0:未开战 1:已开战 + info.wasdeduct = o_room.wasdeduct; //是否扣除了房主的房卡 + info.roommode = o_room.roommode; //是否是星星场 + info.beanfloor = o_room.beanfloor; //星星下限 + info.beanmult = o_room.beanmult; //星星场倍数 + info.needprepare = o_room.needprepare; //是否需要准备 + info.advanced_type = o_room.advanced_type; //是否高级房间 + info.advanced_teabean = o_room.advanced_teabean; //茶水费 + info.infinite = o_room.infinite; //是否无限场 + //房主id + if (o_room.owner){ + info.ownerid = o_room.owner.playerid; + } else { + info.ownerid = null; + } + //玩家列表 + info.seatlist = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (!o_room.seatlist[i]){ + info.seatlist.push(null); + } else { + var p = {}; + p.playerid = o_room.seatlist[i].playerid; + p.nickname = o_room.seatlist[i].nickname; + p.onstate = o_room.seatlist[i].onstate; //在线状态 null未连接 0在线 1离线 2电话中 + p.needroomcard = o_room.seatlist[i].needroomcard; + p.wasdeduct = o_room.seatlist[i].wasdeduct; + p.bean = o_room.seatlist[i].bean; + p.isowner = o_room.seatlist[i].gameinfo.isowner; + p.isbet = o_room.seatlist[i].gameinfo.isbet; + p.grade = o_room.seatlist[i].gameinfo.grade; + p.isprepare = o_room.seatlist[i].gameinfo.isprepare; + var _agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var _agentsvr_player = _agentsvr_agent.method.find_player(o_room.seatlist[i].playerid); + p.unionid = _agentsvr_player.unionid; + info.seatlist.push(p); + } + } + return info; + } +} diff --git a/server/youle/server_room/class.tcplist.js b/server/youle/server_room/class.tcplist.js new file mode 100644 index 0000000..5825c5f --- /dev/null +++ b/server/youle/server_room/class.tcplist.js @@ -0,0 +1,81 @@ +/////////////////////////////////////////////////// +//////////////cls_youle_room_tcplist: tcp连接列表类/////////// +/////////////////////////////////////////////////// +var cls_youle_room_tcplist = cls_youle_room_tcplist || { + new: function() { + var tcplist = {}; + tcplist.list = []; + + //方法 + tcplist.method = {}; + + //载入tcp连接 + tcplist.method.load_tcp = function(str_tcpid, o_player){ + return cls_youle_room_tcplist.load_tcp(tcplist, str_tcpid, o_player); + } + + //查找tcp连接 + tcplist.method.find_tcp = function(str_tcpid){ + return cls_youle_room_tcplist.find_tcp(tcplist, str_tcpid); + } + + //删除tcp连接 + tcplist.method.delete_tcp = function(str_tcpid){ + return cls_youle_room_tcplist.delete_tcp(tcplist, str_tcpid); + } + + return tcplist; + }, + + //载入tcp连接 + load_tcp: function(o_tcplsit, str_tcpid, o_player){ + var idx = cls_youle_room_tcplist.aryidx(str_tcpid); + if (idx == -1){ + return null; + } + if (o_tcplsit.list.length <= idx){ + o_tcplsit.list.length = idx + 1; + } + var new_tcp = {}; + new_tcp.tcpid = str_tcpid; + new_tcp.o_player = o_player; + o_tcplsit.list[idx] = new_tcp; + return new_tcp; + }, + + //查找tcp连接 + find_tcp: function(o_tcplsit, str_tcpid){ + var idx = cls_youle_room_tcplist.aryidx(str_tcpid); + if (idx == -1){ + return null; + } + if (o_tcplsit.list.length <= idx){ + return null; + } + return o_tcplsit.list[idx]; + }, + + //删除tcp连接 + delete_tcp: function(o_tcplsit, str_tcpid){ + var idx = cls_youle_room_tcplist.aryidx(str_tcpid); + if (idx == -1){ + return; + } + if (o_tcplsit.list.length <= idx){ + return; + } + o_tcplsit.list[idx] = null; + }, + + //计算tcp连接的数组下标 + aryidx: function(str_tcpid){ + var idx = parseInt(str_tcpid.replace("tcp_", "")); + if (isNaN(idx)){ + return -1; + } + return idx - 10000; + } +} + +//tcp连接列表 +youle_room.tcps = cls_youle_room_tcplist.new(); \ No newline at end of file diff --git a/server/youle/server_room/mod.js b/server/youle/server_room/mod.js new file mode 100644 index 0000000..0fb182c --- /dev/null +++ b/server/youle/server_room/mod.js @@ -0,0 +1,54 @@ +////////////////////////////////////////////////////////////////// +//////////////////////////游戏平台服务器////////////////////////// +////////////////////////////////////////////////////////////////// +//rpc +min_loadJsFile("youle/server_room/rpc.js", function(){ +//tcp连接 +min_loadJsFile("youle/server_room/class.tcplist.js", function(){ +//运营商 +min_loadJsFile("youle/server_room/class.agent.js", function(){ +//游戏 +min_loadJsFile("youle/server_room/class.game.js", function(){ +//房间 +min_loadJsFile("youle/server_room/class.room.js", function(){ +//玩家 +min_loadJsFile("youle/server_room/class.player.js", function(){ +//对内的输入接口类 +min_loadJsFile("youle/server_room/class.import.js", function(){ +//对内的输出接口类 +min_loadJsFile("youle/server_room/class.export.js", function(){ +//返回结果类 +min_loadJsFile("youle/server_room/class.return.js", function(){ +//数据备份类 +min_loadJsFile("youle/server_room/class.backup.js", function(){ +//抽成系统 +min_loadJsFile("youle/server_room/class.rebate.js", function(){ +}); +}); +}); +}); +}); +}); +}); +}); +}); +}); +}); + +var youle_room = youle_room || cls_mod.new("youle_room", "room", youle_app); + +//运营商列表 +youle_room.agents = null; +//tcp连接 +youle_room.tcps = null; +//mysql操作类 +youle_room.mysql = cls_mysql.new("http://ylyxdata.daoqi88.cn/mysql.php", youle_room.app.SendPack); +youle_room.mysql_grade = cls_mysql.new("http://ylyxdata.daoqi88.cn/mysql_grade.php", youle_room.app.SendPack); +//对内的输入接口类(需要的外部接口) +youle_room.import = null; +//对内的输出接口类(提供给外部使用的接口) +youle_room.export = null; +//返回结果类 +youle_room.return = null; +//数据备份类 +youle_room.backup = null; diff --git a/server/youle/server_room/rpc.js b/server/youle/server_room/rpc.js new file mode 100644 index 0000000..60a99a7 --- /dev/null +++ b/server/youle/server_room/rpc.js @@ -0,0 +1,4019 @@ +////////////////////////////////////////////////////////////////// +//////////////////////////游戏平台服务器////////////////////////// +////////////////////////////////////////////////////////////////// + +//玩家登录(断线重连) +youle_room.player_login = function(pack){ + var playerid = parseInt(pack.data.playerid); + var nickname = pack.data.nickname; + var avatar = pack.data.avatar; + var sex = parseInt(pack.data.sex); + var ip = pack.data.ip; + var onstate = 0; + var conmode = pack.data.conmode; + var fromid = pack.data.fromid; + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + try{ + //纠错 + var _agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var _agentsvr_player = _agentsvr_agent.method.find_player(playerid); + _agentsvr_player.gameinfo[o_game.idx].roomcode = null; + console.log(min_now() + "纠正玩家在房间里的错误,agentid:"+agentid+",gameid:"+gameid+",playerid:"+playerid+",nickname:"+nickname+",roomcode:"+roomcode); + } + catch(e){} + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + try{ + //纠错 + var _agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var _agentsvr_player = _agentsvr_agent.method.find_player(playerid); + _agentsvr_player.gameinfo[o_game.idx].roomcode = null; + console.log(min_now() + "纠正玩家在房间里的错误,agentid:"+agentid+",gameid:"+gameid+",playerid:"+playerid+",nickname:"+nickname+",roomcode:"+roomcode); + } + catch(e){} + return; + } + if (o_room.owner.playerid == o_player.playerid){ + o_player.gameinfo.isowner = 1; + } else { + o_player.gameinfo.isowner = 0; + } + //返回结果 + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + o_player.ip = ip; + o_player.onstate = 0; + o_player.conmode = conmode; + o_player.fromid = fromid; + if (pack.data.location) { + o_player.location = pack.data.location; + } + + //更新tcp连接列表 + if (o_player.conmode == "tcp"){ + youle_room.tcps.method.load_tcp(o_player.fromid, o_player); + } + + pack = youle_room.return.player_login(pack, o_player); + } else { + //房间服务器与代理商服务器不是同一地址 + pack.data = {}; + pack.data.roomtype = o_player.gameinfo.o_room.roomtype; + pack.data.seat = o_player.gameinfo.seat; + if (o_player.gameinfo.o_room.match_data){ + pack.data.match_data = o_player.gameinfo.o_room.match_data; + pack.data.match_roomcard = o_player.gameinfo.o_room.needroomcard; + } + } + pack.data.shortcode = o_room.shortcode; + if(o_room.videoConfig){ + pack.data.videoConfig = o_room.videoConfig; + } + if(o_room.rebateMode){ + pack.data.rebateMode = o_room.rebateMode; + } + if(o_room.rebateNumber){ + pack.data.rebateNumber = o_room.rebateNumber; + } + pack.data.rebateType = o_room.rebateType; + pack.data.location = o_player.location || null; + if (o_room.match_id){ + pack.data.match_id = o_room.match_id; + pack.data.match_idx = o_room.match_idx; + } + //房主公告 + if(o_room.shortcode){ + var o_agent1 = youle_agent.agents.method.find_agent(agentid); + if (o_agent1){ + var rowner = o_agent1.method.find_player(o_room.owner.playerid); + pack.data.ownerNotice = rowner.notice; + + } + + } + return pack; +} + +//玩家离线 +youle_room.OnTcpDisConnected = function(str_tcpid){ + var o_tcp = youle_room.tcps.method.find_tcp(str_tcpid); + if (!o_tcp){ + return; + } + //找到了 + var o_player = o_tcp.o_player; + if (o_player && o_player.fromid == str_tcpid){ + var o_room = o_player.gameinfo.o_room; + //修改玩家状态为离线 + o_player.onstate = 1; + o_player.conmode = null; + o_player.fromid = null; + //通知房间内的其他人有人离线了 + if (o_room) { + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "other_offline"; + msg.data = {}; + msg.data.seat = o_player.gameinfo.seat; + o_room.method.sendpack_toother(msg, o_player.gameinfo.seat); + } + } + //更新tcp连接列表 + youle_room.tcps.method.delete_tcp(str_tcpid); + + // //遍历运营商 + // for (var i = 0; i < youle_room.agents.list.length; i++) { + // var o_agent = youle_room.agents.list[i]; + // if (!o_agent){ + // continue; + // } + // //遍历游戏 + // for (var j = 0; j < o_agent.games.length; j++) { + // var o_game = o_agent.games[j]; + // if (!o_game) { + // continue; + // } + // //遍历房间 + // for (var k = 0; k < o_game.rooms.length; k++) { + // var o_room = o_game.rooms[k]; + // if (!o_room) { + // continue; + // } + // //遍历座位 + // for (var m = 0; m < o_room.seatlist.length; m++) { + // var o_player = o_room.seatlist[m]; + // if (!o_player) { + // continue; + // } + // //找到了 + // if (o_player.fromid == str_tcpid) { + // //修改玩家状态为离线 + // o_player.onstate = 1; + // o_player.conmode = null; + // o_player.fromid = null; + // //通知房间内的其他人有人离线了 + // if (o_room) { + // var msg = {}; + // msg.app = youle_room.app.appname; + // msg.route = youle_room.routename; + // msg.rpc = "other_offline"; + // msg.data = {}; + // msg.data.seat = o_player.gameinfo.seat; + // o_room.method.sendpack_toother(msg, o_player.gameinfo.seat); + // } + // return; + // } + // } + // } + // } + // } +} + +youle_room.robotcount_rate = 80; +youle_room.robotcount_rand = 3; +youle_room.robotcount_rand_max = 3; +youle_room.robotcount_rand_min = 3; +youle_room.robotcount_leave = 0; +//创建房间 +youle_room.create_room = function(pack){ + var agentid = pack.data.agent.agentid; + var agentname = pack.data.agent.name; + var agentserver = pack.data.agent.server; + var agenttcpport = parseInt(pack.data.agent.tcpport); + var agenthttpport = parseInt(pack.data.agent.httpport); + var agentfreeroom = parseInt(pack.data.agent.freeroom); + + var gameid = pack.data.game.gameid; + var gamename = pack.data.game.name; + var seatcount = parseInt(pack.data.game.seatcount); + var makewar = parseInt(pack.data.game.makewar); + var modename = pack.data.game.modename; + var roomserver = pack.data.game.server; + var roomtcpport = parseInt(pack.data.game.tcpport); + var roomhttpport = parseInt(pack.data.game.httpport); + var minroomcode = parseInt(pack.data.game.minroomcode); + var maxroomcode = parseInt(pack.data.game.maxroomcode); + + var playerid = parseInt(pack.data.player.playerid); + var nickname = pack.data.player.nickname; + var avatar = pack.data.player.avatar; + var sex = parseInt(pack.data.player.sex); + var playertype = parseInt(pack.data.player.playertype); + var ip = null; + var onstate = null; + var conmode = null; + var fromid = null; + var address = pack.data.player.address; + var latitude = pack.data.player.latitude; + var longitude = pack.data.player.longitude; + var location = pack.data.location || null; + var playerbean = parseInt(pack.data.player.bean); + + var roomcode = parseInt(pack.data.room.roomcode); + var roomtype = pack.data.room.roomtype; + var needroomcard = parseInt(pack.data.room.needroomcard); + var asetcount = parseInt(pack.data.room.asetcount); + var infinite = parseInt(pack.data.room.infinite); + var roommode = parseInt(pack.data.room.roommode); + var beanfloor = null; + if(pack.data.room.beanfloor){ + beanfloor = parseInt(pack.data.room.beanfloor); + } + + var beanmult = parseInt(pack.data.room.beanmult); + var needprepare = parseInt(pack.data.room.needprepare); + var advanced_type = parseInt(pack.data.room.advanced_type); + var advanced_teabean = parseInt(pack.data.room.advanced_teabean); + var shortcode = parseInt(pack.data.room.shortcode); + var rebateLimit = pack.data.room.rebateLimit; + var sysroom = 0; + if (pack.data.room.sysroom) { + sysroom = 1; + } + var isFree = 0; + if(pack.data.room.isFree){ + isFree = 1; + } + //视频房 + var videoConfig = pack.data.room.videoConfig; + var rebateType = pack.data.room.rebateType; + + //载入代理商(没找到则创建) + var o_agent = youle_room.agents.method.load_agent(agentid, agentname, agentserver, agenttcpport, agenthttpport, agentfreeroom); + //载入游戏(没找到则创建) + var o_game = o_agent.method.load_game(gameid, gamename, seatcount, makewar, modename, roomserver, roomtcpport, roomhttpport, minroomcode, maxroomcode); + var rebateConfig = youle_room.app.youle_platform.config.method.get_paravalue("rebateConfig", o_game.o_agent.agentid); + var rebateMode = 0; + var rebateNumber = 0; + var sysRebateValue = 0; + //var rCardNumber = 0; + //小局抽成设置 + if(infinite){ + //小局抽成设置 + var aset_rebate = youle_room.app.youle_platform.config.method.get_paravalue("aset_rebate", o_game.o_agent.agentid); + if(aset_rebate){ + config = aset_rebate.infinite; + if(config[0] == 3 && config[1] == 2){ + rebateMode = 3; + sysRebateValue = parseInt(config[2]*beanmult/100); + } + + } + } + //if(gameid == "Btke0urRy0cvPd5CIvD5yfhYhKhdSdex" && advanced_type){ + //百人场的vip房房间费不去判断系统设置的最低值 + if(false){ + }else{ + if(rebateConfig){ + rebateMode = rebateConfig.from; + //配置扣除数量 + if(!infinite){ + sysRebateValue = parseInt(rebateConfig.value * beanmult /100); + } + if(!roommode){ + if(!advanced_type){ + + }else{ + //vip房卡房 + // if(beanfloor == null){ + // beanfloor = Math.max(advanced_teabean,sysRebateValue); + + // }else{ + // beanfloor = Math.max(advanced_teabean,sysRebateValue,beanfloor); + // } + // rebateNumber = Math.max(advanced_teabean,sysRebateValue); + rebateMode = 1; + rebateNumber = advanced_teabean; + } + }else{ + if(!advanced_type){ + + var tea = youle_room.import.owner_beanpush(o_game, roomtype); + if(tea){ + if(beanfloor == null){ + beanfloor = Math.max(tea,sysRebateValue); + }else{ + beanfloor = Math.max(tea,sysRebateValue,beanfloor); + } + rebateNumber = Math.max(tea,sysRebateValue); + }else{ + if(beanfloor == null){ + beanfloor = sysRebateValue; + }else{ + beanfloor = Math.max(sysRebateValue,beanfloor); + } + rebateNumber = sysRebateValue; + } + + }else{ + if(beanfloor == null){ + beanfloor = Math.max(advanced_teabean,sysRebateValue); + rebateNumber = beanfloor; + }else{ + beanfloor = Math.max(advanced_teabean,sysRebateValue,beanfloor); + rebateNumber = Math.max(advanced_teabean,sysRebateValue); + //beanfloor = Math.max(advanced_teabean,sysRebateValue,beanfloor); + //rebateNumber = Math.max(advanced_teabean,sysRebateValue); + } + } + } + } + } + // //检查代理商id是否存在 + // var o_agent1 = youle_agent.agents.method.find_agent(agentid); + // //检查玩家是否存在 + // var o_player1 = o_agent1.method.find_player(playerid); + // var starName = youle_agent.app.youle_platform.config.method.get_paravalue("starName", agentid); + // if (!starName){ + // starName = "星星"; + // } + // if(o_player1.bean < beanfloor){ + // var msg1={}; + // msg1.data = {}; + // msg1.data.state = 1; + // msg1.data.state = 1; + // msg1.data.error = starName + "数量不够"; + // msg1.data.showerror = 1; + // return msg1; + // } + + + //检查玩家是否有权限创建房间(定位权限等) + var createroom_permission = youle_room.import.check_createroom_permission(o_game, pack.data); + if (createroom_permission && !createroom_permission.allowed){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = createroom_permission.message || "无法创建房间"; + pack.data.showerror = 1; + return pack; + } + + //创建房间 + var o_room = o_game.method.create_room(roomcode, roomtype, needroomcard, asetcount); + // advanced_teabean 房间费 advanced_type = 1//短号房 + + + + o_room.roommode = roommode; + + o_room.beanmult = beanmult; + o_room.needprepare = needprepare; + o_room.advanced_type = advanced_type; + + o_room.shortcode = shortcode; + o_room.sysroom = sysroom; + o_room.videoConfig = videoConfig; + + + //o_room.rCardNumber = rCardNumber; + o_room.rebateNumber = rebateNumber; + o_room.rebateMode = rebateMode; + o_room.advanced_teabean = advanced_teabean; + o_room.beanfloor = beanfloor; + o_room.rebateType = rebateType; + o_room.sysRebateValue = sysRebateValue; + + o_room.isFree = isFree; + o_room.rebateLimit = rebateLimit; + //机器人金币范围 + if(pack.data.robot){ + o_room.robotGoldRange = pack.data.robot.robotGoldRange; + } + if (!advanced_type){ + if (o_room.freetimer24){ + min_closetime(o_room.freetimer24); + o_room.freetimer24 = null; + } + } + + var room_config = youle_room.app.youle_platform.config.method.get_paravalue("room_config", o_agent.agentid, o_game.gameid); + if (!room_config){ + room_config = {}; + room_config.rate = 80; + room_config.value = 300; + } + var ran = min_random(1, 100); + if (ran <= room_config.rate){ + o_room.control_aset = min_random(3, o_room.asetcount - 2); + o_room.control_value = room_config.value; + } + if (pack.data.room.match_id){ + o_room.match_id = pack.data.room.match_id; + o_room.match_idx = pack.data.room.match_idx; + } + if (pack.data.room.match_makewar){ + o_room.makewar = parseInt(pack.data.room.match_makewar); + } + if (pack.data.room.match_data){ + o_room.match_data = pack.data.room.match_data; + } + o_room.infinite = infinite; + if (o_room.infinite){ + o_room.asetcount = 100000000; + if (o_room.sysroom){ + var haverobot = youle_room.app.youle_platform.config.method.get_paravalue("haverobot", agentid, gameid); + // var forbidEnter = 0; + // var ranMin = 0; + // var ranMax = 0; + // var roomRate = 0; + // var roomLeave = 0; + if(haverobot == 1){ + var sysRoomConfig = youle_room.app.youle_platform.config.method.get_paravalue("system_room", agentid, gameid); + if(sysRoomConfig){ + for(var i=0;i max_playerid){ + max_playerid = o_room.seatlist[i].playerid; + } + } + playeridlist.push(o_room.seatlist[i].playerid); + playernamelist.push(o_room.seatlist[i].nickname); + playeravatarlist.push(o_room.seatlist[i].avatar); + } + } + + if (playercount + robotcount < o_room.seatlist.length * roomRate / 100) { + + var rand; + if(roomLeave){ + var empCount = o_room.seatlist.length - playercount - robotcount; + rand = min_random(ranMin, ranMax); + if(empCount <= rand){ + if(playercount == 0){ + rand = empCount - 1; + }else{ + rand = empCount; + } + + } + // var empCount = o_room.seatlist.length - playercount - robotcount; + // rand = min_random(1, youle_room.robotcount_rand); + // if(empCount - youle_room.robotcount_leave > 0){ + // if(rand > empCount - youle_room.robotcount_leave){ + // rand = empCount - youle_room.robotcount_leave; + // } + // }else{ + // return; + // } + + // if (playercount > 0){ + // rand = min_random(1, youle_room.robotcount_rand); + // if (rand > o_room.seatlist.length - playercount - robotcount){ + // rand = o_room.seatlist.length - playercount - robotcount; + // } + // } else { + // rand = min_random(1, 3); + // rand = o_room.seatlist.length - playercount - robotcount - rand; + // } + }else{ + // if (playercount > 0){ + rand = min_random(ranMin, ranMax); + if (rand > o_room.seatlist.length - playercount - robotcount){ + rand = o_room.seatlist.length - playercount - robotcount; + } + // } else { + // rand = min_random(1, 3); + // rand = o_room.seatlist.length - playercount - robotcount - rand; + // } + } + /* + if (playercount >= youle_room.robotcount_leave){ + rand = min_random(1, youle_room.robotcount_rand); + if (rand > o_room.seatlist.length - playercount - robotcount){ + rand = o_room.seatlist.length - playercount - robotcount; + } + } else { + // var empCount = o_room.seatlist.length - playercount - robotcount; + // rand = min_random(1, youle_room.robotcount_rand); + if(empCount > youle_room.robotcount_leave){ + // rand = min_random(1, empCount - youle_room.robotcount_leave); + rand = min_random(1, youle_room.robotcount_rand); + }else{ + rand = 0; + } + }*/ + // console.log("p:"+playercount+" r:"+robotcount+" rate:"+youle_room.robotcount_rate+"rand:"+rand); + for (var k = 1; k <= rand; k++) { + min_playerid = min_playerid - 1000; + if (min_playerid < 100000) { + min_playerid = 100000; + } + max_playerid = max_playerid + 1000; + if (max_playerid > 999999) { + max_playerid = 999999; + } + + var robot_playerid = null; + for (var i = 1; i < 10; i++) { + robot_playerid = min_random(min_playerid, max_playerid); + //机器人id从200000到899999随机取 + robot_playerid = min_random(200000, 899999); + if (min_ary_indexof(playeridlist, robot_playerid) > -1){ + robot_playerid = null; + } else { + break; + } + }; + if (robot_playerid == null){ + return; + } + + var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + var idx; + var robot_nickname = null; + var robot_avatar = null; + var robot_sex = 1; + + if(o_agentsvr_agent.robotNicknames){ + o_agentsvr_agent.robotArray.sort( + function () { + return 0.5 - Math.random(); + }); + var rid = 0; + + for(var i=0;i -1){ + robot_nickname = null; + } else { + break; + } + } + + if (robot_nickname == null){ + return; + } + + robot_avatar = null; + var pid = rid+100; + robot_avatar = "https://projectimage.tscce.cn/image_132/"+pid+".jpg"; + // var robot_sex = 1; + // if(rid<=788){ + // robot_sex = 2; + // } + if(rid%4 == 0){ + robot_sex = 2; + } + //console.log("1create",rid,robot_sex); + }else{ + for (var i = 1; i < 10; i++) { + idx = min_random(0, o_agentsvr_agent.robot_nicknamelist.length - 1); + robot_nickname = o_agentsvr_agent.robot_nicknamelist[idx]; + if (min_ary_indexof(playernamelist, robot_nickname) > -1){ + robot_nickname = null; + } else { + break; + } + } + if (robot_nickname == null){ + return; + } + + for (var i = 1; i < 10; i++) { + idx = min_random(0, o_agentsvr_agent.robot_avatarlist.length - 1); + robot_avatar = o_agentsvr_agent.robot_avatarlist[idx]; + if (min_ary_indexof(playeravatarlist, robot_avatar) > -1){ + robot_avatar = null; + } else { + break; + } + } + //console.log("2create"); + } + if(deviceLogin){ + robot_nickname = "ID" + robot_playerid; + } + // if (robot_avatar == null){ + // return; + // } + + + //var robot_sex = min_random(0, 1); + var robot_ip = min_random(10, 240) + "." + min_random(10, 240) + "." + min_random(10, 240) + "." + min_random(10, 240); + //读取是否纯机器人保存金币变动 + // var o_cfg = youle_agent.app.youle_platform.config; + // var robotGoldRange = o_cfg.method.get_paravalue("robotGoldRange", agentid, gameid); + var robotGoldRange = o_room.robotGoldRange; + var robot_bean; + if(!robotGoldRange){ + robot_bean = o_room.beanfloor * min_random(3, 6) + min_random(0, o_room.beanfloor); + robot_bean = parseInt((robot_bean + playerbeansum) / (playercount + 1)); + }else{ + robot_bean = min_random(robotGoldRange[0],robotGoldRange[1]); + } + //console.log("robotGoldRange:"+robotGoldRange); + //console.log("robot_bean:"+robot_bean); + var o_robot = cls_youle_room_player.new(robot_playerid, robot_nickname, robot_avatar, robot_sex, 0, robot_ip, 0, null, null); + o_robot.bean = robot_bean; + o_robot.isrobot = 1; + + //处理机器人加入位置问题 + // o_room.robotAva = []; + // for (var i = 0; i < o_room.seatlist.length; i++) { + // if (!o_room.seatlist[i]){ + // o_room.robotAva.push(0); + + // }else{ + // o_room.robotAva.push(1); + // } + // } + // console.log("#####"+o_room.roomcode+"#####"); + //console.log("==============================="); + var empSeatList = []; + var seat0Emp = false; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (!o_room.seatlist[i]) { + empSeatList.push(i); + if(i==0){ + seat0Emp = true; + } + } + } + if(empSeatList.length >= 1){ + var sRam = min_random(0,empSeatList.length-1); + var sindx = empSeatList[sRam]; + if(seat0Emp){ + sindx = 0; + } + if(sindx >= 0){ + if((!o_room.battlestate && forbidEnter) || !forbidEnter){ + //if (!o_room.robotAva[i]){ + //console.log("之前",o_room.robotAva,o_room.roomcode,i,o_robot,robot_bean); + //o_room.robotAva[i] = 1; + //robot_enter(o_room,o_robot,robot_bean); + // console.log(o_room.roomcode + "加入机器人" + robot_playerid); + o_robot.method.enter_room(o_room, sindx, 0); + if (o_room.battlestate){ + youle_room.import.player_enter(o_room, sindx); + } + youle_room.return.self_join_room({}, o_robot, robot_bean); + playeridlist.push(o_robot.playerid); + playernamelist.push(o_robot.nickname); + playeravatarlist.push(o_robot.avatar); + } + } + } + /* + for (var i = 0; i < o_room.seatlist.length; i++) { + if (!o_room.seatlist[i]){ + if((!o_room.battlestate && forbidEnter) || !forbidEnter){ + //if (!o_room.robotAva[i]){ + //console.log("之前",o_room.robotAva,o_room.roomcode,i,o_robot,robot_bean); + //o_room.robotAva[i] = 1; + //robot_enter(o_room,o_robot,robot_bean); + // console.log(o_room.roomcode + "加入机器人" + robot_playerid); + o_robot.method.enter_room(o_room, i, 0); + if (o_room.battlestate){ + youle_room.import.player_enter(o_room, i); + } + youle_room.return.self_join_room({}, o_robot, robot_bean); + playeridlist.push(o_robot.playerid); + playernamelist.push(o_robot.nickname); + playeravatarlist.push(o_robot.avatar); + break; + } + } + }*/ + } + } + }catch(e){} + } + //机器人加入房间 + var robot_enter = function(o_room,o_robot,robot_bean){ + min_ontimeout(function(){ + // //console.log("添加机器人",o_room.robotAva,o_room.roomcode,i,o_robot,robot_bean); + if(!o_room.seatlist[i]){ + //console.log("添加",o_room.robotAva,o_room.roomcode,i,o_robot,robot_bean); + o_robot.method.enter_room(o_room, i, 0); + if (o_room.battlestate){ + youle_room.import.player_enter(o_room, i); + } + youle_room.return.self_join_room({}, o_robot, robot_bean); + }else{ + console.log("冲突",o_room.robotAva,o_room.roomcode,i,o_robot,robot_bean); + } + // for (var i = 0; i < o_room.seatlist.length; i++) { + // if (!o_room.seatlist[i]){ + // o_robot.method.enter_room(o_room, i, 0); + // if (o_room.battlestate){ + // youle_room.import.player_enter(o_room, i); + // } + // youle_room.return.self_join_room({}, o_robot, robot_bean); + // break; + // } + // } + + },Math.random()*4*1000); + } + //if(!o_room.timer_robot){ + //console.log("======"+o_room.roomcode+"======"); + o_room.timer_robot = min_ontime(do_robot, 6 * 1000); + + if (o_room.timer_robot == 12){ + //很奇怪,此处定时器ID等于12时只会执行两次 + min_closetime(o_room.timer_robot); + //console.log("======"+o_room.roomcode+"======"); + o_room.timer_robot = min_ontime(do_robot, 6 * 1000); + } + //} + + //机器人主动撩 + var do_robot_active = function(){ + var rand = min_random(1, 100); + if (rand <= youle_room.robotadmin.active.text + youle_room.robotadmin.active.phiz + youle_room.robotadmin.active.gift){ + var robotseatlist = []; + var haveplayer = false; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]) { + if (o_room.seatlist[i].isrobot){ + robotseatlist.push(i); + } else { + haveplayer = true; + } + } + } + + if (robotseatlist.length > 0 && haveplayer){ + var idx = min_random(0, robotseatlist.length - 1); + var from_seat = robotseatlist[idx]; + if (rand <= youle_room.robotadmin.active.text){ + pack.rpc = "send_text"; + pack.data = {}; + pack.data.type = 3; + pack.data.seat = from_seat; + pack.data.text = min_random(1, 100); + o_room.method.sendpack_toother(pack, -1); + } else if (rand <= youle_room.robotadmin.active.text + youle_room.robotadmin.active.phiz){ + pack.rpc = "send_phiz"; + pack.data = {}; + pack.data.type = 0; + pack.data.seat = from_seat; + pack.data.text = min_random(1, 100); + o_room.method.sendpack_toother(pack, -1); + } else { + var playerseatlist = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]) { + if (i != from_seat){ + playerseatlist.push(i); + } + } + } + if (playerseatlist.length > 0){ + var idx = min_random(0, playerseatlist.length - 1); + var to_seat = playerseatlist[idx]; + pack.rpc = "send_gift"; + pack.data = {}; + pack.data.giftid = min_random(1, 100); + pack.data.sendseat = from_seat; + pack.data.receiveseat = to_seat; + o_room.method.sendpack_toother(pack, -1); + } + } + } + } + } + var time = min_random(youle_room.robotadmin.active.timer1, youle_room.robotadmin.active.timer2); + o_room.timer_robot_active = min_ontime(do_robot_active, time * 1000); + } + } + } + + //创建玩家 + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + ip = pack.data.player.ip; + onstate = 0; + conmode = pack.data.player.conmode; + fromid = pack.data.player.fromid; + } + var o_player = cls_youle_room_player.new(playerid, nickname, avatar, sex, playertype, ip, onstate, conmode, fromid); + o_player.address = address; + o_player.latitude = latitude; + o_player.longitude = longitude; + o_player.location = location; + o_player.bean = playerbean; + + + if (!advanced_type){ + //创建普通房间时房主进入房间 + if (!pack.data.player.notenterroom){ + o_player.method.enter_room(o_room, 0, 1); + } else { + o_room.owner = o_player; + } + + //返回结果 + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + pack = youle_room.return.create_room(pack, o_player); + } else { + //房间服务器与代理商服务器不是同一地址 + pack.data = {}; + pack.data.state = 0; + pack.data.roomcode = roomcode; + pack.data.seat = 0; + } + pack.data.roommode = o_room.roommode; + pack.data.beanlimit = beanfloor; + //视频选项 + if(videoConfig){ + pack.data.videoConfig = videoConfig; + } + pack.data.rebateMode = rebateMode; + pack.data.rebateNumber = rebateNumber; + pack.data.rebateType = rebateType; + pack.data.location = location; + if (o_room.infinite){ + //如果是系统房间无限局则直接创建桌,将控制权交给子游戏 + pack.data.deskwar = youle_room.import.makewar_deskwar(o_room); + o_room.battlestate = 1; + o_room.makewartime = min_now(); + } + return pack; + } else { + if (o_room.infinite){ + //如果是系统房间无限局则直接创建桌,将控制权交给子游戏 + pack.data.deskwar = youle_room.import.makewar_deskwar(o_room); + o_room.battlestate = 1; + o_room.makewartime = min_now(); + } + + //创建固定房间时房主不进入房间 + o_room.owner = o_player; + pack.data = {}; + pack.data.state = 0; + pack.data.roomcode = roomcode; + return pack; + } +} + +//自己加入房间 +youle_room.self_join_room = function(pack){ + var playerid = parseInt(pack.data.playerid); + var nickname = pack.data.nickname; + var avatar = pack.data.avatar; + var sex = parseInt(pack.data.sex); + var playertype = parseInt(pack.data.playertype); + var paycode = null; + if (pack.data.paycode){ + paycode = pack.data.paycode; + } + var playercharm = null; + if (pack.data.charm || pack.data.charm == 0){ + playercharm = pack.data.charm; + } + var ip = null; + var onstate = null; + var conmode = null; + var fromid = null; + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var playerroomcard = parseInt(pack.data.playerroomcard); + var playerbean = parseInt(pack.data.playerbean); + var joinmode = pack.data.joinmode; + var address = pack.data.address; + var latitude = pack.data.latitude; + var longitude = pack.data.longitude; + var location = pack.data.location || null; + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房间不存在或已解散:agentid"; + pack.data.showerror = 1; + if (joinmode == 1){ + pack.data.state = 99; + pack.data.roomcode = roomcode; + } + return pack; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房间不存在或已解散:gameid"; + pack.data.showerror = 1; + if (joinmode == 1){ + pack.data.state = 99; + pack.data.roomcode = roomcode; + } + return pack; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房间不存在或已解散"; + pack.data.showerror = 1; + if (joinmode == 1){ + pack.data.state = 99; + pack.data.roomcode = roomcode; + } + return pack; + } + + //房主加入方式 + if (joinmode == 1){ + if (!o_room.owner || (o_room.owner.playerid != playerid)){ + pack.data = {}; + pack.data.state = 99; + pack.data.error = "房间已结算或已解散"; + pack.data.showerror = 1; + pack.data.roomcode = roomcode; + return pack; + } + } + var roomCardName = youle_room.app.youle_platform.config.method.get_paravalue("roomCardName", agentid); + if (!roomCardName){ + roomCardName = "房卡"; + } + var starName = youle_room.app.youle_platform.config.method.get_paravalue("starName", agentid); + if (!starName){ + starName = "星星"; + } + //获取开房所需的房卡数量 + var needroomcard; + if (typeof pack.data.match_roomcard != "undefined"){ + needroomcard = parseInt(pack.data.match_roomcard); + } else { + needroomcard = youle_room.import.joinroom_needroomcard(o_game, o_room.roomtype); + // needroomcard = o_room.needroomcard; + //加入视频房间需要多房卡 + if(needroomcard>0){ + var os_agent=youle_agent.agents.method.find_agent(agentid); + var vConfig = os_agent.method.alyVideoConfig(o_room.videoConfig); + if(vConfig[0] == 1){ + needroomcard += vConfig[1]; + } + } + } + var xxroomdeduct = youle_room.app.youle_platform.config.method.get_paravalue("xxroomdeduct", agentid); + if(!o_room.isFree){ + if (!o_room.roommode || (o_room.roommode && xxroomdeduct)){ + + //检查玩家房卡是否够用 + if (playerroomcard < needroomcard){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = roomCardName + "数量不够"; + pack.data.showerror = 1; + return pack; + }; + } + } + //检查房主房卡是否够用 + if (!o_room.wasdeduct){ + var _needroomcard = youle_room.import.createroom_needroomcard(o_game, o_room.roomtype); + if(_needroomcard>0){ + var os_agent=youle_agent.agents.method.find_agent(agentid); + var vConfig = os_agent.method.alyVideoConfig(o_room.videoConfig); + if(vConfig[0] == 1){ + _needroomcard += vConfig[1]; + } + } + var _o_agent = youle_agent.agents.method.find_agent(agentid); + var _o_owner = _o_agent.method.find_player(o_room.owner.playerid); + if (_o_owner){ + if (_o_owner.roomcard < _needroomcard){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房主的"+ roomCardName + "数量不够"; + pack.data.showerror = 1; + return pack; + } + } + } + + //获取加入房间需要的豆豆数量 + // var needbean = youle_room.import.createroom_beanfloor(o_game, o_room.roomtype); + var needbean = o_room.beanfloor; + if(!o_room.rebateType){ + if(o_room.advanced_type){ + if(!o_room.roommode){ + var owner1 = o_room.owner; + if(owner1){ + if(owner1.playerid != playerid){ + needbean = o_room.rebateNumber; + } + }else{ + needbean = o_room.rebateNumber; + } + + } + } + } + //房主设置的 + //vip房卡房的限进以服务费为准 + // advanced_teabean 房间费 advanced_type = 1//短号房 + + //检查玩家豆豆是否够用 + //if(!o_room.rebateType){// + if (needbean != null){ + if (playerbean < needbean){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = starName + "数量不够"; + pack.data.showerror = 1; + return pack; + } + }else{ + //如果房间有下限值则不去判断视频加入星星数量 + // var os_agent=youle_agent.agents.method.find_agent(agentid); + // var vConfig = os_agent.method.alyVideoConfig(o_room.videoConfig); + // if(vConfig[0] == 2){ + // if (playerbean < vConfig[1]){ + // pack.data = {}; + // pack.data.state = 1; + // pack.data.error = starName + "数量不够"; + // pack.data.showerror = 1; + // return pack; + // } + // } + } + //} + // } + + //检查房间是否已满 + var makewarplayercount = youle_room.import.makewar_playercount(o_game, o_room.roomtype); + if (makewarplayercount){ + var playercount = 0; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]) { + if (o_room.seatlist[i].playerid == playerid){ + pack.data = {}; + pack.data.state = -1; + return pack; + } + playercount++; + } + } + if (playercount >= makewarplayercount){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房间已满"; + pack.data.showerror = 1; + return pack; + } + } + //检查玩家是否有权限进入房间 + var o_join_data = pack.data; + var joinroom_permission = youle_room.import.check_joinroom_permission(o_game, o_room, o_join_data); + if (joinroom_permission && !joinroom_permission.allowed){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = joinroom_permission.message || "无法进入房间"; + pack.data.showerror = 1; + return pack; + } + var config_ramSeat = youle_agent.app.youle_platform.config.method.get_paravalue("ramSeat", agentid, gameid); + if(!config_ramSeat){ + config_ramSeat = 1; + } + config_ramSeat = Number(config_ramSeat); + if(config_ramSeat == 1){ + var empSeatList = []; + var seat0Emp = false; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (!o_room.seatlist[i]) { + empSeatList.push(i); + if(i == 0){ + seat0Emp = true; + } + } + } + if(empSeatList.length >= 1){ + + var sRam = min_random(0,empSeatList.length-1); + var sindx = empSeatList[sRam]; + if(seat0Emp){ + sindx = 0; + } + if(sindx >= 0){ + if (o_agent.server == o_game.roomserver){ + //如果房间服务器与代理商服务器是同一地址 + ip = pack.data.ip; + onstate = 0; + conmode = pack.data.conmode; + fromid = pack.data.fromid; + } + //创建玩家 + var o_player = cls_youle_room_player.new(playerid, nickname, avatar, sex, playertype, ip, onstate, conmode, fromid); + o_player.needroomcard = needroomcard; + o_player.wasdeduct = false; + o_player.address = address; + o_player.latitude = latitude; + o_player.longitude = longitude; + o_player.location = location; + o_player.bean = playerbean; + if (paycode){ + o_player.paycode = paycode; + } + //玩家加入房间 + o_player.method.enter_room(o_room, sindx, 0); + if (o_room.owner && o_room.owner.playerid == playerid){ + o_player.gameinfo.isowner = 1; + } + //如果房间已经处于开战状态(玩家中途进入)则通知游戏有玩家进入了房间 + if (o_room.battlestate == 1){ + youle_room.import.player_enter(o_room, sindx); + } + } + } + }else{ + + for (var i = 0; i < o_room.seatlist.length; i++) { + if (!o_room.seatlist[i]) { + //有位置 + if (o_agent.server == o_game.roomserver){ + //如果房间服务器与代理商服务器是同一地址 + ip = pack.data.ip; + onstate = 0; + conmode = pack.data.conmode; + fromid = pack.data.fromid; + } + //创建玩家 + var o_player = cls_youle_room_player.new(playerid, nickname, avatar, sex, playertype, ip, onstate, conmode, fromid); + o_player.needroomcard = needroomcard; + o_player.wasdeduct = false; + o_player.address = address; + o_player.latitude = latitude; + o_player.longitude = longitude; + o_player.location = location; + o_player.bean = playerbean; + if (paycode){ + o_player.paycode = paycode; + } + //玩家加入房间 + o_player.method.enter_room(o_room, i, 0); + if (o_room.owner && o_room.owner.playerid == playerid){ + o_player.gameinfo.isowner = 1; + } + //如果房间已经处于开战状态(玩家中途进入)则通知游戏有玩家进入了房间 + if (o_room.battlestate == 1){ + youle_room.import.player_enter(o_room, i); + } + break; + } + }; + + } + if (!o_player) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房间已满"; + pack.data.showerror = 1; + return pack; + } + + //加机器人 + if (o_room.infinite){ + if (o_room.sysroom){ + var haverobot = youle_room.app.youle_platform.config.method.get_paravalue("haverobot", agentid, gameid); + if (haverobot == 1){ + try{ + var playercount = 0; + var playerbeansum = 0; + var robotcount = 0; + var min_playerid = 999999; + var max_playerid = 0; + var playeridlist = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + if (o_room.seatlist[i].isrobot){ + robotcount = robotcount + 1; + } else { + playercount = playercount + 1; + playerbeansum = playerbeansum + o_room.seatlist[i].bean; + if (o_room.seatlist[i].playerid < min_playerid){ + min_playerid = o_room.seatlist[i].playerid; + } + if (o_room.seatlist[i].playerid > max_playerid){ + max_playerid = o_room.seatlist[i].playerid; + } + playeridlist.push(o_room.seatlist[i].playerid); + } + } + } + /* + if (playercount > 0){ + var newrobotcount = 3 - playercount - robotcount; + if (newrobotcount > 0) { + //console.log("p:"+playercount+" r:"+robotcount+" newrobotcount:"+newrobotcount); + min_playerid = min_playerid - 1000; + if (min_playerid < 100000) { + min_playerid = 100000; + } + max_playerid = max_playerid + 1000; + if (max_playerid > 999999) { + max_playerid = 999999; + } + + var o_agentsvr_agent = youle_agent.agents.method.find_agent(o_room.o_game.o_agent.agentid); + for (var i = 0; i < newrobotcount; i++) { + var robot_playerid = null; + while (!robot_playerid){ + robot_playerid = min_random(min_playerid, max_playerid); + //机器人id从200000到899999随机取 + robot_playerid = min_random(200000, 899999); + if (min_ary_indexof(playeridlist, robot_playerid) > -1){ + robot_playerid = null; + } + } + // var idx = min_random(0, o_agentsvr_agent.robot_nicknamelist.length - 1); + // var robot_nickname = o_agentsvr_agent.robot_nicknamelist[idx]; + // var idx = min_random(0, o_agentsvr_agent.robot_avatarlist.length - 1); + // var robot_avatar = o_agentsvr_agent.robot_avatarlist[idx]; + + // o_agentsvr_agent.robotArray.sort( + // function () { + // return 0.5 - Math.random(); + // }); + // var rid = 0; + + // for(var i=0;i -1){ + // robot_nickname = null; + // } else { + // break; + // } + // } + + // if (robot_nickname == null){ + // return; + // } + + // var robot_avatar = null; + var robot_nickname=""; + var robot_avatar = ""; + var robot_sex = 1; + if(o_agentsvr_agent.robotNicknames){ + var rid = min_random(0, o_agentsvr_agent.robotNicknames.length - 1); + robot_nickname = o_agentsvr_agent.robotNicknames[rid]; + var pid = rid+100; + robot_avatar = "https://projectimage.tscce.cn/image_132/"+pid+".jpg"; + // robot_sex = 1; + if(rid<=788){ + robot_sex = 2; + } + //console.log("join",rid,robot_sex); + }else{ + var idx = min_random(0, o_agentsvr_agent.robot_nicknamelist.length - 1); + vrobot_nickname = o_agentsvr_agent.robot_nicknamelist[idx]; + var idx = min_random(0, o_agentsvr_agent.robot_avatarlist.length - 1); + robot_avatar = o_agentsvr_agent.robot_avatarlist[idx]; + robot_sex = min_random(1, 2); + //console.log("2join"); + } + + // var robot_sex = min_random(0, 1); + var robot_ip = min_random(10, 240) + "." + min_random(10, 240) + "." + min_random(10, 240) + "." + min_random(10, 240); + var robot_bean = o_room.beanfloor * min_random(3, 6) + min_random(0, o_room.beanfloor); + robot_bean = parseInt((robot_bean + playerbeansum) / (playercount + 1)); + var o_robot = cls_youle_room_player.new(robot_playerid, robot_nickname, robot_avatar, robot_sex, 0, robot_ip, 0, null, null); + o_robot.bean = robot_bean; + o_robot.isrobot = 1; + + for (var j = 0; j < o_room.seatlist.length; j++) { + if (!o_room.seatlist[j]){ + o_robot.method.enter_room(o_room, j, 0); + youle_room.import.player_enter(o_room, j); + break; + } + } + + //通知房间内的其他人有人加入了房间 + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "other_join_room"; + msg.data = o_robot.method.get_info(); + msg.data.bean = o_robot.bean; + msg.data.seat = o_robot.gameinfo.seat; + msg.data.isprepare = o_robot.gameinfo.isprepare; + // for (var i = 0; i < o_room.seatlist.length; i++) { + // if (o_room.seatlist[i] && (i != o_robot.gameinfo.seat) && (i != o_player.gameinfo.seat)) { + // msg.conmode = o_room.seatlist[i].conmode; + // msg.fromid = o_room.seatlist[i].fromid; + // youle_room.app.SendPack(msg); + // } + // } + for (var j = 0; j < o_room.seatlist.length; j++) { + if (o_room.seatlist[j] && (j != o_robot.gameinfo.seat) && (j != o_player.gameinfo.seat)) { + msg.conmode = o_room.seatlist[j].conmode; + msg.fromid = o_room.seatlist[j].fromid; + youle_room.app.SendPack(msg); + } + } + } + } + } + */ + }catch(e){} + } + } + } + + //返回结果 + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + pack = youle_room.return.self_join_room(pack, o_player, playerbean, playercharm); + } else { + //房间服务器与代理商服务器不是同一地址 + pack.data = {}; + pack.data.state = 0; + pack.data.seat = o_player.gameinfo.seat; + } + if (o_room.owner.playerid == playerid) { + pack.data.isowner = 1; + } else { + pack.data.isowner = 0; + } + pack.data.roommode = o_room.roommode; + pack.data.beanlimit = needbean; + pack.data.shortcode = o_room.shortcode; + //视频选项 + if(o_room.videoConfig){ + pack.data.videoConfig = o_room.videoConfig; + } + if(o_room.rebateNumber){ + pack.data.rebateNumber = o_room.rebateNumber; + } + if(o_room.rebateMode){ + pack.data.rebateMode = o_room.rebateMode; + } + pack.data.rebateType = o_room.rebateType; + pack.data.location = location; + return pack; +} + +//修改房卡 +youle_room.update_roomcard = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var roomcard = parseInt(pack.data.roomcard); + // var type = parseInt(pack.data.type); + // var change = parseInt(pack.data.change); + var text = pack.data.text; + var issameserver = parseInt(pack.data.issameserver); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "update_roomcard"; + msg.conmode = o_player.conmode; + msg.fromid = o_player.fromid; + msg.data = {}; + msg.data.roomcard = roomcard; + // msg.data.type = type; + // msg.data.change = change; + msg.data.text = text; + youle_room.app.SendPack(msg); + + if (issameserver){ + return pack + } else { + youle_room.app.SendPack(pack); + } +} + +//修改星星 +youle_room.update_bean = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var bean = parseInt(pack.data.bean); + var text = pack.data.text; + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + + o_player.bean = bean; + + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "update_bean"; + msg.conmode = o_player.conmode; + msg.fromid = o_player.fromid; + msg.data = {}; + msg.data.bean = bean; + msg.data.text = text; + youle_room.app.SendPack(msg); + + delete msg.data.text; + msg.data.seat = o_player.gameinfo.seat; + o_room.method.sendpack_toother(msg, o_player.gameinfo.seat); + + return pack; +} + +//玩家连接房间服务器并进入房间 +youle_room.connect_roomserver = function(pack){ + var playerid = parseInt(pack.data.data.playerid); + var agentid = pack.data.data.agentid; + var gameid = pack.data.data.gameid; + var roomcode = parseInt(pack.data.data.roomcode); + var seat = parseInt(pack.data.data.seat); + var opt = pack.data.data.opt; + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + if (opt == "player_login"){ + var _info = pack.data.data.info; + pack.rpc = "connect_agentserver"; + pack.data = {}; + pack.data.agentserver = o_agent.server + ":" + o_agent.tcpport; + pack.data.data = {}; + pack.data.data.opt = "player_login"; + pack.data.data.playerid = playerid; + pack.data.data.agentid = agentid; + pack.data.data.gameid = gameid; + pack.data.data.info = _info; + youle_room.app.SendPack(pack); + } + return; + } + //查找玩家 + if (o_room.seatlist.length <= seat){ + return; + } + var o_player = o_room.seatlist[seat]; + if (!o_player){ + return; + } + if (o_player.playerid != playerid){ + return; + } + + //踢原有连接下线 + if (o_player.conmode == "tcp" && o_player.fromid && o_player.fromid != pack.fromid){ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "kick_offline"; + msg.conmode = o_player.conmode; + msg.fromid = o_player.fromid; + msg.data={}; + msg.data.fromOther = 0; + youle_room.app.SendPack(msg); + + // var room_kick_offline = {}; + // room_kick_offline.agentid = agentid; + // room_kick_offline.gameid = gameid; + // room_kick_offline.roomcode = roomcode; + // room_kick_offline.playerid = playerid; + // room_kick_offline.oldfromid = o_player.fromid; + // room_kick_offline.oldtime = o_player.fromidtime; + // room_kick_offline.newfromid = pack.fromid; + // room_kick_offline.newtime = (new Date()).getTime(); + // console.log(min_now() + " roomserver kick_offline"); + // console.log(room_kick_offline); + } + //更新ip、onstate、conmode、fromid + o_player.ip = pack.ip; + o_player.onstate = 0; + o_player.conmode = pack.conmode; + o_player.fromid = pack.fromid; + o_player.fromidtime = (new Date()).getTime(); + + //更新tcp连接列表 + if (pack.conmode == "tcp"){ + youle_room.tcps.method.load_tcp(pack.fromid, o_player); + } + + switch (opt){ + case "create_room": + //返回前端创建房间成功 + pack = youle_room.return.create_room(pack, o_player); + youle_room.app.SendPack(pack); + break; + case "self_join_room": + //返回前端加入房间成功 + pack = youle_room.return.self_join_room(pack, o_player); + youle_room.app.SendPack(pack); + break; + case "player_login": + //返回前端所在房间数据 + var _info = pack.data.data.info; + pack = youle_room.return.player_login(pack, o_player); + pack.data.agentid = _info.agentid; + pack.data.agentname = _info.agentname; + pack.data.agentlogo = _info.agentlogo; + pack.data.wechat_gzh = _info.wechat_gzh; + pack.data.wechat_ewm = _info.wechat_ewm; + pack.data.wechat_kfh = _info.wechat_kfh; + pack.data.qq = _info.qq; + pack.data.tel = _info.tel; + pack.data.scrollmsg = _info.scrollmsg; + pack.data.noticemsg = _info.noticemsg; + pack.data.appdownload = _info.appdownload; + pack.data.playerid = _info.playerid; + pack.data.roomcard = _info.roomcard; + pack.data.taskstate = _info.taskstate; + youle_room.app.SendPack(pack); + break; + } +} + +//换桌 +youle_room.change_room = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + if (o_player.gameinfo.isbet){ + return; + } + //只有系统房间无限局才能换房 + if (!o_room.infinite){ + return; + } + + //查找代理商 + var agtsvr_agent = youle_agent.agents.method.find_agent(agentid); + if (!agtsvr_agent) { + return; + }; + //检查游戏id是否存在 + var agtsvr_game = agtsvr_agent.method.find_game(gameid); + if (!agtsvr_game) { + return; + }; + if(!agtsvr_game.sharerooms_system){ + return; + }; + if(agtsvr_game.sharerooms_system.length == 0){ + return; + }; + var agtsvr_player = agtsvr_agent.method.find_player(playerid); + if (!agtsvr_player) { + return; + }; + var agtsvr_gameinfo = agtsvr_player.method.get_gameinfo(agtsvr_game); + if (!agtsvr_gameinfo){ + return; + }; + + var sysroom_playercount = youle_room.app.youle_platform.config.method.get_paravalue("sysroom_playercount", agentid); + if (sysroom_playercount == null){ + sysroom_playercount = 20; + } + var _sysroom_playercount = 0; + if (o_game.sysroom_playercount){ + for (var i = 0; i < o_game.sysroom_playercount.length; i++) { + if (JSON.stringify(o_game.sysroom_playercount[i].roomtype) == JSON.stringify(o_room.roomtype)){ + _sysroom_playercount = o_game.sysroom_playercount[i].playercount; + break; + } + } + } + var haverobot = youle_room.app.youle_platform.config.method.get_paravalue("haverobot", agentid, gameid); + + var idx = min_ary_indexof(agtsvr_game.sharerooms_system, roomcode, "roomcode"); + if (idx == -1){ + return; + } + for (var i = idx + 1; i < agtsvr_game.sharerooms_system.length; i++) { + if (JSON.stringify(agtsvr_game.sharerooms_system[i].roomtype) == JSON.stringify(o_room.roomtype)){ + var _roomcode = agtsvr_game.sharerooms_system[i].roomcode; + var _room = o_game.method.find_room(_roomcode); + if (_room){ + + //查看是否有空位,是否有真人玩家 + var _have_nullseat = false; + for (var j = 0; j < _room.seatlist.length; j++) { + if (!_room.seatlist[j]){ + _have_nullseat = true; + } else { + if (!_room.seatlist[j].isrobot){ + if ((_sysroom_playercount < sysroom_playercount) && (haverobot == 1)){ + _have_nullseat = false; + break; + } + } + } + } + + if (_have_nullseat){ + for (var j = 0; j < _room.seatlist.length; j++) { + if (!_room.seatlist[j]){ + //退出当前房间 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "self_exit_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = gameid; + msg1.data.playerid = playerid; + youle_room.app.youle_agent[msg1.rpc](msg1); + + var playerseat = o_player.gameinfo.seat; + o_player.method.exit_room(); + if (o_room.battlestate == 1){ + youle_room.import.player_leave(o_room, playerseat); + } + + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "other_exit_room"; + msg2.data = {}; + msg2.data.seat = playerseat; + o_room.method.sendpack_toother(msg2, playerseat); + + //加入新房间 + o_player.method.enter_room(_room, j, 0); + if (_room.owner && _room.owner.playerid == playerid){ + o_player.gameinfo.isowner = 1; + } + if (_room.battlestate == 1){ + youle_room.import.player_enter(_room, j); + } + + + pack = youle_room.return.self_join_room(pack, o_player, o_player.bean); + if (_room.owner.playerid == playerid) { + pack.data.isowner = 1; + } else { + pack.data.isowner = 0; + } + if(o_room.rebateNumber){ + pack.data.rebateNumber = o_room.rebateNumber; + } + if(o_room.rebateMode){ + pack.data.rebateMode = o_room.rebateMode; + } + pack.data.rebateType = o_room.rebateType; + pack.data.roommode = _room.roommode; + pack.data.beanlimit = _room.beanfloor; + + agtsvr_gameinfo.roomcode = _roomcode; + // for (var k = 0; k < pack.data.players.length; k++) { + // if (pack.data.players[k]){ + // var _playerid = parseInt(pack.data.players[k].playerid); + // var _player = agtsvr_agent.method.find_player(_playerid); + // pack.data.players[k].bean = _player.bean; + // } + // } + return pack; + } + } + } + } + } + } + for (var i = 0; i <= idx - 1; i++) { + if (JSON.stringify(agtsvr_game.sharerooms_system[i].roomtype) == JSON.stringify(o_room.roomtype)){ + var _roomcode = agtsvr_game.sharerooms_system[i].roomcode; + var _room = o_game.method.find_room(_roomcode); + if (_room){ + + //查看是否有空位,是否有真人玩家 + var _have_nullseat = false; + for (var j = 0; j < _room.seatlist.length; j++) { + if (!_room.seatlist[j]){ + _have_nullseat = true; + } else { + if (!_room.seatlist[j].isrobot){ + if (_sysroom_playercount < sysroom_playercount){ + _have_nullseat = false; + break; + } + } + } + } + + if (_have_nullseat){ + for (var j = 0; j < _room.seatlist.length; j++) { + if (!_room.seatlist[j]){ + //退出当前房间 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "self_exit_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = gameid; + msg1.data.playerid = playerid; + youle_room.app.youle_agent[msg1.rpc](msg1); + + var playerseat = o_player.gameinfo.seat; + o_player.method.exit_room(); + if (o_room.battlestate == 1){ + youle_room.import.player_leave(o_room, playerseat); + } + + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "other_exit_room"; + msg2.data = {}; + msg2.data.seat = playerseat; + o_room.method.sendpack_toother(msg2, playerseat); + + //加入新房间 + o_player.method.enter_room(_room, j, 0); + if (_room.owner && _room.owner.playerid == playerid){ + o_player.gameinfo.isowner = 1; + } + if (_room.battlestate == 1){ + youle_room.import.player_enter(_room, j); + } + + pack = youle_room.return.self_join_room(pack, o_player, o_player.bean); + if (_room.owner.playerid == playerid) { + pack.data.isowner = 1; + } else { + pack.data.isowner = 0; + } + if(o_room.rebateNumber){ + pack.data.rebateNumber = o_room.rebateNumber; + } + if(o_room.rebateMode){ + pack.data.rebateMode = o_room.rebateMode; + } + pack.data.rebateType = o_room.rebateType; + pack.data.roommode = _room.roommode; + pack.data.beanlimit = _room.beanfloor; + + agtsvr_gameinfo.roomcode = _roomcode; + // for (var k = 0; k < pack.data.players.length; k++) { + // if (pack.data.players[k]){ + // var _playerid = parseInt(pack.data.players[k].playerid); + // var _player = agtsvr_agent.method.find_player(_playerid); + // pack.data.players[k].bean = _player.bean; + // } + // } + return pack; + } + } + } + } + } + } + + //小概率换房失败 + var rand = min_random(1, 100); + if (rand < 10){ + pack.rpc = "show_message"; + pack.data = {}; + pack.data.msg = "目前没有其他房间有空位,换房失败"; + return pack; + } else { + //退出当前房间 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "self_exit_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = gameid; + msg1.data.playerid = playerid; + youle_room.app.youle_agent[msg1.rpc](msg1); + + var playerseat = o_player.gameinfo.seat; + o_player.method.exit_room(); + if (o_room.battlestate == 1){ + youle_room.import.player_leave(o_room, playerseat); + } + + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "other_exit_room"; + msg2.data = {}; + msg2.data.seat = playerseat; + o_room.method.sendpack_toother(msg2, playerseat); + + //创建新的系统房间 + var _roomcode = agtsvr_game.method.new_roomcode(); + if (!_roomcode){ + console.log(min_now() + "创建系统房间失败:没有房号可分配"); + return; + } + + var o_roomserver = agtsvr_game.method.get_roomserver(_roomcode); + + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "create_room"; + msg.data = {}; + + msg.data.agent = {}; + msg.data.agent.agentid = agentid; + msg.data.agent.name = o_agent.agentname; + msg.data.agent.server = o_agent.server; + msg.data.agent.tcpport = o_agent.tcpport; + msg.data.agent.httpport = o_agent.httpport; + msg.data.agent.freeroom = o_agent.freeroom; + + msg.data.game = {}; + msg.data.game.gameid = gameid; + msg.data.game.name = o_game.gamename; + msg.data.game.seatcount = o_game.seatcount; + msg.data.game.makewar = o_game.makewar; + msg.data.game.modename = o_game.modename; + msg.data.game.server = o_roomserver.server; + msg.data.game.tcpport = o_roomserver.tcpport; + msg.data.game.httpport = o_roomserver.httpport; + msg.data.game.minroomcode = o_roomserver.minroomcode; + msg.data.game.maxroomcode = o_roomserver.maxroomcode; + + msg.data.player = {}; + msg.data.player.playerid = playerid; + msg.data.player.nickname = o_player.nickname; + msg.data.player.avatar = o_player.avatar; + msg.data.player.sex = o_player.sex; + msg.data.player.playertype = o_player.playertype; + msg.data.player.ip = o_player.ip; + msg.data.player.conmode = o_player.conmode; + msg.data.player.fromid = o_player.fromid; + msg.data.player.address = o_player.address; + msg.data.player.latitude = o_player.latitude; + msg.data.player.longitude = o_player.longitude; + msg.data.player.bean = o_player.bean; + + //获取开房所需的房卡数量 + var needroomcard = youle_agent.import.createroom_needroomcard(agtsvr_game, o_room.roomtype); + //获取总局数 + var asetcount = youle_agent.import.createroom_asetcount(agtsvr_game, o_room.roomtype); + //获取豆豆下限 + var beanfloor = youle_agent.import.createroom_beanfloor(agtsvr_game, o_room.roomtype); + //获取房间类型 + var roommode = youle_agent.import.createroom_roommode(agtsvr_game, o_room.roomtype); + //获取星星场倍数 + var beanmult = youle_agent.import.createroom_beanmult(agtsvr_game, o_room.roomtype); + //玩家是否需要准备 + var needprepare = youle_agent.import.createroom_needprepare(agtsvr_game, o_room.roomtype); + //系统房间即无限局一定要准备 + needprepare = 1; + + //不是星星场 + if (!roommode){ + beanmult = 1; + } + + msg.data.room = {}; + msg.data.room.roomcode = _roomcode; + msg.data.room.roomtype = o_room.roomtype; + msg.data.room.needroomcard = needroomcard; + msg.data.room.asetcount = asetcount; + msg.data.room.infinite = 1; + msg.data.room.sysroom = 1; + msg.data.room.roommode = roommode; + msg.data.room.beanfloor = beanfloor; + msg.data.room.beanmult = beanmult; + msg.data.room.needprepare = needprepare; + msg.data.room.advanced_type = 0; + msg.data.room.advanced_teabean = 0; + if(o_room.rebateNumber){ + msg.data.room.rebateNumber = o_room.rebateNumber; + } + if(o_room.rebateMode){ + msg.data.room.rebateMode = o_room.rebateMode; + } + msg.data.room.rebateType = o_room.rebateType; + //加入房间成功 + agtsvr_gameinfo.roomcode = _roomcode; + if (o_roomserver.server == agtsvr_agent.server){ + //房间服务器与代理商服务器是同一地址 + var roomserver_data = youle_room[msg.rpc](msg); + //返回前端 + pack.data = roomserver_data.data; + if (pack.data.state != 0){ + //房间服务器创建房间失败 + agtsvr_gameinfo.roomcode = null; + pack.rpc = "create_room"; + pack.data = {}; + pack.data.state = 1; + pack.data.error = "创建系统房间失败"; + pack.data.showerror = 1; + youle_agent.app.SendPack(pack); + return; + } + pack.rpc = "create_room"; + youle_agent.app.SendPack(pack); + + var o_share = {}; + o_share.roomcode = _roomcode; + o_share.roomtype = o_room.roomtype; + o_share.sharetime= min_now(); + agtsvr_game.sharerooms_system.push(o_share); + } + } +} + +//玩家准备 +youle_room.player_prepare = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + + if (o_room.infinite){ + if (!youle_room.import.player_CanPrepare(o_room, o_player.gameinfo.seat)){ + return; + }; + } + o_player.gameinfo.isprepare = 1; + youle_room.import.player_prepare(o_room, o_player.gameinfo.seat); + + //返回前端 + pack.data.seat = o_player.gameinfo.seat; + if (!o_room.battlestate) { + var isfull = o_room.method.get_isfull(); + if (isfull) { + //满桌了自动开战 + o_room.battlestate = 1; + o_room.makewartime = min_now(); + //如果有返回值则后面的代码会发统一的开战包,如果没有返回值则由游戏负责发开战包 + pack.data.deskwar = youle_room.import.makewar_deskwar(o_room); + } else { + //子游戏设置的开战人数 + var makewarplayercount = youle_room.import.makewar_playercount(o_game, o_room.roomtype); + if (makewarplayercount){ + var playercount = 0; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i] && o_room.seatlist[i].gameinfo.isprepare){ + playercount = playercount + 1; + } + } + if (playercount == makewarplayercount){ + o_room.battlestate = 1; + o_room.makewartime = min_now(); + //如果有返回值则后面的代码会发统一的开战包,如果没有返回值则由游戏负责发开战包 + pack.data.deskwar = youle_room.import.makewar_deskwar(o_room); + } + } + } + } + o_room.method.sendpack_toother(pack, -1); +} + + +//未开局之前自己(房主)解散房间 +youle_room.self_break_room = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + //检查房间是否已经开战 + if (o_room.battlestate != 0){ + return; + } + //检查玩家是否是房主 + // if (o_player.gameinfo.isowner != 1){ + // return; + // } + + //通知代理服务器解散房间 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "self_break_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = gameid; + msg1.data.roomcode = roomcode; + //玩家列表 + msg1.data.playeridlist = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + msg1.data.playeridlist.push(o_room.seatlist[i].playerid); + } + } + + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + youle_room.app.youle_agent[msg1.rpc](msg1); + //返回解散房间成功结果 + pack.data = {}; + pack.data.roomcode = roomcode; + youle_room.app.SendPack(pack); + //通知前端房间内的其他人解散房间 + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "other_break_room"; + o_room.method.sendpack_toother(msg2, o_player.gameinfo.seat); + //释放房间 + o_game.method.free_room(o_room); + } else { + //房间服务器与代理商服务器不是同一地址 + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg1); + cfg.success = function(str){ + //此处conmode = null避免触发玩家离线通知其他玩家事件 + if (o_player.conmode == "tcp"){ + //更新tcp连接列表 + youle_room.tcps.method.delete_tcp(o_player.fromid); + } + o_player.conmode = null; + o_player.fromid = null; + + //返回前端需要断开房间服务器,连接代理服务器 + pack.rpc = "connect_agentserver"; + pack.data = {}; + pack.data.agentserver = o_agent.server + ":" + o_agent.tcpport; + pack.data.data = {}; + pack.data.data.opt = "self_break_room"; + pack.data.data.playerid = playerid; + pack.data.data.agentid = agentid; + pack.data.data.gameid = gameid; + youle_room.app.SendPack(pack); + + //通知前端房间内的其他人需要断开房间服务器,连接代理服务器 + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i] && (i != o_player.gameinfo.seat)) { + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "connect_agentserver"; + msg2.data = {}; + msg2.data.agentserver = o_agent.server + ":" + o_agent.tcpport; + msg2.data.data = {}; + msg2.data.data.opt = "other_break_room"; + msg2.data.data.playerid = o_room.seatlist[i].playerid; + msg2.data.data.agentid = agentid; + msg2.data.data.gameid = gameid; + msg2.conmode = o_room.seatlist[i].conmode; + msg2.fromid = o_room.seatlist[i].fromid; + //此处conmode = null是为了避免触发玩家离线通知其他玩家事件 + if (o_room.seatlist[i].conmode == "tcp"){ + //更新tcp连接列表 + youle_room.tcps.method.delete_tcp(o_room.seatlist[i].fromid); + } + o_room.seatlist[i].conmode = null; + o_room.seatlist[i].fromid = null; + youle_room.app.SendPack(msg2); + } + } + //释放房间 + o_game.method.free_room(o_room); + }; + cfg.error = function(str){}; + min_http(cfg); + } +} + +//未开局之前自己(非房主)退出房间 +youle_room.self_exit_room = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + //检查房间是否已经开战,是否已经下注 + if (o_room.battlestate != 0 && o_player.gameinfo.isbet == 1){ + if (o_room.infinite){ + var canLeave = youle_room.import.isPlayerCanLeave(o_room, o_player.gameinfo.seat); + if (!canLeave){ + pack.rpc = "show_message"; + pack.data = {}; + pack.data.msg = "您已参与此次游戏不能退出房间,可在本局结束后再退出房间"; + return pack; + } + } else { + return; + } + } + // //检查玩家是否是房主 + // if (o_player.gameinfo.isowner == 1){ + // return; + // } + //玩家座位 + var playerseat = o_player.gameinfo.seat; + + //通知代理服务器有人退出房间 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "self_exit_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = gameid; + msg1.data.playerid = playerid; + + + var do_player_exitroom = function(){ + //退出房间 + o_player.method.exit_room(); + //如果是开战状态则通知游戏有人退出了房间 + if (o_room.battlestate == 1){ + youle_room.import.player_leave(o_room, playerseat); + } + + //通知房间内的其他人有人退出房间 + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "other_exit_room"; + msg2.data = {}; + msg2.data.seat = playerseat; + o_room.method.sendpack_toother(msg2, playerseat); + } + + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + youle_room.app.youle_agent[msg1.rpc](msg1); + //退出房间 + do_player_exitroom(); + //返回游戏前端退出房间成功结果 + pack.data = {}; + pack.data.roomcode = roomcode; + if (o_room.owner.playerid == playerid){ + pack.data.isowner = 1; + } else { + pack.data.isowner = 0; + } + youle_room.app.SendPack(pack); + } else { + //房间服务器与代理商服务器不是同一地址 + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg1); + cfg.success = function(str){ + //退出房间 + do_player_exitroom(); + //此处conmode = null避免触发玩家离线通知其他玩家事件 + if (o_player.conmode == "tcp"){ + //更新tcp连接列表 + youle_room.tcps.method.delete_tcp(o_player.fromid); + } + o_player.conmode = null; + o_player.fromid = null; + //返回前端需要断开房间服务器,连接代理服务器 + pack.rpc = "connect_agentserver"; + pack.data = {}; + pack.data.agentserver = o_agent.server + ":" + o_agent.tcpport; + pack.data.data = {}; + pack.data.data.opt = "self_exit_room"; + pack.data.data.playerid = playerid; + pack.data.data.agentid = agentid; + pack.data.data.gameid = gameid; + youle_room.app.SendPack(pack); + }; + cfg.error = function(str){}; + min_http(cfg); + } +} + +//自己(房主)手动开局 +youle_room.self_makewar = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + //检查房间是否已经开战 + if (o_room.battlestate != 0){ + return; + } + // //检查玩家是否是房主 + // if (o_player.gameinfo.isowner != 1){ + // return; + // } + + //开战的同时设置自己的准备状态 + if (!o_player.gameinfo.isprepare){ + o_player.gameinfo.isprepare = 1; + } + + //检查是否达到了开战人数 + var playercount = o_room.method.get_playercount_prepare(); + if (o_room.makewar){ + if (playercount < o_room.makewar){ + return; + } + } else { + if (playercount < o_game.makewar){ + return; + } + } + + //通知游戏服务器开局了,并得到游戏的开局数据包,如果有返回值则后面的代码会发统一的开战包,如果没有返回值则由游戏服务器发送开战包给游戏前端 + var deskwar = youle_room.import.makewar_deskwar(o_room); + + //修改房间的开局状态 + o_room.battlestate = 1; + o_room.makewartime = min_now(); + + //通知房间内的其他人开局了 + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = "other_makewar"; + msg.data = {}; + if (deskwar){ + msg.data.deskwar = deskwar; + } + o_room.method.sendpack_toother(msg, o_player.gameinfo.seat); + + //返回前端退出房间成功 + pack.data = {}; + if (deskwar){ + pack.data.deskwar = deskwar; + } + youle_room.app.SendPack(pack); +} + +//开局之后自己申请解散房间 +youle_room.self_apply_free_room = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + //检查房间是否已经开战 + if (o_room.battlestate == 0){ + return; + } + //检查是否已经有人申请了解散 + if (o_room.applyfreetime) { + return; + } + //申请成功 + o_player.gameinfo.agree = 0; + o_room.applyfreetime = min_now(); + //开启默认同意解散房间的定时器 + o_room.method.open_free_timer(); + + //获取房间的申请解散状态 + var agreefree = o_room.method.get_agreefree(); + + //返回前端申请解散房间成功 + pack.data = {}; + pack.data.agreefree = agreefree; + youle_room.app.SendPack(pack); + + //通知房间内的其他人有人申请解散房间 + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = "other_apply_free_room"; + msg.data = {}; + msg.data.seat = o_player.gameinfo.seat; + msg.data.agreefree = agreefree; + o_room.method.sendpack_toother(msg, o_player.gameinfo.seat); +} + +//自己拒绝解散房间 +youle_room.self_refuse_free_room = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + //检查房间是否已经开战 + if (o_room.battlestate == 0){ + return; + } + //检查是否已经有人申请了解散 + if (!o_room.applyfreetime) { + return; + } + + //拒绝解散成功 + o_room.applyfreetime = null; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]) { + o_room.seatlist[i].gameinfo.agree = -1; + } + } + //关闭默认同意解散房间的定时器 + o_room.method.close_free_timer(); + + //返回前端拒绝解散房间成功 + delete pack.data; + youle_room.app.SendPack(pack); + + //通知房间内的其他人有人拒绝解散房间 + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = "other_refuse_free_room"; + msg.data = {}; + msg.data.seat = o_player.gameinfo.seat; + o_room.method.sendpack_toother(msg, o_player.gameinfo.seat); +} + +//自己同意解散房间 +youle_room.self_agree_free_room = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + //检查房间是否已经开战 + if (o_room.battlestate == 0){ + console.log(min_now + "解散房间失败:房间已开战"); + console.log(pack.data); + console.log(o_room); + return; + } + //检查是否已经有人申请了解散 + if (!o_room.applyfreetime) { + return; + } + + o_player.gameinfo.agree = 1; + + //同意解散人数大于游戏人数的一半时可达成解散 + var totalcount = 0; //房间内的总人数 + var agreecount = 0; //同意解散总人数 + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]) { + totalcount = totalcount + 1; + if (o_room.seatlist[i].gameinfo.agree > -1) { + agreecount = agreecount + 1; + } + } + } + + if ((totalcount < 4 && agreecount == totalcount) || + (totalcount >= 4 && agreecount > parseInt(totalcount / 2))){ + //2人或3人时需要全部同意,4人及以上时需要超过半数以上的玩家同意解散则达成解散 + //关闭默认同意解散房间的定时器 + o_room.method.close_free_timer(); + + //通知代理商服务器解散房间 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "free_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = gameid; + msg1.data.roomcode = roomcode; + //玩家列表 + msg1.data.playeridlist = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + msg1.data.playeridlist.push(o_room.seatlist[i].playerid); + } + } + + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + youle_room.app.youle_agent[msg1.rpc](msg1); + //通知前端房间内的所有人解散房间 + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "free_room"; + msg2.data = {}; + //获取房间内各玩家的解散房间状态 + msg2.data.seats = o_room.method.get_agreefree().state; + + //获取游戏的解散包 + // if (o_room.battlestate == 1){ + // msg2.data.deskfree = youle_room.import.get_disbandRoom(o_room); + // } + // o_room.method.sendpack_toother(msg2, -1); + + if (o_room.battlestate == 1){ + var _deskfree = youle_room.import.get_disbandRoom(o_room); + if (_deskfree){ + if (_deskfree.sendtype && _deskfree.sendtype == 1 && _deskfree.seatlist){ + //子游戏返回的解散json数据如果有sendtype属性且值等于1则表示需要向不同的玩家各自发包,且根据seatlist数组对应给各位置的玩家发游戏解散包 + for (var n = 0; n < _deskfree.seatlist.length; n++) { + if (_deskfree.seatlist){ + msg2.data.deskfree = _deskfree.seatlist[n]; + o_room.method.sendpack_toseat(msg2, n); + } + } + } else { + msg2.data.deskfree = _deskfree; + o_room.method.sendpack_toother(msg2, -1); + } + } else { + o_room.method.sendpack_toother(msg2, -1); + } + } else { + o_room.method.sendpack_toother(msg2, -1); + } + + //释放房间 + o_game.method.free_room(o_room); + } else { + //房间服务器与代理商服务器不是同一地址 + var cfg = {}; + cfg.url = "http://" + o_agent.server + ":" + o_agent.httpport + "/index.html"; + cfg.type = "POST"; + cfg.data = JSON.stringify(msg1); + cfg.success = function(str){ + var _info = {}; + //获取房间内各玩家的解散房间状态 + _info.seats = o_room.method.get_agreefree().state; + + //获取游戏的解散包 + // if (o_room.battlestate == 1){ + // _info.deskfree = youle_room.import.get_disbandRoom(o_room); + // } + + var _deskfree = null; + if (o_room.battlestate == 1){ + _deskfree = youle_room.import.get_disbandRoom(o_room); + } + + //通知前端房间内的所有人需要断开房间服务器,连接代理服务器 + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]) { + var msg2 = {}; + msg2.app = youle_room.app.appname; + msg2.route = youle_room.routename; + msg2.rpc = "connect_agentserver"; + msg2.data = {}; + msg2.data.agentserver = o_agent.server + ":" + o_agent.tcpport; + msg2.data.data = {}; + msg2.data.data.opt = "free_room"; + msg2.data.data.playerid = o_room.seatlist[i].playerid; + msg2.data.data.agentid = agentid; + msg2.data.data.gameid = gameid; + + if (_deskfree){ + if (_deskfree.sendtype && _deskfree.sendtype == 1 && _deskfree.seatlist){ + //子游戏返回的解散json数据如果有sendtype属性且值等于1则表示需要向不同的玩家各自发包,且根据seatlist数组对应给各位置的玩家发游戏解散包 + _info.deskfree = _deskfree.seatlist[i]; + } else { + _info.deskfree = _deskfree; + } + } + + msg2.data.data.info = _info; + msg2.conmode = o_room.seatlist[i].conmode; + msg2.fromid = o_room.seatlist[i].fromid; + //此处conmode = null是为了避免触发玩家离线通知其他玩家事件 + if (o_room.seatlist[i].conmode == "tcp"){ + //更新tcp连接列表 + youle_room.tcps.method.delete_tcp(o_room.seatlist[i].fromid); + } + o_room.seatlist[i].conmode = null; + o_room.seatlist[i].fromid = null; + youle_room.app.SendPack(msg2); + } + } + //释放房间 + o_game.method.free_room(o_room); + }; + cfg.error = function(str){}; + min_http(cfg); + } + } else { + //未达成解散 + //返回前端同意解散房间成功 + delete pack.data; + youle_room.app.SendPack(pack); + //通知房间内的其他人有人同意解散房间 + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = "other_agree_free_room"; + msg.data = {}; + msg.data.seat = o_player.gameinfo.seat; + o_room.method.sendpack_toother(msg, o_player.gameinfo.seat); + } +} + +// youle_room.robotadmin = {}; +// youle_room.robotadmin.pwd = "iamad"; //机器人管家密码 +// youle_room.robotadmin.passive = {}; //机器人被动回复设置 +// youle_room.robotadmin.passive.timer1 = 3; //被动回复的时间范围(秒) +// youle_room.robotadmin.passive.timer2 = 8; //被动回复的时间范围(秒) +// youle_room.robotadmin.passive.text = 5; //被动回复文字的概率 +// youle_room.robotadmin.passive.phiz = 5; //被动回复表情的概率 +// youle_room.robotadmin.passive.gift = 25; //被动回复表情的概率 +// youle_room.robotadmin.active = {}; //机器人主动发送消息设置 +// youle_room.robotadmin.active.timer1 = 20; //主动发送消息的时间范围(秒) +// youle_room.robotadmin.active.timer2 = 40; //主动发送消息的时间范围(秒) +// youle_room.robotadmin.active.text = 1; //主动发送文字的概率 +// youle_room.robotadmin.active.phiz = 3; //主动发送表情的概率 +// youle_room.robotadmin.active.gift = 2; //主动发送表情的概率 +youle_room.robotadmin = {}; +youle_room.robotadmin.pwd = "iamad"; //机器人管家密码 +youle_room.robotadmin.passive = {}; //机器人被动回复设置 +youle_room.robotadmin.passive.timer1 = 3; //被动回复的时间范围(秒) +youle_room.robotadmin.passive.timer2 = 8; //被动回复的时间范围(秒) +youle_room.robotadmin.passive.text = 5; //被动回复文字的概率 +youle_room.robotadmin.passive.phiz = 5; //被动回复表情的概率 +youle_room.robotadmin.passive.gift = 6; //被动回复表情的概率 +youle_room.robotadmin.active = {}; //机器人主动发送消息设置 +youle_room.robotadmin.active.timer1 = 20; //主动发送消息的时间范围(秒) +youle_room.robotadmin.active.timer2 = 40; //主动发送消息的时间范围(秒) +youle_room.robotadmin.active.text = 1; //主动发送文字的概率 +youle_room.robotadmin.active.phiz = 3; //主动发送表情的概率 +youle_room.robotadmin.active.gift = 2; //主动发送表情的概率 +//发送文字 +youle_room.send_text = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var text = pack.data.text; + var type = 0; + if (pack.data.type){ + type = parseInt(pack.data.type); + } + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + + switch(type){ + case 0: + pack.data = {}; + pack.data.type = type; + pack.data.seat = o_player.gameinfo.seat; + pack.data.text = text; + if (text == youle_room.robotadmin.pwd){ + var o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var o_agentsvr_game = o_agentsvr_agent.method.find_game(gameid); + var o_agentsvr_player = o_agentsvr_agent.method.find_player(playerid); + o_agentsvr_game.robotadmin = o_agentsvr_player; + + pack.data.text = text + " succeed."; + return pack; + } else { + //通知房间内的所有人 + o_room.method.sendpack_toother(pack, -1); + + //通知机器人管家 + var o_robotadmin = null; + var o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var o_agentsvr_game = o_agentsvr_agent.method.find_game(gameid); + if (o_agentsvr_game.robotadmin){ + var o_gameinfo = o_agentsvr_game.robotadmin.method.get_gameinfo(o_agentsvr_game); + if (o_gameinfo){ + if (o_gameinfo.roomcode && o_gameinfo.conmode && o_gameinfo.fromid){ + o_robotadmin = o_agentsvr_game.robotadmin; + } + } + } + + var haverobot = false; + var toseat = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i] && (i != o_player.gameinfo.seat)) { + if (o_room.seatlist[i].isrobot){ + haverobot = true; + toseat.push(i); + } + } + } + + if (haverobot){ + if (o_robotadmin){ + pack.data.info = {}; + pack.data.info.agentid = agentid; + pack.data.info.gameid = gameid; + pack.data.info.roomcode = roomcode; + pack.data.info.playerid = playerid; + pack.data.info.seat = o_player.gameinfo.seat; + pack.data.info.toseat = toseat; + + pack.data.type = 2; + + pack.conmode = o_gameinfo.conmode; + pack.fromid = o_gameinfo.fromid; + youle_room.app.SendPack(pack); + } else { + var do_robot_passive = function(){ + if (pack.rpc == "send_text" || pack.rpc == "send_phiz"){ + if (o_room.seatlist[pack.data.seat] && o_room.seatlist[pack.data.seat].isrobot){ + o_room.method.sendpack_toother(pack, -1); + } + } else if (pack.rpc == "send_gift"){ + if (o_room.seatlist[pack.data.sendseat] && o_room.seatlist[pack.data.sendseat].isrobot && o_room.seatlist[pack.data.receiveseat]){ + o_room.method.sendpack_toother(pack, -1); + } + } + } + var rand = min_random(1, 100); + if (rand <= youle_room.robotadmin.passive.text){ + var idx = min_random(0, toseat.length - 1); + var from_seat = toseat[idx]; + pack.rpc = "send_text"; + pack.data = {}; + pack.data.type = 3; + pack.data.seat = from_seat; + pack.data.text = min_random(1, 100); + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } else if (rand <= youle_room.robotadmin.passive.text + youle_room.robotadmin.passive.phiz){ + var idx = min_random(0, toseat.length - 1); + var from_seat = toseat[idx]; + pack.rpc = "send_phiz"; + pack.data = {}; + pack.data.type = 0; + pack.data.seat = from_seat; + pack.data.text = min_random(1, 100); + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } else if (rand <= youle_room.robotadmin.passive.text + youle_room.robotadmin.passive.phiz + youle_room.robotadmin.passive.gift){ + var idx = min_random(0, toseat.length - 1); + var from_seat = toseat[idx]; + pack.rpc = "send_gift"; + pack.data = {}; + pack.data.giftid = min_random(1, 100); + pack.data.sendseat = from_seat; + pack.data.receiveseat = o_player.gameinfo.seat; + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } + } + } + } + break; + case 1: + pack.data = {}; + pack.data.type = type; + pack.data.seat = o_player.gameinfo.seat; + pack.data.text = text; + var noticelimit = youle_room.app.youle_platform.config.method.get_paravalue("noticelimit", agentid); + if (noticelimit = null){ + noticelimit = 10000000000; + } + if (noticelimit > 0){ + var _agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var _agentsvr_player = _agentsvr_agent.method.find_player(playerid); + if (_agentsvr_player.bean < noticelimit){ + var starName = youle_room.app.youle_platform.config.method.get_paravalue("starName", agentid); + if (!starName){ + starName = "星星"; + } + pack.rpc = "show_message"; + pack.data = {}; + pack.data.msg = starName + "数量不够,不能发送"; + return pack; + } + } + + var noticecost = youle_room.app.youle_platform.config.method.get_paravalue("noticecost", agentid); + if (!noticecost){ + noticecost = 0; + } + if (noticecost > 0){ + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.change = -1 * noticecost; + msg.data.pushbean = 0; + msg.data.gameid = gameid; + msg.data.type = 4; + youle_room.app.youle_agent[msg.rpc](msg); + } + + //遍历房间 + for (var i = 0; i < o_agent.games.length; i++) { + if(o_agent.games[i]){ + for (var j = 0; j < o_agent.games[i].rooms.length; j++) { + var o_room = o_agent.games[i].rooms[j]; + if (!o_room){ + continue; + } + //遍历玩家 + for (var k = 0; k < o_room.seatlist.length; k++) { + var _player = o_room.seatlist[k]; + if (!_player){ + continue; + } + if (_player.isrobot){ + continue; + } + if (_player.conmode == "tcp" && _player.fromid){ + pack.conmode = _player.conmode; + pack.fromid = _player.fromid; + youle_room.app.SendPack(pack); + } + } + } + } + } + break; + case 2: + var o_to_room = o_game.method.find_room(pack.data.info.roomcode); + if (!o_to_room){ + return; + } + + var from_seat = null; + for (var i = 1; i < 10; i++) { + var idx = min_random(0, pack.data.info.toseat.length - 1); + from_seat = pack.data.info.toseat[idx]; + if (o_to_room.seatlist[from_seat] && o_to_room.seatlist[from_seat].isrobot){ + break; + } else { + from_seat = null; + } + }; + + if (from_seat){ + pack.data = {}; + pack.data.type = 0; + pack.data.seat = from_seat; + pack.data.text = text; + o_to_room.method.sendpack_toother(pack, -1); + } + break; + } +} + +//发送表情 +youle_room.send_phiz = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var text = pack.data.text; + var type = 0; + if (pack.data.type){ + type = parseInt(pack.data.type); + } + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + + switch(type){ + case 0: + //通知房间内的所有人 + pack.data = {}; + pack.data.seat = o_player.gameinfo.seat; + pack.data.text = text; + o_room.method.sendpack_toother(pack, -1); + + //通知机器人管家 + var o_robotadmin = null; + var o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var o_agentsvr_game = o_agentsvr_agent.method.find_game(gameid); + if (o_agentsvr_game.robotadmin){ + var o_gameinfo = o_agentsvr_game.robotadmin.method.get_gameinfo(o_agentsvr_game); + if (o_gameinfo){ + if (o_gameinfo.roomcode && o_gameinfo.conmode && o_gameinfo.fromid){ + o_robotadmin = o_agentsvr_game.robotadmin; + } + } + } + + var haverobot = false; + var toseat = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i] && (i != o_player.gameinfo.seat)) { + if (o_room.seatlist[i].isrobot){ + haverobot = true; + toseat.push(i); + } + } + } + + if (haverobot){ + if (o_robotadmin){ + pack.data.info = {}; + pack.data.info.agentid = agentid; + pack.data.info.gameid = gameid; + pack.data.info.roomcode = roomcode; + pack.data.info.playerid = playerid; + pack.data.info.seat = o_player.gameinfo.seat; + pack.data.info.toseat = toseat; + + pack.data.type = 2; + + pack.conmode = o_gameinfo.conmode; + pack.fromid = o_gameinfo.fromid; + youle_room.app.SendPack(pack); + } else { + var do_robot_passive = function(){ + if (pack.rpc == "send_text" || pack.rpc == "send_phiz"){ + if (o_room.seatlist[pack.data.seat] && o_room.seatlist[pack.data.seat].isrobot){ + o_room.method.sendpack_toother(pack, -1); + } + } else if (pack.rpc == "send_gift"){ + if (o_room.seatlist[pack.data.sendseat] && o_room.seatlist[pack.data.sendseat].isrobot && o_room.seatlist[pack.data.receiveseat]){ + o_room.method.sendpack_toother(pack, -1); + } + } + } + var rand = min_random(1, 100); + if (rand <= youle_room.robotadmin.passive.text){ + var idx = min_random(0, toseat.length - 1); + var from_seat = toseat[idx]; + pack.rpc = "send_text"; + pack.data = {}; + pack.data.type = 3; + pack.data.seat = from_seat; + pack.data.text = min_random(1, 100); + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } else if (rand <= youle_room.robotadmin.passive.text + youle_room.robotadmin.passive.phiz){ + var idx = min_random(0, toseat.length - 1); + var from_seat = toseat[idx]; + pack.rpc = "send_phiz"; + pack.data = {}; + pack.data.type = 0; + pack.data.seat = from_seat; + pack.data.text = min_random(1, 100); + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } else if (rand <= youle_room.robotadmin.passive.text + youle_room.robotadmin.passive.phiz + youle_room.robotadmin.passive.gift){ + var idx = min_random(0, toseat.length - 1); + var from_seat = toseat[idx]; + pack.rpc = "send_gift"; + pack.data = {}; + pack.data.giftid = min_random(1, 100); + pack.data.sendseat = from_seat; + pack.data.receiveseat = o_player.gameinfo.seat; + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } + } + } + break; + case 2: + var o_to_room = o_game.method.find_room(pack.data.info.roomcode); + if (!o_to_room){ + return; + } + var from_seat = null; + for (var i = 1; i < 10; i++) { + var idx = min_random(0, pack.data.info.toseat.length - 1); + from_seat = pack.data.info.toseat[idx]; + if (o_to_room.seatlist[from_seat] && o_to_room.seatlist[from_seat].isrobot){ + break; + } else { + from_seat = null; + } + }; + if (from_seat){ + pack.data = {}; + pack.data.type = 0; + pack.data.seat = from_seat; + pack.data.text = text; + o_to_room.method.sendpack_toother(pack, -1); + } + break; + } +} + +//发送语音 +youle_room.send_voice = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var voiceurl = pack.data.voiceurl; + var time = pack.data.time; + var type = 0; + if (pack.data.type){ + type = parseInt(pack.data.type); + } + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + + switch(type){ + case 0: + //通知房间内的所有人 + pack.data = {}; + pack.data.seat = o_player.gameinfo.seat; + pack.data.voiceurl = voiceurl; + pack.data.time = time; + o_room.method.sendpack_toother(pack, -1); + + //通知机器人管家 + var o_robotadmin = null; + var o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var o_agentsvr_game = o_agentsvr_agent.method.find_game(gameid); + if (o_agentsvr_game.robotadmin){ + var o_gameinfo = o_agentsvr_game.robotadmin.method.get_gameinfo(o_agentsvr_game); + if (o_gameinfo){ + if (o_gameinfo.roomcode && o_gameinfo.conmode && o_gameinfo.fromid){ + o_robotadmin = o_agentsvr_game.robotadmin; + } + } + } + + var haverobot = false; + var toseat = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i] && (i != o_player.gameinfo.seat)) { + if (o_room.seatlist[i].isrobot){ + haverobot = true; + toseat.push(i); + } + } + } + + if (haverobot){ + if (o_robotadmin){ + pack.data.info = {}; + pack.data.info.agentid = agentid; + pack.data.info.gameid = gameid; + pack.data.info.roomcode = roomcode; + pack.data.info.playerid = playerid; + pack.data.info.seat = o_player.gameinfo.seat; + pack.data.info.toseat = toseat; + + pack.data.type = 2; + + pack.conmode = o_gameinfo.conmode; + pack.fromid = o_gameinfo.fromid; + youle_room.app.SendPack(pack); + } else { + var do_robot_passive = function(){ + if (pack.rpc == "send_text" || pack.rpc == "send_phiz"){ + if (o_room.seatlist[pack.data.seat] && o_room.seatlist[pack.data.seat].isrobot){ + o_room.method.sendpack_toother(pack, -1); + } + } else if (pack.rpc == "send_gift"){ + if (o_room.seatlist[pack.data.sendseat] && o_room.seatlist[pack.data.sendseat].isrobot && o_room.seatlist[pack.data.receiveseat]){ + o_room.method.sendpack_toother(pack, -1); + } + } + } + var rand = min_random(1, 100); + if (rand <= youle_room.robotadmin.passive.text){ + var idx = min_random(0, toseat.length - 1); + var from_seat = toseat[idx]; + pack.rpc = "send_text"; + pack.data = {}; + pack.data.type = 3; + pack.data.seat = from_seat; + pack.data.text = min_random(1, 100); + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } else if (rand <= youle_room.robotadmin.passive.text + youle_room.robotadmin.passive.phiz){ + var idx = min_random(0, toseat.length - 1); + var from_seat = toseat[idx]; + pack.rpc = "send_phiz"; + pack.data = {}; + pack.data.type = 0; + pack.data.seat = from_seat; + pack.data.text = min_random(1, 100); + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } else if (rand <= youle_room.robotadmin.passive.text + youle_room.robotadmin.passive.phiz + youle_room.robotadmin.passive.gift){ + var idx = min_random(0, toseat.length - 1); + var from_seat = toseat[idx]; + pack.rpc = "send_gift"; + pack.data = {}; + pack.data.giftid = min_random(1, 100); + pack.data.sendseat = from_seat; + pack.data.receiveseat = o_player.gameinfo.seat; + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } + } + } + break; + case 2: + var o_to_room = o_game.method.find_room(pack.data.info.roomcode); + if (!o_to_room){ + return; + } + var from_seat = null; + for (var i = 1; i < 10; i++) { + var idx = min_random(0, pack.data.info.toseat.length - 1); + from_seat = pack.data.info.toseat[idx]; + if (o_to_room.seatlist[from_seat] && o_to_room.seatlist[from_seat].isrobot){ + break; + } else { + from_seat = null; + } + }; + if (from_seat){ + pack.data = {}; + pack.data.type = 0; + pack.data.seat = from_seat; + pack.data.voiceurl = voiceurl; + pack.data.time = time; + o_to_room.method.sendpack_toother(pack, -1); + } + break; + } +} + +//发送礼物 +youle_room.send_gift = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var giftid = pack.data.giftid; + var receiveseat = parseInt(pack.data.receiveseat); + var type = 0; + if (pack.data.type){ + type = parseInt(pack.data.type); + } + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + + switch(type){ + case 0: + //通知房间内的所有人 + pack.data = {}; + pack.data.giftid = giftid; + pack.data.sendseat = o_player.gameinfo.seat; + pack.data.receiveseat = receiveseat; + o_room.method.sendpack_toother(pack, -1); + + //通知机器人管家 + var o_robotadmin = null; + var o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var o_agentsvr_game = o_agentsvr_agent.method.find_game(gameid); + if (o_agentsvr_game.robotadmin){ + var o_gameinfo = o_agentsvr_game.robotadmin.method.get_gameinfo(o_agentsvr_game); + if (o_gameinfo){ + if (o_gameinfo.roomcode && o_gameinfo.conmode && o_gameinfo.fromid){ + o_robotadmin = o_agentsvr_game.robotadmin; + } + } + } + + var haverobot = false; + var toseat = []; + if (o_room.seatlist[receiveseat] && o_room.seatlist[receiveseat].isrobot){ + haverobot = true; + toseat.push(receiveseat); + } + + if (haverobot){ + if (o_robotadmin){ + pack.data.info = {}; + pack.data.info.agentid = agentid; + pack.data.info.gameid = gameid; + pack.data.info.roomcode = roomcode; + pack.data.info.playerid = playerid; + pack.data.info.seat = o_player.gameinfo.seat; + pack.data.info.toseat = toseat; + + pack.data.type = 2; + + pack.conmode = o_gameinfo.conmode; + pack.fromid = o_gameinfo.fromid; + youle_room.app.SendPack(pack); + } else { + var do_robot_passive = function(){ + if (pack.rpc == "send_text" || pack.rpc == "send_phiz"){ + if (o_room.seatlist[pack.data.seat] && o_room.seatlist[pack.data.seat].isrobot){ + o_room.method.sendpack_toother(pack, -1); + } + } else if (pack.rpc == "send_gift"){ + if (o_room.seatlist[pack.data.sendseat] && o_room.seatlist[pack.data.sendseat].isrobot && o_room.seatlist[pack.data.receiveseat]){ + o_room.method.sendpack_toother(pack, -1); + } + } + } + var rand = min_random(1, 100); + if (rand <= youle_room.robotadmin.passive.text){ + pack.rpc = "send_text"; + pack.data = {}; + pack.data.type = 3; + pack.data.seat = receiveseat; + pack.data.text = min_random(1, 100); + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } else if (rand <= youle_room.robotadmin.passive.text + youle_room.robotadmin.passive.phiz){ + pack.rpc = "send_phiz"; + pack.data = {}; + pack.data.type = 0; + pack.data.seat = receiveseat; + pack.data.text = min_random(1, 100); + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } else if (rand <= youle_room.robotadmin.passive.text + youle_room.robotadmin.passive.phiz + youle_room.robotadmin.passive.gift){ + pack.rpc = "send_gift"; + pack.data = {}; + pack.data.giftid = min_random(1, 100); + pack.data.sendseat = receiveseat; + pack.data.receiveseat = o_player.gameinfo.seat; + + var time = min_random(youle_room.robotadmin.passive.timer1, youle_room.robotadmin.passive.timer2); + if (!o_room.timer_robot_passive){ + o_room.timer_robot_passive = []; + } + o_room.timer_robot_passive.push(min_ontimeout(do_robot_passive, time * 1000)); + } + } + } + break; + case 2: + var o_to_room = o_game.method.find_room(pack.data.info.roomcode); + if (!o_to_room){ + return; + } + var from_seat = null; + for (var i = 1; i < 10; i++) { + var idx = min_random(0, pack.data.info.toseat.length - 1); + from_seat = pack.data.info.toseat[idx]; + if (o_to_room.seatlist[from_seat] && o_to_room.seatlist[from_seat].isrobot){ + break; + } else { + from_seat = null; + } + }; + if (from_seat && o_to_room.seatlist[pack.data.info.seat]){ + var receiveseat = pack.data.info.seat; + pack.data = {}; + pack.data.type = 0; + pack.data.giftid = giftid; + pack.data.sendseat = from_seat; + pack.data.receiveseat = receiveseat; + o_to_room.method.sendpack_toother(pack, -1); + } + break; + } +} + +//在打电话 +youle_room.call_phone = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + + //修改玩家状态 + o_player.onstate = 2; + + //通知房间内的所有人 + pack.data = {}; + pack.data.seat = o_player.gameinfo.seat; + o_room.method.sendpack_toother(pack, -1); +} + +//挂断电话 +youle_room.hangup_phone = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + + //修改玩家状态 + o_player.onstate = 0; + + //通知房间内的所有人 + pack.data = {}; + pack.data.seat = o_player.gameinfo.seat; + o_room.method.sendpack_toother(pack, -1); +} + +//广播即时消息 +youle_room.broadcast = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var msgcontent = pack.data.msgcontent; + var msgtype = 0; + if (pack.data.msgtype){ + msgtype = pack.data.msgtype; + } + //检查代理商id是否存在 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent) { + pack.data = {}; + pack.data.state = 1; + return pack; + }; + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + pack.data = {}; + pack.data.state = 1; + return pack; + } + + //广播 + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.routename; + msg.rpc = "broadcast"; + msg.data = {}; + msg.data.msgtype = msgtype; + msg.data.msgcontent = msgcontent; + //遍历房间 + for (var j = 0; j < o_game.rooms.length; j++) { + var o_room = o_game.rooms[j]; + if (!o_room){ + continue; + } + //遍历玩家 + for (var k = 0; k < o_room.seatlist.length; k++) { + var o_player = o_room.seatlist[k]; + if (!o_player){ + continue; + } + if (o_player.conmode == "tcp" && o_player.fromid){ + msg.conmode = o_player.conmode; + msg.fromid = o_player.fromid; + youle_room.app.SendPack(msg); + } + } + } + + //返回执行结果 + pack.data = {}; + pack.data.state = 0; + return pack; +} + + +//未开局之前房主投降 +youle_room.beanroom_surrender = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var count = parseInt(pack.data.count); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + //检查房间是否已经开战 + if (o_room.battlestate != 0){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房间已开战,不能投降"; + pack.data.showerror = 1; + return pack; + } + //检查玩家是否是房主 + if (o_player.playerid != o_room.owner.playerid){ + return; + } + //检查投降的数量必须为正数 + if (count <= 0){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "投降的数量必须大于0"; + pack.data.showerror = 1; + return pack; + } + + //通知代理服务器解散房间 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "beanroom_surrender"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.gameid = gameid; + msg1.data.roomcode = roomcode; + //玩家列表 + var _ownerseat = -1; + var _players = []; + var _playercount = 0; + msg1.data.playeridlist = []; + for (var i = 0; i < o_room.seatlist.length; i++) { + if (o_room.seatlist[i]){ + msg1.data.playeridlist.push(o_room.seatlist[i].playerid); + + _playercount = _playercount + 1; + if (o_room.seatlist[i].playerid == o_room.owner.playerid){ + if (o_room.seatlist[i].bean < count){//判断房主的星星数量是否够 + pack.data = {}; + pack.data.state = 1; + pack.data.error = starName + "数量不够,不能投降"; + pack.data.showerror = 1; + return pack; + } + _ownerseat = i; + if (count){ + _players.push(-1 * count); + } else { + _players.push(0); + } + } else { + if (count){ + _players.push(count); + } else { + _players.push(0); + } + } + } else { + _players.push(null); + } + } + + //房间内必须只有两人 + if (_playercount != 2 || _ownerseat == -1){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "房间内有多个玩家,不能投降,只有两个玩家时才能投降"; + pack.data.showerror = 1; + return pack; + } + var starName = youle_room.app.youle_platform.config.method.get_paravalue("starName", agentid); + if (!starName){ + starName = "星星"; + } + //检查星星的投降最小数量 + var surrender_min = youle_room.app.youle_platform.config.method.get_paravalue("surrender_min", o_agent.agentid, o_game.gameid); + if (!surrender_min){ + surrender_min = 1; + } + if (count < surrender_min){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "投降的"+starName+"最小数量是"+surrender_min+",请重新设置投降底翻"; + pack.data.showerror = 1; + return pack; + } + + //检查星星的投降最小单位数 + var surrender_unit = youle_room.app.youle_platform.config.method.get_paravalue("surrender_unit", o_agent.agentid, o_game.gameid); + if (!surrender_unit){ + surrender_unit = 1; + } + if (count < surrender_unit || count % surrender_unit != 0){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "投降的"+starName+"最小单位数是"+surrender_unit+",请重新设置投降底翻"; + pack.data.showerror = 1; + return pack; + } + + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + youle_room.app.youle_agent[msg1.rpc](msg1); + + var sql = "insert into gold_account(goac_id,goac_agentid,goac_gameid,goac_roomcode,goac_roommode,goac_createtime,goac_beanmult,goac_ownerid,goac_shortcode,goac_deduct,goac_roomcard,goac_datatype,goac_asetcount,goac_asetnum,goac_playerid,goac_isrobot,goac_grade,goac_rebatefrom,goac_rebateto,goac_toplayerid,goac_rebatemode,goac_setvalue,goac_rebatevalue,goac_time,goac_invitecode,goac_inviteid,goac_marketid) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + //房间id = 时间戳 + 4位随机码 + 房号 + var roomid = new Date().getTime() + min_randomChar(4) + roomcode; + var time = min_now(); + var beanmult = 0; + if (o_room.beanmult){ + beanmult = o_room.beanmult; + } + var shortcode = 0; + if (o_room.shortcode){ + shortcode = o_room.shortcode; + } + var deductmode = youle_room.import.deduct_roomcard_mode(o_room.o_game, o_room.roomtype); + + for (var i = 0; i < o_room.seatlist.length; i++){ + if (o_room.seatlist[i]){ + var _change; + if (o_room.seatlist[i].playerid == o_room.owner.playerid){ + + _change = -1 * count; + } else { + _change = count; + } + + var msg = {}; + msg.app = youle_room.app.appname; + msg.route = youle_room.app.youle_agent.routename; + msg.rpc = "update_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = o_room.seatlist[i].playerid; + msg.data.change = _change; + msg.data.pushbean = 0; + msg.data.gameid = gameid; + youle_room.app.youle_agent[msg.rpc](msg); + + var __o_agentsvr_agent = youle_agent.agents.method.find_agent(agentid); + var __o_agentsvr_player = __o_agentsvr_agent.method.find_player(o_room.seatlist[i].playerid); + var invitecode = __o_agentsvr_player.invitecode; + var inviteid = __o_agentsvr_player.inviteid; + var marketid = __o_agentsvr_player.marketid; + youle_room.mysql_grade.execsql("execsql", sql, [roomid, agentid, gameid, roomcode, 3, o_room.createtime, beanmult, o_room.owner.playerid, shortcode, deductmode, 0, 0, o_room.asetcount, 1, o_room.seatlist[i].playerid, 0, _change, 0, 0, 0, 0, 0, 0, time, invitecode, inviteid, marketid]); + } + } + + //返回解散房间成功结果 + pack.data = {}; + pack.data.state = 0; + pack.data.players = _players; + pack.data.ownerseat = _ownerseat; + o_room.method.sendpack_toother(pack, -1); + //释放房间 + o_game.method.free_room(o_room); + } +} + + +//分享到星星争霸场 +youle_room.share_room = function(pack){ + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + var roomlist = pack.data.roomlist; + //是否为vip房分享 + var shareType;; + if(!pack.data.shareType){ + shareType = 0; + }else{ + shareType = parseInt(pack.data.shareType); + } + var roomlist=pack.data.roomlist; + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + return; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + return; + } + if(!shareType){ + //查找房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + return; + } + + //查找玩家 + var o_player = o_room.method.find_player(playerid); + if (!o_player){ + return; + } + //检查玩家是否是房主 + if (o_player.playerid != o_room.owner.playerid){ + return; + } + //只有星星场才能分享 + if (o_room.roommode != 1){ + return; + } + }else{ + var o_room = o_game.method.find_room(roomlist[0]); + if (!o_room){ + return; + } + //只有星星场才能分享 + if (o_room.roommode != 1){ + return; + } + } + + + //通知代理服务器分享房间到星星争霸场 + var msg1 = {}; + msg1.app = youle_room.app.appname; + msg1.route = youle_room.app.youle_agent.routename; + msg1.rpc = "share_room"; + msg1.data = {}; + msg1.data.agentid = agentid; + msg1.data.playerid = playerid; + msg1.data.gameid = gameid; + msg1.data.roomcode = roomcode; + msg1.data.shareType = shareType; + msg1.data.roomtype = o_room.roomtype; + msg1.data.roomlist = roomlist; + + if (o_agent.server == o_game.roomserver){ + //房间服务器与代理商服务器是同一地址 + var r = youle_room.app.youle_agent[msg1.rpc](msg1); + //返回解散房间成功结果 + if (r){ + youle_room.app.SendPack(pack); + } + } +} + +//获取服务器上房间的调试数据 +youle_room.get_debuggerdata = function(pack){ + var agentid = pack.data.agentid; + var gameid = pack.data.gameid; + var roomcode = parseInt(pack.data.roomcode); + + //查找代理商 + var o_agent = youle_room.agents.method.find_agent(agentid); + if (!o_agent){ + pack.data.msg = "未找到agent"; + return pack; + } + //查找游戏 + var o_game = o_agent.method.find_game(gameid); + if (!o_game){ + pack.data.msg = "未找到game"; + return pack; + } + //查找存在的房间 + var o_room = o_game.method.find_room(roomcode); + if (!o_room){ + pack.data.msg = "未找到room"; + return pack; + } + //查找牌桌 + var o_desk = o_room.o_desk; + if (!o_desk){ + pack.data.msg = "未找到desk"; + return pack; + } + //查找o_desk.pack + var o_pack = o_desk.pack; + if (!o_pack){ + pack.data.msg = "未找到desk.pack"; + return pack; + } + + o_desk.flow.save_pack_curraset( + function(){ + pack.data.msg = "保存牌局调试数据成功"; + youle_room.app.SendPack(pack); + }, + function(){ + pack.data.msg = "保存牌局调试数据失败"; + youle_room.app.SendPack(pack); + } + ); +} diff --git a/server/youle/server_synch/mod.js b/server/youle/server_synch/mod.js new file mode 100644 index 0000000..4c47ae6 --- /dev/null +++ b/server/youle/server_synch/mod.js @@ -0,0 +1,733 @@ +var youle_synch = youle_synch || cls_mod.new("youle_synch", "synch", youle_app); + +//mysql操作类 +youle_synch.mysql = cls_mysql.new("http://localhost/mysql.php"); +youle_synch.mysql_grade = cls_mysql.new("http://localhost/mysql_grade.php"); +youle_synch.mysql_agent = cls_mysql.new("http://localhost/mysql_agent.php"); + +youle_synch.configlist = []; +youle_synch.configfile = []; +youle_synch.configfile.push("http://ylyxservice1.0791ts.cn/config/update_json.txt"); + + +for (var i = 0; i < youle_synch.configfile.length; i++) { + var o_config = cls_youle_platform_config.new(youle_synch.configfile[i]); + o_config.method.get_config(); + youle_synch.configlist.push(o_config); +}; + +youle_synch.do_readdata = function(){ + if (youle_synch.isdoing == 1) { + return; + } + var compressWhiteList = function(data){ + var dataStr = JSON.stringify(data); + if(dataStr.length > 6000){//超出 + var cmpStr = LZString.compressToUTF16(dataStr); + return "zip"+cmpStr; + }else{ + return dataStr; + } + } + var deCompressWhiteList = function(data){ + var dataStr = data; + if(dataStr.indexOf("zip")==0){//压缩 + var str = dataStr.slice(3,dataStr.length); + var deStr = LZString.decompressFromUTF16(str); + return JSON.parse(deStr); + }else{ + return JSON.parse(dataStr); + } + } + //充房卡充星星 + var update_gamedb_roomcard = function(agentid, playerid, changeroomcard, changebean, opttype, optid){ + var db_callback_game1 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + //通知各子游戏服务器 + switch(parseInt(opttype)){ + case 1: //玩家购卡 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_roomcard"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.opt = 1; + msg.data.roomcard = parseInt(dbdata[0].play_roomcard); + msg.data.change = changeroomcard; + break; + case 2: //代理充卡 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_roomcard"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.opt = 2; + msg.data.roomcard = parseInt(dbdata[0].play_roomcard); + msg.data.change = changeroomcard; + break; + case 3: //玩家索卡 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_roomcard"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.opt = 2; + msg.data.roomcard = parseInt(dbdata[0].play_roomcard); + msg.data.change = changeroomcard; + break; + case 11: //玩家购买星星 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.opt = 1; + msg.data.bean = parseInt(dbdata[0].play_bean); + msg.data.change = changebean; + break; + case 12: //代理充值星星 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.opt = 2; + msg.data.bean = parseInt(dbdata[0].play_bean); + msg.data.change = changebean; + break; + case 13: //玩家索要星星 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_bean"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.opt = 2; + msg.data.bean = parseInt(dbdata[0].play_bean); + msg.data.change = changebean; + break; + } + do_synch(optid, agentid, msg); + } else { + do_result(optid, 0); + } + } + var sql_game = "call cp_sales2_player_topup(?,?,?,?)"; + youle_synch.mysql.execsql("opensql", sql_game, [agentid, playerid, changeroomcard, changebean], db_callback_game1); + } + //玩家绑定邀请码 + var update_gamedb_bindinginvitecode = function(agentid, unionid, invitecode, optid){ + var db_callback_game2 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + //通知各子游戏服务器 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_invitecode"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = parseInt(dbdata[0].playerid); + msg.data.invitecode = invitecode; + do_synch(optid, agentid, msg); + } else { + do_result(optid, 0); + } + } + var sql_game = "call cp_sales2_player_bindinginvitecode(?,?,?)"; + youle_synch.mysql.execsql("opensql", sql_game, [agentid, unionid, invitecode], db_callback_game2); + } + //新代理 + var update_gamedb_newsalesman = function(agentid, channelid, salesid, salesinfo, optid){ + var db_callback_game3 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + } else { + do_result(optid, 0); + } + } + var sql_game = "call cp_sales2_new_salesman(?,?,?,?,?,?,?,?,?,?)"; + var o_salesinfo = JSON.parse(salesinfo); + youle_synch.mysql.execsql("opensql", sql_game, [agentid, channelid, o_salesinfo.openid, o_salesinfo.unionid, o_salesinfo.nickname, o_salesinfo.avatar, o_salesinfo.sex, o_salesinfo.province, o_salesinfo.city, salesid], db_callback_game3); + } + //封禁代理 + var update_gamedb_disabledsales = function(agentid, salesid, optid){ + var db_callback_game4 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + } else { + do_result(optid, 0); + } + } + var sql_game = "call cp_sales2_salesman_disabled(?,?)"; + youle_synch.mysql.execsql("opensql", sql_game, [agentid, salesid], db_callback_game4); + } + //解封代理 + var update_gamedb_abledsales = function(agentid, salesid, optid){ + var db_callback_game5 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + } else { + do_result(optid, 0); + } + } + var sql_game = "call cp_sales2_salesman_abled(?,?)"; + youle_synch.mysql.execsql("opensql", sql_game, [agentid, salesid], db_callback_game5); + } + //解绑代理 + var update_gamedb_disbinding_sales = function(agentid, salesid, optid){ + var db_callback_game6 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + } else { + do_result(optid, 0); + } + } + var sql_game = "call cp_sales2_disbinding_sales(?,?)"; + youle_synch.mysql.execsql("opensql", sql_game, [agentid, salesid], db_callback_game6); + } + //解绑玩家 + var update_gamedb_disbinding_player = function(agentid, playerid, optid){ + var db_callback_game7 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + //通知各子游戏服务器 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_invitecode"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.invitecode = null; + do_synch(optid, agentid, msg); + } else { + do_result(optid, 0); + } + } + var sql_game = "call cp_sales2_disbinding_player(?,?)"; + youle_synch.mysql.execsql("opensql", sql_game, [agentid, playerid], db_callback_game7); + } + //封禁玩家 + var update_gamedb_disabledplayer = function(agentid, playerid, optid){ + var db_callback_game8 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + //通知各子游戏服务器 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_playerstate"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.state = 1; + do_synch(optid, agentid, msg); + } else { + do_result(optid, 0); + } + } + var sql_game = "call cp_sales2_player_disabled(?,?)"; + youle_synch.mysql.execsql("opensql", sql_game, [agentid, playerid], db_callback_game8); + } + //解封玩家 + var update_gamedb_abledplayer = function(agentid, playerid, optid){ + var db_callback_game9 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + //通知各子游戏服务器 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_playerstate"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.state = 0; + do_synch(optid, agentid, msg); + } else { + do_result(optid, 0); + } + } + var sql_game = "call cp_sales2_player_abled(?,?)"; + youle_synch.mysql.execsql("opensql", sql_game, [agentid, playerid], db_callback_game9); + } + //开通高级选项 + var update_gamedb_advanced = function(agentid, playerid, shortcode, advanced, desone, destwo, lower_limit, announcement, optid){ + var db_callback_game10 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + //通知各子游戏服务器 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_shortcode"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.shortcode = shortcode; + msg.data.advanced = advanced; + msg.data.desone = desone; + msg.data.destwo = destwo; + msg.data.lower_limit = lower_limit; + msg.data.announcement = announcement; + do_synch(optid, agentid, msg); + } else { + do_result(optid, 0); + } + } + var sql_game = "call cp_game_player_advanced(?,?,?,?,?,?,?,?)"; + var _shortcode = parseInt(shortcode); + var _advanced = parseInt(advanced); + youle_synch.mysql.execsql("opensql", sql_game, [agentid, playerid, _advanced, _shortcode, desone, destwo, lower_limit, announcement], db_callback_game10); + } + //设置短号白名单 + var update_gamedb_whitelist = function(agentid, playerid, whitelist, optid){ + if(agentid == "00bA05haB0d9ZC0fwGD09Q2OA30insbQ"){ + do_result(optid, 1); + return; + } + var db_callback_game11 = function(dbdata){ + if (dbdata[0].result == 0){ + do_result(optid, 1); + //通知各子游戏服务器 + var msg = {}; + msg.app = "youle"; + msg.route = "agent"; + msg.rpc = "synch_whitelist"; + msg.data = {}; + msg.data.agentid = agentid; + msg.data.playerid = playerid; + msg.data.whitelist = whitelist; + do_synch(optid, agentid, msg); + } else { + do_result(optid, 0); + } + } + for(var i=0;i "localhost", + "name" => "youle_games", + "user" => "root", + "pwd" => "root", + "port" => "3333" +); + +//错误编码及提示 +$G_Error = array( + "condb" => array("code" => 81, "msg" => "database connection failed."), + "execsql" => array("code" => 82, "msg" => "failed to operate database."), + "wrong_a" => array("code" => 83, "msg" => "a parameter error."), + "wrong_c" => array("code" => 84, "msg" => "c parameter error."), + "wrong_g" => array("code" => 85, "msg" => "g parameter error."), + "wrong_do" => array("code" => 86, "msg" => "do parameter error."), + "wrong_data" => array("code" => 87, "msg" => "data parameter error."), + "wrong_openid" => array("code" => 88, "msg" => "openid is null."), + "wrong_unionid" => array("code" => 89, "msg" => "unionid is null.") +); + +///////////////////////////// 全局变量 ///////////////////////////// +//接口函数返回的json对象 +$G_Result = array( + "state" => 0, //0:成功 <>0:错误编码 + "error" => "", //错误时的描述 + "data" => array() //成功时的数据 +); + +//返回结果$G_G_Result +function do_return(){ + global $G_Result; + if (count($G_Result["data"]) == 0) { + $G_Result["data"] = new stdClass; + } + echo json_encode($G_Result); + exit(); +} + +////////////////////////// 连接MYSQL数据库 ///////////////////////// +$PDO = null; +try +{ + $PDO = new PDO("mysql:host=".$G_MySql["host"].";port=".$G_MySql["port"].";dbname=".$G_MySql["name"], $G_MySql["user"], $G_MySql["pwd"]); + $PDO->exec("set names utf8;"); + $PDO->exec("use ".$G_MySql["name"].";"); +} +catch (Exception $e) +{ + $G_Result['state'] = $G_Error["condb"]["code"]; + $G_Result['error'] = $G_Error["condb"]["msg"]; + do_return(); +} + +//////////////////////////// 读取参数 //////////////////////////// +$a = GetRequest("a"); +if ($a == null || $a == "") { + $G_Result['state'] = $G_Error["wrong_a"]["code"]; + $G_Result['error'] = $G_Error["wrong_a"]["msg"]; + do_return(); +} + +$c = GetRequest("c"); +if ($c == null || $c == "") { + $G_Result['state'] = $G_Error["wrong_c"]["code"]; + $G_Result['error'] = $G_Error["wrong_c"]["msg"]; + do_return(); +} + +$g = GetRequest("g"); +if ($g == null || $g == "") { + $G_Result['state'] = $G_Error["wrong_g"]["code"]; + $G_Result['error'] = $G_Error["wrong_g"]["msg"]; + do_return(); +} + +$do = GetRequest("do"); +if ($do != 1 && $do != 2) { + $G_Result['state'] = $G_Error["wrong_do"]["code"]; + $G_Result['error'] = $G_Error["wrong_do"]["msg"]; + do_return(); +} + +//读取data参数 +$str_data = GetRequest("data"); +//php收到后将^号替换为=号,替换回来,,以避免=号POST传参会截断的问题 +//$str_data = str_replace("^","=",$str_data); +//检查data是否为空 +if ($str_data == null || $str_data == "") { + $G_Result['state'] = $G_Error["wrong_data"]["code"]; + $G_Result['error'] = $G_Error["wrong_data"]["msg"]; + do_return(); +} + +//检查data是否能转成json +$json_data = json_decode($str_data); +if ($json_data == null) { + $G_Result['state'] = $G_Error["wrong_data"]["code"]; + $G_Result['error'] = $G_Error["wrong_data"]["msg"]; + do_return(); +} + +switch ($do){ + case 1: + get_roomcard($a, $c, $g, $json_data); + do_return(); + break; + case 2: + change_roomcard($a, $c, $g, $json_data); + do_return(); + break; +} + +//根据参数名获取参数 +function GetRequest($name) { + return isset($_REQUEST[$name])?$_REQUEST[$name]:''; +} + +//获取房卡数量 +function get_roomcard($agentid, $channelid, $gameid, $data) { + global $G_Result, $G_Error, $PDO; + $openid = $data->openid; + $unionid = $data->unionid; + $nickname = $data->nickname; + $avatar = $data->avatar; + $sex = $data->sex; + $province = $data->province; + $city = $data->city; + + if ($openid == null || $openid == "") { + $G_Result['state'] = $G_Error["wrong_openid"]["code"]; + $G_Result['error'] = $G_Error["wrong_openid"]["msg"]; + do_return(); + } + + if ($unionid == null || $unionid == "") { + $G_Result['state'] = $G_Error["wrong_unionid"]["code"]; + $G_Result['error'] = $G_Error["wrong_unionid"]["msg"]; + do_return(); + } + + $stmt = $PDO->prepare("call cp_third_get_roomcard(?,?,?,?,?,?,?,?,?,?)"); + + if ($stmt->execute(array($agentid, $channelid, $gameid, $openid, $unionid, $nickname, $avatar, $sex, $province, $city))){ + $aryData = $stmt->fetch(PDO::FETCH_NAMED); + if ($aryData["result"] != 0){ + $G_Result['state'] = $aryData["result"]; + $G_Result['error'] = $aryData["error"]; + } else { + $G_Result['data']["playerid"] = (int)$aryData["playerid"]; + $G_Result['data']["roomcard"] = (int)$aryData["roomcard"]; + } + } else { + $G_Result['state'] = $G_Error["execsql"]["code"]; + $G_Result['error'] = $G_Error["execsql"]["msg"]; + } +} + +//修改房卡数量 +function change_roomcard($agentid, $channelid, $gameid, $data) { + global $G_Result, $G_Error, $PDO; + $openid = $data->openid; + $unionid = $data->unionid; + $change = $data->change; + + $stmt = $PDO->prepare("call cp_third_change_roomcard(?,?,?,?,?,?)"); + + if ($stmt->execute(array($agentid, $channelid, $gameid, $openid, $unionid, $change))){ + $aryData = $stmt->fetch(PDO::FETCH_NAMED); + if ($aryData["result"] != 0){ + $G_Result['state'] = $aryData["result"]; + $G_Result['error'] = $aryData["error"]; + } else { + $G_Result['data']["playerid"] = (int)$aryData["playerid"]; + $G_Result['data']["roomcard"] = (int)$aryData["roomcard"]; + $G_Result['data']["change"] = (int)$aryData["amount"]; + } + } else { + $G_Result['state'] = $G_Error["execsql"]["code"]; + $G_Result['error'] = $G_Error["execsql"]["msg"]; + } +} + +?> \ No newline at end of file diff --git a/server/youle/web_agent/mod.js b/server/youle/web_agent/mod.js new file mode 100644 index 0000000..3ab50f8 --- /dev/null +++ b/server/youle/web_agent/mod.js @@ -0,0 +1,136 @@ +////////////////////////////////////////////////////////////////// +///////////////////////////// 运营商 ///////////////////////////// +////////////////////////////////////////////////////////////////// + +var youle_business = youle_business || cls_mod.new("youle_business", "business", youle_app); + +//mysql操作类 +youle_business.mysql = cls_mysql.new("http://localhost/mysql.php", youle_business.app.SendPack); + +//登录 +youle_business.login = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = dbdata[0].error; + } else { + pack.data = {}; + pack.data.state = 0; + pack.data.agentid = dbdata[0].agentid; + pack.data.agentname = dbdata[0].agentname; + pack.data.profittotal = dbdata[0].profittotal; + pack.data.paymenttotal = dbdata[0].paymenttotal; + pack.data.sharingrate = dbdata[0].sharingrate; + pack.data.playercount = dbdata[0].playercount; + pack.data.newplayer = dbdata[0].newplayer; + pack.data.salecount = dbdata[0].salecount; + pack.data.newsale = dbdata[0].newsale; + } + youle_business.app.SendPack(pack); + } + + var user = pack.data.user; + var pwd = pack.data.pwd; + var sql = "call cp_agent_login(?,?);"; + youle_business.mysql.execsql("opensql", sql, [user, pwd], db_callback, pack); +} + +//修改登录密码 +youle_business.update_pwd = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = dbdata[0].error; + } else { + pack.data = {}; + pack.data.state = 0; + } + youle_business.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var oldpwd = pack.data.oldpwd; + var newpwd = pack.data.newpwd; + var sql = "call cp_agent_updatepwd(?,?,?);"; + youle_business.mysql.execsql("opensql", sql, [agentid, oldpwd, newpwd], db_callback, pack); +} + +//按月度统计流水 +youle_business.profit_month = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_business.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var sql = "call cp_agent_profit_month(?);"; + youle_business.mysql.execsql("opensql", sql, [agentid], db_callback, pack); +} + +//按产品统计流水 +youle_business.profit_product = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_business.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var sql = "call cp_agent_profit_product(?);"; + youle_business.mysql.execsql("opensql", sql, [agentid], db_callback, pack); +} + +//按月度统计新增玩家数量 +youle_business.player_month = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_business.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var sql = "call cp_agent_player_month(?);"; + youle_business.mysql.execsql("opensql", sql, [agentid], db_callback, pack); +} + +//按月度统计同时在线玩家数量峰值和房间峰值 +youle_business.player_max = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_business.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var sql = "call cp_agent_player_max(?);"; + youle_business.mysql.execsql("opensql", sql, [agentid], db_callback, pack); +} + +//按游戏统计玩家数量和房卡消耗数量 +youle_business.player_game = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_business.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var sql = "call cp_agent_player_game(?);"; + youle_business.mysql.execsql("opensql", sql, [agentid], db_callback, pack); +} + +//按月度统计新增个人代理数量 +youle_business.sale_month = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_business.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var sql = "call cp_agent_sale_month(?);"; + youle_business.mysql.execsql("opensql", sql, [agentid], db_callback, pack); +} \ No newline at end of file diff --git a/server/youle/web_sales/class.import.js b/server/youle/web_sales/class.import.js new file mode 100644 index 0000000..0c563e7 --- /dev/null +++ b/server/youle/web_sales/class.import.js @@ -0,0 +1,30 @@ +/////////////////////////////////////////////////// +/////// cls_youle_sales_import: 输入接口 /////// +/////////////////////////////////////////////////// +var cls_youle_sales_import = cls_youle_sales_import || { + + new: function() { + + var imp = {}; + + //玩家购买房卡 + imp.player_buy_roomcard = function(agentid, playerid, roomcard, amount){ + youle_agent.export.buy_topup(agentid, playerid, 1, roomcard, amount); + } + + //玩家充卡 + imp.player_topup_roomcard = function(agentid, playerid, roomcard, amount){ + youle_agent.export.buy_topup(agentid, playerid, 2, roomcard, amount); + } + + //玩家充星星 + imp.player_topup_bean = function(agentid, playerid, bean, amount){ + youle_agent.export.buy_topup_bean(agentid, playerid, 2, bean, amount); + } + + return imp; + } +} + +//数据异常处理 +youle_sales.import = cls_youle_sales_import.new(); \ No newline at end of file diff --git a/server/youle/web_sales/mod.js b/server/youle/web_sales/mod.js new file mode 100644 index 0000000..6cef990 --- /dev/null +++ b/server/youle/web_sales/mod.js @@ -0,0 +1,1157 @@ +////////////////////////////////////////////////////////////////// +/////////////////////////////个人代理///////////////////////////// +////////////////////////////////////////////////////////////////// + +//对内的输入接口类 +min_loadJsFile("youle/web_sales/class.import.js", function(){ + +}); + +var youle_sales = youle_sales || cls_mod.new("youle_sales", "sales", youle_app); + +//mysql操作类 +youle_sales.mysql = cls_mysql.new("http://localhost/mysql.php", youle_sales.app.SendPack); +//对内的输入接口类(需要的外部接口) +youle_sales.import = null; + + +//用户登录 +youle_sales.user_login = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.salesman = parseInt(dbdata[0].salesman); + if (dbdata[0].salesman == 1){ //是个人代理 + pack.data.salesid = parseInt(dbdata[0].salesid); + pack.data.level = parseInt(dbdata[0].saleslevel); + pack.data.salestype = parseInt(dbdata[0].salestype); + pack.data.roomcard = parseInt(dbdata[0].roomcard); + pack.data.bean = parseInt(dbdata[0].bean); + // pack.data.awardstate = parseInt(dbdata[0].awardstate); + pack.data.playerask = parseInt(dbdata[0].playerask); + pack.data.salesask = parseInt(dbdata[0].salesask); + pack.data.salespower = dbdata[0].salespower; + pack.data.agentmode = youle_sales.app.youle_platform.config.method.get_paravalue('agentmode', agentid); + pack.data.pushmoney = dbdata[0].pushmoney; + } else { + pack.data.html_applysales = youle_sales.app.youle_platform.config.method.get_paravalue('html_applysales', agentid); + } + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var channelid= pack.data.channelid; + var openid = pack.data.openid; + var unionid = pack.data.unionid; + var nickname = pack.data.nickname; + //去除&符号 + nickname = min_replaceAll(nickname, "&", ""); + //去除'符号 + nickname = min_replaceAll(nickname, "'", ""); + //去除emoji字符 + var nickname2 = unescape(escape(nickname).replace(/\%uD.{3}/g, '')); + if ((nickname2 != nickname) && (!nickname2)){ + nickname2 = "表情符号"; + } + nickname2 = min_trim(nickname2); + if (!nickname2){ + nickname2 = "@#$2017"; + } + + var avatar = pack.data.avatar; + var sex = pack.data.sex; + var province = pack.data.province; + var city = pack.data.city; + + if (!openid){ + return; + } + if (!nickname2 && !avatar){ + return; + } + + var sql = "call cp_sales_user_login(?,?,?,?,?,?,?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, channelid, openid, unionid, nickname2, avatar, sex, province, city], db_callback, pack); +} + +//获取产品列表 +youle_sales.get_product = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.state = 0; + pack.data.products = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var type = parseInt(pack.data.type); + var sql = " select sapr_productid as productid, sapr_name as name, " + + " sapr_amount as amount, sapr_money as money, " + + " sapr_memo as memo " + + " from sales_product " + + " where sapr_agentid = ? " + + " and sapr_type = ? " + + " and sapr_state = 0 " + + " order by idx;"; + youle_sales.mysql.execsql("opensql", sql, [agentid, type], db_callback, pack); +} + +//新购卡订单 +youle_sales.new_bill = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0){ + console.log(min_now() + " 订单生成失败:result=" + dbdata[0].result + "; error=" + dbdata[0].error); + console.log(pack); + + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = pack.rpc; + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + msg.data = {}; + msg.data.state = dbdata[0].result; + msg.data.error = "订单生成失败:" + dbdata[0].error; + youle_sales.app.SendPack(msg); + return; + } + pack.data = {}; + pack.data.state = 0; + pack.data.billcode = billcode; + pack.data.billtype = billtype; + pack.data.productname = dbdata[0].productname; + pack.data.productamount = dbdata[0].productamount; + pack.data.productmoney = dbdata[0].productmoney; + if (billtype == 0) { + pack.data.playername = dbdata[0].playername; + } + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var channelid = pack.data.channelid; + var productid = pack.data.productid; + var billtype = parseInt(pack.data.billtype); + var playerid = parseInt(pack.data.playerid); + var tel = pack.data.tel; + var wechat = pack.data.wechat; + if (!tel){ + tel = null; + } + if (!wechat){ + wechat = null; + } + var billcode; //订单号 + if (billtype == 0) { + //散客订单 + billcode = "PE" + playerid + new Date().getTime() + min_random(1000, 9999); + } else { + //个人代理订单 + billcode = "SA" + min_random(100000, 999999) + new Date().getTime() + min_random(1000, 9999); + } + var sql = "call cp_sales_new_bill(?,?,?,?,?,?,?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, channelid, billcode, billtype, productid, playerid, tel, wechat], db_callback, pack); +} + +//订单支付成功 +youle_sales.pay_succ = function(pack){ + var agentid = pack.data.agentid; + var channelid = pack.data.channelid; + var openid = pack.data.openid; + var orderNo = pack.data.orderNo; + var outtradeNo = pack.data.outtradeNo; + var transactionid = pack.data.transactionid; + var signkey = pack.data.signkey; + var money = pack.data.money; + var parentid = pack.data.parentid; + + var db_callback = function(dbdata){ + if (dbdata[0].result == 0){ + delete pack.data; + pack.error = '0'; //字符串0表示成功 非0表示失败 + youle_sales.app.SendPack(pack); + + if (dbdata[0].billtype == 0) { //散客订单 + //通知游戏玩家充值成功了 + youle_sales.import.player_buy_roomcard(agentid, dbdata[0].playerid, dbdata[0].roomcard, dbdata[0].amount); + } + } + } + + var sql = "call cp_sales_pay_succ(?,?,?,?,?,?,?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, channelid, openid, orderNo, outtradeNo, transactionid, signkey, money, parentid], db_callback, pack); +} + +//订单支付是否成功查询 +youle_sales.pay_query = function(pack){ + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var billcode = pack.data.billcode; + var sid = pack.data.sid; + var scode = pack.data.scode; + var appid = pack.data.appid; + var devkey = pack.data.devkey; + var parentid = pack.data.parentid; + + var data = {}; + data.sid = sid; + data.scode = scode; + data.appid = appid; + data.devkey = devkey; + data.orderid = billcode; + + var cfg = {}; + cfg.url = "http://api.0791ts.cn/api/pay/paystatus"; + cfg.type = "POST"; + cfg.data = data; + cfg.success = function(str){ + var result = JSON.parse(str); + if (result.error != 0){ + pack.data = {}; + pack.data.state = 1; + pack.data.error = "未查到订单的支付信息"; + youle_sales.app.SendPack(pack); + return; + } + + /////////////订单支付查询成功//////////// + //支付金额 + var fee = result.data.fee; + //商户id + var business_id = result.data.business_id; + + var db_callback = function(dbdata){ + if (dbdata[0].result != 0) { + console.log(min_now() + " 更新订单数据失败:result=" + dbdata[0].result + "; error=" + dbdata[0].error); + console.log(pack); + return; + } + + if (dbdata[0].billtype == 0) { + //返回前端 + pack.data = {}; + pack.data.state = 0; + pack.data.billtype = dbdata[0].billtype; + pack.data.playerid = dbdata[0].playerid; + pack.data.playername = dbdata[0].playername; + pack.data.amount = dbdata[0].amount; + pack.data.roomcard = dbdata[0].roomcard; + pack.data.paymoney = fee; + youle_sales.app.SendPack(pack); + } else { + //个人代理订单 + pack.data = {}; + pack.data.state = 0; + pack.data.billtype = dbdata[0].billtype; + pack.data.salesid = dbdata[0].salesid; + pack.data.salesname = dbdata[0].salesname; + pack.data.amount = dbdata[0].amount; + pack.data.roomcard = dbdata[0].roomcard; + pack.data.paymoney = fee; + if (parentid){ + pack.data.parentid = dbdata[0].parentid; + pack.data.parentname = dbdata[0].parentname; + } + youle_sales.app.SendPack(pack); + } + } + + var sql = "call cp_sales_pay_query(?,?,?,?,?,?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, billcode, fee, appid, devkey, business_id, parentid], db_callback, pack); + }; + cfg.error = function(str){ + console.log(min_now() + " 订单查询失败!"); + console.log(pack); + }; + min_http(cfg); +} + +//玩家查询 +youle_sales.player_query = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0){ + console.log(min_now() + " 玩家查询失败:result=" + dbdata[0].result + "; error=" + dbdata[0].error); + console.log(pack); + + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = pack.rpc; + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + msg.data = {}; + msg.data.state = dbdata[0].result; + msg.data.error = "未查到玩家信息:" + dbdata[0].error; + youle_sales.app.SendPack(msg); + return; + } + + pack.data = {}; + pack.data.state = 0; + pack.data.playerid = dbdata[0].playerid; + pack.data.playername = dbdata[0].playername; + pack.data.roomcard = dbdata[0].playerroomcard; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var playerid = pack.data.playerid; + var sql = "call cp_sales_query_player(?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, playerid], db_callback, pack); +} + +//给玩家充卡 +youle_sales.player_topup = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0) { + console.log(min_now() + " 给玩家充卡失败:result=" + dbdata[0].result + "; error=" + dbdata[0].error); + console.log(pack); + + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = pack.rpc; + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + msg.data = {}; + msg.data.state = dbdata[0].result; + msg.data.error = "给玩家充卡失败:" + dbdata[0].error; + youle_sales.app.SendPack(msg); + return; + } + + //通知游戏玩家充值成功了 + youle_sales.import.player_topup_roomcard(agentid, dbdata[0].playerid, dbdata[0].playerroomcard, dbdata[0].amount); + + //返回前端 + pack.data = {}; + pack.data.state = 0; + pack.data.roomcard = dbdata[0].roomcard; + pack.data.amount = dbdata[0].amount; + pack.data.playerid = dbdata[0].playerid; + pack.data.playername = dbdata[0].playername; + pack.data.playerroomcard = dbdata[0].playerroomcard; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var playerid = pack.data.playerid; + var amount = pack.data.amount; + var sql = "call cp_sales_topup_player(?,?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, playerid, amount], db_callback, pack); +} + +//获取充卡名单 +youle_sales.topup_name = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.state = 0; + pack.data.players = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var sql = "call cp_sales_get_topupname(?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid], db_callback, pack); +} + +//获取充卡记录 +youle_sales.topup_query = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.detail = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var begintime = pack.data.begintime; + var endtime = pack.data.endtime; + var sql = " select date_format(sase_selltime, '%Y-%m-%d %H:%i') as time, sase_playerid as playerid, " + + " play_nickname as name, play_avatar as avatar, " + + " sase_amount as amount " + + " from sales_sellbill " + + " inner join player on play_agentid = sase_agentid " + + " and play_playerid = sase_playerid " + + " where sase_agentid = ? " + + " and sase_openid = ? "; + var ary = [agentid, openid]; + if (begintime) { + sql = sql + " and sase_selltime >= ? "; + ary.push(begintime); + } + if (endtime) { + sql = sql + " and sase_selltime <= ? "; + ary.push(endtime); + } + sql = sql + " order by sase_selltime desc;"; + youle_sales.mysql.execsql("opensql", sql, ary, db_callback, pack); +} + +//获取购卡记录 +youle_sales.buy_query = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.detail = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var begintime = pack.data.begintime; + var endtime = pack.data.endtime; + var sql = " select date_format(sabu_paytime, '%Y-%m-%d %H:%i') as time, sabu_paymoney as money, " + + " sabu_amount as amount " + + " from sales_buybill " + + " where sabu_agentid = ? " + + " and sabu_openid = ? " + + " and sabu_paystate = 1 "; + var ary = [agentid, openid]; + if (begintime) { + sql = sql + " and sabu_paytime >= ? "; + ary.push(begintime); + } + if (endtime) { + sql = sql + " and sabu_paytime <= ? "; + ary.push(endtime); + } + sql = sql + " order by sabu_paytime desc;"; + youle_sales.mysql.execsql("opensql", sql, ary, db_callback, pack); +} + +//我的信息 +youle_sales.myinfo = function(pack){ + var db_callback = function(dbdata){ + if (dbdata.length == 1){ + pack.data = {}; + pack.data.salesid = dbdata[0].saus_salesid; + pack.data.wechat = dbdata[0].saus_wechat; + pack.data.tel = dbdata[0].saus_tel; + pack.data.power = dbdata[0].saus_power; + youle_sales.app.SendPack(pack); + } + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var sql = "select saus_salesid, saus_tel, saus_wechat, saus_power from sales_user where saus_agentid = ? and saus_openid = ?;"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid], db_callback, pack); +} + +//查询推荐人 +youle_sales.query_sales = function(pack){ + var db_callback = function(dbdata){ + if (dbdata.length == 0){ + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = pack.rpc; + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + msg.data = {}; + msg.data.state = 1; + msg.data.error = "未查到推荐人"; + youle_sales.app.SendPack(msg); + return; + } else { + pack.data = {}; + pack.data.state = 0; + pack.data.salesid = salesid; + pack.data.name = dbdata[0].saus_nickname; + pack.data.avatar = dbdata[0].saus_avatar; + youle_sales.app.SendPack(pack); + } + } + + var agentid = pack.data.agentid; + var salesid = pack.data.salesid; + var sql = "select saus_nickname, saus_avatar from sales_user where saus_agentid = ? and saus_salesid = ?;"; + youle_sales.mysql.execsql("opensql", sql, [agentid, salesid], db_callback, pack); +} + +//绑定推荐人 +youle_sales.binding_myparent = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0) { + pack.data = {}; + pack.data.state = dbdata[0].result; + pack.data.error = dbdata[0].error; + if (dbdata[0].result == 2) { + pack.data.salesid = dbdata[0].salesid; + pack.data.wechat = dbdata[0].wechat; + pack.data.tel = dbdata[0].tel; + pack.data.nickname = dbdata[0].nickname; + pack.data.avatar = dbdata[0].avatar; + } + youle_sales.app.SendPack(pack); + return; + } + pack.data = {}; + pack.data.state = 0; + pack.data.salesid = dbdata[0].salesid; + pack.data.wechat = dbdata[0].wechat; + pack.data.tel = dbdata[0].tel; + pack.data.nickname = dbdata[0].nickname; + pack.data.avatar = dbdata[0].avatar; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var salesid = pack.data.salesid; + var sql = "call cp_sales_binding_parent(?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, salesid], db_callback, pack); +} + +//我的推荐人 +youle_sales.myparent = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + if (dbdata.length == 1){ + pack.data.parentid = dbdata[0].saus_salesid; + pack.data.name = dbdata[0].saus_nickname; + pack.data.avatar = dbdata[0].saus_avatar; + } else { + pack.data.parentid = ""; + pack.data.name = ""; + pack.data.avatar = ""; + } + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var type = pack.data.type; + if (!type){ + var sql = "call cp_sales_get_parentinfo(?,?);"; + } else { + var sql = "call cp_sales_get_invitecodeinfo(?,?);"; + } + youle_sales.mysql.execsql("opensql", sql, [agentid, openid], db_callback, pack); +} + +//我的子代理 +youle_sales.mychild = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.childs = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var sql = "call cp_sales_get_child(?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid], db_callback, pack); +} + +//我的奖励 +youle_sales.myaward = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.awards = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var sql = "call cp_sales_get_award(?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid], db_callback, pack); +} + +//领取奖励 +youle_sales.accept_award = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0) { + pack.data = {}; + pack.data.state = dbdata[0].result; + pack.data.error = "领取奖励失败:" + dbdata[0].error; + youle_sales.app.SendPack(pack); + return; + } + pack.data = {}; + pack.data.state = 0; + pack.data.roomcard = parseInt(dbdata[0].myroomcard); + pack.data.awardstate = parseInt(dbdata[0].mystate); + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var awardid = pack.data.awardid; + var sql = "call cp_sales_accept_award(?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, awardid], db_callback, pack); +} + +//获取公告列表 +youle_sales.noticelist = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var sql = " select sano_noticeid as noticeid, sano_title as title, sano_time as time " + + " from sales_notice " + + " where sano_agentid = ? " + + " and (sano_begintime is null or now() >= sano_begintime) " + + " and (sano_endtime is null or now() <= sano_endtime);"; + youle_sales.mysql.execsql("opensql", sql, [agentid], db_callback, pack); +} + +//获取公告详情 +youle_sales.noticedetail = function(pack){ + var db_callback = function(dbdata){ + if (dbdata.length == 1){ + pack.data = {}; + pack.data.title = dbdata[0].title; + pack.data.time = dbdata[0].time; + pack.data.content = dbdata[0].content; + youle_sales.app.SendPack(pack); + } + } + + var agentid = pack.data.agentid; + var noticeid = pack.data.noticeid; + var sql = " select sano_title as title, sano_time as time, sano_content as content " + + " from sales_notice " + + " where sano_agentid = ? " + + " and sano_noticeid = ? ;"; + youle_sales.mysql.execsql("opensql", sql, [agentid, noticeid], db_callback, pack); +} + +//游戏下载列表 +youle_sales.gamelist = function(pack){ + var agentid = pack.data.agentid; + var channelid = pack.data.channelid; + pack.data.html_buyroomcard = youle_sales.app.youle_platform.config.method.get_paravalue('html_buyroomcard', agentid); + pack.data.list = youle_sales.app.youle_platform.config.method.get_gamelist_downhtml(agentid, channelid); + return pack; +} + +//客服联系方式 +youle_sales.get_service = function(pack){ + var agentid = pack.data.agentid; + pack.data = {}; + pack.data.qq = youle_sales.app.youle_platform.config.method.get_paravalue('sale_qq', agentid); + pack.data.wechat = youle_sales.app.youle_platform.config.method.get_paravalue('sale_wechat', agentid); + pack.data.tel = youle_sales.app.youle_platform.config.method.get_paravalue('sale_tel', agentid); + youle_sales.app.SendPack(pack); +} + +//个人代理查询 +youle_sales.salesman_query = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0){ + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = pack.rpc; + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + msg.data = {}; + msg.data.state = dbdata[0].result; + msg.data.error = "未查到代理信息:" + dbdata[0].error; + youle_sales.app.SendPack(msg); + return; + } + + pack.data = {}; + pack.data.state = 0; + pack.data.salesid = dbdata[0].salesid; + pack.data.salesname = dbdata[0].salesname; + pack.data.roomcard = dbdata[0].salesroomcard; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var salesid = pack.data.salesid; + var sql = "call cp_sales_query_salesman(?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, salesid], db_callback, pack); +} + +//给个人代理转卡 +youle_sales.transfer_roomcard = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0) { + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = pack.rpc; + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + msg.data = {}; + msg.data.state = dbdata[0].result; + msg.data.error = "给代理转卡失败:" + dbdata[0].error; + youle_sales.app.SendPack(msg); + return; + } + + //返回前端 + pack.data = {}; + pack.data.state = 0; + pack.data.roomcard = dbdata[0].roomcard; + pack.data.amount = dbdata[0].amount; + pack.data.salesid = dbdata[0].salesid; + pack.data.salesname = dbdata[0].salesname; + pack.data.salesroomcard = dbdata[0].salesroomcard; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var salesid = pack.data.salesid; + var amount = pack.data.amount; + var sql = "call cp_sales_topup_salesman(?,?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, salesid, amount], db_callback, pack); +} + +//获取转卡名单 +youle_sales.transfer_name = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.state = 0; + pack.data.salesman = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var sql = "call cp_sales_get_transfername(?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid], db_callback, pack); +} + +//获取转卡记录 +youle_sales.transfer_query = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.detail = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var begintime = pack.data.begintime; + var endtime = pack.data.endtime; + var sql = " select date_format(satr_transfertime, '%Y-%m-%d %H:%i') as time, satr_salesid as salesid, " + + " saus_nickname as name, saus_avatar as avatar, " + + " satr_amount as amount " + + " from sales_transferbill " + + " inner join sales_user on saus_agentid = satr_agentid " + + " and saus_salesid = satr_salesid " + + " where satr_agentid = ? " + + " and satr_openid = ? "; + var ary = [agentid, openid]; + if (begintime) { + sql = sql + " and satr_transfertime >= ? "; + ary.push(begintime); + } + if (endtime) { + sql = sql + " and satr_transfertime <= ? "; + ary.push(endtime); + } + sql = sql + " order by satr_transfertime desc;"; + youle_sales.mysql.execsql("opensql", sql, ary, db_callback, pack); +} + + +//获取绑定了我的邀请码的玩家列表 +youle_sales.my_player = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var sql = "call cp_sales_get_myplayer(?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid], db_callback, pack); +} + + +//获取我的下级代理列表 +youle_sales.my_salesman = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var sql = "call cp_sales_get_mysalesman(?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid], db_callback, pack); +} + + +//索要房卡-玩家获取个人代理信息 +youle_sales.ask_player_getsalesman = function(pack){ + var db_callback = function(dbdata){ + if (dbdata.length == 0) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "未绑定邀请码"; + youle_sales.app.SendPack(pack); + } else { + pack.data = {}; + pack.data.state = 0; + pack.data.salesid = dbdata[0].sales_id; + pack.data.wechat = dbdata[0].sales_wechat; + pack.data.tel = dbdata[0].sales_tel; + pack.data.nickname = dbdata[0].sales_nickname; + pack.data.avatar = dbdata[0].sales_avatar; + youle_sales.app.SendPack(pack); + } + } + var agentid = pack.data.agentid; + var playerid = parseInt(pack.data.playerid); + var sql = "call cp_sales_ask_player_getsalesman(?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, playerid], db_callback, pack); +} + + +//索要房卡-个人代理获取上级代理信息 +youle_sales.ask_sales_getparentman = function(pack){ + var db_callback = function(dbdata){ + if (dbdata.length == 0) { + pack.data = {}; + pack.data.state = 1; + pack.data.error = "未绑定邀请码"; + youle_sales.app.SendPack(pack); + } else { + pack.data = {}; + pack.data.state = 0; + pack.data.salesid = dbdata[0].sales_id; + pack.data.wechat = dbdata[0].sales_wechat; + pack.data.tel = dbdata[0].sales_tel; + pack.data.nickname = dbdata[0].sales_nickname; + pack.data.avatar = dbdata[0].sales_avatar; + youle_sales.app.SendPack(pack); + } + } + var agentid = pack.data.agentid; + var salesid = parseInt(pack.data.salesid); + var sql = "call cp_sales_ask_sales_getparentman(?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, salesid], db_callback, pack); +} + + +//索要房卡-新增订单 +youle_sales.ask_newbill = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.state = dbdata[0].result; + pack.data.error = dbdata[0].error; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var billtype = parseInt(pack.data.billtype); + var askid = parseInt(pack.data.askid); + var amount = parseInt(pack.data.amount); + var sql = "call cp_sales_ask_newbill(?,?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, billtype, askid, amount], db_callback, pack); +} + + +//索要房卡-获取我发起的索要房卡订单列表 +youle_sales.ask_myaskbill = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var billtype = parseInt(pack.data.billtype); + var askid = parseInt(pack.data.askid); + var sql = "call cp_sales_ask_getmyaskbill(?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, billtype, askid], db_callback, pack); +} + +//索要房卡-获取我受理的索要房卡订单列表 +youle_sales.ask_mydealbill = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.list = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var billtype = parseInt(pack.data.billtype); + var salesid = parseInt(pack.data.salesid); + var sql = "call cp_sales_ask_getmydealbill(?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, billtype, salesid], db_callback, pack); +} + + +//索要房卡-处理索要房卡订单 +youle_sales.ask_dealbill = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0) { + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = pack.rpc; + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + msg.data = {}; + msg.data.state = dbdata[0].result; + msg.data.error = dbdata[0].error; + youle_sales.app.SendPack(msg); + return; + } + + //返回前端 + pack.data = {}; + pack.data.state = 0; + if (dealstate == 1){ + //通知游戏玩家充值成功了 + youle_sales.import.player_topup_roomcard(dbdata[0].agentid, dbdata[0].askid, dbdata[0].askerroomcard, dbdata[0].amount); + + pack.data.amount = dbdata[0].amount; + pack.data.askerroomcard = dbdata[0].askerroomcard; + pack.data.salesroomcard = dbdata[0].salesroomcard; + } + youle_sales.app.SendPack(pack); + } + + var idx = parseInt(pack.data.idx); + var dealid = parseInt(pack.data.dealid); + var dealstate = parseInt(pack.data.dealstate); + var sql = "call cp_sales_ask_dealbill(?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [idx, dealid, dealstate], db_callback, pack); +} + +//玩家绑定推荐人 +youle_sales.ask_player_bindingsales = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0) { + pack.data = {}; + pack.data.state = dbdata[0].result; + pack.data.error = dbdata[0].error; + if (dbdata[0].result == 2) { + pack.data.salesid = dbdata[0].salesid; + pack.data.wechat = dbdata[0].wechat; + pack.data.tel = dbdata[0].tel; + pack.data.nickname = dbdata[0].nickname; + pack.data.avatar = dbdata[0].avatar; + } + youle_sales.app.SendPack(pack); + return; + } + pack.data = {}; + pack.data.state = 0; + pack.data.salesid = dbdata[0].salesid; + pack.data.wechat = dbdata[0].wechat; + pack.data.tel = dbdata[0].tel; + pack.data.nickname = dbdata[0].nickname; + pack.data.avatar = dbdata[0].avatar; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var salesid = pack.data.salesid; + var sql = "call cp_sales_player_binding_invitecode(?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, salesid], db_callback, pack); +} + + +//申请为个人代理 +youle_sales.apply_sales = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.state = dbdata[0].result; + pack.data.error = dbdata[0].error; + + if (dbdata[0].result == 0) { + pack.data.inivtecode = dbdata[0].inivtecode; + } + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var channelid = pack.data.channelid; + var wechat = pack.data.wechat; + var tel = pack.data.tel; + var inivtecode = pack.data.inivtecode; + var sql = "call cp_sales_apply_sales(?,?,?,?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, channelid, wechat, tel, inivtecode], db_callback, pack); +} + + +//修改个人代理的联系方式 +youle_sales.update_mywechat = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.state = dbdata[0].result; + pack.data.error = dbdata[0].error; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var wechat = pack.data.wechat; + var tel = pack.data.tel; + var sql = "call cp_sales_update_mywechat(?,?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, wechat, tel], db_callback, pack); +} + +//代理城市列表 +youle_sales.agentlist = function(pack){ + var agentid = pack.data.agentid; + var channelid = pack.data.channelid; + pack.data.list = youle_sales.app.youle_platform.config.method.get_agentlist(agentid, channelid); + return pack; +} + + +//查询玩家星星数量 +youle_sales.bean_player_query = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0){ + console.log(min_now() + " 玩家查询失败:result=" + dbdata[0].result + "; error=" + dbdata[0].error); + console.log(pack); + + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = pack.rpc; + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + msg.data = {}; + msg.data.state = dbdata[0].result; + msg.data.error = "未查到玩家信息:" + dbdata[0].error; + youle_sales.app.SendPack(msg); + return; + } + + pack.data = {}; + pack.data.state = 0; + pack.data.playerid = dbdata[0].playerid; + pack.data.playername = dbdata[0].playername; + pack.data.playerbean = dbdata[0].playerbean; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var playerid = pack.data.playerid; + var sql = "call cp_sales_query_player_bean(?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, playerid], db_callback, pack); +} + + +//给玩家充豆豆 +youle_sales.bean_player_topup = function(pack){ + var db_callback = function(dbdata){ + if (dbdata[0].result != 0) { + console.log(min_now() + " 给玩家充值星星失败:result=" + dbdata[0].result + "; error=" + dbdata[0].error); + console.log(pack); + + var msg = {}; + msg.app = pack.app; + msg.route = pack.route; + msg.rpc = pack.rpc; + msg.conmode = pack.conmode; + msg.fromid = pack.fromid; + msg.data = {}; + msg.data.state = dbdata[0].result; + msg.data.error = "给玩家充值" + starName + "失败:" + dbdata[0].error; + youle_sales.app.SendPack(msg); + return; + } + + //通知游戏玩家充值成功了 + youle_sales.import.player_topup_bean(agentid, dbdata[0].playerid, dbdata[0].playerbean, dbdata[0].amount); + + //返回前端 + pack.data = {}; + pack.data.state = 0; + pack.data.salesbean = dbdata[0].salesbean; + pack.data.amount = dbdata[0].amount; + pack.data.playerid = dbdata[0].playerid; + pack.data.playername = dbdata[0].playername; + pack.data.playerbean = dbdata[0].playerbean; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var playerid = pack.data.playerid; + var amount = pack.data.amount; + + var starName = youle_sales.app.youle_platform.config.method.get_paravalue("starName", agentid); + if (!starName){ + starName = "星星"; + } + + var sql = "call cp_sales_topup_player_bean(?,?,?,?);"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid, playerid, amount], db_callback, pack); +} + +//获取星星充值记录 +youle_sales.bean_topup_query = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.detail = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var begintime = pack.data.begintime; + var endtime = pack.data.endtime; + var sql = " select date_format(ssbe_selltime, '%Y-%m-%d %H:%i') as time, ssbe_playerid as playerid, " + + " play_nickname as name, play_avatar as avatar, " + + " ssbe_amount as amount " + + " from sales_sellbill_bean " + + " inner join player on play_agentid = ssbe_agentid " + + " and play_playerid = ssbe_playerid " + + " where ssbe_agentid = ? " + + " and ssbe_openid = ? "; + var ary = [agentid, openid]; + if (begintime) { + sql = sql + " and ssbe_selltime >= ? "; + ary.push(begintime); + } + if (endtime) { + sql = sql + " and ssbe_selltime <= ? "; + ary.push(endtime); + } + sql = sql + " order by ssbe_selltime desc;"; + youle_sales.mysql.execsql("opensql", sql, ary, db_callback, pack); +} + + +//获取散客购卡记录 +youle_sales.player_buy_query = function(pack){ + var db_callback = function(dbdata){ + pack.data = {}; + pack.data.detail = dbdata; + youle_sales.app.SendPack(pack); + } + + var agentid = pack.data.agentid; + var openid = pack.data.openid; + var sql = " select date_format(sabu_paytime, '%Y-%m-%d %H:%i') as time, " + + " sabu_playerid as playerid, play_nickname as name, play_avatar as avatar, " + + " sabu_paymoney as money, sabu_amount as amount " + + " from sales_buybill " + + " inner join player on play_agentid = sabu_agentid " + + " and play_playerid = sabu_playerid " + + " where sabu_agentid = ? " + + " and sabu_openid = ? " + + " and sabu_billtype = 0 " + + " and sabu_paystate = 1 " + + " order by time desc " + + " limit 50;"; + youle_sales.mysql.execsql("opensql", sql, [agentid, openid], db_callback, pack); +}