这三处都是「精灵或按钮已在清单里定案,但配置层缺东西」,不补的话 C/D/E 阶段
写渲染代码时会发现没有布局节点,极易就地硬编码坐标(违反零裸值红线)。
1) 底栏「扣底」按钮没有精灵(清单 §2.6 列了 5 个功能按钮,只登记了 4 个)
- 新增 BTN_BURY_CARDS(1056) / BTN_BURY_CARDS_TEXT(1057),群组 205 号段内空闲号,
资源沿用 BTN_BOTTOM_FUNC,与其余四个按钮同构。
- FOOTER_FUNC_BUTTONS 的 items 由 4 项扩到 5 项,扣底紧邻底牌(照 §2.6 表序)。
扣底宽 78 是【估值】:清单未给,取同为两字按钮的「底牌」实测值,已在配置与清单里注明。
- 扣底看【埋牌底牌】(burycards)、底牌看【底牌】(bottomcards),两批不同的牌,
JSDoc 里补了防写反的提示(对应验收清单第 13 条)。
2) 出牌阶段「提示」按钮有精灵、无布局槽位
- PLAY_OPERATION_BAR.items 补上 PLAY_BTN_TIP,顺序照清单 §1.7 行文:倒计时 / 出牌 / 提示。
- 宽 180 是【估值】:清单 §6.8 自身漏了该项;与埋牌条的提示按钮同资源 BTN_TIP
(§3.3 实测 180×63),埋牌条 items 首项亦取 180,故沿用。
- 顺带记下一处待裁定项:本条 itemHeight 为 55(对齐 BTN_PLAY 157×55),
而 BTN_TIP 原生高 63,高度差待设计稿定。
3) AccountView / HistoryView / MingPaiView 一个布局节点都没有
- AccountView(§6.9a 新增):外壳 6 个节点 + 玩家栏「容器 ACC_ROW_CONTAINER +
行节点 ACC_ROW(itemHeight 即行高,行数由 ctx.count 给出)+ 行内模板贴附到行矩形」
+ 面板外 4 按钮,共 14 个节点。行内节点求出的是相对容器的坐标,可直接喂
SpriteCopyUtils.create(与 §5.6a 冲关牌同一用法)。
- HistoryView / MingPaiView(§6.9c 新增):遮罩 + 容器 + 每家一排牌(fan) + 座位标签,
各 4 个节点。照 CHONGGUAN_FAN 同构、锚点沿用 §6.7 冲关牌的三家落点;
差别只在张数上限——出牌历史摊平后可达 28 张以上,故 maxWidth 放宽到 500、
spacingMin 收到 -96【估值】。MING_* 只有 LEFT/RIGHT 两个 bySeat 变体(明牌不看自己)。
- 坐标来源:大局结算.png / 冲关牌型显示.png 目视实测,w/h 取资源尺寸;
配置与清单里均已注明「目视实测估值,待设计稿复核」。
- 运行时才确定的字段(动态文字宽高、随行浮动的 anchorY、数字精灵宽)一律
runtime 显式声明。
钉死断言按实际重新数并更新:精灵总数 378 → 380,布局节点总数 61 → 83。
另把 ACC_TPL_SCORE_NUM 加进 NUM_STYLE 同源校验表(与 RESULT_SCORE_NUM 同一套资源)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
89 lines
4.0 KiB
JavaScript
89 lines
4.0 KiB
JavaScript
// View → group → 精灵 的常量树 → 扁平键名索引
|
||
const { load, throws } = require('./_load');
|
||
const t = require('./_assert')();
|
||
|
||
load('client/js/01_SubGame/codes/core/SpriteIndex.js');
|
||
|
||
// ---- 展平:Layer 与 group 的 id 都不进索引,精灵进 ----
|
||
const tree = {
|
||
TopInfoView: {
|
||
Layer: 101,
|
||
TopInfo: { id: 201, BTN_A: 1001, BTN_B: 1002 }
|
||
},
|
||
PlayerMarkView: {
|
||
Layer: 101,
|
||
LeftMark: { id: 202, P_LEFT_BANKER: 1100 },
|
||
RightMark: { id: 203, P_RIGHT_BANKER: 1110 }
|
||
}
|
||
};
|
||
t.eq('展平结果', EQW_SpriteIndex.build(tree),
|
||
{ BTN_A: 1001, BTN_B: 1002, P_LEFT_BANKER: 1100, P_RIGHT_BANKER: 1110 });
|
||
|
||
// ---- Layer 与 group 的 id 不进索引 ----
|
||
const flat = EQW_SpriteIndex.build(tree);
|
||
t.eq('Layer 不进索引', flat.Layer, undefined);
|
||
t.eq('group 的 id 不进索引', flat.id, undefined);
|
||
|
||
// ---- 群组映射:精灵 → 它所属的群组 ID ----
|
||
t.eq('群组映射', EQW_SpriteIndex.buildGroupMap(tree),
|
||
{ BTN_A: 201, BTN_B: 201, P_LEFT_BANKER: 202, P_RIGHT_BANKER: 203 });
|
||
|
||
// ---- 反面:一个 View 恰好一个 Layer;Layer1/Layer2 这种多图层写法要报错 ----
|
||
// (跨图层的界面应拆成两个 View,各带自己的 Layer——一个 View 将来对应一个 BaseComponent)
|
||
const multiLayer = {
|
||
OverlayView: { Layer1: 104, Layer2: 105, Bar: { id: 230, TIP_BG: 1750 } }
|
||
};
|
||
t.eq('多 Layer 写法报错', throws(() => EQW_SpriteIndex.build(multiLayer)), true);
|
||
|
||
// ---- 反面:View 缺 Layer 声明要报错 ----
|
||
const noLayer = { ViewA: { G1: { id: 201, BTN: 1001 } } };
|
||
t.eq('View 缺 Layer 报错', throws(() => EQW_SpriteIndex.build(noLayer)), true);
|
||
|
||
// ---- 反面:重复键报错,不静默覆盖 ----
|
||
const dupTree = {
|
||
ViewA: { Layer: 101, G1: { id: 201, SAME: 1001 } },
|
||
ViewB: { Layer: 102, G2: { id: 202, SAME: 1002 } }
|
||
};
|
||
t.eq('重复键报错', throws(() => EQW_SpriteIndex.build(dupTree)), true);
|
||
|
||
// ---- 反面:精灵值不是数字要报错 ----
|
||
const badValue = { ViewA: { Layer: 101, G1: { id: 201, NESTED: { x: 1 } } } };
|
||
t.eq('精灵值非数字报错', throws(() => EQW_SpriteIndex.build(badValue)), true);
|
||
|
||
// ---- 反面:View 下混入标量键(不是 group 容器)要报错 ----
|
||
// 例如把群组 ID 误写成 View 级的 `Group: 201`,而不是包进 group 容器
|
||
const strayScalar = { ViewA: { Layer: 101, Group: 201, G1: { id: 202, BTN: 1001 } } };
|
||
t.eq('View 下混入标量键报错', throws(() => EQW_SpriteIndex.build(strayScalar)), true);
|
||
|
||
// ---- 反面:group 容器缺 id 要报错 ----
|
||
const noGroupId = { ViewA: { Layer: 101, G1: { BTN: 1001 } } };
|
||
t.eq('group 缺 id 报错', throws(() => EQW_SpriteIndex.build(noGroupId)), true);
|
||
|
||
// ---- idOf / groupIdOf:查得到 / 查不到抛错 ----
|
||
EQW_SpriteIndex.init(tree);
|
||
t.eq('idOf 查到', EQW_SpriteIndex.idOf('BTN_A'), 1001);
|
||
t.eq('groupIdOf 查到', EQW_SpriteIndex.groupIdOf('P_RIGHT_BANKER'), 203);
|
||
t.eq('idOf 查不到抛错', throws(() => EQW_SpriteIndex.idOf('NOT_EXIST')), true);
|
||
t.eq('groupIdOf 查不到抛错', throws(() => EQW_SpriteIndex.groupIdOf('NOT_EXIST')), true);
|
||
|
||
// ---- 空树 ----
|
||
t.eq('空树', EQW_SpriteIndex.build({}), {});
|
||
|
||
// ---- 真实常量:EQW_Sprites 能完整建索引且无重复键 ----
|
||
load('client/js/01_SubGame/codes/config/Layers.js');
|
||
load('client/js/01_SubGame/codes/config/Groups.js');
|
||
load('client/js/01_SubGame/codes/config/Sprites_Table.js');
|
||
load('client/js/01_SubGame/codes/config/Sprites_Cards.js');
|
||
load('client/js/01_SubGame/codes/config/Sprites_Action.js');
|
||
load('client/js/01_SubGame/codes/config/Sprites_Result.js');
|
||
load('client/js/01_SubGame/codes/config/Sprites_CreateRoom.js');
|
||
|
||
let realOk = true;
|
||
let realIndex = {};
|
||
try { realIndex = EQW_SpriteIndex.build(EQW_Sprites); } catch (e) { realOk = false; console.log('建索引失败: ' + e.message); }
|
||
t.eq('真实精灵常量无重复键', realOk, true);
|
||
// 钉住精确数量(与 test_constants.js 同一个数):松下界删掉整个 View 都还能通过
|
||
t.eq('真实精灵数量', Object.keys(realIndex).length, 380);
|
||
|
||
process.exit(t.done('spriteindex') ? 0 : 1);
|