From 5ac445d08d855ce5a7e64345cbd33faa15ed3e31 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 26 Aug 2026 20:44:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E4=B8=83=E7=8E=8B=EF=BC=9A=E7=B2=BE?= =?UTF-8?q?=E7=81=B5=E5=B8=B8=E9=87=8F=E6=94=B9=E4=B8=BA=E4=BB=A5=20UI=20V?= =?UTF-8?q?iew=20=E4=B8=BA=E7=BB=84=E7=BB=87=E5=8D=95=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layer/Group 扁平声明在 View 定义下,精灵只写 id、所属关系写在注释里; 一个 View 对应一个 UI 组件,拿到 View 即拿到它的全部精灵与图层群组。 SpriteIndex 的展平逻辑随之调整:保留键 Layer*/Group* 跳过,其余键即精灵; 新增「值非数字即报错」——两者结构上同级、只靠键名区分,写错前缀会静默变成假精灵。 Co-Authored-By: Claude Opus 5 (1M context) --- .../2026-08-26-二七王前端A-地基与逻辑层.md | 149 +++++++++++------- ...6-08-26-二七王前端A-地基与逻辑层-design.md | 14 +- 2 files changed, 101 insertions(+), 62 deletions(-) diff --git a/docs/superpowers/plans/2026-08-26-二七王前端A-地基与逻辑层.md b/docs/superpowers/plans/2026-08-26-二七王前端A-地基与逻辑层.md index 5c042b0..0f65438 100644 --- a/docs/superpowers/plans/2026-08-26-二七王前端A-地基与逻辑层.md +++ b/docs/superpowers/plans/2026-08-26-二七王前端A-地基与逻辑层.md @@ -1529,15 +1529,20 @@ EOF - Create: `client/js/01_SubGame/codes/config/Sprites_CreateRoom.js`(清单 §5.8) **Interfaces:** -- Produces: 全局 `EQW_Sprites`,五个文件用保护性声明合并为一份。结构照 `GameView_Sprites.template.js`: +- Produces: 全局 `EQW_Sprites`,五个文件用保护性声明合并为一份。**以 UI View 为组织单位**——一个 View 将来对应一个 `BaseComponent` 组件(前端红线:每个界面的数据与渲染只由其对应 UI 组件负责): ```js -EQW_Sprites.<界面名> = { - LAYER: <图层ID>, - <群组名>: { GROUP_ID: <群组ID>, SPRITES: { <键名>: <精灵ID>, ... } } +EQW_Sprites. = { + Layer: <图层常量>, // 该 View 用到的图层;用到多个时写 Layer1 / Layer2 … + Group: <群组常量>, // 该 View 用到的群组;用到多个时写 Group1 / Group2 … + + <精灵键名>: <精灵ID>, // 精灵只写 id;所属 Layer/Group 写在注释里 + ... }; ``` +- **保留键约定**(`SpriteIndex` 与守卫测试据此机械区分):View 内键名为 `Layer` / `Group`,或以二者为前缀(`Layer1`、`Group2`…)的,是 **View 级声明**;**其余键一律是精灵 id**。 +- 精灵条目**不重复写 Layer/Group**——它们扁平放在 View 定义下,精灵的所属关系在**注释**里说明。 - 键名与 ID 一律照抄清单 §5 各表,**不改名、不改号**(Task 11 的 `SpriteIndex` 与 Task 12 的布局配置都按键名引用) - [ ] **Step 1: 写 `Sprites_Table.js`** @@ -1549,31 +1554,42 @@ EQW_Sprites.<界面名> = { ////////// EQW_Sprites: 精灵结构(清单 §5.1 牌桌常驻)///////// /////////////////////////////////////////////////////////////// // 子游戏精灵段 1001–2999(段内仅 3000 被平台占用)。 -// 每个精灵注明:类型(图片/文字)+ 用途 + 资源键 + 帧说明—— +// 以 UI View 为组织单位:Layer / Group 扁平声明在 View 下, +// 精灵只写 id,其所属 Layer/Group 在注释里说明。 +// 每个精灵注明:类型(图片/文字)+ 用途 + 资源键 + 帧说明 + 所属群组—— // 后期即照这些注释在编辑器里逐个创建。 var EQW_Sprites = EQW_Sprites || {}; -EQW_Sprites.TABLE = { - LAYER: EQW_Layers.TABLE_STATIC, +//顶部信息条:主 / 叫分 / 抓分 三列 +EQW_Sprites.TopInfoView = { + Layer: EQW_Layers.TABLE_STATIC, //101 + Group: EQW_Groups.TOP_INFO, //201 - //顶部信息条:主 / 叫分 / 抓分 三列 - TOP_INFO: { - GROUP_ID: EQW_Groups.TOP_INFO, - SPRITES: { - TOP_INFO_BG: 1001, //图片:三列表格底,资源 PANEL_TOP_INFO - TOP_SUIT_ICON: 1002, //图片:「主」列花色,资源 SUIT_ICON_S,帧 = flower(1方块 2梅花 3红心 4黑桃) - TOP_CALL_TEXT: 1003, //文字:叫分数值 - TOP_CALL_BADGE_BG: 1004, //图片:叫分角标底,资源 BADGE_MULTIPLE 帧1 - TOP_CALL_BADGE_TEXT: 1005, //文字:「N子」 - TOP_GRADE_TEXT: 1006, //文字:抓分数值 - TOP_GRADE_BADGE_BG: 1007, //图片:抓分角标底,资源 BADGE_MULTIPLE 帧2 - TOP_GRADE_BADGE_TEXT: 1008 //文字:「N倍」,取 Math.abs(curmultiple)(T-16/S-8) - } - }, - - // ... 其余按清单 §5.1 续:群组 206 亮牌条(1030–1031)、 - // 群组 205 底栏(1040–1055)、群组 202/203/204 三家附加标记(1100–1121) + TOP_INFO_BG: 1001, //图片:三列表格底,资源 PANEL_TOP_INFO + TOP_SUIT_ICON: 1002, //图片:「主」列花色,资源 SUIT_ICON_S,帧 = flower(1方块 2梅花 3红心 4黑桃) + TOP_CALL_TEXT: 1003, //文字:叫分数值 + TOP_CALL_BADGE_BG: 1004, //图片:叫分角标底,资源 BADGE_MULTIPLE 帧1 + TOP_CALL_BADGE_TEXT: 1005, //文字:「N子」 + TOP_GRADE_TEXT: 1006, //文字:抓分数值 + TOP_GRADE_BADGE_BG: 1007, //图片:抓分角标底,资源 BADGE_MULTIPLE 帧2 + TOP_GRADE_BADGE_TEXT: 1008 //文字:「N倍」,取 Math.abs(curmultiple)(T-16/S-8) }; + +//三家玩家位附加标记:跨三个群组,故写 Group1/2/3 +EQW_Sprites.PlayerMarkView = { + Layer: EQW_Layers.TABLE_STATIC, //101 + Group1: EQW_Groups.P_LEFT_MARK, //202 左上家 + Group2: EQW_Groups.P_RIGHT_MARK, //203 右上家 + Group3: EQW_Groups.P_SELF_MARK, //204 自己 + + //—— Group1 左上家(群组 202)—— + P_LEFT_BANKER: 1100, //图片:「庄」印章,资源 MARK_BANKER + P_LEFT_ZHU_BG: 1101, //图片:「主N」角标底,资源 BADGE_ZHU_PAIR 帧1 + // ... 其余按清单 §5.1 续 +}; + +// ... 其余 View 按清单 §5.1 续:亮牌条(群组 206,1030–1031)、 +// 底栏(群组 205,1040–1055) ``` 清单 §5.1 里 `1110–1115 P_RIGHT_*`「结构同 202」是省略写法,**必须展开写全**:`P_RIGHT_BANKER:1110`、`P_RIGHT_ZHU_BG:1111`、`P_RIGHT_ZHU_TEXT:1112`、`P_RIGHT_PAIR_BG:1113`、`P_RIGHT_PAIR_TEXT:1114`、`P_RIGHT_STATUS_TEXT:1115`。 @@ -1627,44 +1643,57 @@ EOF - Consumes: `EQW_Sprites`(Task 10) - Produces: 全局 `EQW_SpriteIndex`,含 `build(spriteTree)` → `{ 键名: 精灵ID }`、`idOf(key)` → `number`(查不到抛错)、`init()`(用 `EQW_Sprites` 建好内部索引) -> 布局配置里 `attach.target` 写的是**键名**而非数字 ID(清单 §6.1:「`target` 一律写键名,ID 回填时只改一处映射表」),而精灵常量是按图层/群组嵌套的,故需要这张扁平索引。 +> 布局配置里 `attach.target` 写的是**键名**而非数字 ID(清单 §6.1:「`target` 一律写键名,ID 回填时只改一处映射表」),而精灵常量是按 View 组织的,故需要这张扁平索引。 > **重复键必须报错**而非后者覆盖前者——覆盖会让某个界面静默指向另一个界面的精灵。 +> **保留键约定**:View 内键名为 `Layer` / `Group` 或带数字后缀(`Layer1`、`Group2`…)的是 View 级声明,跳过不进索引;其余键一律是精灵 id。 +> **值必须是数字**:View 级声明与精灵条目在结构上同级、只靠键名区分,所以把「值不是数字」当作错误抓出来——否则写错前缀(如 `Grup: 201`)会静默变成一个假精灵。 - [ ] **Step 1: 写失败的测试 `client/tests/test_spriteindex.js`** ```js -// 嵌套精灵常量 → 扁平键名索引 +// View 组织的精灵常量 → 扁平键名索引 const { load, throws } = require('./_load'); const t = require('./_assert')(); load('client/js/01_SubGame/codes/core/SpriteIndex.js'); -// ---- 展平嵌套结构 ---- +// ---- 展平:Layer/Group 声明跳过,精灵进索引 ---- const tree = { - VIEW_A: { - LAYER: 101, - GROUP_X: { GROUP_ID: 201, SPRITES: { BTN_A: 1001, BTN_B: 1002 } }, - GROUP_Y: { GROUP_ID: 202, SPRITES: { LABEL_C: 1003 } } + TopInfoView: { + Layer: 101, + Group: 201, + BTN_A: 1001, + BTN_B: 1002 }, - VIEW_B: { - LAYER: 102, - GROUP_Z: { GROUP_ID: 210, SPRITES: { CARD_1: 1200 } } + PlayerMarkView: { + Layer: 101, + Group1: 202, + Group2: 203, + P_LEFT_BANKER: 1100, + P_RIGHT_BANKER: 1110 } }; -t.eq('展平结果', EQW_SpriteIndex.build(tree), { BTN_A: 1001, BTN_B: 1002, LABEL_C: 1003, CARD_1: 1200 }); +t.eq('展平结果', EQW_SpriteIndex.build(tree), + { BTN_A: 1001, BTN_B: 1002, P_LEFT_BANKER: 1100, P_RIGHT_BANKER: 1110 }); -// ---- LAYER / GROUP_ID 不进索引 ---- +// ---- View 级声明不进索引(含带数字后缀的)---- const flat = EQW_SpriteIndex.build(tree); -t.eq('LAYER 不进索引', flat.LAYER, undefined); -t.eq('GROUP_ID 不进索引', flat.GROUP_ID, undefined); +t.eq('Layer 不进索引', flat.Layer, undefined); +t.eq('Group 不进索引', flat.Group, undefined); +t.eq('Group1 不进索引', flat.Group1, undefined); +t.eq('Group2 不进索引', flat.Group2, undefined); // ---- 反面:重复键报错,不静默覆盖 ---- const dupTree = { - VIEW_A: { LAYER: 101, G1: { GROUP_ID: 201, SPRITES: { SAME: 1001 } } }, - VIEW_B: { LAYER: 102, G2: { GROUP_ID: 202, SPRITES: { SAME: 1002 } } } + ViewA: { Layer: 101, Group: 201, SAME: 1001 }, + ViewB: { Layer: 102, Group: 202, SAME: 1002 } }; t.eq('重复键报错', throws(() => EQW_SpriteIndex.build(dupTree)), true); +// ---- 反面:精灵值不是数字要报错(防写错保留键前缀变成假精灵)---- +const badTree = { ViewA: { Layer: 101, Group: 201, NESTED: { id: 1001 } } }; +t.eq('值非数字报错', throws(() => EQW_SpriteIndex.build(badTree)), true); + // ---- idOf:查得到 / 查不到抛错 ---- EQW_SpriteIndex.init(tree); t.eq('idOf 查到', EQW_SpriteIndex.idOf('BTN_A'), 1001); @@ -1702,33 +1731,39 @@ Expected: `EQW_SpriteIndex is not defined`。 /////////////////////////////////////////////////////////////// ////////// EQW_SpriteIndex: 精灵键名 → ID 扁平索引 ///////////// /////////////////////////////////////////////////////////////// -// 精灵常量按「界面 → 群组 → 精灵」嵌套(照 GameView_Sprites 模板), -// 而布局配置里 attach.target 写的是【键名】(清单 §6.1:ID 回填时只改一处映射表)。 -// 本模块把嵌套结构展平成 { 键名: 精灵ID } 供求解器查用。 -// 【显式失败】重复键、查不到的键一律抛错——静默覆盖会让某个界面 -// 指向另一个界面的精灵,且极难排查(工程总则 §7)。 +// 精灵常量以 UI View 为组织单位:Layer / Group 扁平声明在 View 下, +// 精灵只写 id(所属关系在注释里)。而布局配置里 attach.target 写的是 +// 【键名】(清单 §6.1:ID 回填时只改一处映射表),故需把 View 结构 +// 展平成 { 键名: 精灵ID } 供求解器查用。 +// 【显式失败】重复键、值非数字、查不到的键一律抛错——静默覆盖会让某个 +// 界面指向另一个界面的精灵,且极难排查(工程总则 §7)。 var EQW_SpriteIndex = EQW_SpriteIndex || { _index: null, - //展平:遍历 树 → 界面 → 群组 → SPRITES + //View 级保留键:Layer / Group,可带数字后缀(Layer1、Group2…) + _isReserved: function (key) { + return (/^Layer\d*$/).test(key) || (/^Group\d*$/).test(key); + }, + + //展平:遍历 树 → View → 键 build: function (spriteTree) { var index = {}; for (var viewName in spriteTree) { if (!spriteTree.hasOwnProperty(viewName)) { continue; } var view = spriteTree[viewName]; - for (var groupName in view) { - if (!view.hasOwnProperty(groupName)) { continue; } - var group = view[groupName]; - if (!group || !group.SPRITES) { continue; } //跳过 LAYER 等标量字段 - for (var key in group.SPRITES) { - if (!group.SPRITES.hasOwnProperty(key)) { continue; } - if (index.hasOwnProperty(key)) { - throw new Error('[EQW_SpriteIndex] 精灵键名重复: ' + key + - '(' + viewName + '.' + groupName + ' 与更早的定义冲突)'); - } - index[key] = group.SPRITES[key]; + for (var key in view) { + if (!view.hasOwnProperty(key)) { continue; } + if (this._isReserved(key)) { continue; } //View 级 Layer/Group 声明 + if (typeof view[key] !== 'number') { + throw new Error('[EQW_SpriteIndex] 精灵 ' + key + ' 的值不是数字(View ' + + viewName + ')——Layer/Group 声明请用保留键名 Layer* / Group*'); } + if (index.hasOwnProperty(key)) { + throw new Error('[EQW_SpriteIndex] 精灵键名重复: ' + key + + '(View ' + viewName + ' 与更早的定义冲突)'); + } + index[key] = view[key]; } } return index; diff --git a/docs/superpowers/specs/2026-08-26-二七王前端A-地基与逻辑层-design.md b/docs/superpowers/specs/2026-08-26-二七王前端A-地基与逻辑层-design.md index c6f3f32..db8f3aa 100644 --- a/docs/superpowers/specs/2026-08-26-二七王前端A-地基与逻辑层-design.md +++ b/docs/superpowers/specs/2026-08-26-二七王前端A-地基与逻辑层-design.md @@ -104,7 +104,7 @@ client/tests/ | `Groups.js` | `EQW_Groups` | 清单 §0.3 群组表 201–250 | | `ImageResources.js` | `EQW_Images` | 清单 §3 图片资源总表,**每条按 ImageResources.template 的要求注明用途 / 帧数 / 每帧含义 / 尺寸** | | `SoundResources.js` | `EQW_Sounds` | 清单 §4——**整节 T-20 未定**,本阶段只建文件骨架与说明注释,不臆造条目(见 §11) | -| `Sprites_*.js` | `EQW_Sprites` | 清单 §5.1–§5.8,嵌套结构 `{ LAYER, 群组: { GROUP_ID, SPRITES: { 键名: ID } } }`(照 `GameView_Sprites.template.js`),每个精灵注明类型 / 用途 / 资源键 / 帧说明 | +| `Sprites_*.js` | `EQW_Sprites` | 清单 §5.1–§5.8。**以 UI View 为组织单位**(一个 View 将来对应一个 `BaseComponent`):`Layer` / `Group` 扁平声明在 View 下(用到多个时写 `Layer1`/`Group2`…),精灵只写 id、所属关系在注释里。每个精灵注明类型 / 用途 / 资源键 / 帧说明 / 所属群组 | | `LayoutConstants.js` | `EQW_Layout` | 清单 §6.3 `textStyle` 预设、§6.4 `CARD_SIZE` 三档、座位键常量 `SELF/LEFT/RIGHT` | | `Layout_Table.js` | `EQW_Layout` | 清单 §6.5 常驻区 + §6.6 玩家位(含气泡朝向 `arrowRight`) | | `Layout_Cards.js` | `EQW_Layout` | 清单 §6.7 牌区(手牌单排/双排、底牌、埋牌底牌、已出牌 `bySeat`、冲关牌) | @@ -189,16 +189,20 @@ EQW_SeatMap.toSeat(displayKey, mySeat) // → 服务端座位号 ### 5.5 `SpriteIndex` -精灵常量是按图层/群组嵌套的(照模板),而布局配置里 `attach.target` 写的是**键名**(清单 §6.1:「`target` 一律写键名而非数字 ID」)。故需一张扁平索引: +精灵常量以 UI View 为组织单位(`Layer`/`Group` 扁平声明在 View 下,精灵只写 id),而布局配置里 `attach.target` 写的是**键名**(清单 §6.1:「`target` 一律写键名而非数字 ID」)。故需一张扁平索引: ```js -EQW_SpriteIndex.build(spriteTrees) // 遍历嵌套结构 → { 键名: 精灵ID } +EQW_SpriteIndex.build(spriteTree) // 遍历 树 → View → 键 → { 键名: 精灵ID } EQW_SpriteIndex.idOf(key) // 查 ID;查不到显式抛错,不返回 undefined ``` -**显式失败**:键名拼错时立即报错(工程总则 §7),而不是把 `undefined` 传给 `SpriteManager` 静默无效。 +**保留键约定**:View 内键名为 `Layer` / `Group` 或带数字后缀(`Layer1`、`Group2`…)的是 View 级声明,跳过不进索引;其余键一律是精灵 id。 -同名键在不同群组下重复(如三家结构相同的 `PLAY_*`)由清单 §5 的键名设计避免(`PLAY_SELF_*` / `PLAY_LEFT_*` / `PLAY_RIGHT_*`);`build` 遇到重复键**报错**而非后者覆盖前者。 +**显式失败**三处(工程总则 §7): + +- 键名拼错时 `idOf` 立即报错,而不是把 `undefined` 传给 `SpriteManager` 静默无效; +- **重复键报错**而非后者覆盖前者——覆盖会让某个界面静默指向另一个界面的精灵。同名键在不同 View 下重复由清单 §5 的键名设计避免(`PLAY_SELF_*` / `PLAY_LEFT_*` / `PLAY_RIGHT_*`); +- **值非数字报错**——View 级声明与精灵条目在结构上同级、只靠键名区分,写错前缀(如 `Grup: 201`)会静默变成一个假精灵,故把「值不是数字」当作错误抓出来。 ---