Files
erqiwang_youle/client/js/01_SubGame/codes/config/Layout_Cards.js
T
joywayerandClaude Opus 5 92edb6a3f5 二七王:布局求解器补齐显式失败与运行时注入契约,配置补 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>
2026-08-26 22:24:16 +08:00

101 lines
4.8 KiB
JavaScript
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.
///////////////////////////////////////////////////////////////
////////// EQW_Layout: 牌区配置(清单 §6.7)////////////////////
///////////////////////////////////////////////////////////////
// 【纯数据文件】无函数、无副作用;依赖 LayoutConstants.js 先加载
// (引用其 EQW_Layout.CARD_SIZE,属 EQW_Layout 内部自引用)。
var EQW_Layout = EQW_Layout || {};
//===== 自己手牌区(清单 §6.7)=====
//自己手牌 · 单排:36 张时 spacing ≈ -78(露 32px),与参考图实测 33px 吻合
EQW_Layout.HAND_FAN_SINGLE = {
kind: 'fan', direction: 'horizontal',
anchorX: 640, anchorY: 455, maxWidth: 1215,
itemWidth: EQW_Layout.CARD_SIZE.L.w, itemHeight: EQW_Layout.CARD_SIZE.L.h,
spacingMax: 0, spacingMin: -78, anchor: 'center', overlapFrom: 'left'
};
//自己手牌 · 双排(埋牌)。分行策略是数据标记 splitStrategy,由调用方据此分行,本文件不写函数:
//floorTop = 上排 floor(n/2)、下排 ceil(n/2)(36 张 = 18/18;参考图 17/19 属填充画法,不作依据,见清单 §0.5)。
//spacingMax / anchor / overlapFrom 清单双排行未重复给出,沿用单排同参数(同一批牌、同一压缩规则)
EQW_Layout.HAND_TWO_ROW = {
splitStrategy: 'floorTop',
ROW_TOP: {
kind: 'fan', direction: 'horizontal',
anchorX: 644, anchorY: 285, maxWidth: 928,
itemWidth: EQW_Layout.CARD_SIZE.L.w, itemHeight: EQW_Layout.CARD_SIZE.L.h,
spacingMax: 0, spacingMin: -65, anchor: 'center', overlapFrom: 'left'
},
ROW_BOTTOM: {
kind: 'fan', direction: 'horizontal',
anchorX: 644, anchorY: 455, maxWidth: 928,
itemWidth: EQW_Layout.CARD_SIZE.L.w, itemHeight: EQW_Layout.CARD_SIZE.L.h,
spacingMax: 0, spacingMin: -65, anchor: 'center', overlapFrom: 'left'
}
};
//手牌 · 选中上浮偏移量定义在 EQW_Anim.HAND_SELECT_OFFSET_Y(AnimConstants.js),此处不重复(SSOT)
//底牌区(8 张,未揭示为牌背、揭示后翻正面,同一批精灵)
EQW_Layout.BOTTOM_CARDS = {
kind: 'line', direction: 'horizontal',
anchorX: 655, anchorY: 110,
itemWidth: EQW_Layout.CARD_SIZE.L.w, itemHeight: EQW_Layout.CARD_SIZE.L.h,
spacing: -36, anchor: 'center'
};
//埋牌底牌区(结算,8 张,尺寸档 S)
EQW_Layout.BURY_CARDS = {
kind: 'line', direction: 'horizontal',
anchorX: 640, anchorY: 163,
itemWidth: EQW_Layout.CARD_SIZE.S.w, itemHeight: EQW_Layout.CARD_SIZE.S.h,
spacing: 2, anchor: 'center'
};
//===== 已出牌区(清单 §6.2 示例 / §6.7)=====
//三家已出牌区:三家共用 base,LEFT 与 RIGHT 左右镜像(overlapFrom 相反)
EQW_Layout.PLAY_AREA = {
kind: 'fan', direction: 'horizontal',
itemWidth: EQW_Layout.CARD_SIZE.M.w, itemHeight: EQW_Layout.CARD_SIZE.M.h,
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' }
}
};
//已出牌 · 牌型标签:贴该区第一张牌。首槽是固定精灵(PLAY_SELF_1 / PLAY_LEFT_1 / PLAY_RIGHT_1),非动态目标。
//资源 LABEL_CARDTYPE 76×24(清单 §3)
EQW_Layout.PLAY_TYPE_LABEL = {
kind: 'attach', targetKind: 'sprite', corner: 'bottomLeft', offsetX: 0, offsetY: -24, w: 76, h: 24,
bySeat: {
SELF: { target: 'PLAY_SELF_1' },
LEFT: { target: 'PLAY_LEFT_1' },
RIGHT: { target: 'PLAY_RIGHT_1' }
}
};
//已出牌 · 牌角『庄』/『大』标:贴的是本轮打出的哪张具体牌,运行时才能确定,不是固定精灵键。
//本节点只是模板(corner/offset),调用方在确定具体是哪张牌之后,以此为基础补上 target 再喂给
//EQW_LayoutSolver.solve——这是有意的动态目标模式,不在配置里预置 target(清单未给尺寸;
//两种资源分别是 MARK_BANKER_CORNER 28×28 / MARK_JOKER_BIG 34×34,按实际显示的是哪个徽标取值)
EQW_Layout.PLAY_CARD_CORNER_MARK = {
kind: 'attach', targetKind: 'sprite', corner: 'topRight', offsetX: -28, offsetY: 0,
runtime: ['target', 'w', 'h']
};
//冲关牌(叠加层):精灵复制场景(§5.6a),spacingMin 放宽到 -88 以容纳 20+ 张的极端情况。
//anchor / direction 清单未重复给出,沿用其余 fan 配置的通用约定(center / horizontal)
EQW_Layout.CHONGGUAN_FAN = {
kind: 'fan', direction: 'horizontal', anchor: 'center',
itemWidth: EQW_Layout.CARD_SIZE.L.w, itemHeight: EQW_Layout.CARD_SIZE.L.h,
maxWidth: 420, spacingMax: -40, spacingMin: -88,
bySeat: {
LEFT: { anchorX: 260, anchorY: 120 },
RIGHT: { anchorX: 1035, anchorY: 120 },
SELF: { anchorX: 640, anchorY: 400 }
}
};