Files
erqiwang_youle/docs_dev/二七王-UI资源与精灵清单.md
T
joywayerandClaude Opus 5 8dba099717 二七王:HIST_FAN/MING_FAN maxWidth 由 490 改判定为 480,让 spacingMin 真正生效
上一版把 maxWidth 收窄到 490 只是让 RIGHT 座位恰好贴住画布边界(零余量),且
spacingMin:-96 在 28 张时永远不触底、成了死参数。裁定改为 480:
(480-110)/27-110≈-96.30 超过 spacingMin,spacing 被截断为 -96,总宽变为
28×110+27×(-96)=488,与清单原注释「每张露14px、总宽488」精确对齐——500 是笔误,
488 才是当初想要的值。

求解器验证(count=28):LEFT 16→504、RIGHT 791→1279、SELF 396→884,三座位均落在
0-1280 内且有余量。同步订正 Layout_Result.js 注释与清单 §6.9c 文字/数值。

client/tests/run.js 与 server/games/erqiwang/test/run.js 均全绿,精灵总数(380)/
布局节点数(83) 未变。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 09:08:48 +08:00

2193 lines
157 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 二七王 · 前端 UI 资源与精灵清单
> **本文用途**:把二七王的完整游戏流程翻译成「前端需要哪些图片资源、声音、精灵、图层、群组、坐标」的**可执行规格书**,供美术出图与 gameabc 编辑器建资源使用。
>
> **本文不是代码**。文中给出的 ID 是**规划建议值**(相应号段已核实为空闲,见 §0.3),用途是指导在编辑器里按此建立;**代码里引用的 ID 必须与编辑器中实际建成的完全一致**,若编辑器实际取值与本文不同,**以编辑器为准并回头修订本文**。前端红线明令「绝不编造 ID」,指的就是代码侧。
>
> **权威源**:玩法规则以 `server/games/erqiwang/docs/design/design.md` 为准;下发字段以 `server/games/erqiwang/docs/protocol/packet_protocol.md` 为准;前端规范以 `docs/client/development-guide/` 为准。本文只做「规则/协议 → 界面资源」的映射,**不重新定义规则**。
>
> **参考图**:`./uiref/*.png`,共 16 张,设计分辨率 1280×720。
---
## 目录
- [0. 总则](#0-总则)
- [0.0 术语(本项目修订版)](#00-术语本项目修订版优先于-designmd)
- [0.1 设计基准与座位映射](#01-设计基准与座位映射)
- [0.2 平台已提供、子游戏不重建的部分](#02-平台已提供子游戏不重建的部分)
- [0.3 ID 号段规划](#03-id-号段规划)
- [0.4 牌面资源与 cardId → 帧号映射(唯一权威转换)](#04-牌面资源与-cardid--帧号映射唯一权威转换)
- [0.5 标注约定](#05-标注约定)
- [1. 按阶段的界面清单](#1-按阶段的界面清单)
- [2. 常驻 UI](#2-常驻-ui)
- [3. 图片资源总表](#3-图片资源总表)
- [4. 声音资源总表](#4-声音资源总表)
- [5. 精灵结构表](#5-精灵结构表)
- [6. 布局配置规范与配置清单](#6-布局配置规范与配置清单)
- [6.0 与框架 AlignmentUtils 的对齐](#60-与框架-alignmentutils-的对齐)
- [6.1 五种布局器(kind)](#61-五种布局器kind)
- [6.2 座位变体 bySeat](#62-座位变体-byseat)
- [6.3 文字样式 / 6.4 牌尺寸档](#63-文字样式-textstyle)
- [6.5–6.9 各区配置清单](#65-常驻区配置)
- [6.10 配置文件组织](#610-配置文件组织)
- [6.11 与平台布局配置的衔接](#611-与平台布局配置的衔接)
- [7. 待补充清单](#7-待补充清单)
- [7.5 服务端待补项](#75-服务端待补项需改服务端才能实现的前端需求)
---
## 0. 总则
### 0.0 术语(本项目修订版,优先于 design.md)
**2026-08-25 修订**:原 design.md 里「暗牌 / 底牌」的命名与界面按钮倒挂,现统一为:
| 术语 | 指哪 8 张 | design.md 原称 | 当前协议字段 |
| --- | --- | --- | --- |
| **底牌** | 发牌时**没有发给玩家**、留在桌面的 8 张 | ~~暗牌~~ | **`bottomcards`** |
| **埋牌底牌** | 庄家埋牌时从手里扣下的 8 张 | ~~底牌~~ | **`burycards`**(结算包为 `bottom.cards`) |
其余术语(埋牌、扣底、甩牌、毙牌、垫牌、捡分、子、奖…)沿用 design.md §1 不变。注意「**扣底**」仍指 design §6.3 那条规则——闲家用主牌赢下最后一轮后翻开**埋牌底牌**计分;底栏那个 `扣底` 按钮是「查看埋牌底牌」的入口,两者同名但不同义。
> ✅ `design.md` 与 `packet_protocol.md` **已同步此变更**(S-5 完成),协议字段名也已拆分为 `bottomcards` / `burycards`(S-4 完成)。
#### 算奖 = 冲关 + 傍王
| 术语 | 范围 | 界面 | 字段 |
| --- | --- | --- | --- |
| **冲关** | design §8.1,**只计庄家**(旧称「常规算奖」) | 小局结算「**冲关分**」、「**冲关牌型**」按钮 | `chongguan`、`cards` |
| **傍王** | design §8.3,可选规则,勾选后**庄闲都算**,每张王 1 奖 | 大局结算「傍王分」 | `wang`、`bangwang` |
| **算奖** | **上位概念** = 冲关 + 傍王 | —— | `naward`(总奖数 N)、`grade_aw`(算奖得分) |
说「**冲关**」指庄家那一份,说「**算奖**」指两者合计。
> 小局结算的「冲关分」取 **`grade_cg`**(S-6 已把算奖得分拆成冲关 / 傍王两个分量),开傍王时也不会把傍王的贡献算进来。
#### 四类「公开信息」——名字像、含义差很远
| 术语 | 谁公开给谁 | 给的是什么 | 触发 | 界面位置 | 字段 |
| --- | --- | --- | --- | --- | --- |
| **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后固定主牌达门槛(design §8.2) | 遮罩面板,同明牌版式(§1.6) | `liangpai.cards` |
| **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(design §9) | 他家头像下 `主N` `对N` 角标(§2.5) | `seatlist[seat][4]`、`baozhu` |
| **明牌** | 他家 → 请求者 | **具体牌面** | 可查牌 + **场上有人报无主**,三家均可点、随时可查(design §9) | 遮罩面板(§1.7 D-6) | `mingpai.others[].zhucards` |
| **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(design §4) | 复用底牌区翻牌(§1.4 D-2) | `ancard3s` + `bottomcards` |
一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。
另有一组「底」字族专管发牌留桌的那 8 张牌:
| 术语 | 含义 | 前端对应 |
| --- | --- | --- |
| **摸底** | 庄家坐定后把 8 张底牌摸起查看并入手牌 | 底牌区翻正面(非 70 分只有庄家看得到) |
| **开底** | 70 分坐庄,摸底**之前**向全场翻开 3 秒 | 同上,但闲家侧也翻开 |
| **查底牌** | 对局中随时回看这 8 张 | 底栏 `底牌` 按钮(§2.6) |
| **扣底** | 末轮闲家用主牌赢下,翻开**埋牌底牌**计分 | 底栏 `扣底` 按钮看的是埋牌底牌 |
> **「亮主」不是术语**——它是**选主界面的标题文案**(庄家选主牌花色那一步,见 `亮主.png`),与上表四项都无关。文档与代码里不要拿它指代任何规则。
>
> 另:**自己**的主牌统计条(手牌上方 `x对 x主`,§2.5)**不属于上表任何一项**——那是本地统计、始终显示,不涉及"公开"。它在代码里叫 `ZHU_STAT_*`(群组 206);**2026-08-27 前叫 `LIANGPAI_*`,因与上表第一项「亮牌」同名异实而改名**,见 §0.3 群组表下的修订说明。
---
### 0.1 设计基准与座位映射
| 项 | 值 | 来源 |
| --- | --- | --- |
| 设计分辨率 | **1280 × 720** | `client/output/gameabc_Project.json` `ScreenWidth/ScreenHeight`;11 张牌桌参考图均为 1280×720;叫分按钮面板与建房参考图为局部截图 |
| 适配模式 | `ScreenFitMode: 1` | 同上 |
| 帧率 | 30 fps | 同上 |
| 玩家数 | **3 人** | design §2(三家各摸 28 张) |
**座位映射**:服务端座位是绝对序号 `0/1/2`;前端一律把**自己**放在底部,另两家按 design §4 的**逆时针**顺序分居左上、右上。
```
右上 = (mySeat + 1) % 3 左上 = (mySeat + 2) % 3
┌───────────┐
[左上玩家] │ 桌面中央 │ [右上玩家]
└───────────┘
[ 自己 · 底部 ]
```
**方向已定论**(`server/games/erqiwang/class.paiju.js:264`):
```js
get_nextseat: function(seat){ return (seat + 1) % 3; }
```
即**下家 = `(seat + 1) % 3`**。逆时针出牌在屏幕视觉上「下家在右手边」(时钟 6 点逆时针走向 3 点),与中式牌桌「下家 = 右手边」的惯例一致。因此:
| 显示位 | 座位 |
| --- | --- |
| 底部(自己) | `mySeat` |
| **右上(下家)** | `(mySeat + 1) % 3` |
| **左上(上家)** | `(mySeat + 2) % 3` |
本文各表均按此书写。轮庄「顺延到其下家」(design §4.6)走的也是同一个 `get_nextseat`。
### 0.2 平台已提供、子游戏不重建的部分
参考图里有若干元素来自平台框架,**美术不必重复出图、编辑器不必重复建精灵**:
| 元素 | 平台提供方式 | 精灵/资源 | 子游戏要做的 |
| --- | --- | --- | --- |
| 头像框 / 头像 / 昵称 / 分数 | `02_SubGame_Input.js` 转发壳 `updatePlayerInfoUI` 的 D 类默认渲染 | 底框 `346+ind`、头像框 `376+ind`、昵称 `406+ind`、分数 `436+ind`、头像图 `116+ind`、群组 `43+ind` | 只在 `01_SubGame_modify.js` 配置区调 `Game_Modify.PLAYER_INFO_LAYOUT` 的坐标(三人局用 ind0 南 / ind1 东 / ind3 西) |
| 桌面聊天气泡 / 语音气泡 | 同上,`ShowChat` / `gameui_play_voice` / `gameui_stop_voice` | 聊天 背景 `552+ind` 文字 `582+ind`;语音 背景 `612+ind` 内容 `642+ind` | 只调 `Game_Modify.BUBBLE_LAYOUT` 坐标 |
| 右侧语音圆钮 / 聊天圆钮 | 平台外壳 | 参考图右侧 (1210,310)/(1210,385) 两枚圆钮 | 无(仅坐标核对) |
| Loading / Message / Confirm 全局弹窗 | 框架 `UIManager` 全局 UI | 由子游戏在 `UIManager.init` 注入精灵常量 | 若要用,需**另建**这三套弹窗精灵并注入;不用则跳过 |
| 战绩列表 | 框架 `RecordView` 组件 + 平台入口精灵 `13` | `gameabc-framework/ui/RecordView.js` | 提供 `RecordView` 配置;战绩数据结构见协议「gameinfo1 / gameinfo2」 |
| 帮助界面 | 平台 `GameUI.OpenHelp()`,入口精灵 `150` | —— | 提供二七王玩法说明文案 |
| 创建房间界面**骨架** | 平台界面 **Layer 27 `CreateRoom_Layer`** + 约定精灵 `25`(确认/提交按钮) | 见 `01_SubGame_modify.js` 顶部注释 | **规则选项区要子游戏自建**(§1.1):渲染选项、拼 `roomtype` 串、注册 25 号点击后 `Net.Send_create_room` |
| **解散申请 / 投票 / 同意拒绝** | **平台全包**:`Layer 420 CheckFree_Layer` + `GameUI.openCheck/closeCheck` + `Desk.self_apply_free_room` / `other_apply_free_room` / `self_agree_free_room` / `self_refuse_free_room` / `agreefree` / `free_room` | —— | **无**。子游戏只需处理解散**结算包**(§1.10),投票界面不用做 |
| 高级选项 / 代开房 | `Layer 28 SeniorOptions_Layer` + `GameUI.openSeniorOptions` | —— | 无。它管短号 / 抽水 / 房间列表,**与玩法规则选项无关**,二七王的规则选项不走这里 |
**平台已占用的图层一览**(读 `gameabc_Layer.json`,凡此列出者子游戏都不必重建):
| 图层 | 名称 | 用途 |
| --- | --- | --- |
| 27 / 28 | `CreateRoom_Layer` / `SeniorOptions_Layer` | 创建房间 / 高级选项 |
| 50 | `MainScene_Layer` | 牌桌主场景 |
| 202 / 416 | `Player_Head_Score_Layer` / `MainScene_PlayerInfo_Layer` | 玩家头像与分数 |
| 409 / 410 / 418 | `Chat_Voice_Layer` / `Chat_Text_Layer` / `ChatPannel_Layer` | 聊天与语音 |
| 420 | `CheckFree_Layer` | **解散投票** |
| 608 / 609 | `Help_Layer` / `Setting_Layer` | 帮助 / 设置 |
| 612 | `Tips_Layer` | 通用提示 |
| 613 | `updateRoomCard_Layer` | 房卡更新 |
| 614 / 615 | `Loading_Layer` / `Reconnect_Layer` | 加载 / 断线重连 |
| 616 / 619 | `Kick_Layer` / `BackHall_Layer` | 踢人 / 返回大厅 |
| 620 | `recordLayer` | 战绩 |
> 这些都落在框架保留段(1–100 / 201–300 / 401–500 / 601–700),与子游戏段 101–200 / 301–400 不冲突。
> ⚠️ 参考图底栏左侧的「头像 + 玩家昵称 + 10 + 庄」一行里,**头像/昵称/分数是平台的**,只有 **`庄` 印章**是二七王专属、需子游戏新建。左上/右上玩家位同理:头像框是平台的,**`庄` 印章、`主N`、`对N` 角标、状态文字**是子游戏的。
### 0.3 ID 号段规划
**占用现状核实**(读 `client/output/gameabc_*.json`,日期 2026-08-24):
| 类别 | 已占用 | 子游戏合法段 | 段内空闲情况 |
| --- | --- | --- | --- |
| 精灵 ObjectID | 1–3292 中 991 个 | **1001–3000** | 段内**仅 `3000` 被占**(`Spirit3000`,Layer 602 `inputPannel`,群组 87)→ 实际可用 **1001–2999** |
| 图层 LayerID | 1–29, 50, 202, 402–420, 601–620 | 101–200 / 301–400 / 501–600 / 701+ | **全部空闲** |
| 群组 GroupID | 0–79 | ≥ 201 | **全部空闲** |
| 图片资源 ID | 1–439 | ≥ 501 | **全部空闲** |
| 声音资源 ID | 1–5 | ≥ 101 | **全部空闲** |
**二七王的分段规划**:
#### 图层(Layer)
| 图层 | 名称 | 用途 |
| --- | --- | --- |
| **101** | `EQW_Table_Static` | 牌桌常驻:顶部信息条、玩家位附加标记、**主牌统计条**、底栏(局数 + 功能钮) |
| **102** | `EQW_Hand` | 自己手牌区(单排 / 埋牌双排) |
| **103** | `EQW_TableCards` | 桌面牌:底牌区、三家已出牌区、埋牌底牌展示区 |
| **104** | `EQW_Action` | 阶段操作区:叫分面板、选主/投降面板、埋牌/出牌操作条、倒计时 |
| **105** | `EQW_Overlay` | 桌面浮层:提示气泡、报无主、甩错、捡分飘字、70 分底牌亮 3 秒 |
| **302** | `EQW_Popup_AsetResult` | 小局结算 |
| **303** | `EQW_Popup_Account` | 大局总结算 / 解散结算 |
| **304** | `EQW_Popup_History` | 出牌历史面板 |
| **305** | `EQW_Popup_MingPai` | 明牌面板 |
| **306** | `EQW_Popup_Disband` | 解散投票面板 —— **T-2:是否由平台提供待确认** |
> 建房规则选项区(§1.1)的精灵挂在**平台创建房间界面所在的图层**上,不新开图层;具体图层号需在编辑器里查证后回填(T-3)。
#### 群组(Group)
| 群组 | 用途 |
| --- | --- |
| 201 | 顶部信息条(主 / 叫分 / 抓分) |
| 202 / 203 / 204 | 左上家 / 右上家 / 自己 的附加标记(庄印章、主N对N、状态文字) |
| 205 | 底栏功能按钮组(明牌 / 已出牌 / 上一轮 / 扣底 / 底牌) |
| 206 | **主牌统计条**(自己的 `x对 x主`,§2.5)—— 常量名 `ZHU_STAT_BAR` |
| 210 | 自己手牌 |
| 211 | 底牌区(发牌留桌 8 张) |
| 212 / 213 / 214 | 自己 / 左上家 / 右上家 的已出牌区 |
| 215 | 埋牌底牌展示区(结算时 8 张) |
| 220 | 叫分面板 |
| 221 | 选主 / 投降面板 |
| 222 | 埋牌操作条 |
| 223 | 出牌操作条 |
| 224 | 倒计时(三位置) |
| 230 | 桌面浮层(气泡 / 飘字 / 报无主 / 甩错) |
| 240 | 小局结算 |
| 241 | 大局总结算 |
| 242 | 出牌历史 |
| 243 | 明牌面板 |
| 244 | 冲关牌型叠加层(§5.6a) |
| 250 | 建房规则选项 |
> **2026-08-27 修订 · 群组 206 改名**:原名「亮牌信息条 / `LIANGPAI_BAR`」,实为 §2.5 的**自己的主牌统计条**(手牌上方 `x对 x主`,本地统计、有手牌时常显),与 design §8.2 的「**亮牌**」(庄家埋牌后向两个闲家亮出自己全部固定主牌的**具体牌面**,走遮罩面板、群组 242)**同名异实**——§0.0 已专门花一节区分「亮牌 / 余主公示 / 明牌 / 开底」四类公开信息,名字撞车极易把渲染写到错误的界面上。
> 故统一改名为**主牌统计条**:群组 `ZHU_STAT_BAR`、精灵 `ZHU_STAT_BG` / `ZHU_STAT_TEXT`、布局节点 `ZHU_STAT_BG`、图片资源 `BAR_ZHU_STAT`(605,ID 不变)。
> 「亮牌」一词自此**只指** design §8.2 的那件事(协议字段 `liangpai`、群组 242、`EQW_Anim.LIANGPAI_AUTO_CLOSE`),不再指本条。
#### 精灵(Sprite,1001–2999)
| 号段 | 用途 | 预估用量 |
| --- | --- | --- |
| 1001–1099 | 顶部信息条 + 主牌统计条 + 底栏(局数、功能钮) | ~30 |
| 1100–1199 | 三家玩家位附加标记 + 三处倒计时 | ~24 |
| 1200–1249 | 自己手牌 36 张 + 牌上标记 | ~45 |
| 1250–1299 | 底牌 8 张 + 埋牌底牌 8 张 | 16 |
| 1300–1399 | 自己已出牌区(28 张 + 牌型标签) | ~30 |
| 1400–1499 | 左上家已出牌区(28 张 + 牌型标签) | ~30 |
| 1500–1599 | 右上家已出牌区(28 张 + 牌型标签) | ~30 |
| 1600–1649 | 叫分面板 | ~46 |
| 1650–1699 | 选主 / 投降面板 | ~21 |
| 1700–1749 | 埋牌 / 出牌操作条 | ~8 |
| 1750–1799 | 桌面浮层(气泡 / 飘字 / 报无主 / 甩错) | ~15 |
| 1800–1819 | 小局结算面板 | ~20 |
| 1820–1829 | 冲关牌型叠加层(**精灵复制**:遮罩 1 + 容器 3 + 模板 1 + 奖数 3) | **8** |
| 1900–1949 | 大局总结算 / 解散结算 | 待设计 |
| 1950–1999 | 出牌历史面板 | 待设计 |
| 2000–2049 | 明牌面板 | 待设计 |
| 2050–2099 | 建房规则选项(3 类别标题 + 8 选项 × 2 + 房卡附注) | 20 |
| 2100–2999 | **预留** | —— |
#### 图片资源(Image,≥501)
| 号段 | 用途 |
| --- | --- |
| 501–510 | 牌面(3 套尺寸,见 §0.4) |
| 511–530 | 花色图标(小 / 大两种尺寸) |
| 531–570 | 按钮类 |
| 571–600 | 标记类(庄印章、角标底、牌型标签、选中标记等) |
| 601–630 | 面板底图 / 光晕 |
| 631–650 | 数字与符号图 |
| 651–680 | 提示气泡 / 报无主 / 甩错 |
| 681–700 | 建房选项控件 |
| 701+ | 预留 |
#### 声音(Voice,≥101)
见 §4,整节标【待确认】。
### 0.4 牌面资源与 cardId → 帧号映射(唯一权威转换)
#### 帧排列(通用规范,跨游戏统一)
牌面资源统一按 **10 列 × 6 行 = 60 帧**排版,行优先编号:
| 帧 | 内容 |
| --- | --- |
| 1–13 | **黑桃** A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K |
| 14–26 | **红桃** A – K |
| 27–39 | **梅花** A – K |
| 40–52 | **方块** A – K |
| 53 | 小王 |
| 54 | 大王 |
| 55–60 | **牌背 1 – 6**(预留多套牌背,二七王使用帧 **55**) |
二七王规则去掉了 3 和 4(design §2),因此**不引用**帧 `3, 4, 16, 17, 29, 30, 42, 43`;但资源仍按 60 帧通用排版**照常出图**,不留空、不重排——这套排版是所有牌类游戏共用的。
#### cardId → 帧号(前端只此一处实现)
服务端牌 id 的编码(`server/games/erqiwang/class.paiju.js:57`):
```
id = (deck - 1) * 54 + (flower - 1) * 13 + (number - 1) // deck 1..2, flower 1..4, number 1..13(1=A)
小王 id = (deck - 1) * 54 + 52
大王 id = (deck - 1) * 54 + 53
```
服务端 `flower` 编号是 **1 = 方块、2 = 梅花、3 = 红心、4 = 黑桃**(`class.pai.js:16`、协议 §6),因此 id 的花色段序为 **方块 → 梅花 → 红心 → 黑桃**,与美术帧的花色顺序(黑桃 → 红桃 → 梅花 → 方块)**正好相反**。点数在段内是一致的(都 A → K)。转换必须把花色段反过来数:
```js
// 唯一权威转换:cardId → 牌面资源帧号。全前端只此一处实现,其余模块调用它。
function cardIdToFrame(cardId) {
var n = cardId % 54; // 去掉副数,deck1 / deck2 共用同一帧
if (n === 52) { return 53; } // 小王
if (n === 53) { return 54; } // 大王
return (3 - Math.floor(n / 13)) * 13 + (n % 13) + 1;
}
var CARD_BACK_FRAME = 55; // 牌背(二七王固定用牌背 1)
```
**校验样例**(建资源后必须逐条对上):
| cardId (deck1) | 牌 | `n = id % 54` | 期望帧 |
| --- | --- | --- | --- |
| 0 | 方块 A | 0 | **40** |
| 4 | 方块 5 | 4 | **44** |
| 12 | 方块 K | 12 | **52** |
| 13 | 梅花 A | 13 | **27** |
| 26 | 红桃 A | 26 | **14** |
| 39 | 黑桃 A | 39 | **1** |
| 51 | 黑桃 K | 51 | **13** |
| 52 | 小王 | 52 | **53** |
| 53 | 大王 | 53 | **54** |
| 106 | 小王(deck2) | 52 | **53** |
> deck1 与 deck2 的同一张牌**共用同一帧**(牌面完全相同)。区分两副牌靠 `cardId` 本身,不靠帧。
#### 三套尺寸
按 client 02 §2「不同显示区域尺寸不同,各自用一个独立资源 ID」的要求,开 3 套:
| 资源 ID | 键名 | 用途 | 单帧尺寸 | 整图尺寸 (10×6) |
| --- | --- | --- | --- | --- |
| 501 | `CARD_FACE_L` | 自己手牌、底牌区 | **110 × 190** | 1100 × 1140 |
| 502 | `CARD_FACE_M` | 三家已出牌区 | **90 × 155** | 900 × 930 |
| 503 | `CARD_FACE_S` | 结算埋牌底牌区 | **50 × 70** | 500 × 420 |
> 单帧尺寸为参考图实测估值(§6),出图前请与最终设计稿核对。三套的**帧排列完全一致**,只有尺寸不同,`cardIdToFrame` 对三套通用。
### 0.5 标注约定
| 标记 | 含义 |
| --- | --- |
| 【图】 | 该界面/元素在 `./uiref/` 有参考图,规格按图写 |
| 【待设计】 | 流程/协议要求存在,但**尚无设计稿**,本文只列出「必须有什么」,具体视觉待美术出稿后补 |
| 【待确认】 | 规格本身尚未定论,需与规则设计者/美术确认,编号见 §7 |
#### 怎么读参考图(重要)
参考图是**界面设计示意**,**只看版式、位置、样式,不看数值与状态组合**:
| 图上的东西 | 是否可信 |
| --- | --- |
| 元素的**位置、尺寸、层级、配色、字体样式** | ✅ 按图做 |
| 元素**长什么样**(按钮形态、角标形状、牌面排布方式) | ✅ 按图做 |
| 具体**数值**(分数、张数、倍数、局数、ID…) | ❌ **一律是填充用的假数据**,不要据此反推规则或校验自洽性 |
| **同时出现的互斥元素** | ❌ 是为了在一张图里把各种情况都标出来。典型:**多个玩家位同时挂庄标**——那是在告诉你「庄标在每个位置分别摆哪儿」,不是真会同时出现三个庄 |
| 同一张图里**跨阶段并置**的状态 | ❌ 同上,属于叠画示意 |
| 底栏右侧那排功能按钮 | ❌ 只是示意,**不代表该阶段的实际显隐**;显隐规则以 §2.6 为准 |
> 所以:**发现图上数值算不通、或两个不该同时出现的东西同时出现,不要当成待确认项**——那是示意画法。真正需要确认的是「规则本身没定」的地方。
---
## 1. 按阶段的界面清单
本节按 design §12.3 的阶段顺序走,每节说明**该阶段收哪些包、渲染什么、哪些 UI 显隐**。资源与精灵的完整表在 §3 / §5,本节只引用不重复。
```
建房(§1.1) → 发牌(§1.2) → 叫分坐庄(§1.3) → 看底牌·上庄(§1.4) → 选主/投降(§1.5) → 埋牌(§1.6)
→ 出牌对局(§1.7) → 末轮扣底 + 小局结算(§1.8) → 大局结算(§1.9) → 解散(§1.10) → 重连(§1.11)
```
### 1.1 建房 / 房间设置 【图:创建房间选项的UI参考内容根据实际二七王游戏改动.png(**样式范式,非内容**)】
**平台提供界面骨架 + 确认按钮(精灵 25);子游戏提供规则选项区。**
> ⚠️ 参考图是**另一个游戏**的建房选项(局数 8/16/24、人数 2/3/4、霸王 ×2/×4 等,二七王都没有)。
> 如文件名所示:**只照搬视觉范式与控件样式,内容一律按二七王的 `roomtype` 重写**。
#### 结构模型:类别标题 + 选项组
界面由**类别**纵向堆叠而成,每个类别是「类别标题 + 若干选项组」:
```
[类别标题] 选项组1 选项组2 …
牌局 ◉6局 ◯12局 ◉房主扣卡 ◯AA每人 【2张】
模式 ◉可查牌 ◯不查牌
规则 ▣傍王 ▣爬坡
```
**选项组有三种类型**(这是通用控件规格,不只为二七王):
| 类型 | 语义 | 用的选择框图 |
| --- | --- | --- |
| **单选** | 组内必选其一,互斥 | 单选(圆钮) |
| **多选** | 组内各项独立开关,可全不选、可全选 | 复选(方框) |
| **单选(可不选)** | 组内至多选一,也可一个都不选 | **复选(方框)**——因为它需要能表达「全不选」,视觉上不适合用单选圆钮 |
**每个选项 = 选择框(图片精灵)+ 文字精灵**,两者成对出现。
**选择框图片资源为 2 × 2 = 4 帧**:
| 帧 | 内容 |
| --- | --- |
| 1 | 单选 · 未选中 |
| 2 | 单选 · 已选中 |
| 3 | 复选 · 未选中 |
| 4 | 复选 · 已选中 |
帧号计算:`frame = (useCheckbox ? 3 : 1) + (selected ? 1 : 0)`,其中 `useCheckbox = (类型 === 多选 || 类型 === 单选可不选)`。
**类别标题放在一张图上多帧**,一个标题一帧:
| 帧 | 内容 |
| --- | --- |
| 1 | 牌局 |
| 2 | 模式 |
| 3 | 规则 |
其余视觉细节照搬参考图:标题青绿粗体、选中项文字变橙、灰色小字附注(参考图的 `【3张/人】`,二七王用来显示房卡消耗)、选项多时可折行。
#### 内容(二七王实际,对应 `roomtype` 5 位,协议 §0.5)
| 类别(帧) | 选项组 | 类型 | 选项 | `roomtype` 位 | design |
| --- | --- | --- | --- | --- | --- |
| **牌局**(帧1) | 局数 | 单选 | **6 局** / 12 局 | 位 0 | §10.1 |
| **牌局**(帧1) | 扣卡 | 单选 | **房主扣卡** / AA 每人扣卡 + 附注房卡数 | 位 1 | §10.1 |
| **模式**(帧2) | 查牌 | 单选 | **可查牌** / 不查牌 | 位 4 | §9 / §10.2 |
| **规则**(帧3) | 附加规则 | **多选** | 傍王、爬坡(可同时勾选,互不冲突) | 位 2、位 3 | §8.3 / §7.3 / §10.3 |
(加粗为缺省值,即 `roomtype = "00000"`。三个类别正好对应 design §10.1 / §10.2 / §10.3 三个小节。)
**二七王当前用不到「单选(可不选)」**——四个选项组里三个是单选、一个是多选。该类型仍须在控件层实现,供后续玩法与模板克隆使用。
**没有「人数」行**——二七王固定 3 人(design §2),不给玩家选。参考图里的「人数」「霸王」两行**不要照抄**。
**房卡消耗联动**(协议 §0.5,随「局数 + 扣卡」两行实时变化):
| 扣卡方式 | 6 局 | 12 局 |
| --- | --- | --- |
| 房主扣卡 | 房主 2 张 | 房主 4 张 |
| AA 每人扣卡 | 每人 1 张 | 每人 2 张 |
#### 职责边界(D-1 已定)
**子游戏只管「类别标签 + 选项」这一块内容区**。弹窗背景、标题、关闭按钮、确认按钮**都由平台提供**——所以 §5.8 只列内容区的精灵,不要重复建弹窗外壳。
#### 实现要点
- 交互:点击平台约定的 25 号精灵 → 拼 `roomtype` 串 → `Net.Send_create_room({agentid, playerid, gameid, roomtype})`。
- 精灵:群组 250,号段 2050–2099,**预置 20 个**(见 §5.8);挂在平台创建房间界面所在图层(T-3)。
- 资源:选择框(4 帧)、类别标题(3 帧)。
- **整个界面由一份「类别 → 选项组 → 选项」的配置数据驱动渲染**,不为每个选项写死代码。加一个选项 = 配置里加一条,不改渲染逻辑(工程总则 §5 OCP、§6 配置优先)。
- **`roomtype` 的拼串必须只在一处实现**——与服务端 `class.config.js` `parse()` 对称的 SSOT 要求,不得在多处按下标硬拼。每个选项在配置里自带 `bit` 与 `value`,拼串就是遍历配置,无需按下标硬编码。
- 房卡数**不要硬编码在 UI 代码里**,做成「局数 × 扣卡方式 → 房卡数」的配置表(工程总则 §6)。
配置数据形态(示意,落地位置见 §6.10):
```js
EQW_RoomOptions = {
categories: [
{ titleFrame: 1, /* 牌局 */ groups: [
{ key: 'aset', type: 'radio', bit: 0,
items: [ {label:'6局', value:'0'}, {label:'12局', value:'1'} ] },
{ key: 'card', type: 'radio', bit: 1, note: 'cardCost',
items: [ {label:'房主扣卡', value:'0'}, {label:'AA每人扣卡', value:'1'} ] }
]},
{ titleFrame: 2, /* 模式 */ groups: [
{ key: 'peek', type: 'radio', bit: 4,
items: [ {label:'可查牌', value:'0'}, {label:'不查牌', value:'1'} ] }
]},
{ titleFrame: 3, /* 规则 */ groups: [
{ key: 'rules', type: 'checkbox',
items: [ {label:'傍王', bit:2}, {label:'爬坡', bit:3} ] } // 多选:bit 落在每一项上
]}
]
};
```
> `type` 取 `'radio'` / `'checkbox'` / `'radioOptional'`(单选可不选)。选择框帧号由 `type` 推出,见上文公式——**渲染代码不认识「傍王」「爬坡」这些具体玩法**,只认识三种控件类型,这样框架侧保持游戏中立、子游戏只提供配置。
### 1.2 发牌 `fapai` 【图:等待叫分.png】
**收包字段**:`asetidx`(当前局数)、`asetcount`(总局数)、`cards`(自己 28 张)、`seat`(当前等待叫分者)、`countdown`。
**渲染**:
| 要做的事 | 涉及 UI |
| --- | --- |
| 底栏局数显示 `1/4 局` | §2.6 底栏 · 局数文字 |
| 自己手牌 28 张,按主牌顺序排序后铺开 | §2.7 手牌区(群组 210) |
| 桌面中央 8 张底牌**背面** | 底牌区(群组 211,帧 = 牌背 55) |
| 顶部信息条清零:主 = 无、叫分 = 0、抓分 = 0 | §2.1 顶部信息条 |
| 三家玩家位状态文字清空,`主N/对N` 角标隐藏 | §2.2 玩家位附加标记 |
| 倒计时显示在 `seat` 对应的位置 | §2.4 倒计时 |
- **发牌动画(需要做)**:**从右往左铺开**。须遵守 client 02 §3「数据优先、表现延后」——先 `setHandCards()` 把 28 张写进 `this.data`,再播铺开动画;动画的开始/结束/出错回调里**只刷界面、不写核心数据**。动画缺失或卡住时,数据与界面仍须正确。动画时长、每张延迟进配置(§6.7)。
- **排序**:手牌排序规则(主牌在左还是在右、副牌花色顺序)由前端自行决定,**但选主前后主牌集合会变**,选主后必须重排。见 §7 T-5。
### 1.3 叫分坐庄 `jiaofen` 【图:等待叫分.png / 自己叫分.png / 自己已经叫分轮到下家叫分.png】
**收包字段**:`seat`(叫分者)、`call`(0 = 不叫)、`currcall`(当前叫到的分)、`multiple`(该叫分对应基础子数)、`nextseat`、`countdown`。
**三种界面状态**:
| 状态 | 参考图 | 表现 |
| --- | --- | --- |
| 等待他家叫分 | `等待叫分.png` | 中央保持 8 张底牌背面;倒计时数字显示在**当前叫分者**的位置旁;已叫分的玩家位显示金色 `60分` 大字,未叫的为空 |
| 轮到自己叫分 | `自己叫分.png` | 中央弹出**叫分面板**(14 档按钮,两行 × 7)+ `不叫` 按钮;顶部一条提示 `70分坐庄才可投降`;倒计时移到自己操作区 |
| 自己已叫、轮到下家 | `自己已经叫分轮到下家叫分.png` | 叫分面板收起,中央显示自己刚叫的 `50分` 金色大字;已表态的玩家位显示 `不叫` 白色大字 |
**叫分面板规格**(群组 220,号段 1600–1649):
- 14 档:`70 65 60 55 50 45 40 35 30 25 20 15 10 5`,两行 × 7(行1 = 70…40,行2 = 35…5)。
- 每档 3 个精灵:按钮底(**含角标底,同一张图**)+ 分数数字精灵 + 角标子数文字精灵。
- 角标显示该档的 `multiple`,**随房间「爬坡」开关取值**(协议 §3):
- 常规算子:65→2、60→3、55→4、**50 及以下统一 →6**、70→2
- 爬坡: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
**按钮配色:按档位位置固定,不随状态或数据变** 【图:叫分按钮面板.png】
| 位置 | 档位 | 按钮底 | 角标底 |
| --- | --- | --- | --- |
| 1–4 | 70 / 65 / 60 / 55 | 浅黄 | 蓝 |
| 5–7 | 50 / 45 / 40 | 金黄 | 绿 |
| 8–14 | 35 / 30 / 25 / 20 / 15 / 10 / 5 | 橙 | 深橙 |
> ⚠️ 参考图里角标写的 `1子`/`2子`/`4子` 是**填充假数据**(§0.5),恰好与三组配色一一对应,**不要据此以为「颜色随子数变」**——颜色按档位位置写死,角标数值才随爬坡开关变。叫分越低(承诺越苛刻、子数越高)颜色越热。
>
> 反证:常规算子下 50/45/40 与 35…5 的 `multiple` 同为 6 子,但参考图里它们是两种颜色。
**灰色 = 已经叫过的分数**(不可再叫)。**颜色只因「不可叫」变灰,不随子数档位变化。**
**按钮底与右上角角标(不含文字)画在同一张图上**,即一个精灵一帧就同时包含按钮底色和角标底色。共 **4 帧**:
| 帧 | 内容 |
| --- | --- |
| 1 | 浅黄按钮底 + 蓝角标底(高分档 70/65/60/55) |
| 2 | 金黄按钮底 + 绿角标底(中分档 50/45/40) |
| 3 | 橙按钮底 + 深橙角标底(低分档 35…5) |
| 4 | **灰按钮底 + 灰角标底(已叫过 / 不可选)** |
**两种数字,两种精灵**:
| 元素 | 精灵类型 | 说明 |
| --- | --- | --- |
| **叫分的分数**(70/65/…/5) | **多帧图数字精灵**(资源 `NUM_CALL_SCORE`,`SpriteManager.setNumberImage`) | 带描边、需与图一致的美术字;**一个精灵显示整串**(两位数不拆精灵) |
| **角标的子数**(`2子`/`7子`…) | **文字精灵**(`SpriteManager.setText`) | 随爬坡开关动态变,值域 2–15,用文字精灵最省 |
故每档 **3 个精灵**:按钮底(含角标底)+ 分数数字精灵 + 角标文字精灵。
> **2026-08-26 补充(档位分数的实现方式,务必照做)**:14 档里 **13 档是两位数**(70…10),而每档只有**一个**分数精灵——这是**对的**,不要拆成十位/个位两个精灵。
> 显示走平台既有接口 `SpriteManager.setNumberImage(spriteId, '70', charWidth)`:整串文本交给一个多帧图片精灵,引擎按字符逐帧渲染,精灵宽度按**字符数 × `charWidth`** 自动调整。
> **不要用 `setFrame` 逐位切**——`setFrame` 是"整个精灵显示第 N 帧",一次只能显示一位,用它做多位数必然要拆精灵。
> 资源 `NUM_CALL_SCORE` 须按 §3.6 的 **16 帧固定帧序**出图;`charWidth` 取 `EQW_Layout.NUM_STYLE.CALL_SCORE.charWidth`(§6.8,业务代码不写裸值)。
- 可选性规则(design §4.2,**由服务端权威 `currcall` 决定,前端只据其置灰、不自行推导规则**):
- 暂定庄家首叫:5–70 全部可选,**无 `不叫`**;
- 其后:只有**严格低于 `currcall`** 的档位可选,`不叫` 可选。
- 交互:点击某档 → 发 `jiaofen { seat, call }`;点 `不叫` → 发 `jiaofen { seat, call: 0 }`。
- **悲观 UI**:点击后**只发包**,面板的收起与状态文字的出现一律等 `jiaofen` 推送包到达再做(前端 05 §6)。唯一允许的乐观处理是按钮本身的防连点禁用。
**上方提示条** `70分坐庄才可投降`:静态文字条,仅在叫分面板展开时显示。
### 1.4 看底牌 / 上庄 `shangzhuang` 【部分待设计】
**收包字段**:`seat`、`call`、`banker`、`grade`(庄家叫分)、`multiple`、`bottomcards`、`ancard3s`、`cards`、`countdown`、`touxiang`。
| 分支 | 条件 | 表现 |
| --- | --- | --- |
| 常规坐庄 | 无 `ancard3s` | 庄家收到 `bottomcards`(8 张)与 `cards`(36 张)→ 底牌翻正面短暂展示后并入手牌;闲家**收不到** `bottomcards`,底牌区直接收起 |
| **70 分坐庄** | `ancard3s === 1` | **全场三家都收到 `bottomcards`** → 8 张底牌翻正面**向所有玩家亮 3 秒**(design §4/§7.1),3 秒后庄家摸入手牌、闲家侧收起 |
- **开底(D-2 已定)**:**复用「庄家摸底时翻开底牌」那套 UI**(见 `叫分确定庄.png`——中央 8 张牌由背面翻正面),**不要倒计时**。
两种情形用的是**同一套界面**,唯一差别是**谁能看到**:
| 情形 | 谁能看到这 3 秒 |
| --- | --- |
| 非 70 分坐庄(只有**摸底**) | **只有庄家自己**(闲家侧那 8 张保持背面 / 直接收起) |
| **70 分坐庄**(**开底** → 摸底) | **全场三人都能看到** 3 秒 |
因此**不需要单独的「公示」面板**,只是同一套翻牌表现的可见范围不同——精灵复用 §5.3 群组 211(底牌区),无需新增。
【图:等待庄家选主.png】即闲家视角下的**开底**:中央 8 张底牌翻正面摊开,下方一条 `等待庄家选主` 状态提示条(见 §2.8)。
> 该图把「开底摊牌」与「等待庄家选主」两个状态画在了一起,属**叠画示意**(见 §0.5「怎么读参考图」)。实际先后仍按 design §4:**开底 3 秒 → 庄家摸底入手 → 选主**,摸底后底牌区即收起。
- 上庄后统一更新:顶部信息条 `叫分` 列 = `grade`、角标 = `multiple`;`banker` 对应玩家位显示 **`庄` 印章**(含底栏自己位)。
- 三处 `庄` 印章是三个独立精灵(左上 / 右上 / 底栏),按 `banker` 显隐其一。
- **重连不重放**:协议明确 70 分的 3 秒亮牌是上庄时的一次性事件,`deskinfo` 重连**不重放**——前端不得在重连时补播。
### 1.5 选主 / 投降 `xuanzhu` · `touxiang` 【图:亮主.png】
**同一决策点、二选一、互斥**(design §4.4)。
**选主面板规格**(群组 221,号段 1650–1699):
> **2026-08-26 修订(其一)**:美术侧实际出图方式与原规格不符——花色按钮改为**一张图 5 帧**(帧1–4=四花色按钮,每帧已含按钮底+花色图标+右上角蓝色角标底;帧5=投降按钮,已含按钮底+「投降」字),花色图标、角标底、投降文字不再单独出精灵。按此改写以下条目,代码同步见 `ImageResources.js`/`Sprites_Action.js`/`Layout_Action.js`。
>
> **2026-08-26 修订(其二)**:张数数字用平台既有的 `SpriteManager.setNumberImage`(§3.6)——一个精灵就能显示整串 `26张`,故**每花色一个数字精灵**(`MAIN_SUIT_COUNT_1..4` = 1674–1677);当日早前定的「十位/个位各一个精灵」(8 个)**作废**,空出的 1678–1681 不复用。
- 标题斜角条:`亮主` 金色字 + 倒计时数字。
- **4 个花色按钮**:每个按钮由**一个精灵**呈现,帧 = 花色序号 1–4(服务端 flower 编号),该帧已含「按钮底 + 花色图标 + 右上角蓝色角标底」;角标底之上叠加一个**对数文字精灵**(`N对`)。
- 按钮下方叠加**张数数字精灵**:该花色在庄家手中的**总张数**——两副牌单花色最多 26 张(两位数),用 `SpriteManager.setNumberImage(spriteId, '26张', charWidth)` 在**一个**精灵上显示整串(无需拆十位/个位),资源见 §3.6 的 `NUM_MAIN_SUIT_COUNT`(第16帧=`张`)、`charWidth` 见 §6.8 的 `NUM_STYLE.MAIN_SUIT_COUNT`
- 右上角标文字 = 该花色的**对子数**(design §4/§11 明确要求)
- **张数与对子数都由前端据 `MyCards`/`cards` 自行统计**,协议 §「ChooseMain」明确「服务端不额外下发」。
- **投降按钮**:仅 `touxiang === 1`(即叫分 70)时显示,与 4 个花色按钮并列;按钮为**一个精灵**(`BTN_SUIT_CHOOSE` 帧5,已含底与「投降」字),不再单独出文字精灵。
- 交互:点花色 → `xuanzhu { seat, flower }`(flower:1方块 2梅花 3红心 4黑桃);点投降 → `touxiang { seat }`。
- **投降的后果**:直接跳到结算(§1.8),**不选主、不埋牌、不出牌**。
**收到 `xuanzhu` 推送后**(字段 `banker`、`flower`、`countdown`、`cards`):
- 顶部信息条 `主` 列显示 `flower` 对应的花色图标;
- **手牌必须按新的主牌集合重排**(正2/正7 升格为主牌,见 design §3);
- 选主面板收起,进入埋牌。
### 1.6 埋牌 `maipai` 【图:庄家埋牌.png】
**仅庄家**,闲家此阶段等待。
**界面变化**:
- 手牌从**单排 36 张**改为**双排**(上排 17 + 下排 19,实测估值)。
- 操作条(群组 222):`提示` 按钮 + 倒计时 + **`埋牌 (N/8)`** 按钮,N 为已选张数,`N < 8` 时按钮置灰不可提交。
- 选中的牌**上浮**表示待埋。
- 牌面上的三种标记(资源 `MARK_CARD_GROUP` 3 帧):
- **`拖` 红色圆标** = 该牌属于一组**拖拉机**
- **橙色五角星** = **正 2 / 正 7**(选定花色的 2 和 7,design §3 里大小仅次于双王)
- **蓝色五角星** = 除正2正7外的**其他主牌**(双王、副2、副7、主花色普通牌)
三种标记互斥、每张牌至多一个;由前端据 `flower`(主牌花色)在本地标注,**不需要服务端下发**。
**收到 `maipai` 推送后**(字段 `cards`、`bottomcards`、`seat`、`countdown`、`liangpai`):
- 庄家手牌回到单排 28 张;
- **闲家**若收到 `liangpai`(仅可查牌模式 + 庄家达标),弹出下方「亮牌」**遮罩面板**(群组 242,精灵 `HIST_*`,见 §5.7b / §6.9c)——**不是** §2.5 那条主牌统计条(原表述「渲染 §2.5 亮牌信息条」是同名异实造成的笔误,2026-08-27 订正);
- 进入出牌,`seat` = 首出者(必为庄家)。
**亮牌**(design §8.2,协议 `liangpai`):
庄家埋牌后、出牌开始前,若手中**固定主牌**达门槛,就向两个闲家亮出**自己全部固定主牌的具体牌面**。
| 项 | 说明 |
| --- | --- |
| 触发门槛(任一) | 固定主牌总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6 |
| 固定主牌 | 双王 + 全部花色的 2 + 全部花色的 7;**不含**主花色普通牌 A/K/Q/J/10/9/8/6/5 |
| 亮出内容 | `liangpai.cards`,已按主牌序排好。**固定的一份**,不随被哪条门槛触发而增减 |
| 叫分限制 | **无**,任何叫分档位都适用 |
| 查牌模式 | 仅可查牌模式下发;不查牌时闲家收不到该字段 |
| 谁能看 | 只有两个闲家收到(庄家自己的牌自己本来就看得见) |
**展示形式**:与**明牌**同一套版式(半透黑遮罩 + 一排牌,照 `冲关牌型显示.png`),只是这里只有一排(庄家的)。可与 §5.7b 的明牌面板复用同一套精灵与渲染,仅数据源不同。
> **三个遮罩面板的关闭方式各不相同,别写混**:
>
> | 面板 | 关闭方式 | 遮罩是否可点 |
> | --- | --- | --- |
> | **亮牌**(本节) | **仅自动时长**,到点自动收起 | ❌ 不注册点击 |
> | **冲关牌型**(§1.8) | **点遮罩关闭** | ✅ 是关闭热区 |
> | **明牌 / 出牌历史**(§1.7) | 点遮罩关闭;明牌另可再点按钮切换 | ✅ 是关闭热区 |
**数量前端自己数**——协议不再下发 `zhu`/`zhupair`/`wang`/`qi`/`er` 等统计,需要显示「x 张王」之类就从 `cards` 里算。
**展示时机(T-34 已定)**:**埋牌后、出牌前**——庄家埋牌完成、服务端下发 `maipai` 包(其中带 `liangpai`)时弹出,出牌开始前收起。
- 与 design §8.2「庄家埋牌后……出牌开始前」的规则时点一致。
- 这是**一次性弹出**,不是常驻入口。**关闭方式:只按自动时长关闭,不做点击关闭**——遮罩不注册点击事件。时长进配置(`liangpaiAutoClose`,默认值待定)。
- 只有**闲家**会收到 `liangpai` 并弹出;庄家自己不弹(自己的牌自己看得见)。
- **不阻塞出牌**:面板关闭与否不影响服务端推进,出牌包照常到达;若面板还开着就收到了出牌推送,直接收起面板即可。
### 1.7 出牌对局 `chupai1` / `chupai2` / `chupai3` 【图:出牌.png / 出牌2.png】
这是最复杂的阶段。
**出牌操作条**(群组 223):倒计时 + `出牌` 按钮(选中牌数为 0 时置灰)+ **`提示` 按钮(出牌阶段必须有)**。
**自动选中**:某些情况下(如跟牌时存在**必出牌**、或整手只有唯一合法出法),服务端会在下发包里带上**应当选中的牌**,前端收到后**自动把这些牌选中**(上浮),玩家确认后点 `出牌` 即可。
> ⚠️ **该字段目前协议里没有,需服务端补**——见 §7.5 **S-2**。好消息是服务端已经算过了:`class.arith.js:737` `get_followcard()` 返回 `{ mustcard, cancard, cantype }`,其中 `mustcard` 就是「必出的牌列表」,现在只用于内部校验(`class.arith.js:1055`)、未下发。补一个下发字段即可,无需新增算法。
>
> 这条**不违反**「请求包只带意图」红线——方向是**服务端 → 前端**的建议选中,前端仍只回传 `cards`;且合法性最终仍由服务端校验。
**自己出牌**:点手牌选中(上浮)→ 点 `出牌` → 发 `chupai { seat, cards }`。
- `cards` 必须是**非空、元素为 0–107 整数、互不重复**的数组(协议 §0.2),否则服务端按 `PARAM` 拒绝。
- **只发意图,不发结论**:不回传牌型、不回传「我最大」之类判定。
- 前端可用 `shared/` 做本地预校验与提示,**结果不回传**。
**收 `chupai1`(首家出牌)**,字段最丰富:
| 字段 | 渲染用途 |
| --- | --- |
| `seat` / `cards` | 在该家已出牌区摆牌(资源 `CARD_FACE_M`) |
| `cardtype` | 牌型标签(`>100` 单张 / `>200` 对子 / `>300` 拖拉机) |
| `shuai` | **甩牌**分量构成 `{tractors:[], pairs, singles}`——**甩牌的真实结构只能读它,不得据 `cardtype` 反推**(协议 §11 明确警告) |
| `shuaicuo` | **甩错**标志(= 1):整套甩牌收回,本轮只强制打出最小一张(design §5.4.5)。表现见下方「甩错提示」 |
| `flower` / `count` | 本轮首出花色与张数,供本地跟牌提示 |
| `nextseat` / `countdown` | 倒计时移到下一家 |
| `seatlist` | **仅可查牌模式**:三家牌况,用于 §2.2 的 `主N` / `对N` 角标 |
| `baozhu` | 是否已有**任一**玩家**报无主**:= 1 时触发余主公示(各家主牌数/对数),并让**三家**的底栏 `明牌` 按钮可用 |
| `cardsinhand` | **仅出牌者本人有**:出牌后剩余手牌,据此重画手牌区 |
**收 `chupai2` / `chupai3`**:字段类似,`chupai3` 额外带 `maxseat`(本轮谁最大)与 `grade`(本轮闲家得分,仅闲家有得分时才有)。
**一轮结束的表现**:
- `maxseat` 高亮,随后三家出牌区收牌;
- 若有 `grade`,播捡分飘字(`+20分` 金色,参考图 `出牌2.png` 中央 `20分`)→ 同时更新顶部信息条 `抓分` 列累计值;
- 下一轮由 `maxseat` 先出。
**牌型标签**(参考图 `出牌2.png` 左下角橙色 `拖拉机`):需要一套标签资源,至少含 `单张` / `对子` / `拖拉机` / `甩牌` / `毙` / `垫`【待确认 T-11:标签种类以美术稿为准;「毙/垫/混合出牌」是否也要标】。
**闲家提示 `tishi`** 【图:自己是闲家时,底下的踩有分没分按钮.png】(design §11):
- 闲家在出牌阶段可向**对家**(另一闲家)发三种提示:`1 = 踩`(我能大过庄家)、`2 = 没分`、`3 = 有分`。
- 发送方**收不到成功回执**(协议 §13.7),只有失败才回包。
- 接收方(对家)收到 `tishi { seat, tip }` → 在发送者的玩家位弹出气泡。
- 庄家无对家,不提供此功能。
- **自己发的提示需要本地回显**:服务端只把提示转发给对家、不回执给发送者,所以**发送者自己的气泡由前端在点击时本地弹出**。
> 这是「悲观 UI」红线的一处**受控例外**,成立的理由是:提示**不含任何对局状态**,服务端明确「不校验提示真实性」(协议 §13.7、design §11),它既不改阶段、不改控制权、不改分数,纯属玩家间的主观喊话。因此本地回显不会造成与服务端状态不一致。仍须遵守:失败回包(`RULE`/`STEP`/`PARAM`)到达时要把本地回显撤掉。
**发起入口**:底栏三个按钮 `踩` `有分` `没分`,**位置就是庄标的位置**——自己是闲家时没有 `庄` 印章,这块区域正好空出来给这三个按钮。
| 自己的身份 | 底栏该区域显示 |
| --- | --- |
| 庄家 | `庄` 印章(1 个精灵) |
| 闲家 | `踩` `有分` `没分`(3 个按钮) |
两者**互斥**,同一块区域二选一。注意按钮顺序是 **踩 → 有分 → 没分**,与协议 `tip` 编号(1踩 / 2没分 / 3有分)**不同**,映射不要按位置下标推。
**气泡资源**:`3 × 2 = 6 帧`。
| | 箭头向左 | 箭头向右 |
| --- | --- | --- |
| 踩 | 帧 1 | 帧 4 |
| 没分 | 帧 2 | 帧 5 |
| 有分 | 帧 3 | 帧 6 |
**箭头朝向按头像相对气泡的位置选**:头像在气泡**左**侧 → 用**箭头向左**的帧;头像在气泡**右**侧 → 用**箭头向右**的帧。三个座位的取用见 §6.5。
**甩错提示(D-3 已定)** 【图:强甩失败.png】:
- 收到 `shuaicuo === 1` 时,用**通用状态提示条**(§2.8)显示 **`强甩失败`**,取「即时反馈类」位置预设。
- **不做收回动画**——甩出的牌不演"飞回手里",直接按服务端下发的 `cards`(那张被强制打出的最小主牌单张)落牌即可。
- 提示条自动淡出,时长进配置。
**报无主 → 余主公示(D-4 已定)**:
- **不做全场提示**。报无主本身没有独立的提示表现。
- 唯一的界面变化是**余主公示**生效:他家的 `主N` / `对N` 角标出现(服务端 `baozhu=1` 并整表刷新 `seatlist`,仅可查牌模式)。详见 §2.5。
- 自己的主牌统计条**本来就一直显示**,不因报无主而变化。
**明牌**(design §9.3):
- **按钮显示条件(D-6 已定,按 design 手册)**:**可查牌模式 + 出牌阶段 + 已有任一玩家报无主**(`baozhu == 1`)→ **三家都显示** `明牌` 按钮,且**可随时查看**、不限次数。
> 判定的是「场上**有人**报无主」,不是「**自己**报无主」——报无主的那位自己也能看。与 design §9.3「一旦有玩家的主牌全部打空……为**全体三人**(含刚刚报无主的这位玩家自己)显示……同时界面上会出现一个『明牌』按钮」一致,也与服务端 `mingpai` 的受理条件 `have_baofu()` 一致。
- 点击 → 发 `mingpai { seat }` → 收 `mingpai { seat, others: [{seat, zhucards}] }` → 弹出面板展示另两家全部未出主牌的**具体牌面**。
- **再点一次取消是纯前端开关**,不再请求服务端(协议 §13.6)。
- **展示形式(D-6 已定)**:**照 `冲关牌型显示.png` 那一套**——半透黑遮罩 + 两家各一排牌(`CARD_FACE_L`、`fan` 重叠排列),点遮罩关闭。与冲关牌型叠加层是同一种版式,可复用同一套渲染逻辑,仅数据源与座位数不同(明牌是**另两家**,冲关牌型是**三家**)。
- 面板:图层 305,群组 243;牌**用精灵复制**(张数不定,同 §5.6a 的判据)。
**出牌历史**(design §9.1):
- 仅可查牌模式;底栏 `出牌历史` / `上一轮` 两个按钮(§2.6)。
- 数据来自重连包 `pushlist`(按轮次 × 3 家),**出牌过程中前端需自行累积**(`chupai1/2/3` 逐包记录)。
- **展示形式(D-7 已定)**:**不按轮次分组**。把每家出过的牌全部**摊平聚合到该玩家名下**,再**按手牌那套主牌序排序**(同 §7.2 T-5 的 `order_cards` 顺序),三家各显示一排——版式**照 `冲关牌型显示.png`**(遮罩 + 三家各一排牌)。
> 前端自行摊平 + 排序即可,**不需要改服务端**:`pushlist` 已含全部轮次数据,且各轮内部已按本局主牌花色排好。
- `上一轮` 按钮则只取**最近一轮**的三家出牌(`pushlist` 末尾一项),版式同上。
- 面板:图层 304,群组 242;牌**用精灵复制**(张数不定)。
- **不可查牌模式下这两个按钮整体不出现**(design §9)。
**失败回包**(协议 §0.2):任何请求被拒都会回同名 rpc 的 `{ success: false, errcode }`。前端一律 `if (!data.success)` 判断,据 `errcode` 提示:
| errcode | 含义 | 建议提示 |
| --- | --- | --- |
| 1 PLAYER | 玩家/房间/座位校验不过 | 「身份校验失败」 |
| 2 NODESK | 牌桌或牌局不存在 | 「牌局不存在」 |
| 3 STEP | 当前阶段不允许 | 「当前阶段不能这样操作」 |
| 4 SEAT | 位置不符 / 还没轮到 | 「还没轮到你」 |
| 5 PARAM | 参数非法 | 「出牌数据有误」 |
| 6 RULE | 规则不允许 | 「这样出不符合规则」 |
复用框架 `UIManager.showMessage`,无需专门资源。
### 1.8 末轮扣底 + 小局结算 `jiesuan` 【图:小局结算.png】
**结算包三种来源**(协议 §14):正常出牌结算(`chupai` + `bottom` + `aset`)、投降结算(只 `aset`)、解散结算(见 §1.10,投递方式完全不同)。
**扣底表现**(`bottom` 分组,design §6.3):
- `cards`(8 张**埋牌底牌**)**恒有** → 埋牌底牌区(群组 215,资源 `CARD_FACE_S`)翻开展示,design §11 明确「牌局结束需要亮出底牌」;
- `multiple` / `grade1` / `grade2` **仅闲家扣底时才有** → 展示 `底牌 20 × 4` 之类的翻倍算式。
- 文案格式:`叫分{aset.multiple}子 底牌 {bottom.grade1}x{bottom.multiple}`——**`x` 后面是扣底倍数**(design §6.3:单张 ×1、主对 ×2、N 连对 ×2N)。参考图 `底牌 20x0` 即底牌有 20 分、扣底倍数 0(本局闲家未扣底)。
- 闲家未扣底时 `bottom` 只有 `cards`,`multiple`/`grade1`/`grade2` 都不下发 → 倍数位显示 **0**。
**三家结算数字**(`aset.seatlist`,每家两行):
| 行 | 内容 | 字段 | 样式 |
| --- | --- | --- | --- |
| 大字 | 本局总分 | `grade` | `+20` 橙色(赢)/ `-10` 蓝色(输),底带光晕 |
| 小字左 | **`牌局分 N`** | `grade_jf`(捡分子数得分) | 白标签 + 数值 |
| 小字右 | **`冲关分 N`** | **`grade_cg`**(算奖得分里的冲关分量,S-6 新增;开傍王时不含傍王那部分) | 白标签 + 数值 |
> 两个标签的文案已由更新后的 `小局结算.png` 确定(原图两个都写「牌局分」是 mock 重复,现已修正为 `牌局分` / `冲关分`)。
**两个按钮**:
| 按钮 | 位置 | 作用 |
| --- | --- | --- |
| `冲关牌型`(蓝色) | 中央偏左 | 打开冲关牌型叠加层(见下) |
| `下一局`(金黄) | 中央偏右 | 发 `zhunbei { seat }` 进入下一局(§1.12) |
#### 「冲关」是什么(`chongguan` 字段的准确定义)
服务端 `class.arith.js:1277` `get_chongguan(mainflower, cards)`。`chongguan` 即 design §8.1 **冲关**的奖数(该规则旧称「常规算奖」,现统一叫冲关)。函数返回三项:
| 返回字段 | 含义 | 去向 |
| --- | --- | --- |
| `count` | 关数 = 冲关奖数 | `aset.seatlist[i].chongguan` |
| `wang` | 手牌中的王数 | `aset.seatlist[i].wang`(傍王按此计奖) |
| `cards` | **王牌 + 所有参与冲关的牌**,已按主牌序从大到小排好 | `aset.seatlist[i].cards` → **冲关牌型叠加层要显示的就是它** |
计数规则(与 design §8.1 逐条对应):
1. **前置**:`cards.length < 28` 直接返回 0 —— 手牌不足 28 张不参与冲关。
2. **三王 / 四王**:≥3 王 → `count = 1`;= 4 王 → `count = 3`。
3. **连对链续奖**:**必须先满足 ≥3 王**(整段逻辑在 `if (_wlist.length >= 3)` 内)。从小王编码 `9553` 起,沿主牌序往下逐对扫描,每遇到一个与上一节**位置相邻**的对子就 `count + 1`,链条为 `正7 → 副7 → 正2 → 副2 → 主A → 主K → …`,断则止。**没有三王就没有连对链奖**。
4. **六/七/八个 7**:`count += 张数 − 5`(6→1、7→2、8→3 奖)。
5. **六/七/八个 2**:同上。
6. **固定主牌 ≥10 张不产生冲关奖**——它只触发 design §8.2 的「亮牌」,代码里有明确注释。§1.6 的亮牌与这里的冲关是两回事。
> 前端**不重算冲关**,`chongguan` / `wang` / `naward` / `cards` 一律直接取服务端下发值(服务端权威,红线)。上面的规则只用于理解要显示什么。
**冲关牌型叠加层** 【图:冲关牌型显示.png】:
点 `冲关牌型` 后,在结算面板之上盖一层**半透黑遮罩**,三家各展示自己的 `aset.seatlist[i].cards`,资源用 `CARD_FACE_L`、重叠排列。`下一局` 按钮在遮罩层上**继续可见可点**,`冲关牌型` 按钮本身被遮住。
**关闭方式:整块全屏半透明黑遮罩本身就是关闭热区,点击遮罩即关闭。** 不另设关闭按钮。因此 `CG_MASK` 必须注册点击事件,且 `下一局` 的层级要高于它、点它不被遮罩吃掉。
- 三家牌位:左上家、右上家在各自头像内侧;自己在中央偏下。
- 布局配置见 §6.7。
**牌用精灵复制生成,不预置固定数量**(见 §5.6a):`cards` 的张数**没有小上限**——四王 + 一条长连对链 + 八个 7 + 八个 2 叠加时可达 20 余张。预置精灵既会不够用,又在绝大多数局里白占 ID。
**`aset` 其余字段的用途**:
| 字段 | 用途 |
| --- | --- |
| `banker` / `call` / `flower` | 顶部信息条最终态 |
| `multiple` | 基础子数 |
| `grade` | 闲家最终捡分(含扣底)→ 顶部 `抓分` 列 |
| `upgrade` | **判定倍率(带符号)**:`3` 大光 / `2` 小光 / `1` 过庄 / `-N` 升 N 级(倒庄)/ `-99` 投降 / `0` 解散。**结算面板不显示判定文字**——判定结果由**动画**呈现,见 §1.8「判定结果动画」 |
| `bangwang` / `climb` | 本局规则开关,用于结算面板上标注 |
| **`cards`** | **冲关牌型叠加层**的牌(见上) |
| `chongguan` / `wang` / `naward` | 冲关奖数 / 王数 / **总奖数**。叠加层**需要并列显示奖数**——以 `naward`(该家总奖数 N,8.4 节的支付基数)为主;`chongguan` / `wang` 作为明细可选展示 |
#### 判定结果动画(D-8)
**结算面板上不做判定文字**。大光 / 小光 / 过庄 / 升N级 / 投降这几个判定,由**动画**在两个时机呈现:
| 时机 | 数据来源 | 说明 |
| --- | --- | --- |
| **对局过程中** | `curmultiple`(随 `chupai1/2/3` 下发,S-1) | 捡分变化导致判定档位跳变时提示,例如从「大光」掉到「小光」、或闲家捡够分数「过庄→升级」 |
| **结算前** | `aset.upgrade`(结算包,带符号) | 末轮打完、结算面板出现**之前**播一次最终判定 |
**动画播放遵守「数据优先、表现延后」**(client 02 §3):先 `setXxx` 把结算数据写进 `this.data`,再播动画;动画的开始/结束/出错回调里**只刷界面、不写核心数据**。动画缺失或卡住时,结算面板与数据仍须正确。
> `curmultiple` 是**带符号**的(S-8 已改):`3/2/1` = 庄家大光/小光/过庄,`-N` = 闲家升 N 级,`0` = 叫分未定。判定动画按符号 + 绝对值两维映射即可,与结算包 `aset.upgrade` 同一套逻辑。
**投降结算**:只有 `aset`,无 `chupai`、无 `bottom` → 结算面板必须容忍**底牌区缺失**,不能因为读不到 `bottom.cards` 就崩或显示空框。
### 1.9 大局总结算 `account` 【图:大局结算.png】
- 仅当打到最后一局(`idx >= asetcount`)时,`jiesuan` 包才带 `account` 分组。
- 界面:图层 303,群组 241。**三条玩家栏用精灵复制生成**(见 §5.7a)。
**版式**(弹窗面板,圆角):
| 区 | 内容 |
| --- | --- |
| 标题栏(青绿底) | `二七王-3人 房号{roomcode} 共{asetcount}局 {开战时间}` + 右上角关闭钮 |
| 玩家栏 ×3(按名次排序,行间分隔线) | 名次徽章 → 头像 → 昵称 / `ID:{playerid}`(两行)→ `基础分{n}` / `总得分{n}`(两行)→ `冲关分{n}` → `傍王分{n}` → 右侧大字总分 |
| 规则栏(浅绿底) | `规则:可查牌、傍王、爬坡算子` —— 据 `roomtype` 位串拼出已启用项 |
| 面板外底部 | `再来一局`(黄) `分享好友`(蓝) `复制战绩`(蓝) `退出房间`(红) |
- **名次徽章**:3 帧(1 金 / 2 橙 / 3 蓝,花瓣形),按总分排名取帧。
- **总分大字**:正分橙色带 `+`、负分蓝色带 `-`,复用 §3.6 的 `NUM_RESULT_WIN` / `NUM_RESULT_LOSE`。
> **四项分数已由 S-6 补齐**:`account` 改为对象数组,含 `grade_jf_total`(基础分)/ `grade_cg_total`(冲关分)/ `grade_bw_total`(傍王分)/ `score`(总分),三项之和恒等于 `score`。
### 1.10 解散结算 `free_room` 【待设计】
**投递方式与 §1.8 完全不同**(协议 §14.1),前端必须单独处理:
```
{ app:"youle", route:"room", rpc:"free_room",
data:{ seats:[], deskfree:{ rpc:"jiesuan", data:{ success, aset, account } } } }
```
- **rpc 是平台的 `free_room`、route 是 `room`**,不是 `erqiwang/jiesuan`;
- 真实取值路径是 **`data.deskfree.data.aset`** 与 `data.deskfree.data.account`(多一层 `data` 包装);
- 外层 `data.success` 由平台填,子游戏的 `success` 在 `data.deskfree.data.success`;
- 解散局 `aset.multiple = 0`、`upgrade = 0`、各家 `grade = 0`,`account` 恒有;
- **`data.deskfree` 可能缺失**(开战后、首局发牌前解散时 `get_disbandRoom` 返回 `null`)→ 前端必须容忍。
界面复用 §1.9 的大局总结算面板。
> **解散的申请/投票/同意拒绝界面由平台全包**(`Layer 420 CheckFree_Layer` + `GameUI.openCheck` + `Desk.self_apply_free_room` / `other_apply_free_room` / `self_agree_free_room` / `self_refuse_free_room` / `agreefree`),子游戏**不做投票界面**,只处理上面这个结算包。
### 1.11 断线重连 `deskinfo`
**不新增任何界面**。重连的本质是「恢复数据 → 调各 UI 组件 set-refresh 恢复界面」,**复用与正常流程完全相同的一条重画路径**(前端 04 §3.3 / 05 §6)。
平台把 `deskinfo` 挂在 `pack.data.deskinfo` 下,按当前 `step` 只带对应分组:
| `step` | 分组 | 恢复到 §1.x |
| --- | --- | --- |
| 1 | `CallRun`(`seat` / `countdown` / `nowcall` / `multiple` / `call[3]`) | §1.3 叫分 |
| 2 | `ChooseMain`(`banker` / `call` / `multiple` / `countdown` / `bottomcards` 仅庄家 / `touxiang`) | §1.5 选主 |
| 3 | `BuryCards`(+ `flower`) | §1.6 埋牌 |
| 5 | `PushCards`(`playproc` / `seatlist` / `liangpai` / `pushlist` / `grade` / `gradecards` / `bottomcards` 仅庄家) | §1.7 出牌 |
| 6 | `Balance`(`readystate` / `aset`) | §1.8 结算 |
另有 `count` / `idx` / `PlayerInfo[3]` / `MyCards` 全阶段通用。
**关键点**:
- `playproc.cards`(当前轮桌面上的牌)**两种查牌模式下都有** —— 否则后出的人无从跟牌;
- `pushlist`(往轮历史)与 `seatlist` **仅可查牌模式有** —— 不查牌模式下前端不得渲染出牌历史;
- 70 分的 3 秒亮底牌**不重放**;
- **验收判据**(前端 05 §6.2):任意时刻丢弃 `this.data`、仅凭最近一次服务端快照重画,界面必须完全一致。做不到即「前端私存了状态」或「服务端漏发了字段」,两者都要修。
### 1.12 准备 `zhunbei`
小局结算后各家点 `下一局` 进入下一局(§1.8),发 `zhunbei { seat }`。收 `zhunbei { seat }` → 该家玩家位显示「已准备」标记——**用平台的准备标识**(§0.2),子游戏不另做资源。
---
## 2. 常驻 UI
跨阶段常驻的部件,图层 101。
### 2.1 顶部信息条 【图:全部】
位置 x≈486–762, y≈4–72。三列表格式。
| 列 | 表头 | 数据 | 数据来源 |
| --- | --- | --- | --- |
| 1 | `主` | 花色图标 | `xuanzhu.flower` / `aset.flower`(1方块 2梅花 3红心 4黑桃);选主前显示占位 |
| 2 | `叫分`(绿字) | 分数 + 蓝色角标 | `shangzhuang.grade` + `multiple` |
| 3 | `抓分`(橙字) | 分数 + 橙色角标 | 分数 = 闲家累计捡分(逐轮 `chupai3.grade` 累加,结算时以 `aset.grade` 为准);角标 = **`Math.abs(curmultiple)`**(该字段带符号,角标只取大小,见 S-8) |
【待确认 T-16】:`抓分` 列的角标在 `等待叫分.png` 里是 `1子`、在 `小局结算.png` 里是 `1倍`,两图不一致。推定为 `aset.upgrade` 判定倍率(仅结算后有意义),需确认。
- 精灵:群组 201,号段 1001–1029。
- 资源:表格底图、花色小图标(4 帧)、角标底(2 种配色)。
### 2.2 玩家位附加标记 【图:全部】
**平台负责头像/昵称/分数**(§0.2),子游戏在其旁叠加:
| 元素 | 数据来源 | 显隐规则 |
| --- | --- | --- |
| **`庄` 印章** | `shangzhuang.banker` / `aset.banker` | 三家各一个精灵,按 `banker` 显隐其一(含底栏自己位) |
| **`主N` 角标**(黄底黑字) | `seatlist[seat][4][0]` 剩余主牌数 | **仅可查牌模式 + 已报无主**;初始 `[-1,-1]` 时隐藏 |
| **`对N` 角标**(白底黑字) | `seatlist[seat][4][1]` 剩余主对数 | 同上 |
| **状态文字区** | 叫分阶段:`60分` 金色 / `不叫` 白色 | 叫分阶段显示,进入选主后清空 |
- 精灵:群组 202(左上)/ 203(右上)/ 204(自己),号段 1100–1149。
### 2.3 已出牌区 【图:出牌2.png】
三家各一个区域,展示本轮打出的牌(资源 `CARD_FACE_M`)。
- 自己:中央偏下
- 右上家(下家):右上,头像内侧
- **左上家(上家):与右上家左右镜像**——排列方式、牌尺寸、标签位置全部参照右上家,只是 x 方向对称、牌的堆叠朝向相反。已确认无需单独设计稿。
- 每张牌可叠加角标:`庄`(橙色斜标)、`大`(大王圆标)等
- 区域左下角:**牌型标签**(`拖拉机` 等,§1.7)
- 三家的布局参数走同一份配置的 `bySeat` 三个变体,见 §6.5
- **每家预置 28 个牌精灵**(= 单次出牌张数上限,甩牌最多可甩满手牌)。号段每家 100 个,容量充裕,因此**不使用动态复制精灵**,避免 `SpriteCopyUtils` 的清理负担。
- 精灵:群组 212 / 213 / 214,号段 1300–1599。
### 2.4 倒计时 【图:多张】
参考图里倒计时数字(`12` / `15` / `20`)出现在**当前操作者的位置旁**:
- 轮到左上家 → 左上家头像右侧(x≈195–270, y≈165–235)
- 轮到自己 → 自己操作区左侧(x≈445–520, y≈345–400)
- 轮到右上家 → 对称位置
因此建 **3 个倒计时精灵**,按当前操作者显隐其一。
- 数据来源:各包的 `countdown`。
- **只做展示,不触发任何操作**(design §11 / 协议 §0.3):倒计时归零后**不自动叫分/选主/埋牌/出牌**,也不判负、不跳过。前端**不得**在归零时做任何乐观界面推进(不跳阶段、不清控制权),一切仍等服务端推送。
- **归零后停在 `0` 继续显示,不隐藏**:协议 §0.3 明确归零不触发任何动作、牌局原地等待该玩家;隐藏会被误读为「已超时/已跳过」。
- 精灵:群组 224,号段 1150–1159。
- **精灵类型(2026-08-27 订正)**:3 个倒计时精灵均为**多帧图数字精灵**(非文字精灵)——玩家视线焦点、视觉权重最高的数字,参考图是带描边/渐变的美术字,文字精灵做不出这个效果。显示走 `SpriteManager.setNumberImage`,资源 `NUM_COUNTDOWN`(§3.6),纯秒数无后缀。见 §5.4 / §6.8。
### 2.5 自己的主牌统计 + 余主公示(他家)【图:多张】
**这是两套不同的东西,别混为一谈**:
| 谁的 | 显示在 | 何时显示 | 数据来源 |
| --- | --- | --- | --- |
| **自己的主牌统计** | 手牌**上方偏左**(x≈35 y≈400 那条) | **有手牌时始终显示**,与报无主无关 | 前端据 `flower` 本地统计自己手牌 |
| **他家的主牌数 / 对数**(**余主公示**) | 左上、右上家**头像下方**的 `主N` `对N` 角标 | **仅可查牌模式 + 已报无主**(design §9) | `seatlist[seat][4]` = `[剩余主牌数, 剩余主对数]` |
**自己的主牌统计条**(群组 206 `ZHU_STAT_BAR`,精灵 `ZHU_STAT_BG` / `ZHU_STAT_TEXT`):
- 文案 **`x对 x主`**(暂定,见 §7.2 T-9),例如 `3对 12主`。
- **始终显示、本地计算**——自己的手牌自己完全可见,不需要服务端下发,也不受查牌模式与报无主影响。
- 参考图里的 `主牌对子: 1对` 即此条。
- **2026-08-27 改名**:本条原名「亮牌条 / `LIANGPAI_*`」,与下方「亮牌」同名异实,已统一改为 `ZHU_STAT_*`(详见 §0.3 群组表下的修订说明)。
**余主公示:他家的 `主N` / `对N` 角标**(群组 202 / 203,结构见 §2.2):
- 一个**图片底 + 一个文字精灵**,两家各一组。
- 初始 `[-1,-1]` 时隐藏;一旦全场有人报无主,服务端整表刷新 `seatlist` 并下发,两家的角标同时出现。
- 不可查牌模式下**永不出现**。
> **「亮牌」(`liangpai`)与上面两者都不是一回事**:它是 design §8.2 的规则——庄家达门槛时向**闲家**亮出自己**全部固定主牌的具体牌面**,用遮罩面板展示(§1.6,群组 **242**,复用 `HIST_*` 精灵),不是这里的统计条。
> 正因两者曾经同名,本条统计条在 2026-08-27 改名为 `ZHU_STAT_*`;代码里凡 `LIANGPAI` 字样自此**只属于**亮牌那一侧(协议字段 `liangpai`、`EQW_Anim.LIANGPAI_AUTO_CLOSE`)。
### 2.6 底栏 【图:全部】
y≈648–720 深色条。左侧头像/昵称/分数由平台渲染(§0.2),子游戏负责:
| 元素 | 位置(实测估值) | 显隐规则 |
| --- | --- | --- |
| `庄` 印章 | x≈338–370 | **自己是庄**时显示 |
| `踩` / `有分` / `没分` 按钮 | x≈335–573(占用同一区域) | **自己是闲家 + 出牌阶段**时显示,与 `庄` 印章互斥(§1.7) |
| 局数 `1/4 局` | 居中 x≈635–700 | 常驻,`asetidx / asetcount` |
**右侧功能按钮共 5 个**,按情况显隐(不是全部常驻):
| 按钮 | 看什么 | 显示条件 |
| --- | --- | --- |
| `上一轮` | **上一轮**所有人出的牌 | 可查牌 + 出牌阶段 + 已打过至少一轮 |
| `出牌历史` | 所有人**出过的全部牌** | 可查牌 + 出牌阶段 |
| `明牌` | 另两家手中全部未出主牌的具体牌面 | 可查牌 + 出牌阶段 + **场上已有人报无主**(`baozhu==1`)→ **三家都显示** |
| **`扣底`** | **埋牌底牌**:庄家埋下的 8 张 | **仅庄家** + 已埋牌之后 |
| **`底牌`**(**查底牌**) | **底牌**:发牌时没发给玩家、留在桌面的 8 张 | 庄家全程;闲家仅**开底**过的局(70 分坐庄)(S-3) |
> ### 术语已统一:按钮名 = 术语名
>
> **本项目采用修订后的术语**(2026-08-25 定,取代 design.md 原表述):
>
> | 新术语 | 指哪 8 张 | design.md 原称 |
> | --- | --- | --- |
> | **底牌** | 发牌时**没有发给玩家**、留在桌面的 8 张 | ~~暗牌~~ |
> | **埋牌底牌** | 庄家埋牌时从手里扣下的 8 张 | ~~底牌~~ |
>
> 改后底栏按钮**名实相符**:`底牌` 按钮看底牌、`扣底` 按钮看埋牌底牌,不再有命名倒挂。
>
> **协议字段也已拆开**(S-4 已完成),前端按字段名取值即可,不必再按所在包判断语义:
>
> | 按钮 | 展示 | 协议字段 |
> | --- | --- | --- |
> | `底牌` | 底牌(发牌留桌 8 张) | **`bottomcards`** —— `shangzhuang`(庄家恒有,闲家仅 70 分时有)、`ChooseMain`/`BuryCards`(仅庄家) |
> | `扣底` | 埋牌底牌(庄家埋的 8 张) | **`burycards`** —— `maipai`、`PushCards`(仅庄家);结算包为 `bottom.cards` |
>
> **实现要求**:前端内部变量仍建议用 `bottomCards` / `buryCards` 区分,与字段名一一对应。已列入验收清单(§7.3)。
- 精灵:群组 205,号段 1040–1069(5 个功能按钮 × 2 精灵 = 10,`扣底` 为 1056/1057,2026-08-27 补齐;另有 3 个提示按钮 × 2 精灵)。
### 2.7 手牌区 【图:全部】
| 形态 | 张数 | 位置(实测估值) |
| --- | --- | --- |
| **单排**(叫分 / 出牌) | 最多 36 | y≈455–645,x≈35–1250,重叠排列 |
| **双排**(庄家埋牌) | 36(17 + 19) | 上排 y≈285–455,下排 y≈455–645 |
- 建 **36 个手牌精灵**(庄家摸底牌后的上限),资源 `CARD_FACE_L`。
- 张数少于 36 时隐藏多余精灵并重算间距。
- 选中态:牌**上浮**(Y 偏移),另可叠加选中标记。
- 牌上标记(`拖` 圆标、⭐、🔵)见 T-8。
- 精灵:群组 210,号段 1200–1249。
### 2.8 状态提示条(通用)【图:等待庄家选主.png / 强甩失败提示.png】
一条**深色半透圆角条 + 居中白字**,用于显示当前在等谁、或一次性的即时反馈。整个牌局复用**同一个**精灵,只换文字与位置。
| 用途 | 文案示例 | 位置(实测估值) |
| --- | --- | --- |
| **等待类**(常驻到状态结束) | `等待庄家选主`、`等待庄家埋牌`、`等待XX叫分` | 桌面中央 x≈500 y≈373 w≈320 h≈47 |
| **即时反馈类**(自动淡出) | `强甩失败` | 桌面中央偏上 x≈462 y≈172 w≈363 h≈50 |
- 两类**共用一套精灵**(`BAR_TOAST` + 文字精灵),位置由布局配置的两个预设切换。
- 文案表进配置,不在代码里写死字符串。
- 精灵:图层 105 群组 230,`1760` 条底 + `1761` 文字。
#### 文案清单(T-33 已定)
**全部 3 条**。
| # | 文案 | 类型 | 谁看到 | 显示区间 |
| --- | --- | --- | --- | --- |
| 1 | **`等待庄家选主`** | 状态类 | 两个闲家 | 收 `shangzhuang` 后 → 收 `xuanzhu` 前(§1.5) |
| 2 | **`等待庄家埋牌`** | 状态类 | 两个闲家 | 收 `xuanzhu` 后 → 收 `maipai` 前(§1.6) |
| 3 | **`强甩失败`** | 即时反馈 | 全场 | 收到 `chupai1.shuaicuo == 1`,自动淡出(§1.7) |
> 前 2 条是「状态类」——常驻到该状态结束、由收包驱动收起;第 3 条是「即时反馈类」——自动淡出。
> 两类**共用同一套精灵**(`BAR_TOAST` + 文字),只切位置预设与文案。
**明确不做的**(已确认):
| 不做 | 理由 |
| --- | --- |
| 叫分等待 | **倒计时已经指明在等谁**——它就显示在当前叫分者的位置旁(§2.4) |
| 出牌等待 | 同上;且一局约 84 次切换,弹条只会吵 |
| 准备等待 | **平台自带已准备 / 未准备标识**(§0.2),不重复造 |
| 玩家掉线 / 重连提示 | **平台已覆盖**(`Layer 615 Reconnect_Layer` 等),子游戏不实现 |
| **开底提示** | **画面本身已经说清楚了**——闲家看到底牌由背面翻成正面,就是开底;再加一行字是多余的 |
文案表进配置,不在代码里写死字符串。
> 原 §5.5 的 `BAR_SHUAICUO`(1754/1759)并入本部件——甩错只是这条通用提示条的一种文案,不必单独出图。
---
## 3. 图片资源总表
> 给美术清点用。**按钮一律只出 1 帧普通态**——点击的缩放/透明反馈由框架统一处理(client 02 §2)。
> 尺寸列为参考图实测估值,出图前请与最终设计稿核对。
### 3.1 牌面(3 套,见 §0.4)
| ID | 键名 | 用途 | 帧数 | 帧说明 | 单帧尺寸 | 整图 |
| --- | --- | --- | --- | --- | --- | --- |
| 501 | `CARD_FACE_L` | 手牌、底牌 | **60** | 见 §0.4 帧表(10×6 通用排版) | 110 × 190 | 1100 × 1140 |
| 502 | `CARD_FACE_M` | 已出牌区 | **60** | 同上 | 90 × 155 | 900 × 930 |
| 503 | `CARD_FACE_S` | 结算埋牌底牌区 | **60** | 同上 | 50 × 70 | 500 × 420 |
### 3.2 花色图标
| ID | 键名 | 用途 | 帧数 | 帧说明 | 尺寸 |
| --- | --- | --- | --- | --- | --- |
| 511 | `SUIT_ICON_S` | 顶部信息条 `主` 列 | 4 | 帧1=方块♦ 帧2=梅花♣ 帧3=红心♥ 帧4=黑桃♠(**按服务端 flower 编号 1–4 排序**) | 30 × 30 |
| ~~512~~ | ~~`SUIT_ICON_L`~~ | **不需要**:选主面板花色图标已并入 `BTN_SUIT_CHOOSE` 帧1–4(2026-08-26,美术改一张图 5 帧出图) | —— | —— | —— |
> ⚠️ 花色图标的帧序按**服务端 flower 编号**(1方块 2梅花 3红心 4黑桃),与牌面资源的花色顺序**不同**。两者互不换算,各自独立。
### 3.3 按钮
| ID | 键名 | 用途 | 帧数 | 帧说明 | 尺寸 |
| --- | --- | --- | --- | --- | --- |
| 531 | `BTN_CALL_SCORE` | 叫分档位按钮底 **+ 右上角角标底(同一张图,不含文字)** | **4** | 帧1=浅黄+蓝角标(70/65/60/55) 帧2=金黄+绿角标(50/45/40) 帧3=橙+深橙角标(35…5) 帧4=**灰**(已叫过/不可选)。**颜色按档位位置固定,只因不可叫变灰** | 74 × 56 |
| 532 | `BTN_NO_CALL` | `不叫` | 1 | 蓝色渐变 | 184 × 57 |
| 533 | `BTN_SUIT_CHOOSE` | **选主 / 投降面板按钮**(花色按钮 + 投降按钮,一张图 5 帧) | **5** | **帧1–4=四花色按钮,每帧已含「按钮底 + 花色图标 + 右上角蓝色角标底」,花色序按服务端 flower 编号(帧1=方块♦ 帧2=梅花♣ 帧3=红心♥ 帧4=黑桃♠);帧5=投降按钮,已含「按钮底 + 投降二字」**(2026-08-26 修订:原为按钮底单帧,花色图标/角标底/投降文字各自单独出精灵;美术侧实际按一张图 5 帧出图,改按此表) | 128 × 55 |
| ~~534~~ | ~~`BTN_SURRENDER`~~ | **不需要**:投降按钮已并入 `BTN_SUIT_CHOOSE` 帧5(2026-08-26) | —— | —— | —— |
| 535 | `BTN_TIP` | `提示` | 1 | 蓝色渐变 | 180 × 63 |
| 536 | `BTN_BURY` | `埋牌 (N/8)` | 2 | 帧1=可提交(金黄) 帧2=置灰(未满 8 张) | 180 × 63 |
| 537 | `BTN_PLAY` | `出牌` | 2 | 帧1=可出 帧2=置灰 | 157 × 55 |
| 538 | `BTN_BOTTOM_FUNC` | 底栏功能钮底(明牌/已出牌/上一轮/底牌共用) | 1 | 深色圆角 | 88 × 30 |
| ~~539~~ | ~~`BTN_READY`~~ | **不需要**:准备标识用平台的(§0.2) | —— | —— |
| 540 | `BTN_TISHI` | 底栏 `踩` / `有分` / `没分` 按钮底(三者共用) | 1 | 深色圆角,同 `BTN_BOTTOM_FUNC` 风格但更窄 | 52 × 30 |
| 541 | `BTN_NEXT_ASET` | 结算 `下一局` | 1 | 金黄渐变 | 180 × 65 |
| 542 | `BTN_CG_CARDS` | 结算 `冲关牌型` | 1 | 蓝色渐变 | 183 × 63 |
| 543 | `BTN_ACCOUNT_AGAIN` | 大局结算 `再来一局` | 1 | 黄色渐变 | 190 × 60 |
| 544 | `BTN_ACCOUNT_SHARE` | 大局结算 `分享好友` | 1 | 蓝色渐变 | 190 × 60 |
| 545 | `BTN_ACCOUNT_COPY` | 大局结算 `复制战绩` | 1 | 蓝色渐变 | 190 × 60 |
| 546 | `BTN_ACCOUNT_EXIT` | 大局结算 `退出房间` | 1 | 红色渐变 | 190 × 60 |
| 547 | `BTN_ACC_CLOSE` | 大局结算标题栏右上角关闭 | 1 | —— | 36 × 36 |
### 3.4 标记类
| ID | 键名 | 用途 | 帧数 | 帧说明 | 尺寸 |
| --- | --- | --- | --- | --- | --- |
| 571 | `MARK_BANKER` | `庄` 红色印章 | 1 | —— | 40 × 40 |
| 572 | `MARK_BANKER_CORNER` | 牌角 `庄` 橙色斜标 | 1 | —— | 28 × 28 |
| 573 | `MARK_JOKER_BIG` | 大王 `大` 圆标 | 2 | 帧1=大王 帧2=小王 | 34 × 34 |
| 574 | `BADGE_ZHU_PAIR` | `主N` / `对N` 角标底 | 2 | 帧1=黄底(主) 帧2=白底(对) | 44 × 20 |
| 575 | `BADGE_MULTIPLE` | 子数/倍数角标底 | 2 | 帧1=蓝底 帧2=橙底 | 34 × 18 |
| 576 | `LABEL_CARDTYPE` | 牌型标签 | **1** | **只做「毙」一种**(前端只展示这一个标签)。数据层仍保留 毙 / 垫 / 混合出牌 三种标识以备扩展,但当前不出图、不显示 | 76 × 24 |
| 577 | `MARK_CARD_GROUP` | 手牌牌型标记 | **3** | **帧1=`拖`(拖拉机)帧2=橙色五角星(正2 / 正7)帧3=蓝色五角星(除正2正7外的其他主牌)** | 26 × 26 |
| 578 | `MARK_SELECTED` | 手牌选中标记 | 1 | 待定(也可只用上浮,不出图) | 待定 |
| 579 | `BADGE_RANK` | 大局结算名次徽章(花瓣形) | **3** | 帧1=第1名(金) 帧2=第2名(橙) 帧3=第3名(蓝) | 44 × 44 |
### 3.5 面板底图
| ID | 键名 | 用途 | 帧数 | 尺寸 |
| --- | --- | --- | --- | --- |
| 601 | `PANEL_TOP_INFO` | 顶部三列信息条底 | 1 | 276 × 68 |
| 602 | `PANEL_CALL_SCORE` | 叫分面板底 | 1 | 575 × 170 |
| 603 | `BAR_HINT_TOP` | `70分坐庄才可投降` 提示条底 | 1 | 345 × 33 |
| 604 | `BAR_CHOOSE_MAIN` | `亮主` 斜角标题条 | 1 | 425 × 50 |
| 605 | `BAR_ZHU_STAT` | 主牌统计条底(半透圆角,§2.5;2026-08-27 由 `BAR_LIANGPAI` 改名,ID 不变) | 1 | 210 × 35 |
| 606 | `PANEL_BOTTOM_CARDS` | 结算底牌区面板底 | 1 | 420 × 148 |
| 607 | `GLOW_RESULT` | 结算分数光晕背景 | 2 | 帧1=赢(橙) 帧2=输(蓝) | 225 × 100 |
| 608 | `BAR_FOOTER` | 底栏深色条 | 1 | 1280 × 72 |
| 609 | `MASK_DIM` | 半透黑遮罩(冲关牌型 / 出牌历史 / 明牌 共用) | 1 | 1280 × 720(纯色可拉伸,出 1 张小图即可) |
| 610 | `PANEL_ACCOUNT` | 大局总结算面板底(圆角浅灰绿) | 1 | 1100 × 540 |
| 611 | `BAR_ACCOUNT_TITLE` | 大局结算标题栏(青绿底,含圆角上沿) | 1 | 1100 × 56 |
| 612 | `BAR_ACCOUNT_RULE` | 大局结算底部规则栏(浅绿底,含圆角下沿) | 1 | 1100 × 62 |
| 613 | `LINE_DIVIDER` | 玩家栏之间的分隔线 | 1 | 1060 × 2 |
### 3.6 数字与符号
> **2026-08-26 修订(重要,出图规范订正)**:数字类资源 631/632/633/635/636 一律出成 **16 帧、固定帧序 `0123456789.+-x/p`** 的多帧图片资源。
> 缘由:平台的 `SpriteManager.setNumberImage(spriteId, text, charWidth)` 就是**为多位美术字数字准备的现成接口**——把整串文本设给**一个**多帧图片精灵,引擎按字符逐帧渲染,并按**字符数 × `charWidth`** 自动调整精灵宽度。所以**一个精灵就能显示一整串数字**:既不用拆十位/个位,也不用 `setFrame` 逐位切(`setFrame` 是"整个精灵显示第 N 帧",只适合状态/花色/牌面这类"选其一")。
> 帧号由字符**直接换算**(`'0'–'9'` → 帧1–10,后缀字 → 帧16),因此帧序**不可重排、不可省略占位**;用不到的帧位也要按顺序留出来,否则帧号对不上、画面串位。
> 三种显示机制的选用判据见前端文档 `02-渲染与UI组件体系.md`;`charWidth`/`charHeight` 等数值在 `EQW_Layout.NUM_STYLE.*`(`LayoutConstants.js`)集中配置,本表只记出图规格。
**固定帧序(所有 NUM_\* 通用)**:
| 帧 | 1–10 | 11 | 12 | 13 | 14 | 15 | 16 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 内容 | `0`–`9` | `.` | `+` | `-` | `x` | `/` | **可变后缀**(该套资源自己的后缀字:`分`/`倍`/`张`) |
| ID | 键名 | 用途 | 帧数 | 第 16 帧(后缀) | 单字符尺寸 | 整图尺寸 |
| --- | --- | --- | --- | --- | --- | --- |
| 631 | `NUM_COUNTDOWN` | 倒计时数字 | **16** | **无后缀**(出空白占位) | 44 × 60 | **704 × 60**(16 × 44) |
| 635 | `NUM_CALL_SCORE` | **叫分档位的分数数字**(深棕描边美术字) | **16** | 【**待美术与规则确认**】档位只显示分数数字;若日后要显示 `70分` 则此帧出 `分` | 30 × 40 | **480 × 40**(16 × 30) |
| 632 | `NUM_RESULT_WIN` | 结算得分数字(赢,橙)——用到帧12 `+`、帧13 `-` | **16** | 【**待美术与规则确认**】结算大字目前只显示数字,也可能是 `分` | 46 × 62 | **736 × 62**(16 × 46) |
| 633 | `NUM_RESULT_LOSE` | 结算得分数字(输,蓝) | **16** | 同 632(含待确认项),仅配色不同 | 46 × 62 | **736 × 62**(16 × 46) |
| 634 | `ANI_JUDGE` | **判定结果动画**(大光 / 小光 / 过庄 / 升N级 / 投降)——帧动画资源,播放参数进 `AnimationConfigs`【待设计 D-8】。**注意:这是帧动画,不适用本节的 16 帧数字规范** | 待定 | —— | 待定 | 待定 |
| 636 | `NUM_MAIN_SUIT_COUNT` | **选主面板花色张数数字**(美术单独出图,字号配色与以上几套不同;**一个花色一个精灵**) | **16** | **`张`**(显示形如 `26张`) | 24 × 32(估值) | **384 × 32**(16 × 24) |
> **出图要求**:16 帧**等宽**(后缀字与数字同宽,因为宽度按"字符数 × 单字宽"算);帧序严格照上表,用不到的帧位(如某些套的 `.` `x` `/`)**仍要占位**。
> **带描边/渐变的美术字用多帧图数字精灵 + `setNumberImage`**(倒计时、结算得分、叫分分数、选主张数),文字精灵做不出这种效果——631–636 都要出图。
> **纯数据型小字用文字精灵**(`SpriteManager.setText` / `setTextWithWidth`):叫分角标的子数(值域 2–15,随爬坡开关变)、`主N`/`对N` 角标、局数、主牌统计条文案等。
> **2026-08-26 新增 636**:选主张数原计划走文字精灵,但美术侧单独出了一套数字图(字号配色不同于既有几套);同日一度定为「十位/个位各一个精灵」,**现已撤销**——`setNumberImage` 一个精灵就能显示 `26张`。
### 3.7 浮层与气泡
| ID | 键名 | 用途 | 帧数 | 帧说明 | 尺寸 |
| --- | --- | --- | --- | --- | --- |
| 651 | `BUBBLE_TISHI` | 踩 / 没分 / 有分 提示气泡 | **6**(3×2) | **帧1=踩·箭头向左 帧2=没分·箭头向左 帧3=有分·箭头向左 帧4=踩·箭头向右 帧5=没分·箭头向右 帧6=有分·箭头向右**。帧序内层是 `tip`(1踩/2没分/3有分)、外层是箭头朝向 → `frame = (arrowRight ? 3 : 0) + tip`。**箭头向左 = 头像在气泡左侧**、箭头向右 = 头像在气泡右侧 | 待定 |
| ~~652~~ | ~~`TXT_BAOZHU`~~ | **不需要**:报无主不做全场提示(D-4),只让他家 `主N`/`对N` 角标出现 | —— | —— | —— |
| 653 | `BAR_TOAST` | **通用状态提示条**底(深色半透圆角,白字居中)——`等待庄家选主` / `强甩失败` 等共用(§2.8) | 1 | 可九宫拉伸,按文案长度变宽 | 363 × 50 |
| 654 | `TXT_GRADE_FLOAT` | 捡分飘字底(`20分` 金色) | 1 | 待定 | 待定 |
| ~~655~~ | ~~`PANEL_BOTTOM_3S`~~ | **不需要**:底牌 3 秒展示复用庄家翻底牌那套 UI(D-2),仅可见范围不同 | —— | —— | —— |
### 3.8 建房选项
| ID | 键名 | 用途 | 帧数 | 帧说明 | 尺寸 |
| --- | --- | --- | --- | --- | --- |
| 681 | `ROOM_OPT_BOX` | 建房**选择框**(单选钮 + 复选框合一) | **4**(2×2) | **帧1=单选·未选中(灰空心环) 帧2=单选·已选中(橙实心环) 帧3=复选·未选中(浅绿空方块) 帧4=复选·已选中**。帧号 = `(useCheckbox ? 3 : 1) + (selected ? 1 : 0)`;「单选(可不选)」用复选帧 3/4 | 22 × 22 |
| 682 | `ROOM_CAT_TITLE` | 建房**类别标题**(一张图多帧) | **3** | 帧1=牌局 帧2=模式 帧3=规则 | 60 × 24 |
| 683 | `ROW_ROOM_OPT` | 选项行底(可选,参考图为无底透明) | 1 | —— | 待定 |
---
## 4. 声音资源总表 【整节待确认 T-20】
design 与协议均**未定义任何音效**。以下是按牌类游戏常规推导的候选清单,**未经确认,不得直接作为出音依据**:
| ID | 键名 | 触发时机 |
| --- | --- | --- |
| 101 | `SND_DEAL` | 发牌 |
| 102 | `SND_CALL_SCORE` | 叫分 |
| 103 | `SND_NO_CALL` | 不叫 |
| 104 | `SND_BE_BANKER` | 上庄 |
| 105 | `SND_CHOOSE_MAIN` | 选主 |
| 106 | `SND_SURRENDER` | 投降 |
| 107 | `SND_BURY` | 埋牌 |
| 108 | `SND_PLAY_CARD` | 出牌 |
| 109 | `SND_SHUAI` | 甩牌 |
| 110 | `SND_SHUAICUO` | 甩错 |
| 111 | `SND_BI` | 毙牌 |
| 112 | `SND_WIN_ROUND` | 赢下一轮 / 捡分 |
| 113 | `SND_BAOZHU` | 报无主 |
| 114 | `SND_TISHI_1/2/3` | 踩 / 没分 / 有分 |
| 115 | `SND_KOUDI` | 扣底 |
| 116 | `SND_RESULT_WIN` | 小局结算 · 赢 |
| 117 | `SND_RESULT_LOSE` | 小局结算 · 输 |
| 118 | `SND_COUNTDOWN_TICK` | 倒计时最后 N 秒 |
| 119 | `SND_BTN_CLICK` | 通用按钮点击 |
**要确认的**:是否需要男/女声区分、是否需要方言语音包、`SND_TISHI` 是否用真人语音、倒计时提示音从第几秒开始。
---
## 5. 精灵结构表
> 格式遵循 `gameabc-framework/templates/GameView_Sprites.template.js`。
> **ID 为规划建议值**,编辑器建成后须逐个回填校准(§0 前言)。
> 每条注明:类型(图片/文字/容器/热区)· 用途 · 资源键 · 帧说明。
### 5.1 Layer 101 · 牌桌常驻
| 群组 | 精灵 ID | 键名 | 类型 | 用途 | 资源 / 帧 |
| --- | --- | --- | --- | --- | --- |
| **201** 顶部信息条 | 1001 | `TOP_INFO_BG` | 图片 | 三列表格底 | `PANEL_TOP_INFO` |
| | 1002 | `TOP_SUIT_ICON` | 图片 | `主` 列花色 | `SUIT_ICON_S` 帧 = flower |
| | 1003 | `TOP_CALL_TEXT` | 文字 | 叫分数值 | —— |
| | 1004 | `TOP_CALL_BADGE_BG` | 图片 | 叫分角标底 | `BADGE_MULTIPLE` 帧1 |
| | 1005 | `TOP_CALL_BADGE_TEXT` | 文字 | `N子` | —— |
| | 1006 | `TOP_GRADE_TEXT` | 文字 | 抓分数值 | —— |
| | 1007 | `TOP_GRADE_BADGE_BG` | 图片 | 抓分角标底 | `BADGE_MULTIPLE` 帧2 |
| | 1008 | `TOP_GRADE_BADGE_TEXT` | 文字 | `N倍`【T-16】 | —— |
| **206** 主牌统计条 | 1030 | `ZHU_STAT_BG` | 图片 | 半透圆角条 | `BAR_ZHU_STAT` |
| | 1031 | `ZHU_STAT_TEXT` | 文字 | 自己主牌统计文案 `x对 x主` | —— |
| **205** 底栏 | 1040 | `FOOTER_BG` | 图片 | 底栏深色条 | `BAR_FOOTER` |
| | 1041 | `FOOTER_ASET_TEXT` | 文字 | `1/4 局` | —— |
| | 1042 | `BTN_MINGPAI` | 图片 | `明牌` | `BTN_BOTTOM_FUNC` |
| | 1043 | `BTN_MINGPAI_TEXT` | 文字 | 按钮文案 | —— |
| | 1044 | `BTN_HISTORY` | 图片 | `已出牌` | `BTN_BOTTOM_FUNC` |
| | 1045 | `BTN_HISTORY_TEXT` | 文字 | —— | —— |
| | 1046 | `BTN_LAST_ROUND` | 图片 | `上一轮` | `BTN_BOTTOM_FUNC` |
| | 1047 | `BTN_LAST_ROUND_TEXT` | 文字 | —— | —— |
| | 1048 | `BTN_BOTTOM_CARDS` | 图片 | `底牌` | `BTN_BOTTOM_FUNC` |
| | 1049 | `BTN_BOTTOM_CARDS_TEXT` | 文字 | —— | —— |
| | 1050 | `BTN_TISHI_CAI` | 图片 | `踩`(闲家,占庄标区) | `BTN_TISHI` |
| | 1051 | `BTN_TISHI_CAI_TEXT` | 文字 | `踩` | —— |
| | 1052 | `BTN_TISHI_HAS` | 图片 | `有分` | `BTN_TISHI` |
| | 1053 | `BTN_TISHI_HAS_TEXT` | 文字 | `有分` | —— |
| | 1054 | `BTN_TISHI_NONE` | 图片 | `没分` | `BTN_TISHI` |
| | 1055 | `BTN_TISHI_NONE_TEXT` | 文字 | `没分` | —— |
| | **1056** | `BTN_BURY_CARDS` | 图片 | **`扣底`**(看**埋牌底牌**,2026-08-27 补齐) | `BTN_BOTTOM_FUNC` |
| | **1057** | `BTN_BURY_CARDS_TEXT` | 文字 | 按钮文案 `扣底` | —— |
| **202** 左上家标记 | 1100 | `P_LEFT_BANKER` | 图片 | `庄` 印章 | `MARK_BANKER` |
| | 1101 | `P_LEFT_ZHU_BG` | 图片 | `主N` 角标底 | `BADGE_ZHU_PAIR` 帧1 |
| | 1102 | `P_LEFT_ZHU_TEXT` | 文字 | 剩余主牌数 | —— |
| | 1103 | `P_LEFT_PAIR_BG` | 图片 | `对N` 角标底 | `BADGE_ZHU_PAIR` 帧2 |
| | 1104 | `P_LEFT_PAIR_TEXT` | 文字 | 剩余主对数 | —— |
| | 1105 | `P_LEFT_STATUS_TEXT` | 文字 | `60分` / `不叫` | —— |
| **203** 右上家标记 | 1110–1115 | `P_RIGHT_*` | | 结构同 202 | |
| **204** 自己标记 | 1120 | `P_SELF_BANKER` | 图片 | 底栏 `庄` 印章 | `MARK_BANKER` |
| | 1121 | `P_SELF_STATUS_TEXT` | 文字 | `50分` / `不叫` | —— |
### 5.2 Layer 102 · 手牌区(群组 210)
| 精灵 ID | 键名 | 类型 | 用途 | 资源 / 帧 |
| --- | --- | --- | --- | --- |
| 1200–1235 | `HAND_CARD_1` … `HAND_CARD_36` | 图片 ×36 | 自己手牌 | `CARD_FACE_L`,帧 = `cardIdToFrame(id)` |
| 2100–2135 | `HAND_MARK_1` … `HAND_MARK_36` | 图片 ×36 | 手牌牌面标记【T-8】,与 `HAND_CARD_1..36` **一一对应**(`HAND_MARK_i` 属于 `HAND_CARD_i`) | `MARK_CARD_GROUP` |
> **2026-08-26 修订**:原表写「牌型**分组**标记 ×8,1236–1243」,与本文 §1.6 的「三种标记互斥、**每张牌**至多一个」相矛盾——按分组理解只需 ≤8 个(一手至多 8 组拖拉机),按每张牌理解则需与手牌等量。
> **裁决按「每张牌」**:§1.6 是行为规格、比本表的措辞更权威,且标记的三种取值(`拖` / 正2正7 / 其他主牌)本就是逐张判定的属性,而非只标在拖拉机组上。实测一手 28 张牌通常带 13 个以上标记、36 张埋牌手更多,8 个远远不够。
> 号段改为 **2100–2135**(1236–1243 段太窄容不下 36 个,且其后 1250 起已被底牌区占用);`1236–1243` 作废、不再分配。前端 `config/Sprites_Cards.js` 已按此落地,`core/CardMark.js` 亦按每张牌返回标记。
### 5.3 Layer 103 · 桌面牌
| 群组 | 精灵 ID | 键名 | 类型 | 用途 | 资源 / 帧 |
| --- | --- | --- | --- | --- | --- |
| **211** 底牌 | 1250–1257 | `BOTTOM_CARD_1..8` | 图片 ×8 | 底牌(发牌留桌 8 张);未揭示时帧 = 55 牌背,70 分公示 / 庄家查看时翻正面 | `CARD_FACE_L` |
| **215** 埋牌底牌 | 1258–1265 | `BURY_CARD_1..8` | 图片 ×8 | 结算时亮出的埋牌底牌 | `CARD_FACE_S` |
| **212** 自己出牌 | 1300–1327 | `PLAY_SELF_1..28` | 图片 ×28 | 本轮打出的牌 | `CARD_FACE_M` |
| | 1328 | `PLAY_SELF_TYPE_BG` | 图片 | 牌型标签 | `LABEL_CARDTYPE` |
| | 1329 | `PLAY_SELF_TYPE_TEXT` | 文字 | 标签文案 | —— |
| **213** 左上家出牌 | 1400–1429 | `PLAY_LEFT_*` | | 结构同 212 | |
| **214** 右上家出牌 | 1500–1529 | `PLAY_RIGHT_*` | | 结构同 212 | |
### 5.4 Layer 104 · 阶段操作区
| 群组 | 精灵 ID | 键名 | 类型 | 用途 | 资源 / 帧 |
| --- | --- | --- | --- | --- | --- |
| **220** 叫分面板 | 1600 | `CALL_PANEL_BG` | 图片 | 面板底 | `PANEL_CALL_SCORE` |
| | 1601 | `CALL_HINT_BG` | 图片 | 提示条底 | `BAR_HINT_TOP` |
| | 1602 | `CALL_HINT_TEXT` | 文字 | `70分坐庄才可投降` | —— |
| | 1603–1616 | `CALL_BTN_1..14` | 图片 ×14 | 档位按钮底(**含角标底**,70…5) | `BTN_CALL_SCORE` 帧1–3 按档位位置固定 / 帧4 灰 |
| | 1617–1630 | `CALL_BTN_NUM_1..14` | **多帧图数字精灵** ×14 | 档位分数(70…5),**一个精灵显示整串**(`setNumberImage`,两位数不拆精灵;2026-08-26,见 §1.3/§3.6) | `NUM_CALL_SCORE`(16 帧)+ `NUM_STYLE.CALL_SCORE.charWidth` |
| | 1631–1644 | `CALL_BTN_BADGE_1..14` | **文字** ×14 | 该档 `multiple` 子数(`2子`…`15子`),随爬坡开关变 | —— |
| | 1645 | `CALL_BTN_NONE` | 图片 | `不叫` | `BTN_NO_CALL` |
| | 1646 | `CALL_BTN_NONE_TEXT` | 文字 | —— | —— |
| **221** 选主面板(2026-08-26 修订:美术改一张图 5 帧出图,花色图标/角标底/投降文字并入按钮帧,见 §1.5/§3.3) | 1650 | `MAIN_TITLE_BG` | 图片 | `亮主` 斜角条 | `BAR_CHOOSE_MAIN` |
| | 1651 | `MAIN_TITLE_TEXT` | 文字 | `亮主` | —— |
| | 1652–1655 | `MAIN_SUIT_BTN_1..4` | 图片 ×4 | 花色按钮(**已含按钮底+花色图标+角标底**),帧=花色序1–4 | `BTN_SUIT_CHOOSE` 帧1–4 |
| | 1668–1671 | `MAIN_SUIT_PAIR_TEXT_1..4` | 文字 ×4 | `N对`,叠在对应按钮角标底之上 | —— |
| | 1674–1677 | `MAIN_SUIT_COUNT_1..4` | **多帧图数字精灵** ×4 | 该花色在庄家手中的**张数**,显示 `26张`(**一个花色一个精灵**,`setNumberImage`;2026-08-26 由十位/个位 8 个精灵改回 4 个,见 §3.6) | `NUM_MAIN_SUIT_COUNT`(16 帧,第16帧=`张`)+ `NUM_STYLE.MAIN_SUIT_COUNT.charWidth` |
| | 1672 | `MAIN_BTN_SURRENDER` | 图片 | `投降`(**已含按钮底+文字**) | `BTN_SUIT_CHOOSE` 帧5 |
| | ~~1656–1659 / 1660–1663 / 1664–1667 / 1673~~ | ~~`MAIN_SUIT_ICON_1..4` / 原 `MAIN_SUIT_COUNT_1..4`(文字精灵)/ `MAIN_SUIT_PAIR_BG_1..4` / `MAIN_BTN_SURRENDER_TEXT`~~ | —— | **不需要**:分别并入按钮帧、或改为上方 1674–1677 的数字精灵,空出的 ID 不复用 | —— |
| | ~~1678–1681~~ | ~~`MAIN_SUIT_COUNT_3/4_TENS/ONES`~~ | —— | **不需要**(2026-08-26):`setNumberImage` 一个精灵就能显示整串,十位/个位拆分作废,空出的 4 个 ID 不复用 | —— |
| **222** 埋牌操作条 | 1700 | `BURY_BTN_TIP` | 图片 | `提示` | `BTN_TIP` |
| | 1701 | `BURY_BTN_TIP_TEXT` | 文字 | —— | —— |
| | 1702 | `BURY_BTN_SUBMIT` | 图片 | `埋牌` | `BTN_BURY` 帧1可提交/帧2灰 |
| | 1703 | `BURY_BTN_SUBMIT_TEXT` | 文字 | `埋牌 (N/8)` | —— |
| **223** 出牌操作条 | 1710 | `PLAY_BTN_SUBMIT` | 图片 | `出牌` | `BTN_PLAY` 帧1可出/帧2灰 |
| | 1711 | `PLAY_BTN_SUBMIT_TEXT` | 文字 | —— | —— |
| | 1712 | `PLAY_BTN_TIP` | 图片 | `提示`(出牌阶段必备) | `BTN_TIP` |
| | 1713 | `PLAY_BTN_TIP_TEXT` | 文字 | 按钮文案 | —— |
| **224** 倒计时(2026-08-27 订正:由文字精灵改为多帧图数字精灵,见 §2.4/§6.8) | 1150 | `CD_LEFT` | **多帧图数字精灵** | 左上家位倒计时 | `NUM_COUNTDOWN`(16 帧)+ `NUM_STYLE.COUNTDOWN.charWidth` |
| | 1151 | `CD_RIGHT` | **多帧图数字精灵** | 右上家位倒计时 | `NUM_COUNTDOWN`(16 帧)+ `NUM_STYLE.COUNTDOWN.charWidth` |
| | 1152 | `CD_SELF` | **多帧图数字精灵** | 自己操作区倒计时 | `NUM_COUNTDOWN`(16 帧)+ `NUM_STYLE.COUNTDOWN.charWidth` |
### 5.5 Layer 105 · 桌面浮层(群组 230)
| 精灵 ID | 键名 | 类型 | 用途 | 资源 / 帧 |
| --- | --- | --- | --- | --- |
| 1750–1752 | `BUBBLE_TISHI_LEFT/RIGHT/SELF` | 图片 ×3 | 踩/没分/有分 气泡(按发送者位置) | `BUBBLE_TISHI` 帧 = tip |
| ~~1753~~ | ~~`TXT_BAOZHU`~~ | —— | **不需要**(D-4) | —— |
| ~~1754 / 1759~~ | ~~`BAR_SHUAICUO`~~ | —— | 并入下方通用状态提示条 | —— |
| 1760 | `BAR_TOAST` | 图片 | 通用状态提示条底(§2.8) | `BAR_TOAST` |
| 1761 | `TXT_TOAST` | 文字 | 提示文案(等待类 / 即时反馈类共用) | —— |
| 1755 | `GRADE_FLOAT_BG` | 图片 | 捡分飘字底 | `TXT_GRADE_FLOAT` |
| 1756 | `GRADE_FLOAT_TEXT` | 文字 | `20分` | —— |
| ~~1757 / 1758~~ | ~~`BOTTOM_3S_*`~~ | —— | **不需要**(D-2:底牌 3 秒展示复用底牌区翻牌,仅可见范围不同) | —— |
### 5.6 Layer 302 · 小局结算(群组 240)
| 精灵 ID | 键名 | 类型 | 用途 | 资源 / 帧 |
| --- | --- | --- | --- | --- |
| 1800 | `RESULT_BOTTOM_PANEL` | 图片 | 底牌区面板底 | `PANEL_BOTTOM_CARDS` |
| 1801 | `RESULT_BOTTOM_TEXT` | 文字 | `叫分{multiple}子 底牌 {grade1}x{bottom.multiple}` | —— |
| 1802–1804 | `RESULT_GLOW_LEFT/RIGHT/SELF` | 图片 ×3 | 三家分数光晕 | `GLOW_RESULT` 帧1赢/帧2输 |
| 1805–1807 | `RESULT_SCORE_LEFT/RIGHT/SELF` | **多帧图数字精灵** ×3(2026-08-27 由文字精灵改,见 §1.9/§6.9) | 三家总分(`aset.seatlist[i].grade`),正分橙带 `+`/负分蓝带 `-`,与 `ACC_TPL_SCORE_NUM`(1914)同一套资源与用法 | `NUM_RESULT_WIN`(赢)/ `NUM_RESULT_LOSE`(输)+ `NUM_STYLE.RESULT_WIN/RESULT_LOSE.charWidth` |
| 1808–1810 | `RESULT_JF_LEFT/RIGHT/SELF` | 文字 ×3 | 捡分子数得分 `grade_jf`【T-13】 | —— |
| 1811–1813 | `RESULT_AW_LEFT/RIGHT/SELF` | 文字 ×3 | 算奖得分 `grade_aw`【T-13】 | —— |
| ~~1814~~ | ~~`RESULT_JUDGE_TEXT`~~ | —— | **不需要**:结算面板不显示判定文字,改由动画呈现(§1.8) | —— |
| 1815 | `RESULT_BTN_CG` | 图片 | `冲关牌型` 按钮 | `BTN_CG_CARDS` |
| 1816 | `RESULT_BTN_CG_TEXT` | 文字 | 按钮文案 | —— |
| 1817 | `RESULT_BTN_NEXT` | 图片 | `下一局` 按钮 | `BTN_NEXT_ASET` |
| 1818 | `RESULT_BTN_NEXT_TEXT` | 文字 | 按钮文案 | —— |
> 底牌 8 张精灵复用 §5.3 群组 215(1258–1265),不在此重复。
### 5.6a 冲关牌型叠加层(Layer 302,群组 **244**,**精灵复制**)
**本层的牌用 `SpriteCopyUtils` 运行时复制,不预置固定张数。** `aset.seatlist[i].cards` 的长度不定(四王 + 长连对链 + 八个 7 + 八个 2 可达 20 余张),预置既可能不够、又在多数局里白占 ID。
**只需在编辑器里建 5 个精灵**(3 个容器 + 1 个牌模板 + 1 个遮罩):
| 精灵 ID | 键名 | 类型 | 用途 | 资源 / 帧 |
| --- | --- | --- | --- | --- |
| 1820 | `CG_MASK` | 图片 | 半透黑遮罩(拉伸满屏,兼作点击关闭热区) | `MASK_DIM` |
| 1821 | `CG_BOX_LEFT` | **容器** | 左上家冲关牌容器(复制的父精灵) | —— |
| 1822 | `CG_BOX_RIGHT` | **容器** | 右上家冲关牌容器 | —— |
| 1823 | `CG_BOX_SELF` | **容器** | 自己冲关牌容器 | —— |
| 1824 | `CG_CARD_TPL` | **模板** | 冲关牌模板(编辑器里放好、默认隐藏,只作样式来源) | `CARD_FACE_L` |
| 1825–1827 | `CG_COUNT_LEFT/RIGHT/SELF` | 文字 ×3 | **奖数**,与该家的冲关牌**并列显示**(取 `aset.seatlist[i].naward`;是否另列 `chongguan`/`wang` 明细见下) | —— |
**生成方式**:
```js
// 每个座位一个容器,tag 用牌在 cards 里的下标(父精灵内唯一即可)
var pos = LayoutSolver.solve(EQW_Layout.CG_CARDS, seatKey, cards.length); // fan 布局,见 §6.7
for (var i = 0; i < cards.length; i++) {
var sid = SpriteCopyUtils.create(boxId, sprites.CG_CARD_TPL, pos[i].x, pos[i].y, i);
SpriteManager.setFrame(sid, cardIdToFrame(cards[i])); // §0.4
}
```
**清理是硬要求**:关闭叠加层 / 组件 `onDestroy` 时必须 `SpriteCopyUtils.removeRange(boxId, 0, cards.length)`,否则复制精灵残留(client 02 §6)。
> `RESULT_BTN_NEXT`(1817/1818)在遮罩之上仍需可见可点 —— 其图层内层级须高于 `CG_MASK`。
#### 预置 vs 精灵复制的选用判据
| 判据 | 预置固定精灵 | 精灵复制 |
| --- | --- | --- |
| 数量 | 有明确且不大的上限 | 上限不定或很大 |
| 常态占用率 | 多数时候都在用 | 多数时候只用极少几个 |
| 逐个控制 | 需要独立 z 序 / 动画 / 命名引用 | 只需批量摆位 + 设帧 |
| 清理成本 | 无 | 必须显式 `remove`,忘了就残留 |
据此,本清单的选用——**全项目只有「冲关牌型」一处用复制,其余一律预置**:
| 区域 | 方式 | 理由 |
| --- | --- | --- |
| **冲关牌型** | **复制** | 张数不定、无小上限,多数局用不满 |
| 自己手牌(36) | 预置 | 上限确定;需逐张选中上浮、独立动画与点击 |
| 已出牌区(28 × 3) | 预置 | 需叠压 z 序与出牌动画,逐张独立控制 |
| 底牌(8)/ 埋牌底牌(8) | 预置 | 固定 8 张 |
| 建房规则选项 | 预置 | 选项集固定(4 组 8 项),数量小 |
> 精灵段 1001–2999 有近 2000 个空位(§0.3),预置的 ID 开销不构成压力;复制只用在「数量真的不定」的地方,避免给每处都背上显式清理的负担。
### 5.7a Layer 303 · 大局总结算(群组 241,**玩家栏用精灵复制**)
三条玩家栏结构完全相同、仅数据不同,用 `SpriteCopyUtils` 复制生成;面板外壳与按钮预置。
| 精灵 ID | 键名 | 类型 | 用途 | 资源 / 帧 |
| --- | --- | --- | --- | --- |
| 1900 | `ACC_PANEL_BG` | 图片 | 面板底(圆角浅灰绿) | `PANEL_ACCOUNT` |
| 1901 | `ACC_TITLE_BAR` | 图片 | 标题栏(青绿底) | `BAR_ACCOUNT_TITLE` |
| 1902 | `ACC_TITLE_TEXT` | 文字 | `二七王-3人 房号… 共…局 …` | —— |
| 1903 | `ACC_BTN_CLOSE` | 图片 | 右上角关闭 | `BTN_ACC_CLOSE` |
| 1904 | `ACC_RULE_BAR` | 图片 | 底部规则栏(浅绿底) | `BAR_ACCOUNT_RULE` |
| 1905 | `ACC_RULE_TEXT` | 文字 | `规则:可查牌、傍王、爬坡算子` | —— |
| **1910** | `ACC_ROW_CONTAINER` | **容器** | 玩家栏的复制父精灵 | —— |
| 1911 | `ACC_TPL_RANK` | **模板** | 名次徽章 | `BADGE_RANK`,帧 = 名次 1–3 |
| 1912 | `ACC_TPL_AVATAR` | **模板** | 头像 | 运行时贴玩家头像 |
| 1913 | `ACC_TPL_TEXT` | **模板** | 通用文字(昵称/ID/各项分数,一行一个) | —— |
| 1914 | `ACC_TPL_SCORE_NUM` | **模板** | 右侧总分大字(数字精灵) | `NUM_RESULT_WIN` / `NUM_RESULT_LOSE` |
| 1915 | `ACC_TPL_DIVIDER` | **模板** | 行间分隔线 | `LINE_DIVIDER` |
| 1920–1927 | `ACC_BTN_*` | 图片×4 + 文字×4 | `再来一局` / `分享好友` / `复制战绩` / `退出房间` | `BTN_ACCOUNT_*` |
**tag 分段**(`SpriteCopyUtils.createTagManager`):`rank 1–20` / `avatar 21–40` / `text 41–140`(每行 6 条文字)/ `score 141–160` / `divider 161–180`。
**清理**:关闭面板时 `removeRange` 清掉各段。
> 解散结算复用本面板(协议 §14.1,解散包也带 `account`),只是 `aset` 各项为 0。
>
> **布局配置见 §6.9a**(2026-08-27 补齐;此前本 View 没有任何布局节点)。
### 5.7b Layer 304 / 305 · 出牌历史与明牌(群组 242 / 243,**牌用精灵复制**)
两者版式相同(遮罩 + 每家一排牌,照 `冲关牌型显示.png`),差别只在数据源与家数,**共用同一套渲染**:
| 精灵 ID | 键名 | 类型 | 用途 | 资源 / 帧 |
| --- | --- | --- | --- | --- |
| 1950 | `HIST_MASK` | 图片 | 半透黑遮罩(兼关闭热区) | `MASK_DIM` |
| 1951–1953 | `HIST_BOX_LEFT/RIGHT/SELF` | **容器** ×3 | 每家一个牌容器 | —— |
| 1954 | `HIST_CARD_TPL` | **模板** | 牌模板 | `CARD_FACE_L` |
| 1955–1957 | `HIST_LABEL_LEFT/RIGHT/SELF` | 文字 ×3 | 座位标签(昵称 / 张数) | —— |
| 2000 | `MING_MASK` | 图片 | 明牌遮罩 | `MASK_DIM` |
| 2001–2002 | `MING_BOX_A/B` | **容器** ×2 | 另两家各一个容器(明牌只看他家) | —— |
| 2003 | `MING_CARD_TPL` | **模板** | 牌模板 | `CARD_FACE_L` |
| 2004–2005 | `MING_LABEL_A/B` | 文字 ×2 | 座位标签 | —— |
- **出牌历史**:三家都显示,数据 = `pushlist` 摊平聚合到各家、按主牌序排序(§1.7 D-7)。
- **上一轮**:同一套精灵,数据只取 `pushlist` 末尾一项。
- **明牌**:只显示另两家,数据 = `mingpai.others[].zhucards`(§1.7 D-6)。
- **亮牌**:只显示一排(庄家的固定主牌),数据 = `liangpai.cards`(§1.6)。复用同一套遮罩 + 容器 + 牌模板,仅数据源与排数不同。
⚠ 这里的「亮牌」是 design §8.2 那件事,**与 §2.5 手牌上方的主牌统计条(群组 206 `ZHU_STAT_*`)无关**——后者 2026-08-27 前也叫「亮牌条」,已改名以消歧义。
> **布局配置见 §6.9c**(2026-08-27 补齐;此前 `HIST_*` / `MING_*` 都没有布局节点)。
>
> 解散投票由平台提供(Layer 420),子游戏无需精灵。
### 5.8 建房规则选项(群组 250,2050–2099,预置)
**3 个类别、4 个选项组、8 个选项**(§1.1)。选项集固定,全部预置。
| 精灵 ID | 键名 | 类型 | 用途 | 资源 / 帧 |
| --- | --- | --- | --- | --- |
| 2050–2052 | `ROOM_CAT_TITLE_1..3` | 图片 ×3 | 类别标题(牌局 / 模式 / 规则) | `ROOM_CAT_TITLE`,帧 = 1 / 2 / 3 |
| 2053–2054 | `ROOM_BOX_ASET_1..2` | 图片 ×2 | 局数选择框(6局 / 12局) | `ROOM_OPT_BOX`,单选帧 1–2 |
| 2055–2056 | `ROOM_TEXT_ASET_1..2` | 文字 ×2 | 局数选项文字 | —— |
| 2057–2058 | `ROOM_BOX_CARD_1..2` | 图片 ×2 | 扣卡选择框(房主 / AA) | `ROOM_OPT_BOX`,单选帧 1–2 |
| 2059–2060 | `ROOM_TEXT_CARD_1..2` | 文字 ×2 | 扣卡选项文字 | —— |
| 2061 | `ROOM_TEXT_CARD_COST` | 文字 | 房卡消耗附注(灰色小字,随局数 × 扣卡联动) | —— |
| 2062–2063 | `ROOM_BOX_PEEK_1..2` | 图片 ×2 | 查牌选择框(可查 / 不查) | `ROOM_OPT_BOX`,单选帧 1–2 |
| 2064–2065 | `ROOM_TEXT_PEEK_1..2` | 文字 ×2 | 查牌选项文字 | —— |
| 2066–2067 | `ROOM_BOX_RULE_1..2` | 图片 ×2 | 附加规则选择框(傍王 / 爬坡) | `ROOM_OPT_BOX`,**复选帧 3–4** |
| 2068–2069 | `ROOM_TEXT_RULE_1..2` | 文字 ×2 | 附加规则选项文字 | —— |
共 20 个精灵。挂在平台创建房间界面所在图层(T-3)。布局配置见 §6.9b。
> 精灵虽预置,**渲染仍由 §1.1 的配置数据驱动**:配置里每个选项声明自己用哪个精灵键、`type`(决定选择框帧号)、`bit`/`value`(决定 `roomtype` 拼串)。渲染代码不认识「傍王」「爬坡」这些具体玩法名。
---
## 6. 布局配置规范与配置清单
> **硬要求:所有布局参数一律外提为配置,业务代码零裸值。**
> 位置、大小、排列方式、间距、对齐、朝向、缩放、文字样式——凡是「摆在哪、多大、怎么排」的参数,
> 全部写进布局配置文件,代码只读配置。这既是工程总则 §6「配置优先于硬编码」,也是 client 02 §2
> 「布局文件是纯数据:无函数、无副作用」的要求。
>
> ⚠️ 本节所有数值均为参考图(1280×720)目视实测估值,误差约 ±5px,**是给美术和编辑器的起点、不是最终值**。
> 设计稿到位后必须整体复核。**但布局器类型(`kind`)与参数结构是设计决定,不随数值变动。**
### 6.0 与框架 `AlignmentUtils` 的对齐
参数命名**直接采用框架 `gameabc-framework/ui/AlignmentUtils.js` 的入参名**,配置对象可原样喂进框架方法,中间零转换:
| 配置的 `kind` | 落到框架的方法 |
| --- | --- |
| `line` | `AlignmentUtils.distribute({direction, anchorX, anchorY, count, itemWidth, itemHeight, spacing, anchor})` |
| `fan` | 先算出 `spacing`,再走同一个 `AlignmentUtils.distribute` |
| `attach`(`corner` 为空) | `AlignmentUtils.alignSpriteToSprite(targetId, spriteId, hAlign, vAlign, offset)` |
| `attach`(`corner` 有值) | `AlignmentUtils.alignSpriteCornerTopLeft/TopRight/BottomLeft/BottomRight(targetId, spriteId, offset)` |
| `grid` | 逐行调 `distributeHorizontally` |
| `point` | 直接 `SpriteManager.setPosition` |
对齐常量沿用框架的 `AlignmentUtils.ALIGN`:水平 `left/center/right`、垂直 `top/middle/bottom`。
> 精灵锚点**恒在左上角、不可更改**(AlignmentUtils 文件头注释),所有对齐都是基于锚点的换算。配置里写的 `anchorX/anchorY` 是**对齐基准点**,不是精灵左上角坐标。
### 6.1 五种布局器(`kind`)
每个部件的配置必须声明 `kind`,`kind` 决定它合法的参数集。
#### `point` — 定点
单个元素放在固定位置。
```js
{ kind: 'point', x: 486, y: 4, w: 276, h: 68 }
// 可选:scale(倍数,业务单位)、opacity(0.0–1.0)
```
#### `line` — 等距排列
数量可变但**间距固定**的一排(底牌 8 张、埋牌底牌 8 张)。
```js
{ kind: 'line', direction: 'horizontal',
anchorX: 655, anchorY: 110, // 对齐基准点
itemWidth: 110, itemHeight: 190,
spacing: -36, // 负值 = 重叠
anchor: 'center' } // left | center | right
```
`count` 由运行时数据给出,不进配置。
**逐项不等宽**:给出可选的 `items` 时逐项取宽,`itemWidth` 失效——用于底栏功能钮组(74/87/93/78)与埋牌操作条(180/60/180)这类宽度不一的排:
```js
{ kind: 'line', direction: 'horizontal',
anchorX: 1250, anchorY: 672, itemHeight: 30, spacing: 15, anchor: 'right',
items: [ {key:'BTN_MINGPAI', width:74}, {key:'BTN_HISTORY', width:87},
{key:'BTN_LAST_ROUND', width:93}, {key:'BTN_BOTTOM_CARDS', width:78} ] }
```
#### `fan` — 自适应压缩排
张数可变、**总宽受限**、需要动态压缩间距的牌排(手牌、出牌区、冲关牌)。
```js
{ kind: 'fan', direction: 'horizontal',
anchorX: 640, anchorY: 455,
maxWidth: 1215, // 可用总宽上限
itemWidth: 110, itemHeight: 190,
spacingMax: 0, // 张数少时的最大间距(0 = 紧贴不重叠)
spacingMin: -84, // 张数多时的最大重叠量(负值)
anchor: 'center',
overlapFrom: 'left' } // left = 右边的牌压在左边的牌之上
```
间距算法(**唯一实现,不散落**):
```
count <= 1 → spacing 不参与,直接按 anchor 摆一张
otherwise → raw = (maxWidth - itemWidth) / (count - 1) - itemWidth
spacing = clamp(raw, spacingMin, spacingMax)
```
`overlapFrom` 决定 z 序方向:`'left'` 时后面的牌盖住前面的(自己手牌、右上家);`'right'` 时反过来(左上家镜像用)。
#### `grid` — 网格
固定行列的按钮阵(叫分 14 档 = 7×2)。
```js
{ kind: 'grid',
anchorX: 640, anchorY: 160,
cols: 7, rows: 2,
itemWidth: 74, itemHeight: 56,
spacingX: 7, spacingY: 24,
anchor: 'center',
fillOrder: 'row' } // row = 先填满一行再换行
```
#### `attach` — 相对贴附
贴在另一个精灵上的从属元素(角标贴按钮、庄标贴头像、气泡贴头像)。
```js
{ kind: 'attach',
target: 'P_LEFT_AVATAR', // 目标精灵的键名,不写 ID
hAlign: 'right', vAlign: 'top',
offsetX: 8, offsetY: -4,
corner: 'topRight' } // 可选;有值 = 贴到目标【外侧】角,无值 = 在目标【内部】对齐
```
> `target` 一律写**键名**而非数字 ID——ID 回填时只改一处映射表,配置文件不用动。
### 6.2 座位变体 `bySeat`
三个显示位上的同名部件共用一份配置,差异写在 `bySeat` 里;未列出的字段继承外层 base。
```js
PLAY_AREA: {
kind: 'fan', direction: 'horizontal', // ← base:三家共同的部分
itemWidth: 90, itemHeight: 155, // (牌尺寸、压缩规则三家一致)
maxWidth: 170, spacingMax: -35, spacingMin: -62,
bySeat: {
SELF: { anchorX: 650, anchorY: 265, anchor: 'center', overlapFrom: 'left' },
RIGHT: { anchorX: 1022, anchorY: 120, anchor: 'center', overlapFrom: 'left' },
LEFT: { anchorX: 258, anchorY: 120, anchor: 'center', overlapFrom: 'right' } // 与 RIGHT 左右镜像
}
}
```
座位键固定为 `SELF` / `LEFT` / `RIGHT`(显示位,不是服务端座位号)。服务端座位 → 显示位的换算见 §0.1,**只在一处实现**。
### 6.3 文字样式 `textStyle`
文字精灵的样式也进配置,不在代码里写字号颜色。
```js
{ fontSize: 24, color: '#FFFFFF', align: 'center', maxWidth: 130, bold: false }
```
### 6.4 牌尺寸档
三套牌面尺寸集中定义,各区域**引用档名**,不重复写数字。
```js
CARD_SIZE: {
L: { w: 110, h: 190, res: 'CARD_FACE_L' }, // 手牌、底牌、冲关牌
M: { w: 90, h: 155, res: 'CARD_FACE_M' }, // 已出牌区
S: { w: 50, h: 70, res: 'CARD_FACE_S' } // 结算埋牌底牌
}
```
改牌面尺寸只动这一处,所有引用它的区域自动跟随。
---
### 6.5 常驻区配置
| 部件 | kind | 关键参数(实测估值) |
| --- | --- | --- |
| 顶部信息条底 | `point` | `x:486 y:4 w:276 h:68` |
| 顶部 · 花色图标 | `attach` | `target:TOP_INFO_BG, hAlign:left, vAlign:bottom, offsetX:20, offsetY:-8` |
| 顶部 · 叫分角标 | `attach` | `target:TOP_CALL_TEXT, corner:topRight, offsetX:4, offsetY:6` |
| 顶部 · 抓分角标 | `attach` | `target:TOP_GRADE_TEXT, corner:topRight, offsetX:4, offsetY:6` |
| 主牌统计条(节点 `ZHU_STAT_BG`) | `point` | `x:35 y:400 w:210 h:35` |
| 底栏条 | `point` | `x:0 y:648 w:1280 h:72` |
| 底栏 · 局数文字 | `point` | `x:635 y:672 w:65 h:26`,`textStyle{fontSize:20, align:center}` |
| 底栏 · 功能钮组(明牌/已出牌/上一轮/**扣底**/底牌) | `line` + `items` | `direction:horizontal, anchorX:1250, anchorY:672, itemHeight:30, spacing:15, anchor:right`,`items:[74,87,93,78†,78]`(逐项不等宽,见 §6.1)。†`扣底` 的 78 是估值(2026-08-27 补项,本表原只列 4 项)——参考图(等待叫分.png)底栏当时也只画出 `底牌` 一项;依据取自 `底牌` 实测的 78,不能笼统说「两字按钮 = 78」:同为两字的 `明牌` 实测只有 74,两者不同,待设计稿复核 |
| 底栏 · 庄标 | `point` | `x:338 y:668 w:32 h:34` |
| 底栏 · 提示按钮组(踩/有分/没分) | `line` | `direction:horizontal, anchorX:335, anchorY:672, itemWidth:52, itemHeight:30, spacing:26, anchor:left` |
| 右侧语音钮(平台) | `point` | `x:1210 y:310 w:56 h:56` |
| 右侧聊天钮(平台) | `point` | `x:1210 y:385 w:56 h:56` |
### 6.6 玩家位配置(含气泡朝向)
平台负责头像/昵称/分数,子游戏的附加标记全部 `attach` 到平台头像框上——这样平台布局一调,标记自动跟随。
| 部件 | kind | 参数 |
| --- | --- | --- |
| 头像框(平台) | `point` | `bySeat{ LEFT:{x:22,y:122}, RIGHT:{x:1160,y:122}, SELF:{x:22,y:645} }`,`w:91 h:118`(SELF 为 84×53)。**需同步写进 `Game_Modify.PLAYER_INFO_LAYOUT`**(§6.11) |
| `庄` 印章 | `attach` | `bySeat{ LEFT:{corner:'topRight',offsetX:7,offsetY:8}, RIGHT:{corner:'topLeft',offsetX:-40,offsetY:8}, SELF:{point x:338 y:668} }` |
| `主N` 角标 | `attach` | `target:头像框, corner:bottomLeft, offsetX:0, offsetY:6, w:40 h:19` |
| `对N` 角标 | `attach` | `target:主N角标, hAlign:right, vAlign:middle, offsetX:44, offsetY:0` |
| 状态文字(`60分`/`不叫`) | `point` | `bySeat{ LEFT:{x:170,y:165}, RIGHT:{x:980,y:165} }`,`w:130 h:70`,`textStyle{fontSize:46, align:center}` |
| 倒计时(多帧图数字精灵,2026-08-27 订正) | `point` | `bySeat{ LEFT:{x:195,y:165}, RIGHT:{x:1010,y:165}, SELF:{x:445,y:345} }`,`h:NUM_STYLE.COUNTDOWN.charHeight(60)`;`w` 运行时注入(1–2 位数字,`setNumberImage` 按字符数 × `charWidth` 自动改宽) |
**提示气泡的朝向与位置**(资源 `BUBBLE_TISHI` 6 帧,§3.7):
| 气泡属于 | 头像在气泡的 | 用帧 | 贴附方式 |
| --- | --- | --- | --- |
| 左上家 | **左**侧 | 箭头向左(帧 1–3) | `attach{ target:LEFT头像框, hAlign:right, vAlign:middle, offsetX:+10 }` |
| 右上家 | **右**侧 | 箭头向右(帧 4–6) | `attach{ target:RIGHT头像框, hAlign:left, vAlign:middle, offsetX:-10 }`(气泡向左展开) |
| 自己 | **左**侧 | 箭头向左(帧 1–3) | `attach{ target:SELF头像, hAlign:right, vAlign:top, offsetX:+10, offsetY:-70 }` |
帧号计算:`frame = (arrowRight ? 3 : 0) + tip`(`tip`:1踩 / 2没分 / 3有分)。
朝向本身也应是**配置项**(`arrowRight: true|false`)而非代码里按座位硬判。
> 自己发的提示,本人**看不到自己的气泡**(协议 §13.7 不回执,且只转发给对家)。表格里列出 SELF 的气泡配置,是为了「另一闲家看到我的气泡」时——那时在**对方屏幕上**我处于 LEFT 或 RIGHT 位。SELF 位的气泡实际不会出现【待确认 T-26:是否需要给自己一个本地回显】。
### 6.7 牌区配置
| 区域 | kind | 参数(实测估值) |
| --- | --- | --- |
| **自己手牌 · 单排** | `fan` | `anchorX:640 anchorY:455 maxWidth:1215 size:L spacingMax:0 spacingMin:-78 anchor:center overlapFrom:left`。36 张时算出 `spacing = 1215−110)/35 − 110 ≈ −78`,即每张露 32px,与参考图实测 33px 吻合 |
| **自己手牌 · 双排(埋牌)** | `fan` ×2 | 上排 `anchorX:644 anchorY:285 maxWidth:928 size:L spacingMin:-65`;下排 `anchorX:644 anchorY:455 maxWidth:928 size:L spacingMin:-65`。**分行策略:上排 `floor(n/2)`、下排 `ceil(n/2)`**(36 张 = 18/18)。参考图的 17/19 属填充画法、不作依据(§0.5) |
| 手牌 · 选中上浮 | —— | `selectedOffsetY: -40`(配置项,不写死) |
| **底牌区** | `line` | `anchorX:655 anchorY:110 size:L spacing:-36 anchor:center`(8 张) |
| **埋牌底牌区(结算)** | `line` | `anchorX:640 anchorY:163 size:S spacing:2 anchor:center`(8 张) |
| **已出牌区** | `fan` | 见 §6.2 的 `bySeat` 示例(三家共用 base,LEFT 镜像) |
| 已出牌 · 牌型标签 | `attach` | `target:该区首张牌, corner:bottomLeft, offsetX:0, offsetY:-24` |
| 已出牌 · 牌角 `庄`/`大` 标 | `attach` | `target:对应牌, corner:topRight, offsetX:-28, offsetY:0` |
| **冲关牌(叠加层)** | `fan` | `size:L spacingMax:-40 spacingMin:-88 maxWidth:420`;`bySeat{ LEFT:{anchorX:260,anchorY:120}, RIGHT:{anchorX:1035,anchorY:120}, SELF:{anchorX:640,anchorY:400} }`。**精灵复制**(§5.6a):求解结果直接作为 `SpriteCopyUtils.create` 的 `x/y`(相对容器)。`spacingMin` 放宽到 `-88`(即最窄只露 22px)以容纳 20+ 张的极端情况 |
### 6.8 阶段操作区配置
> **2026-08-26 修订(其一)**:选主面板花色图标、对数角标底已并入按钮帧(见 §1.5/§3.3),对应布局节点删除。
> **2026-08-26 修订(其二)**:数字统一走 `SpriteManager.setNumberImage`(§3.6)后,一个多位数只占一个精灵——花色张数的「十位 / 个位」两个节点**合并为一个** `MAIN_SUIT_COUNT`。
> 两个数字节点的 `h` 恒为单字高,直接引用 `EQW_Layout.NUM_STYLE.*.charHeight`(SSOT:数值只在 `LayoutConstants.js` 定义一处,布局文件不再手抄);`w` 则**留到运行时注入**——`setNumberImage` 会按「字符数 × `charWidth`」自动改精灵宽,位数变宽度就变。
| 部件 | kind | 参数(实测估值) |
| --- | --- | --- |
| 叫分 · 面板底 | `point` | `x:355 y:140 w:575 h:170` |
| 叫分 · 提示条 | `point` | `x:470 y:100 w:345 h:33` |
| **叫分 · 14 档按钮** | `grid` | `anchorX:640 anchorY:160 cols:7 rows:2 itemWidth:74 itemHeight:56 spacingX:7 spacingY:24 anchor:center fillOrder:row` |
| 叫分 · 档位分数数字 | `attach` | `target:对应档位按钮, hAlign:center, vAlign:middle, offsetY:4, h:NUM_STYLE.CALL_SCORE.charHeight(40)`;`w` 运行时注入(= 字符数 × 30,由 `setNumberImage` 自动设) |
| 叫分 · 档位子数角标 | `attach` | `target:对应档位按钮, corner:topRight, offsetX:-30, offsetY:2` |
| 叫分 · `不叫` 按钮 | `point` | `x:553 y:345 w:184 h:57` |
| 叫分 · 自己叫分大字 | `point` | `x:570 y:320 w:140 h:70`,`textStyle{fontSize:52, align:center}` |
| 选主 · 标题斜角条 | `point` | `x:330 y:315 w:425 h:50` |
| **选主 · 4 花色 + 投降** | `line` | `direction:horizontal anchorX:330 anchorY:373 itemWidth:128 itemHeight:55 spacing:9 anchor:left`(共 5 项;投降不显示时只排 4 项,`anchor:left` 保证前 4 个位置不变) |
| 选主 · 张数 | `attach` | `target:对应花色按钮, hAlign:center, vAlign:bottom, offsetY:-6, h:NUM_STYLE.MAIN_SUIT_COUNT.charHeight(32)`;`w` 运行时注入(`9张`=2 字符、`26张`=3 字符,宽度随之变)。一个花色一个精灵 |
| 选主 · 对数角标文字 | `attach` | `target:对应花色按钮, corner:topRight, offsetX:-36, offsetY:2`(按钮帧内已含角标底,本节点只定位叠加文字) |
| 埋牌 · 操作条 | `line` + `items` | `direction:horizontal anchorX:640 anchorY:165 itemHeight:63 spacing:45 anchor:center`,`items:[180,60,180]`(提示 / 倒计时 / 埋牌) |
| 出牌 · 操作条 | `line` + `items` | `direction:horizontal anchorX:446 anchorY:345 itemHeight:55 spacing:35 anchor:left`,`items:[75,157,180‡]`(倒计时 / 出牌 / **提示**)。‡`提示` 的 180 是估值(2026-08-27 补项,本表原漏了 §1.7 明确要求的 `提示` 按钮)——与埋牌条的 `提示` 同资源 `BTN_TIP`(§3.3 实测 180×63),埋牌条 `items` 首项亦取 180,故沿用;顺序照 §1.7 行文。⚠ 本条 `itemHeight:55` 对齐的是 `BTN_PLAY`(157×55),而 `BTN_TIP` 原生高 63,高度差待设计稿裁定。**2026-08-27 再订正**:补「提示」项后若沿用 `anchor:center`,整排重新居中会把已有的倒计时/出牌两项相对原先(仅 2 项时)左移 107.5px,导致「出牌」按钮偏离参考图 `出牌.png` 的实测位置(556–711)。改为 `anchor:left, anchorX:446`(= 参考图出牌按钮实测起点 556 − 倒计时宽 75 − 间距 35),求解结果:倒计时 x=446、出牌 x=556(与实测吻合)、提示 x=748 |
| 捡分飘字 | `point` | `x:570 y:180 w:170 h:55`;另配 `floatRise:-60`、`floatDuration:800`(动画参数也进配置) |
| 状态提示条 · 等待类 | `point` | `x:500 y:373 w:320 h:47`(§2.8) |
| 状态提示条 · 即时反馈类 | `point` | `x:462 y:172 w:363 h:50`;另配 `toastDuration:1500` |
**多帧图数字样式配置 `EQW_Layout.NUM_STYLE`**(2026-08-26 新增,在 `LayoutConstants.js`,与 `CARD_SIZE`/`TEXT_STYLE` 并列)——`setNumberImage` 所需数值的**唯一出处**,业务代码不许内联:
| 键 | `res`(资源键名) | `charWidth` | `charHeight` | `suffix`(第16帧后缀) | 典型显示 |
| --- | --- | --- | --- | --- | --- |
| `COUNTDOWN` | `NUM_COUNTDOWN` | 44 | 60 | `''`(无) | `15` |
| `RESULT_WIN` | `NUM_RESULT_WIN` | 46 | 62 | `''`(第16帧待确认) | `+120` |
| `RESULT_LOSE` | `NUM_RESULT_LOSE` | 46 | 62 | `''`(同上) | `-120` |
| `CALL_SCORE` | `NUM_CALL_SCORE` | 30 | 40 | `''`(第16帧待确认) | `70` |
| `MAIN_SUIT_COUNT` | `NUM_MAIN_SUIT_COUNT` | 24 | 32 | `张` | `26张` |
> - `charWidth` 是 `setNumberImage` 的入参,**精灵宽度 = 字符数 × `charWidth`**(该接口自动设),所以布局节点的 `w` 是运行时值、不写死在配置里。
> - `charHeight` 供布局节点定 `h`(`setNumberImage` 不改高度)。
> - `suffix` 只能是 `''` / `分` / `倍` / `张`——接口内部把这三个字统一编码到第 16 帧,写别的字不会被识别。
> - `res` 存的是**资源键名**而非 ID(与 `CARD_SIZE.res` 同一约定):布局文件是纯数据、不引用其他模块,调用方用 `EQW_Images[style.res]` 取真实 ID。
### 6.9 结算区配置
| 部件 | kind | 参数(实测估值) |
| --- | --- | --- |
| 底牌面板底 | `point` | `x:430 y:140 w:420 h:148` |
| 底牌说明文字 | `attach` | `target:底牌面板底, hAlign:center, vAlign:bottom, offsetY:-14` |
| **三家分数光晕** | `point` | `bySeat{ LEFT:{x:175,y:150}, RIGHT:{x:865,y:150}, SELF:{x:490,y:340} }`,`w:225 h:100` |
| 三家总分大字(多帧图数字精灵,2026-08-27 由文字精灵改,见 §1.9/§5.6) | `attach` | `target:对应光晕, hAlign:center, vAlign:middle`,`h:NUM_STYLE.RESULT_WIN.charHeight(62)`(与 `RESULT_LOSE` 相同);`w` 运行时注入 |
| 三家 `牌局分` | `attach` | `target:对应光晕, hAlign:left, vAlign:bottom, offsetX:-15, offsetY:24` |
| 三家 `冲关分` | `attach` | `target:对应光晕, hAlign:right, vAlign:bottom, offsetX:15, offsetY:24` |
| `冲关牌型` 按钮 | `point` | `x:535 y:515 w:183 h:63` |
| `下一局` 按钮 | `point` | `x:835 y:518 w:180 h:65` |
| 冲关遮罩 | `point` | `x:0 y:0 w:1280 h:720`,`opacity:0.72`(透明度也是配置项) |
#### 6.9a 大局总结算配置(§1.9 / §5.7a,2026-08-27 补齐)
> 本表原先**整块缺失**——`Layout_Result.js` 里一个 `ACC_*` 节点都没有,实现时极易在渲染代码里就地硬编码坐标(违反 §6 零裸值)。
> **坐标全部是 `uiref/大局结算.png` 的目视实测估值(±5px)**,`w`/`h` 取 §3.5 / §3.3 的资源尺寸(那是权威值)。**待设计稿复核**。
> **玩家栏是精灵复制**(§5.7a),故不逐行写死,而是给「容器 + 行节点(行高即 `itemHeight`)+ 行内模板贴附到行矩形」;行数由数据给出(`ctx.count`),不进配置。
> ⚠ `ACC_ROW` 及贴附其上的节点求出的坐标都是**相对 `ACC_ROW_CONTAINER` 的**——复制精灵是容器的子精灵,`x/y` 本就相对容器,求解结果可直接喂 `SpriteCopyUtils.create`(与 §5.6a 冲关牌同一用法)。
| 部件 | 节点名 | kind | 参数(目视实测估值) |
| --- | --- | --- | --- |
| 面板底 | `ACC_PANEL_BG` | `point` | `x:80 y:88 w:1100 h:540` |
| 标题栏 | `ACC_TITLE_BAR` | `attach` | `target:ACC_PANEL_BG, corner:topLeft, offset(0,0), w:1100 h:56` |
| 标题文字 | `ACC_TITLE_TEXT` | `attach` | `target:ACC_TITLE_BAR, hAlign:left, vAlign:middle, offsetX:30`;`w/h` 运行时注入(文案长度不定) |
| 关闭按钮 | `ACC_BTN_CLOSE` | `attach` | `target:ACC_TITLE_BAR, corner:topRight, offset(-20,10), w:36 h:36` |
| 规则栏 | `ACC_RULE_BAR` | `attach` | `target:ACC_PANEL_BG, corner:bottomLeft, offset(0,0), w:1100 h:62` |
| 规则文字 | `ACC_RULE_TEXT` | `attach` | `target:ACC_RULE_BAR, hAlign:left, vAlign:middle, offsetX:30`;`w/h` 运行时注入 |
| 玩家栏**容器** | `ACC_ROW_CONTAINER` | `point` | `x:80 y:144 w:1100 h:422`(标题栏下沿 → 规则栏上沿) |
| 玩家栏**行** | `ACC_ROW` | `line` | `direction:vertical, anchorX:0, anchorY:14, itemWidth:1100, **itemHeight:132(行高)**, spacing:0, anchor:top`(相对容器;行数 = `ctx.count`) |
| 行内 · 名次徽章 | `ACC_TPL_RANK` | `attach` | `target:ACC_ROW, hAlign:left, vAlign:middle, offsetX:28, w:44 h:44` |
| 行内 · 头像 | `ACC_TPL_AVATAR` | `attach` | `target:ACC_ROW, hAlign:left, vAlign:middle, offsetX:92, w:80 h:80` |
| 行内 · 文字栅格(6 条文字的槽位) | `ACC_ROW_TEXT_GRID` | `grid` | `anchorX:192, cols:4, rows:2, itemWidth:176, itemHeight:34, spacingX:0, spacingY:10, anchor:left, fillOrder:row`;`anchorY` 运行时注入(随所在行浮动)。列序:昵称/`ID:` · 基础分/总得分 · 冲关分 · 傍王分(第 2 行仅前两列有文字)。**估值精度**:本节点是等距列模型(相对容器 192/368/544/720),参考图目视实测列 x 约 272/455/634/800(列距 183/179/166,并不等距),偏差约 10px,比本节头部通用的「±5px」更大,待设计稿复核 |
| 行内 · 右侧总分大字 | `ACC_TPL_SCORE_NUM` | `attach` | `target:ACC_ROW, hAlign:right, vAlign:middle, offsetX:-25, h:NUM_STYLE.RESULT_WIN.charHeight(62)`;`w` 运行时注入 |
| 行内 · 分隔线 | `ACC_TPL_DIVIDER` | `attach` | `target:ACC_ROW, hAlign:center, vAlign:bottom, offset(0,0), w:1060 h:2`(`vAlign:bottom` 的语义是"目标底边之下",见 §6.0) |
| 面板**外**底部 4 按钮 | `ACC_FUNC_BUTTONS` | `line` | `direction:horizontal, anchorX:345, anchorY:640, itemWidth:190, itemHeight:60, spacing:25, anchor:left`(参考图里这排不居中于屏幕、整体偏右,故用 `anchor:left` 定起点) |
#### 6.9b 建房规则选项配置(§1.1)
布局是**两层嵌套**:类别竖排,类别内的选项组横排。
| 部件 | kind | 参数(按参考图比例估,实际以平台建房界面尺寸为准) |
| --- | --- | --- |
| 类别(竖排) | `line` | `direction:vertical, anchorX:0, anchorY:0, itemHeight:32, spacing:8, anchor:left`;`itemHeight` 为**单行**类别高,折行类别按实际行数累加 |
| 类别标题 | `attach` | `target:所在类别行, hAlign:left, vAlign:middle, offsetX:0`;`ROOM_CAT_TITLE` 帧 = 类别序号 |
| 类别内选项槽(横排) | `line` | `direction:horizontal, anchorX:70, itemWidth:150, spacing:0, anchor:left`(`itemWidth` = 一个「选择框 + 文字」的槽宽) |
| 选项组之间的额外间隔 | —— | `groupGap: 40`(同类别下两个选项组之间比组内选项多留的间距,配置项) |
| 选择框 | `attach` | `target:所在选项槽, hAlign:left, vAlign:middle` |
| 选项文字 | `attach` | `target:同槽的选择框, hAlign:right, vAlign:middle, offsetX:8` |
| 房卡附注 | `attach` | `target:扣卡组最后一个选项, hAlign:right, vAlign:middle, offsetX:20`,`textStyle{fontSize:16, color:'#999999'}` |
| 折行 | `grid` | 选项数超过 `maxPerRow` 时改用 `grid{cols:maxPerRow, spacingX:0, spacingY:8, fillOrder:'row'}`;`maxPerRow: 4`(配置项,参考图「玩法」行即 4 列折 2 行)。二七王每组最多 2 项,不会触发 |
**文字选中态也是配置**(单选选中时文字变橙):
```js
ROOM_OPT_TEXT: {
normal: { fontSize: 18, color: '#DDDDDD' },
selected: { fontSize: 18, color: '#E8873A' }
},
ROOM_CAT_TITLE_STYLE: { fontSize: 18, color: '#7FBFB0', bold: true, align: 'right', maxWidth: 60 }
```
不要在代码里写死 `'#E8873A'`。
#### 6.9c 出牌历史 / 亮牌 · 明牌配置(§5.7b,2026-08-27 补齐)
> 本表同样原先**整块缺失**(`HIST_*` / `MING_*` 一个节点都没有)。
> 两者与冲关牌型叠加层是**同一种版式**(半透黑遮罩 + 每家一排重叠大牌,参考图同为 `uiref/冲关牌型显示.png`),故照 §6.7 的 `CHONGGUAN_FAN` 同构给出,三家锚点沿用冲关牌的 `LEFT 260,120 / RIGHT 1035,120 / SELF 640,400`。
> **与冲关牌的唯一差别是张数上限**:出牌历史摊平后可达 28 张以上,`maxWidth:420` 装不下。
> **2026-08-27 订正**:原估值 `maxWidth:500` 的推导算错了——`fan` 的间距公式是 `spacing = max(spacingMin, min(spacingMax, (maxWidth-itemWidth)/(count-1) - itemWidth))`,代入 `maxWidth 500 / itemWidth 110 / count 28` 得 `(500-110)/27-110 ≈ -95.56`,**没有**触到 `spacingMin:-96` 的下限;而 `fan` 的总宽恒等于 `maxWidth`(只要 spacing 未被下限截断),并不是旧注释算的「488」。`RIGHT` 座位锚点 `anchorX:1035` 配合总宽 500、`anchor:center`,整排右边缘落在 `1035+250=1285`,**超出 1280 设计画布 5px**(`LEFT`/`SELF` 因锚点更靠左未暴露)。
> **2026-08-27 再订正(裁定 maxWidth:480)**:曾先收窄到 `maxWidth:490` 让 `RIGHT` 恰好贴住 1280,但零余量太脆弱(这些锚点本身是参考图目视实测、±5px 误差),且 490 时 `spacingMin:-96` 永远不触底、是个死参数,与「每张最窄只露 14px」这条约束名存实亡;旧注释原写的「每张露 14px、总宽 488」正是 `spacingMin` 触底后的结果,说明 488 才是当初想要的值,`maxWidth:500` 是与注释不符的笔误。改为 `maxWidth:480`:代入公式 `(480-110)/27-110 ≈ -96.30`,**触及** `spacingMin:-96` 下限,实际 spacing 截断为 `-96`(不再等于 raw),总宽 `= 28×110 + 27×(-96) = 488`(不再等于 `maxWidth`,因为 spacing 已被下限截断)。`spacingMin:-96` 就此真正生效、不再是死参数;每张露 `110-96=14px`,与旧注释精确一致。求解验证(count=28):`LEFT` 首尾 x = `16 → 504`,`RIGHT` 首尾 x = `791 → 1279`,`SELF` 首尾 x = `396 → 884`,均落在 0–1280 内且有余量(`RIGHT` 距边界 1px、`LEFT`/`SELF` 余量更大)。`maxWidth:480` 与 `spacingMin:-96` 仍是**估值,待设计稿复核**。
> **已知限制**:每张仅露 14px、视觉偏窄,待设计稿复核是否改为分两排显示,或改用 `CARD_SIZE.M`——这是估值阶段接受的限制,留给后续复核。
> 牌用精灵复制:三个容器精灵摆在原点、尺寸铺满屏,故 `fan` 求出的屏幕坐标可直接当容器内坐标用。
| 部件 | 节点名 | kind | 参数 |
| --- | --- | --- | --- |
| 出牌历史 · 遮罩 | `HIST_MASK` | `point` | `x:0 y:0 w:1280 h:720, opacity:0.72`(【亮牌】用途下**不注册点击**,见 §1.6) |
| 出牌历史 · 三个牌容器 | `HIST_BOX` | `point` | `x:0 y:0 w:1280 h:720`(同摆原点、铺满屏) |
| 出牌历史 · 每家一排牌 | `HIST_FAN` | `fan` | `size:L, anchor:center, maxWidth:480, spacingMax:-40, spacingMin:-96`;`bySeat{ LEFT:{260,120}, RIGHT:{1035,120}, SELF:{640,400} }`。求解验证(count=28,spacing 触底 -96、总宽 488):`LEFT` 首尾 x = `16 → 504`,`RIGHT` 首尾 x = `791 → 1279`,`SELF` 首尾 x = `396 → 884`,均落在 0–1280 内且有余量 |
| 出牌历史 · 座位标签 | `HIST_LABEL` | `point` | `bySeat{ LEFT:{200,318}, RIGHT:{975,318}, SELF:{580,598} }`;`w/h` 运行时注入 |
| 明牌 · 遮罩 | `MING_MASK` | `point` | 同 `HIST_MASK` |
| 明牌 · 两个牌容器 | `MING_BOX` | `point` | 同 `HIST_BOX` |
| 明牌 · 每家一排牌 | `MING_FAN` | `fan` | 同 `HIST_FAN`,但 **`bySeat` 只有 LEFT / RIGHT**(明牌只看他家,没有自己那排;精灵 `MING_BOX_A` 落 LEFT 位、`MING_BOX_B` 落 RIGHT 位) |
| 明牌 · 座位标签 | `MING_LABEL` | `point` | `bySeat{ LEFT:{200,318}, RIGHT:{975,318} }`;`w/h` 运行时注入 |
### 6.10 配置文件组织
按 client 02 §2 的三件套拆分,布局部分建议再按界面拆文件,用保护性声明合并:
```
codes/config/
LayoutConstants.js // var EQW_Layout = EQW_Layout || {}; —— 总则:CARD_SIZE、textStyle 预设、座位键
Layout_Table.js // 常驻区(§6.5)+ 玩家位(§6.6)
Layout_Cards.js // 牌区(§6.7)
Layout_Action.js // 阶段操作区(§6.8)
Layout_Result.js // 结算区(§6.9)
Layout_CreateRoom.js // 建房规则选项(§6.9b)
ImageResources.js // 图片资源 ID(§3)
SoundResources.js // 声音资源 ID(§4)
Sprites_*.js // 精灵结构(§5)
```
每个文件顶部 `var EQW_Layout = EQW_Layout || {};`,加载后合并为一份(client 02 §2「多文件用保护性声明」)。
**布局文件是纯数据**:不得出现函数、不得引用其他模块、不得有副作用。`fan` 的间距算法、`bySeat` 的合并、`attach` 的换算都属于**布局求解器**的职责,放在 `codes/ui/LayoutSolver.js`(或直接调 `AlignmentUtils`),不写进配置文件。
### 6.11 与平台布局配置的衔接
平台的玩家信息与气泡布局在 `01_SubGame_modify.js` 的配置区(该文件**只允许在配置区填值**):
- `Game_Modify.PLAYER_INFO_LAYOUT.POSITIONS` —— 平台默认 ind0 南 `(6,592)`、ind1 东 `(1186,85)`、ind3 西 `(7,59)`,与 §6.6 实测值不一致,**须改为 §6.6 的值**。
- `Game_Modify.BUBBLE_LAYOUT.POSITIONS` —— 平台聊天/语音气泡锚点,同样按三人局位置调整。
> 这两处是**平台契约配置**,与子游戏自己的 `EQW_Layout` 是两份东西,但**坐标必须一致**(否则平台画的头像和子游戏贴的庄标会错位)。建议:`EQW_Layout` 里的头像框坐标为权威,`01_SubGame_modify.js` 配置区手工抄写并在注释里注明「与 EQW_Layout.PLAYER.AVATAR 保持一致」【T-28:转发壳配置区不能引用 codes,只能手工同步,需在验收清单里加一条一致性检查】。
---
## 7. 待补充清单
> **状态汇总**(2026-08-26):待确认规格 34 项**只剩 T-20 音效**(已明确后补);设计稿只剩 **D-8**(判定结果**动画**)。
> **§7.5 服务端待补项 8 条全部处理完毕**。**服务端侧无遗留**——D-8 只差动画设计稿与触发细节。
### 7.1 待出设计稿的界面
**只剩 D-8 一项**(已定稿的保留在表内备查)。
| 编号 | 界面 | 状态 |
| --- | --- | --- |
| **D-8** | **判定结果动画**(大光 / 小光 / 过庄 / 升N级 / 投降) | ⬜ 已定形态:**不做结算面板上的静态文字,改为动画**,在**对局过程中**与**结算前**播放(§1.8)。缺动画设计稿与触发细节 |
| ~~D-1~~ | 建房规则选项区 | ✅ 定稿:版式照 `创建房间选项…png`,内容按 §1.1 的 3 类别 / 4 选项组。**子游戏只做「类别标签 + 选项」内容区**,弹窗背景 / 标题 / 关闭 / 确认按钮均由平台提供 |
| ~~D-2~~ | 底牌 3 秒展示 | ✅ 定稿:**复用庄家翻开底牌那套 UI**(`叫分确定庄.png`),**不要倒计时**。非 70 分只有庄家能看、70 分全场能看——同一套界面,只差可见范围 |
| ~~D-3~~ | 甩错提示 | ✅ 定稿:桌面中央深色半透条 **`强甩失败`**(`强甩失败提示.png`),**不做收回动画**,直接落下那张被强制打出的最小主牌 |
| ~~D-4~~ | 报无主 | ✅ 定稿:**不做全场提示**。只让他家 `主N`/`对N` 角标出现;自己的主牌统计条本来就常显(§2.5) |
| ~~D-5~~ | 踩 / 没分 / 有分 | ✅ 定稿:底栏三按钮(占庄标区)+ 3×2 六帧气泡 |
| ~~D-6~~ | 明牌面板 | ✅ 定稿:版式照 `冲关牌型显示.png`(遮罩 + 每家一排牌)。按钮条件**按 design 手册**:可查牌 + 已有任一玩家报无主 → 三家都显示、可随时查看 |
| ~~D-7~~ | 出牌历史面板 | ✅ 定稿:版式同上。**不按轮次**,按玩家聚合、用主牌序排序。前端摊平 `pushlist` 即可,无需改服务端 |
| ~~D-9~~ | 大局总结算 | ✅ 定稿:`大局结算.png`,玩家栏用精灵复制(§5.7a)。**四项分数协议缺** → 见 **S-6** |
| ~~D-10~~ | 解散结算 | ✅ 复用 D-9 面板;投票界面平台全包(Layer 420) |
| ~~D-11~~ | 准备标识 | ✅ 用平台的 |
| ~~D-12~~ | 左上家已出牌区 | ✅ 与右上家左右镜像 |
| ~~D-13~~ | `扣底` 查看面板 | ✅ 与 `底牌` 按钮复用同一个「8 张牌」展示面板,仅数据源不同 |
### 7.2 待确认的规格问题
**仍未决(1 项)**
| 编号 | 问题 | 影响 |
| --- | --- | --- |
| **T-20** | **整套音效**:清单、是否需要语音包、是否分男女声、倒计时提示音从第几秒起 | §4 全节;已明确「等待后续补充」 |
**已定论(33 项)**
| 编号 | 结论 |
| --- | --- |
| ~~T-9~~ | ~~主牌统计条文案(原称"亮牌条")~~ | **已定**:自己的主牌统计条显示 `x对 x主`、有手牌时常显(§2.5)。注意这条**不是** design §8.2 的亮牌——2026-08-27 已把该条改名为 `ZHU_STAT_*` 以杜绝混用 |
| ~~T-1~~ | `class.paiju.js:264` `get_nextseat = (seat+1)%3`,下家显示在右上(§0.1) |
| ~~T-2~~ | 解散投票**平台全包**(Layer 420 + `GameUI.openCheck` + `Desk.*_free_room`),子游戏只处理结算包 |
| ~~T-3~~ | 建房界面 = **Layer 27 `CreateRoom_Layer`**(Layer 28 是代开房/抽水,无关) |
| ~~T-4~~ | **需要发牌动画**:从右往左铺开(§1.2) |
| ~~T-5~~ | 见下方「T-5 手牌排序」小节 |
| ~~T-6~~ | 叫分按钮**按档位位置固定三档配色**(浅黄/金黄/橙)+ 灰=已叫过;按钮底与角标底各 4 帧(§1.3) |
| ~~T-7~~ | 选主按钮中央大数字 = 该花色**张数**,右上角标 = 该花色**对数**(§1.5) |
| ~~T-8~~ | 手牌标记 3 帧:`拖`=拖拉机 / 橙星=**正2正7** / 蓝星=**其他主牌**;前端据 `flower` 本地标注(§1.6) |
| ~~T-10~~ | 出牌阶段**必有** `提示` 按钮;服务端已下发 `mustcard` 供自动选中(S-2 ✅) |
| ~~T-11~~ | 牌型标签**只做「毙」一种**;数据层保留 毙/垫/混合出牌 三种标识备扩展,当前不出图 |
| ~~T-12~~ | `底牌 20x0` 的 `x` 后面是**扣底倍数**(`bottom.multiple`);未扣底显示 0(§1.8) |
| ~~T-13~~ | 结算两个小字 = `牌局分`(`grade_jf`) / `冲关分`(`grade_aw`) |
| ~~T-14~~ | 冲关明细走独立的**冲关牌型叠加层** |
| ~~T-15~~ | 准备标识**用平台的** |
| ~~T-16~~ | `抓分` 角标 = **当前捡分对应判定倍率的实时预览**;服务端已下发 `curmultiple`(S-1 ✅) |
| ~~T-17~~ | 倒计时归零**停在 `0`** 不隐藏 |
| ~~T-18~~ | 底栏 5 按钮:`上一轮`/`出牌历史`/`明牌`/`扣底`(看**埋牌底牌**)/`底牌`(看**底牌**);术语已统一见 §2.6;可见性按手册 → **S-3** |
| ~~T-19~~ | 数字**一律用图片多帧精灵**(参考图数字带描边渐变,文字精灵做不出),资源 631–634 全部要出 |
| ~~T-21~~ | 手牌单排 `spacingMin:-78`、双排 `-65`(§6.7) |
| ~~T-22~~ | 冲关牌型叠加层:**全屏半透明遮罩本身即关闭热区,点击遮罩关闭**,不另设关闭钮 |
| ~~T-23~~ | 叠加层**需要并列显示奖数**(以 `naward` 为主) |
| ~~T-24~~ | 冲关牌用**精灵复制**,张数不设上限 |
| ~~T-25~~ | `line` 支持可选 `items` 逐项不等宽 |
| ~~T-26~~ | 自己发的提示**需要本地回显**(受控例外,理由见 §1.7) |
| ~~T-27~~ | 埋牌双排 `floor(n/2)` / `ceil(n/2)` |
| ~~T-28~~ | `EQW_Layout` 为权威,配置区手工同步 + 验收检查 |
| ~~T-29~~ | 建房整体位置尺寸随 Layer 27 定,编辑器建好后回填 |
| ~~T-30~~ | ~~亮牌没有界面位置~~ | **已定**:亮牌下发庄家全部固定主牌的**具体牌面**,用与明牌相同的遮罩版式展示;时机见 T-34(§1.6) |
| ~~T-31~~ | ~~大局结算「基础分」与「总得分」两行的对应~~ | **已定**:参考图数值是填充假数据、不作依据(§0.5)。取 `grade_jf_total`(基础分)与 `score`(总分),见 S-6 |
| ~~T-32~~ | ~~`等待庄家选主.png` 里底牌仍摊着与 design §4 顺序不符~~ | **已定**:叠画示意(§0.5)。实际按 design §4 的先后:开底 3 秒 → 摸底入手 → 选主 |
| ~~T-34~~ | ~~亮牌的展示时机~~ | **已定**:**埋牌后、出牌前**一次性弹出(收 `maipai` 包时),点遮罩关闭 + 自动关闭时长兜底。见 §1.6 |
| ~~T-33~~ | ~~等待提示条文案~~ | **已定**:全部 3 条(`等待庄家选主` / `等待庄家埋牌` / `强甩失败`)。叫分·出牌·准备·掉线·开底五处不做——倒计时、平台标识、翻牌画面本身已表达清楚。见 §2.8 |
#### T-5 手牌排序(已定)
前端排序**对齐服务端主牌序**(design §3),不另造一套——服务端 `class.arith.js` 的 `order_cards(mainflower, cards)` 即权威顺序(从大到小),下发的 `pushlist` 等字段也已按此排好。
- **选主前**(叫分阶段):`flower` 未定,只有固定主牌(双王 + 全部 2、7)是主牌,其余按花色分组。
- **选主后**:`flower` 确定,正 2 / 正 7 升格、主花色普通牌并入主牌段,**必须整体重排**。重排动画暂不做(先直接重画),后续可加。
- 主牌段落在**左侧**——排序从大到小、主牌最大,自然靠左。
### 7.3 验收检查清单
编码完成后必须逐条核对(每条都对应一处已知易错点):
| # | 检查项 | 依据 |
| --- | --- | --- |
| 1 | `cardIdToFrame` 通过 §0.4 的 10 条校验样例 | 花色段与 id 段序相反,最易静默画错牌 |
| 2 | 三家显示位映射正确:下家 `(mySeat+1)%3` 在右上 | §0.1 |
| 3 | 提示按钮顺序(踩/有分/没分)与 `tip` 编号(1踩/2没分/3有分)的映射未按下标硬推 | §1.7 |
| 4 | 气泡帧号 = `(箭头向右?3:0)+tip`,朝向按头像相对位置取 | §1.7 / §6.6 |
| 5 | 不可查牌模式下:`明牌`/`已出牌`/`上一轮` 按钮不出现、**亮牌面板不弹出**(design §8.2 那个遮罩面板,群组 242)、`主N`/`对N` 角标不出现。⚠ §2.5 **自己的主牌统计条(群组 206)不在此列——它任何模式下都常显** | design §9 / §2.5 |
| 6 | 投降结算(无 `bottom`)与解散结算(无 `deskfree` 的情形)都不崩、不显示空框 | §1.8 / §1.10 |
| 7 | 解散包从 `data.deskfree.data.aset` 取值,不是 `data.aset` | §1.10 |
| 8 | 倒计时归零不做任何界面推进 | 协议 §0.3 |
| 9 | 冲关牌型叠加层关闭时 `SpriteCopyUtils.removeRange` 已清理 | §5.6a |
| 10 | `EQW_Layout` 与 `Game_Modify.PLAYER_INFO_LAYOUT` 的头像框坐标一致 | §6.11 |
| 11 | 丢弃 `this.data`、仅凭最近一次服务端快照重画,界面完全一致 | 前端 05 §6.2 |
| 12 | 所有布局参数都在配置文件里,业务代码无裸值 | §6 |
| 13 | **底牌 / 埋牌底牌未写反**:`底牌` 按钮读 `bottomcards`、`扣底` 按钮读 `burycards`;内部变量 `bottomCards`/`buryCards` 一一对应 | §2.6 |
| 14 | 叫分按钮配色按**档位位置**取(非按子数),已叫过的档位置灰 | §1.3 |
| 15 | 自己发的提示本地回显后,收到失败回包时能撤掉 | §1.7 |
| 16 | 自己的主牌统计条**有手牌时常显**、不受查牌模式与报无主影响;他家 `主N`/`对N` 角标只在「可查牌 + 已报无主」时出现 | §2.5 |
| 17 | 出牌历史**按玩家聚合 + 主牌序排序**,不是按轮次分组 | §1.7 D-7 |
| 18 | `明牌` 按钮在「场上**有人**报无主」时**三家都显示**(含报无主那家自己),不是只给自己无主的那家 | §1.7 D-6 |
### 7.4 开发前置条件
在本清单的资源补齐前,前端**不具备开发条件**的部分:
1. **美术出图** → §3 全部资源(尤其 3 套牌面 × 60 帧)
2. **编辑器建资源与精灵** → §5 全部 ID 回填本文
3. **D-1 建房界面** → 影响 `roomtype` 拼串与进入牌局的第一步
不受阻、可先行的部分:
- `client/js/01_SubGame/codes/` 目录结构与 `SubGameHooks.js` 骨架
- `cardIdToFrame` 转换与其单元测试(§0.4 的 10 条校验样例可直接作为用例)
- 收包分发骨架(一 rpc 一处理器)与 `data.success` 判定
- **布局配置骨架与布局求解器**(§6)——`kind` 五型的参数结构与求解逻辑是设计决定、不依赖具体数值,可先写好并用占位坐标跑通,等设计稿到位只改配置值
- `shared/` 同步机制
### 7.5 服务端待补项(需改服务端才能实现的前端需求)
以下 3 条来自本轮确认的需求,**当前协议/服务端不支持**,需在服务端补齐后前端才能实现。三条都要同步更新 `packet_protocol.md`。
#### S-1 · 「当前抓分的倍数」字段 `curmultiple` —— ✅ 已完成(2026-08-25)
**语义**:当前捡分对应判定倍率的**实时预览**——「若此刻结束是几倍」,取绝对值。
| 当前捡分 `grade` 相对叫分 `call` | 判定 | `curmultiple` |
| --- | --- | --- |
| `grade == 0` | 庄家:大光 | **3** |
| `0 < grade < Q` | 庄家:小光 | **2** |
| `Q ≤ grade < call` | 庄家:过庄 | **1** |
| `grade ≥ call` | 闲家:升 N 级 | **N** |
(`Q` = 小光/过庄分界兼升级级距,常规算子固定 40、爬坡按叫分分段,design §7.2.0 / §7.3.2。)
**下发位置**(随现有包,**不另开推送**):`shangzhuang`、`chupai1`、`chupai2`、`chupai3`、`deskinfo.PushCards`。
**要点**:
- 三家**同值**,整表下发不涉及泄露(`grade` 本就公开)。
- 与结算包 `aset.upgrade` **同源同算法**(`get_qvalue` + `get_upgrade`),差别只在这里**不含扣底**——扣底要到末轮打完才产生。
- 叫分未定时为 **0**,前端不显示角标。
- 前端**不得自算**(服务端权威)。
**实现**:`class.paiju.js` 新增 `get_curmultiple()`;`mod.js` / `class.export.js` 组包处下发。
#### S-2 · 下发「应当选中的牌」`mustcard` —— ✅ 已完成(2026-08-25)
**语义**:下一个出牌者本轮跟牌的**必出牌**(design §5.2),前端收到后自动选中(上浮),玩家确认后点 `出牌`。
**下发位置**:`chupai1` / `chupai2`(给 `nextseat`)、`deskinfo.PushCards`(给轮到的那一家)。
**可见性(关键)**:**只发给 `nextseat` 一家**。它是该玩家自己手牌的子集,发给他本人不泄露;**整表下发会把另两家的手牌结构透露出去**(server 红线:发全 ≠ 发多,按可见性下发)。已纳入 `test_leak.js` 的 `CARD_FIELDS` 审计白名单。
**不下发的情形**:
1. 非出牌阶段;
2. 该座位是本轮**首家**(自由出牌,无必出);
3. 首家是**甩牌**——甩牌跟牌走 `flush_follow_ok` 的逐分量匹配(design §5.4.4),与 `get_followcard` 不是同一条路径,给建议会给错,宁可不发;
4. 算出的必出牌为空(玩家可自由选)。
**实现**:`class.paiju.js` 新增 `get_mustcard(seat)`,内部复用**与出牌校验完全相同**的 `get_followcard`,故建议与校验结果天然一致。
**不违反「请求包只带意图」**:方向是服务端 → 前端的建议,前端仍只回传 `cards`,合法性最终由服务端校验。
#### S-3 · 底牌的可见性:按手册办(对应 T-18)—— 已定
**结论:按 design 手册**,即前述方案 (a)。底栏 `底牌` 按钮只在**能合法看到底牌**时可用:
| 谁 | 何时可用 |
| --- | --- |
| 庄家 | **全程可用**(他本来就摸过这 8 张) |
| 闲家 | **仅 70 分坐庄局**可用(这 8 张已向全场公示过 3 秒);其余局**按钮不显示** |
依据 design §4:非 70 分坐庄时这 8 张**只有庄家可见**;协议也一致(`shangzhuang.bottomcards` 闲家仅 70 分时才有,重连包的同名字段标注「只有庄家有此属性」)。
**服务端无需改动**——现有下发面正好支持这条规则:闲家在非 70 分局本来就收不到数据,按钮不显示即可,不存在「拿到了但不渲染」的泄露风险(服务端红线:下发即泄露)。
#### S-4 · 拆分 `bottomcards` 字段名 —— ✅ 已完成(2026-08-25)
两批牌原先共用 `bottomcards`、只靠所在包区分语义,现已拆分:
| 字段名 | 含义 | 出现在 |
| --- | --- | --- |
| **`bottomcards`** | **底牌**(发牌留桌 8 张) | `shangzhuang`、`deskinfo.ChooseMain`、`deskinfo.BuryCards` |
| **`burycards`** | **埋牌底牌**(庄家埋下 8 张) | `maipai`、`deskinfo.PushCards` |
| `bottom.cards` | **埋牌底牌** | 结算包 `bottom` 分组(分组名已表明是抠底相关,未改名) |
**服务端改动**:`mod.js`(`maipai` 组包)、`class.export.js`(`deskinfo.PushCards`),另同步了两处的注释术语。
**测试**:`test/test_leak.js` 的 `CARD_FIELDS` 白名单加入 `burycards`(否则新字段会静默脱离下发面审计);`test/test_rpc.js` 新增 11 条正反用例锁死拆分结果。
**协议文档**:`packet_protocol.md` §0.0 与相关字段表已更新。
**前端取值**:直接按字段名区分即可,不再需要「按所在包判断语义」:
```js
// 底牌(发牌留桌 8 张)——底栏「底牌」按钮
data.bottomcards // shangzhuang / ChooseMain / BuryCards
// 埋牌底牌(庄家埋下 8 张)——底栏「扣底」按钮
data.burycards // maipai / PushCards
data.bottom.cards // 结算包
```
#### S-5 · 同步 design.md 与 packet_protocol.md 的术语 —— ✅ 已完成(2026-08-25)
术语变更(暗牌→**底牌**、底牌→**埋牌底牌**)已在各处生效:
| 文档 / 代码 | 状态 |
| --- | --- |
| `docs_dev/二七王-UI资源与精灵清单.md`(本文) | ✅ 全文新术语 |
| `server/games/erqiwang/docs/design/design.md` | ✅ 全文 38 处替换;术语表后补「术语变更记录」说明改名缘由 |
| `server/games/erqiwang/docs/protocol/packet_protocol.md` | ✅ 新增 §0.0 术语小节;字段说明同步;字段名随 S-4 拆分 |
| `mod.js` / `class.export.js` / `test/test_leak.js` 注释 | ✅ 随 S-4 一并更新 |
| `docs/compliance/*.md`(3 份,24 处) | ⬜ **有意不改**——这三份是历次核对的**历史记录**,反映当时的 design 表述;下轮核对按新 design 重做即可,不追溯修改 |
| `test/*.js` 其余注释(约 14 处) | ⬜ 仅注释、不影响行为,后续顺手清理 |
#### S-6 · 大局结算的三项分解 —— ✅ 已完成(2026-08-26)
**问题**:大局结算面板要显示 `基础分` / `冲关分` / `傍王分`,而原 `account` 只有「累积得分 + 每局得分数组」。其中**傍王分尤其拆不出来**——`N = 冲关奖数 + 傍王王数` 两个来源在**求和之后**才代入 `grade_aw = X×(2Ni−Nj−Nk)`,事后无法反推各自占比。
**解法**:在算钱时就拆。把 `N` 分成 `N_冲关`(只计庄家)与 `N_傍王`(勾选后庄闲都算),**分别代入同一公式**各算一次。公式对 `N` 线性,故恒有 `grade_cg + grade_bw == grade_aw`——这条恒等式正好当回归断言。
**小局包 `aset.seatlist[i]` 新增**:
| 字段 | 说明 |
| --- | --- |
| `grade_cg` | 算奖得分里的**冲关**分量(不含傍王) |
| `grade_bw` | 算奖得分里的**傍王**分量(未勾傍王时恒 0) |
**大局包 `account` 结构改了**(原为二元数组,现为对象数组):
```json
[{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 }, …]
```
| 面板栏位 | 取值 |
| --- | --- |
| 基础分 | `grade_jf_total` |
| 冲关分 | `grade_cg_total` |
| 傍王分 | `grade_bw_total` |
| 右侧总分大字 | `score` |
恒等式:`grade_jf_total + grade_cg_total + grade_bw_total == score`。
**顺带解决了小局结算「冲关分」标签的不严谨**(见 §0.0):该标签现在应改取 `grade_cg` 而非 `grade_aw`,开傍王时也名副其实了。
> 面板上「基础分」取 `grade_jf_total`、「总得分」取 `score`。参考图里这两行数值相同、且与右侧大字对不上,是填充假数据所致,不作依据(§0.5)。
#### S-7 · 明牌按钮的显示条件 —— ✅ 已定论:按手册,服务端无需改动(2026-08-26)
**结论:按 design §9.3 手册办**——**任一玩家报无主后,三家都显示 `明牌` 按钮,且可随时查看。**
| 项 | 取值 |
| --- | --- |
| 显示条件 | 可查牌模式 **+** 出牌阶段 **+** `baozhu == 1`(**任一**玩家主牌出空) |
| 谁能看 | **全体三人**,包括刚刚报无主的那位自己 |
| 次数 | **不限**,随时可点开、可关闭 |
**服务端无需改动**——`mingpai` 现有的受理条件 `have_baofu()`(任一玩家报无主)**正好就是这条规则**,与 design §9.3 一致。此前我把前端条件收紧成「自己是无主玩家」,比手册严格,反而与服务端校验对不上;现已改回。
> 之前记录的「有主牌的玩家伪造请求也能拿到明牌数据」这一泄露担忧**不成立**:按手册他本来就有权查看,服务端受理是正确行为,不是校验缺失。
#### S-8 · `curmultiple` 改为带符号 —— ✅ 已完成(2026-08-26)
**问题**:S-1 实现时取了绝对值,而判定的**正负号恰恰是「谁赢」的区分**——`3` 大光与 `-3` 升3级、`2` 小光与 `-2` 升2级、`1` 过庄与 `-1` 升1级,取绝对值后三对完全撞在一起,D-8 的过程中动画无从分辨该播哪个。
**改法**:`get_curmultiple` 直接返回 `get_upgrade` 的原值,**与结算包 `aset.upgrade` 完全同口径**:
| 值 | 含义 |
| --- | --- |
| `3` / `2` / `1` | 庄家 **大光** / **小光** / **过庄** |
| `-N` | 闲家**升 N 级** |
| `0` | 叫分未定 |
**前端用法**:
- 顶部「抓分」角标只关心倍数大小 → `Math.abs(curmultiple)`(§2.1)。
- **判定动画**靠符号分辨该播哪个(§1.8 D-8),一套映射通吃「过程中」与「结算前」两条路径。
**测试**(hint 33 → 46 checks):新增两组——一组直接钉住三对「绝对值相同、判定相反」的组合可区分;另一组**直接调 `get_curmultiple` 本身**覆盖负数分支。
> 后一组是补上的**测试盲点**:原有断言里,算法层的 `cm()` 走的是 `A.get_upgrade`、**绕过了 `get_curmultiple`**,而端到端那局恰好是大光(正数),取不取绝对值结果一样。第一次反向验证(把实现退回取绝对值)竟然全绿,正是因为这个盲点。补齐后再验,4 条断言立刻失败。
---
## 附:本文与其他文档的关系
| 文档 | 关系 |
| --- | --- |
| `server/games/erqiwang/docs/design/design.md` | **玩法规则权威源**。本文引用其条款,不重新定义规则;术语「底牌 / 埋牌底牌」已同步(见 §0.0) |
| `server/games/erqiwang/docs/protocol/packet_protocol.md` | **协议字段权威源**。本文的「渲染什么」全部溯源到具体字段 |
| `docs/client/development-guide/` | **前端规范权威源**。ID 范围、组件范式、红线均以其为准 |
| `docs_dev/uiref/*.png` | 参考图,16 张(牌桌态 + 叫分按钮面板 + 大局结算 + 强甩失败 + 等待庄家选主 + 1 张建房**样式范式**,末者内容属他游戏、不可照抄) |
| `client/js/gameabc-framework/ui/AlignmentUtils.js` | **布局求解的框架能力**。§6 的配置参数名与其入参一一对应,配置可原样喂入 |
| `client/js/gameabc-framework/ui/SpriteCopyUtils.js` | 精灵复制。用于冲关牌型、大局结算玩家栏、出牌历史 / 明牌(§5.6a 判据表) |
| `client/js/gameabc-framework/templates/*.template.js` | 常量文件写法蓝本。本清单落地为常量文件时按其格式 |