二七王:布局求解器补齐显式失败与运行时注入契约,配置补 runtime/targetKind 声明

求解器(ui/LayoutSolver.js):
- 按方向校验 anchor:竖排只收 top/bottom/center、横排只收 left/right/center,
  未知 direction 一并拒绝。框架 AlignmentUtils 的 switch 对写错方向的 anchor 落到
  default(居中),笔误会被静默吞掉——ROOM_CATEGORY_COLUMN 就是这么把前两行摆到画布外的。
- grid 校验合并 ctx 后的 cols/rows 必须是数字:缺 rows 时 capacity=NaN 让超载守卫失效、
  循环一次不跑,静默返回空数组(一个矩形都不产出)。
- line/fan/grid 要求 anchorX/anchorY 必须是数字(框架的 anchorY||0 会把缺失变成 0);
  point 要求 x/y 必须是数字。
- apply() 精灵数与矩形数不等时抛错,不再静默截断:截断会让多余精灵停在上一手牌的旧坐标上。
- 统一运行时注入:ctx 里的 INJECT_KEYS(target/x/y/w/h/anchorX/anchorY/rows/
  itemWidth/itemHeight)覆盖配置同名字段,优先级 ctx > bySeat > base。
- line + items 竖排显式拒绝(只实现了水平)。

配置:
- ROOM_CATEGORY_COLUMN anchor 由 'left' 改为 'top'(竖排语义)。
- ROOM_OPTION_OVERFLOW_GRID 补 rows(runtime)、槽尺寸与 anchor 沿用 ROOM_OPTION_ROW,
  cols 直接引用 ROOM_OPTION_MAX_PER_ROW;ROOM_OPTION_ROW 补上注释里已声明的 itemHeight。
- 所有依赖运行时注入的节点加 runtime 声明,所有 attach 节点加 targetKind
  (sprite / layout / platform),显式区分「忘了写」与「故意延后到运行时」。

守卫(tests/test_constants.js):
- 求解冒烟改为只对 runtime 声明过的键注入假值,没声明却缺字段的照常抛错变红。
- 补矩形数量断言(line/fan/grid 按项数、point/attach 恒 1)——此前只查数组与坐标类型,
  空数组照样通过,「少画了几张牌」对守卫完全不可见。
- attach.target 改为只在 targetKind 指定的那一个命名空间里校验存在。
- 补 runtime 声明自身的合法性检查与布局节点总数(62)钉死。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 22:24:16 +08:00
co-authored by Claude Opus 5
parent b81be36f6e
commit 92edb6a3f5
8 changed files with 374 additions and 63 deletions
@@ -12,19 +12,28 @@ EQW_Layout.TOP_INFO_BG = { kind: 'point', x: 486, y: 4, w: 276, h: 68 };
//顶部 · 花色图标:贴信息条内部左下角(资源 SUIT_ICON_S 30×30,清单 §3)
EQW_Layout.TOP_SUIT_ICON = {
kind: 'attach', target: 'TOP_INFO_BG',
kind: 'attach', targetKind: 'layout', target: 'TOP_INFO_BG',
hAlign: 'left', vAlign: 'bottom', offsetX: 20, offsetY: -8, w: 30, h: 30
};
//顶部 · 叫分数值文字 / 抓分数值文字:【清单 §6.5 未给出位置】——那张表只列了两个角标底与角标
//文字,而角标底又必须贴在这两段数值文字身上。此处显式占位:坐标与宽高全部标为运行时注入,
//由调用方按信息条内的三列表格实际排布给出;设计稿补齐后把 x/y/w/h 回填成固定值即可。
//显式占位是为了避免 C 阶段在渲染代码里就地硬编码一组坐标——那样这两处位置就再也无从统一修改
EQW_Layout.TOP_CALL_TEXT = { kind: 'point', runtime: ['x', 'y', 'w', 'h'] };
EQW_Layout.TOP_GRADE_TEXT = { kind: 'point', runtime: ['x', 'y', 'w', 'h'] };
//顶部 · 叫分角标底:贴叫分数值文字右上角(资源 BADGE_MULTIPLE 帧1 34×18)。
//角标数值文字复用同一矩形,不单独配置(清单未给独立位置,下同)
EQW_Layout.TOP_CALL_BADGE_BG = {
kind: 'attach', target: 'TOP_CALL_TEXT', corner: 'topRight', offsetX: 4, offsetY: 6, w: 34, h: 18
kind: 'attach', targetKind: 'layout', target: 'TOP_CALL_TEXT',
corner: 'topRight', offsetX: 4, offsetY: 6, w: 34, h: 18
};
//顶部 · 抓分角标底:贴抓分数值文字右上角(资源 BADGE_MULTIPLE 帧2 34×18)
EQW_Layout.TOP_GRADE_BADGE_BG = {
kind: 'attach', target: 'TOP_GRADE_TEXT', corner: 'topRight', offsetX: 4, offsetY: 6, w: 34, h: 18
kind: 'attach', targetKind: 'layout', target: 'TOP_GRADE_TEXT',
corner: 'topRight', offsetX: 4, offsetY: 6, w: 34, h: 18
};
//亮牌信息条
@@ -81,7 +90,7 @@ EQW_Layout.PLAYER_AVATAR_FRAME_REF = {
//会合并包括 kind 在内的所有字段,这里利用该机制表达「底栏庄标是固定坐标、不贴附任何目标」,
//与 LEFT/RIGHT 两个上家贴头像框(attach)是不同的定位方式,不是笔误
EQW_Layout.P_BANKER_MARK = {
kind: 'attach', w: 40, h: 40,
kind: 'attach', targetKind: 'platform', w: 40, h: 40,
bySeat: {
LEFT: { target: 'P_LEFT_AVATAR', corner: 'topRight', offsetX: 7, offsetY: 8 },
RIGHT: { target: 'P_RIGHT_AVATAR', corner: 'topLeft', offsetX: -40, offsetY: 8 },
@@ -91,7 +100,7 @@ EQW_Layout.P_BANKER_MARK = {
//『主N』角标底:仅左右两家(自己主/对数已在手牌区可见,无此角标)。w/h 为清单 §6.6 实测值
EQW_Layout.P_ZHU_BADGE = {
kind: 'attach', corner: 'bottomLeft', offsetX: 0, offsetY: 6, w: 40, h: 19,
kind: 'attach', targetKind: 'platform', corner: 'bottomLeft', offsetX: 0, offsetY: 6, w: 40, h: 19,
bySeat: {
LEFT: { target: 'P_LEFT_AVATAR' },
RIGHT: { target: 'P_RIGHT_AVATAR' }
@@ -101,7 +110,7 @@ EQW_Layout.P_ZHU_BADGE = {
//『对N』角标底:贴主N角标右侧。w/h 清单未重复给出,沿用主N角标同尺寸
//(同资源 BADGE_ZHU_PAIR 的另一帧,帧1黄底=主N、帧2白底=对N,尺寸一致)
EQW_Layout.P_PAIR_BADGE = {
kind: 'attach', target: 'P_ZHU_BADGE', hAlign: 'right', vAlign: 'middle',
kind: 'attach', targetKind: 'layout', target: 'P_ZHU_BADGE', hAlign: 'right', vAlign: 'middle',
offsetX: 44, offsetY: 0, w: 40, h: 19
};
@@ -127,7 +136,7 @@ EQW_Layout.P_COUNTDOWN = {
//提示气泡(踩/没分/有分):朝向 arrowRight 是配置项,不按座位硬判(清单 §6.6)。
//w/h 清单 §3 标『待定』,此处用示意尺寸占位,资源定稿后回填(不影响 kind/字段结构)
EQW_Layout.TISHI_BUBBLE = {
kind: 'attach', w: 96, h: 44,
kind: 'attach', targetKind: 'platform', w: 96, h: 44,
bySeat: {
LEFT: { target: 'P_LEFT_AVATAR', hAlign: 'right', vAlign: 'middle', offsetX: 10, arrowRight: false },
RIGHT: { target: 'P_RIGHT_AVATAR', hAlign: 'left', vAlign: 'middle', offsetX: -10, arrowRight: true },