二七王:布局求解器补齐显式失败与运行时注入契约,配置补 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:
@@ -21,10 +21,16 @@ EQW_Layout.CALL_BTN_GRID = {
|
|||||||
//档位分数文字:贴对应档位按钮居中。CALL_BTN_1..14 是 14 个固定精灵,但本节点是共用模板,
|
//档位分数文字:贴对应档位按钮居中。CALL_BTN_1..14 是 14 个固定精灵,但本节点是共用模板,
|
||||||
//target 由调用方按第几档注入(不预置单一 target)。宽高随数字位数变化,清单未给固定值,
|
//target 由调用方按第几档注入(不预置单一 target)。宽高随数字位数变化,清单未给固定值,
|
||||||
//资源 NUM_CALL_SCORE 单字位 30×40 仅供参考,不作为本节点的 w/h
|
//资源 NUM_CALL_SCORE 单字位 30×40 仅供参考,不作为本节点的 w/h
|
||||||
EQW_Layout.CALL_BTN_SCORE_TEXT = { kind: 'attach', hAlign: 'center', vAlign: 'middle', offsetY: 4 };
|
EQW_Layout.CALL_BTN_SCORE_TEXT = {
|
||||||
|
kind: 'attach', targetKind: 'sprite', hAlign: 'center', vAlign: 'middle', offsetY: 4,
|
||||||
|
runtime: ['target', 'w', 'h']
|
||||||
|
};
|
||||||
|
|
||||||
//档位子数角标:贴对应档位按钮右上角,纯文字无底图,清单未给宽高(同上,调用方注入 target)
|
//档位子数角标:贴对应档位按钮右上角,纯文字无底图,清单未给宽高(同上,调用方注入 target)
|
||||||
EQW_Layout.CALL_BTN_MULTIPLE_BADGE = { kind: 'attach', corner: 'topRight', offsetX: -30, offsetY: 2 };
|
EQW_Layout.CALL_BTN_MULTIPLE_BADGE = {
|
||||||
|
kind: 'attach', targetKind: 'sprite', corner: 'topRight', offsetX: -30, offsetY: 2,
|
||||||
|
runtime: ['target', 'w', 'h']
|
||||||
|
};
|
||||||
|
|
||||||
EQW_Layout.CALL_BTN_NONE = { kind: 'point', x: 553, y: 345, w: 184, h: 57 };
|
EQW_Layout.CALL_BTN_NONE = { kind: 'point', x: 553, y: 345, w: 184, h: 57 };
|
||||||
|
|
||||||
@@ -44,14 +50,23 @@ EQW_Layout.MAIN_SUIT_LINE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//花色图标:贴对应花色按钮左侧(资源 SUIT_ICON_L 46×46)。target 由调用方按花色序号注入
|
//花色图标:贴对应花色按钮左侧(资源 SUIT_ICON_L 46×46)。target 由调用方按花色序号注入
|
||||||
EQW_Layout.MAIN_SUIT_ICON = { kind: 'attach', hAlign: 'left', vAlign: 'middle', offsetX: 14, w: 46, h: 46 };
|
EQW_Layout.MAIN_SUIT_ICON = {
|
||||||
|
kind: 'attach', targetKind: 'sprite', hAlign: 'left', vAlign: 'middle', offsetX: 14, w: 46, h: 46,
|
||||||
|
runtime: ['target']
|
||||||
|
};
|
||||||
|
|
||||||
//张数大字:贴对应花色按钮居中,纯文字,清单未给宽高(调用方注入 target)
|
//张数大字:贴对应花色按钮居中,纯文字,清单未给宽高(调用方注入 target)
|
||||||
EQW_Layout.MAIN_SUIT_COUNT_TEXT = { kind: 'attach', hAlign: 'center', vAlign: 'middle', offsetX: 14 };
|
EQW_Layout.MAIN_SUIT_COUNT_TEXT = {
|
||||||
|
kind: 'attach', targetKind: 'sprite', hAlign: 'center', vAlign: 'middle', offsetX: 14,
|
||||||
|
runtime: ['target', 'w', 'h']
|
||||||
|
};
|
||||||
|
|
||||||
//对数角标底:贴对应花色按钮右上角(资源 BADGE_MULTIPLE 帧1 34×18)。
|
//对数角标底:贴对应花色按钮右上角(资源 BADGE_MULTIPLE 帧1 34×18)。
|
||||||
//角标文字复用同一矩形,不单独配置(调用方注入 target)
|
//角标文字复用同一矩形,不单独配置(调用方注入 target)
|
||||||
EQW_Layout.MAIN_SUIT_PAIR_BADGE = { kind: 'attach', corner: 'topRight', offsetX: -36, offsetY: 2, w: 34, h: 18 };
|
EQW_Layout.MAIN_SUIT_PAIR_BADGE = {
|
||||||
|
kind: 'attach', targetKind: 'sprite', corner: 'topRight', offsetX: -36, offsetY: 2, w: 34, h: 18,
|
||||||
|
runtime: ['target']
|
||||||
|
};
|
||||||
|
|
||||||
//===== 埋牌 / 出牌操作条 =====
|
//===== 埋牌 / 出牌操作条 =====
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ EQW_Layout.PLAY_AREA = {
|
|||||||
//已出牌 · 牌型标签:贴该区第一张牌。首槽是固定精灵(PLAY_SELF_1 / PLAY_LEFT_1 / PLAY_RIGHT_1),非动态目标。
|
//已出牌 · 牌型标签:贴该区第一张牌。首槽是固定精灵(PLAY_SELF_1 / PLAY_LEFT_1 / PLAY_RIGHT_1),非动态目标。
|
||||||
//资源 LABEL_CARDTYPE 76×24(清单 §3)
|
//资源 LABEL_CARDTYPE 76×24(清单 §3)
|
||||||
EQW_Layout.PLAY_TYPE_LABEL = {
|
EQW_Layout.PLAY_TYPE_LABEL = {
|
||||||
kind: 'attach', corner: 'bottomLeft', offsetX: 0, offsetY: -24, w: 76, h: 24,
|
kind: 'attach', targetKind: 'sprite', corner: 'bottomLeft', offsetX: 0, offsetY: -24, w: 76, h: 24,
|
||||||
bySeat: {
|
bySeat: {
|
||||||
SELF: { target: 'PLAY_SELF_1' },
|
SELF: { target: 'PLAY_SELF_1' },
|
||||||
LEFT: { target: 'PLAY_LEFT_1' },
|
LEFT: { target: 'PLAY_LEFT_1' },
|
||||||
@@ -82,7 +82,8 @@ EQW_Layout.PLAY_TYPE_LABEL = {
|
|||||||
//EQW_LayoutSolver.solve——这是有意的动态目标模式,不在配置里预置 target(清单未给尺寸;
|
//EQW_LayoutSolver.solve——这是有意的动态目标模式,不在配置里预置 target(清单未给尺寸;
|
||||||
//两种资源分别是 MARK_BANKER_CORNER 28×28 / MARK_JOKER_BIG 34×34,按实际显示的是哪个徽标取值)
|
//两种资源分别是 MARK_BANKER_CORNER 28×28 / MARK_JOKER_BIG 34×34,按实际显示的是哪个徽标取值)
|
||||||
EQW_Layout.PLAY_CARD_CORNER_MARK = {
|
EQW_Layout.PLAY_CARD_CORNER_MARK = {
|
||||||
kind: 'attach', corner: 'topRight', offsetX: -28, offsetY: 0
|
kind: 'attach', targetKind: 'sprite', corner: 'topRight', offsetX: -28, offsetY: 0,
|
||||||
|
runtime: ['target', 'w', 'h']
|
||||||
};
|
};
|
||||||
|
|
||||||
//冲关牌(叠加层):精灵复制场景(§5.6a),spacingMin 放宽到 -88 以容纳 20+ 张的极端情况。
|
//冲关牌(叠加层):精灵复制场景(§5.6a),spacingMin 放宽到 -88 以容纳 20+ 张的极端情况。
|
||||||
|
|||||||
@@ -9,45 +9,70 @@ var EQW_Layout = EQW_Layout || {};
|
|||||||
//两层嵌套:类别竖排(ROOM_CATEGORY_COLUMN)+ 类别内选项槽横排(ROOM_OPTION_ROW)。
|
//两层嵌套:类别竖排(ROOM_CATEGORY_COLUMN)+ 类别内选项槽横排(ROOM_OPTION_ROW)。
|
||||||
//本节数值按参考图比例估,实际以平台建房界面尺寸为准(清单 §6.9b 原文,不是本文件误差)。
|
//本节数值按参考图比例估,实际以平台建房界面尺寸为准(清单 §6.9b 原文,不是本文件误差)。
|
||||||
|
|
||||||
//类别竖排。itemWidth 清单未给——类别行宽度等于平台内容区实际宽度,由调用方按实际面板尺寸处理
|
//类别竖排。itemWidth 清单未给——类别行宽度等于平台内容区实际宽度,由调用方按实际面板尺寸注入。
|
||||||
|
//anchor 必须是竖排语义的 top/bottom/center:写成水平语义的 'left' 会被框架 switch 的 default
|
||||||
|
//当成居中(3 个类别时算出 y=[-56,-16,24],前两行跑到画布外),求解器现已按方向校验并抛错
|
||||||
EQW_Layout.ROOM_CATEGORY_COLUMN = {
|
EQW_Layout.ROOM_CATEGORY_COLUMN = {
|
||||||
kind: 'line', direction: 'vertical', anchorX: 0, anchorY: 0,
|
kind: 'line', direction: 'vertical', anchorX: 0, anchorY: 0,
|
||||||
itemHeight: 32, spacing: 8, anchor: 'left'
|
itemHeight: 32, spacing: 8, anchor: 'top',
|
||||||
|
runtime: ['itemWidth']
|
||||||
};
|
};
|
||||||
|
|
||||||
//类别标题:贴所在类别行。该行是 ROOM_CATEGORY_COLUMN 求解结果的第 N 项,不是固定精灵,
|
//类别标题:贴所在类别行。该行是 ROOM_CATEGORY_COLUMN 求解结果的第 N 项,不是固定精灵,
|
||||||
//target 由调用方按类别序号注入。资源 ROOM_CAT_TITLE 60×24(清单 §3)
|
//target 由调用方按类别序号注入。资源 ROOM_CAT_TITLE 60×24(清单 §3)
|
||||||
EQW_Layout.ROOM_CATEGORY_TITLE = { kind: 'attach', hAlign: 'left', vAlign: 'middle', offsetX: 0, w: 60, h: 24 };
|
EQW_Layout.ROOM_CATEGORY_TITLE = {
|
||||||
|
kind: 'attach', targetKind: 'layout', hAlign: 'left', vAlign: 'middle', offsetX: 0, w: 60, h: 24,
|
||||||
|
runtime: ['target']
|
||||||
|
};
|
||||||
|
|
||||||
//类别内选项槽横排。anchorY 清单未给(随所在类别行的 y 浮动,由调用方按该行位置注入);
|
//类别内选项槽横排。anchorY 清单未给(随所在类别行的 y 浮动,由调用方按该行位置注入);
|
||||||
//itemHeight 清单未给,沿用 ROOM_CATEGORY_COLUMN.itemHeight(同一行内选择框与文字纵向居中)
|
//itemHeight 清单未给,沿用 ROOM_CATEGORY_COLUMN.itemHeight(同一行内选择框与文字纵向居中)
|
||||||
EQW_Layout.ROOM_OPTION_ROW = {
|
EQW_Layout.ROOM_OPTION_ROW = {
|
||||||
kind: 'line', direction: 'horizontal', anchorX: 70,
|
kind: 'line', direction: 'horizontal', anchorX: 70,
|
||||||
itemWidth: 150, spacing: 0, anchor: 'left'
|
itemWidth: 150, itemHeight: EQW_Layout.ROOM_CATEGORY_COLUMN.itemHeight, spacing: 0, anchor: 'left',
|
||||||
|
runtime: ['anchorY']
|
||||||
};
|
};
|
||||||
|
|
||||||
//选项组之间的额外间隔(同类别下两个选项组之间比组内选项多留的间距)
|
//选项组之间的额外间隔(同类别下两个选项组之间比组内选项多留的间距)
|
||||||
EQW_Layout.ROOM_OPTION_GROUP_GAP = 40;
|
EQW_Layout.ROOM_OPTION_GROUP_GAP = 40;
|
||||||
|
|
||||||
//选择框:贴所在选项槽左侧(资源 ROOM_OPT_BOX 22×22)。target 由调用方按选项槽注入
|
//选择框:贴所在选项槽左侧(资源 ROOM_OPT_BOX 22×22)。target 由调用方按选项槽注入
|
||||||
EQW_Layout.ROOM_OPTION_BOX = { kind: 'attach', hAlign: 'left', vAlign: 'middle', w: 22, h: 22 };
|
//(槽是 ROOM_OPTION_ROW 求解结果的第 N 项,故 targetKind 是 layout 而非精灵键)
|
||||||
|
EQW_Layout.ROOM_OPTION_BOX = {
|
||||||
|
kind: 'attach', targetKind: 'layout', hAlign: 'left', vAlign: 'middle', w: 22, h: 22,
|
||||||
|
runtime: ['target']
|
||||||
|
};
|
||||||
|
|
||||||
//选项文字:贴同槽选择框右侧,纯文字,清单未给宽高。
|
//选项文字:贴同槽选择框右侧(target 是该槽的 ROOM_BOX_* 精灵,由调用方按选项配置的 box 键注入),
|
||||||
|
//纯文字,清单未给宽高,由调用方按渲染出的文案尺寸注入。
|
||||||
//选中态变橙见 EQW_Layout.TEXT_STYLE.ROOM_OPT(normal / selected 两态,由调用方按选中状态二选一)
|
//选中态变橙见 EQW_Layout.TEXT_STYLE.ROOM_OPT(normal / selected 两态,由调用方按选中状态二选一)
|
||||||
EQW_Layout.ROOM_OPTION_TEXT = { kind: 'attach', hAlign: 'right', vAlign: 'middle', offsetX: 8 };
|
EQW_Layout.ROOM_OPTION_TEXT = {
|
||||||
|
kind: 'attach', targetKind: 'sprite', hAlign: 'right', vAlign: 'middle', offsetX: 8,
|
||||||
|
runtime: ['target', 'w', 'h']
|
||||||
|
};
|
||||||
|
|
||||||
//房卡附注:贴扣卡组最后一个选项(固定精灵 ROOM_BOX_CARD_2)右侧
|
//房卡附注:贴扣卡组最后一个选项(固定精灵 ROOM_BOX_CARD_2)右侧。纯文字,宽高随文案运行时注入
|
||||||
EQW_Layout.ROOM_CARD_COST_NOTE = {
|
EQW_Layout.ROOM_CARD_COST_NOTE = {
|
||||||
kind: 'attach', target: 'ROOM_BOX_CARD_2', hAlign: 'right', vAlign: 'middle', offsetX: 20,
|
kind: 'attach', targetKind: 'sprite', target: 'ROOM_BOX_CARD_2',
|
||||||
textStyle: EQW_Layout.TEXT_STYLE.ROOM_CARD_COST_NOTE
|
hAlign: 'right', vAlign: 'middle', offsetX: 20,
|
||||||
|
textStyle: EQW_Layout.TEXT_STYLE.ROOM_CARD_COST_NOTE,
|
||||||
|
runtime: ['w', 'h']
|
||||||
};
|
};
|
||||||
|
|
||||||
//折行:选项数超过 maxPerRow 时改用 grid。二七王每组最多 2 项,不会触发(清单 §6.9b 原文);
|
//折行:选项数超过 maxPerRow 时改用 grid。二七王每组最多 2 项,不会触发(清单 §6.9b 原文);
|
||||||
//清单原文是 grid{cols:maxPerRow, spacingX:0, spacingY:8, fillOrder:'row'},cols 直接取自 maxPerRow,
|
//清单原文是 grid{cols:maxPerRow, spacingX:0, spacingY:8, fillOrder:'row'},cols 直接取自 maxPerRow,
|
||||||
//故与 spacingX/spacingY/fillOrder 一并记为已定的四个字段;itemWidth / itemHeight / anchorX / anchorY
|
//故与 spacingX/spacingY/fillOrder 一并记为已定的四个字段。
|
||||||
//清单未给(该分支未被实际使用)
|
//rows 由实际选项数决定(ceil(选项数 / cols)),anchorX/anchorY 随所在类别行浮动 —— 三者都是运行时值,
|
||||||
|
//在 runtime 里显式声明;缺 rows 时求解器会抛错,不再静默返回空数组(一个矩形都不产出)。
|
||||||
|
//itemWidth / itemHeight 清单同样未给:折行摆的还是同一批选项槽,沿用 ROOM_OPTION_ROW 的槽尺寸,
|
||||||
|
//不另编一套数值;anchor 同理沿用 ROOM_OPTION_ROW 的 'left'(折行前后左边缘对齐一致)
|
||||||
EQW_Layout.ROOM_OPTION_MAX_PER_ROW = 4;
|
EQW_Layout.ROOM_OPTION_MAX_PER_ROW = 4;
|
||||||
EQW_Layout.ROOM_OPTION_OVERFLOW_GRID = { kind: 'grid', cols: 4, spacingX: 0, spacingY: 8, fillOrder: 'row' };
|
EQW_Layout.ROOM_OPTION_OVERFLOW_GRID = {
|
||||||
|
kind: 'grid', cols: EQW_Layout.ROOM_OPTION_MAX_PER_ROW, spacingX: 0, spacingY: 8, fillOrder: 'row',
|
||||||
|
itemWidth: EQW_Layout.ROOM_OPTION_ROW.itemWidth, itemHeight: EQW_Layout.ROOM_OPTION_ROW.itemHeight,
|
||||||
|
anchor: 'left',
|
||||||
|
runtime: ['rows', 'anchorX', 'anchorY']
|
||||||
|
};
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////
|
||||||
////////// EQW_RoomOptions: 建房规则选项配置(清单 §1.1)///////
|
////////// EQW_RoomOptions: 建房规则选项配置(清单 §1.1)///////
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ EQW_Layout.RESULT_BOTTOM_PANEL = { kind: 'point', x: 430, y: 140, w: 420, h: 148
|
|||||||
|
|
||||||
//底牌说明文字:贴面板底部,纯文字,清单未给宽高
|
//底牌说明文字:贴面板底部,纯文字,清单未给宽高
|
||||||
EQW_Layout.RESULT_BOTTOM_TEXT = {
|
EQW_Layout.RESULT_BOTTOM_TEXT = {
|
||||||
kind: 'attach', target: 'RESULT_BOTTOM_PANEL', hAlign: 'center', vAlign: 'bottom', offsetY: -14
|
kind: 'attach', targetKind: 'layout', target: 'RESULT_BOTTOM_PANEL',
|
||||||
|
hAlign: 'center', vAlign: 'bottom', offsetY: -14,
|
||||||
|
runtime: ['w', 'h']
|
||||||
};
|
};
|
||||||
|
|
||||||
//三家分数光晕
|
//三家分数光晕
|
||||||
@@ -24,7 +26,8 @@ EQW_Layout.RESULT_GLOW = {
|
|||||||
|
|
||||||
//三家总分大字:贴对应光晕居中。target 直接指向光晕精灵键(固定,非动态)
|
//三家总分大字:贴对应光晕居中。target 直接指向光晕精灵键(固定,非动态)
|
||||||
EQW_Layout.RESULT_SCORE_TEXT = {
|
EQW_Layout.RESULT_SCORE_TEXT = {
|
||||||
kind: 'attach', hAlign: 'center', vAlign: 'middle', textStyle: EQW_Layout.TEXT_STYLE.SCORE_BIG,
|
kind: 'attach', targetKind: 'sprite', hAlign: 'center', vAlign: 'middle',
|
||||||
|
textStyle: EQW_Layout.TEXT_STYLE.SCORE_BIG, runtime: ['w', 'h'],
|
||||||
bySeat: {
|
bySeat: {
|
||||||
LEFT: { target: 'RESULT_GLOW_LEFT' },
|
LEFT: { target: 'RESULT_GLOW_LEFT' },
|
||||||
RIGHT: { target: 'RESULT_GLOW_RIGHT' },
|
RIGHT: { target: 'RESULT_GLOW_RIGHT' },
|
||||||
@@ -34,7 +37,8 @@ EQW_Layout.RESULT_SCORE_TEXT = {
|
|||||||
|
|
||||||
//三家『牌局分』:贴对应光晕左下
|
//三家『牌局分』:贴对应光晕左下
|
||||||
EQW_Layout.RESULT_JF_TEXT = {
|
EQW_Layout.RESULT_JF_TEXT = {
|
||||||
kind: 'attach', hAlign: 'left', vAlign: 'bottom', offsetX: -15, offsetY: 24,
|
kind: 'attach', targetKind: 'sprite', hAlign: 'left', vAlign: 'bottom', offsetX: -15, offsetY: 24,
|
||||||
|
runtime: ['w', 'h'],
|
||||||
bySeat: {
|
bySeat: {
|
||||||
LEFT: { target: 'RESULT_GLOW_LEFT' },
|
LEFT: { target: 'RESULT_GLOW_LEFT' },
|
||||||
RIGHT: { target: 'RESULT_GLOW_RIGHT' },
|
RIGHT: { target: 'RESULT_GLOW_RIGHT' },
|
||||||
@@ -44,7 +48,8 @@ EQW_Layout.RESULT_JF_TEXT = {
|
|||||||
|
|
||||||
//三家『冲关分』:贴对应光晕右下
|
//三家『冲关分』:贴对应光晕右下
|
||||||
EQW_Layout.RESULT_AW_TEXT = {
|
EQW_Layout.RESULT_AW_TEXT = {
|
||||||
kind: 'attach', hAlign: 'right', vAlign: 'bottom', offsetX: 15, offsetY: 24,
|
kind: 'attach', targetKind: 'sprite', hAlign: 'right', vAlign: 'bottom', offsetX: 15, offsetY: 24,
|
||||||
|
runtime: ['w', 'h'],
|
||||||
bySeat: {
|
bySeat: {
|
||||||
LEFT: { target: 'RESULT_GLOW_LEFT' },
|
LEFT: { target: 'RESULT_GLOW_LEFT' },
|
||||||
RIGHT: { target: 'RESULT_GLOW_RIGHT' },
|
RIGHT: { target: 'RESULT_GLOW_RIGHT' },
|
||||||
|
|||||||
@@ -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)
|
//顶部 · 花色图标:贴信息条内部左下角(资源 SUIT_ICON_S 30×30,清单 §3)
|
||||||
EQW_Layout.TOP_SUIT_ICON = {
|
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
|
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)。
|
//顶部 · 叫分角标底:贴叫分数值文字右上角(资源 BADGE_MULTIPLE 帧1 34×18)。
|
||||||
//角标数值文字复用同一矩形,不单独配置(清单未给独立位置,下同)
|
//角标数值文字复用同一矩形,不单独配置(清单未给独立位置,下同)
|
||||||
EQW_Layout.TOP_CALL_BADGE_BG = {
|
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)
|
//顶部 · 抓分角标底:贴抓分数值文字右上角(资源 BADGE_MULTIPLE 帧2 34×18)
|
||||||
EQW_Layout.TOP_GRADE_BADGE_BG = {
|
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 在内的所有字段,这里利用该机制表达「底栏庄标是固定坐标、不贴附任何目标」,
|
//会合并包括 kind 在内的所有字段,这里利用该机制表达「底栏庄标是固定坐标、不贴附任何目标」,
|
||||||
//与 LEFT/RIGHT 两个上家贴头像框(attach)是不同的定位方式,不是笔误
|
//与 LEFT/RIGHT 两个上家贴头像框(attach)是不同的定位方式,不是笔误
|
||||||
EQW_Layout.P_BANKER_MARK = {
|
EQW_Layout.P_BANKER_MARK = {
|
||||||
kind: 'attach', w: 40, h: 40,
|
kind: 'attach', targetKind: 'platform', w: 40, h: 40,
|
||||||
bySeat: {
|
bySeat: {
|
||||||
LEFT: { target: 'P_LEFT_AVATAR', corner: 'topRight', offsetX: 7, offsetY: 8 },
|
LEFT: { target: 'P_LEFT_AVATAR', corner: 'topRight', offsetX: 7, offsetY: 8 },
|
||||||
RIGHT: { target: 'P_RIGHT_AVATAR', corner: 'topLeft', offsetX: -40, 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 实测值
|
//『主N』角标底:仅左右两家(自己主/对数已在手牌区可见,无此角标)。w/h 为清单 §6.6 实测值
|
||||||
EQW_Layout.P_ZHU_BADGE = {
|
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: {
|
bySeat: {
|
||||||
LEFT: { target: 'P_LEFT_AVATAR' },
|
LEFT: { target: 'P_LEFT_AVATAR' },
|
||||||
RIGHT: { target: 'P_RIGHT_AVATAR' }
|
RIGHT: { target: 'P_RIGHT_AVATAR' }
|
||||||
@@ -101,7 +110,7 @@ EQW_Layout.P_ZHU_BADGE = {
|
|||||||
//『对N』角标底:贴主N角标右侧。w/h 清单未重复给出,沿用主N角标同尺寸
|
//『对N』角标底:贴主N角标右侧。w/h 清单未重复给出,沿用主N角标同尺寸
|
||||||
//(同资源 BADGE_ZHU_PAIR 的另一帧,帧1黄底=主N、帧2白底=对N,尺寸一致)
|
//(同资源 BADGE_ZHU_PAIR 的另一帧,帧1黄底=主N、帧2白底=对N,尺寸一致)
|
||||||
EQW_Layout.P_PAIR_BADGE = {
|
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
|
offsetX: 44, offsetY: 0, w: 40, h: 19
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,7 +136,7 @@ EQW_Layout.P_COUNTDOWN = {
|
|||||||
//提示气泡(踩/没分/有分):朝向 arrowRight 是配置项,不按座位硬判(清单 §6.6)。
|
//提示气泡(踩/没分/有分):朝向 arrowRight 是配置项,不按座位硬判(清单 §6.6)。
|
||||||
//w/h 清单 §3 标『待定』,此处用示意尺寸占位,资源定稿后回填(不影响 kind/字段结构)
|
//w/h 清单 §3 标『待定』,此处用示意尺寸占位,资源定稿后回填(不影响 kind/字段结构)
|
||||||
EQW_Layout.TISHI_BUBBLE = {
|
EQW_Layout.TISHI_BUBBLE = {
|
||||||
kind: 'attach', w: 96, h: 44,
|
kind: 'attach', targetKind: 'platform', w: 96, h: 44,
|
||||||
bySeat: {
|
bySeat: {
|
||||||
LEFT: { target: 'P_LEFT_AVATAR', hAlign: 'right', vAlign: 'middle', offsetX: 10, arrowRight: false },
|
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 },
|
RIGHT: { target: 'P_RIGHT_AVATAR', hAlign: 'left', vAlign: 'middle', offsetX: -10, arrowRight: true },
|
||||||
|
|||||||
@@ -7,22 +7,36 @@
|
|||||||
//
|
//
|
||||||
// solve(node, ctx) 是【纯函数】:不碰精灵、不查引擎,故可完整单测。
|
// solve(node, ctx) 是【纯函数】:不碰精灵、不查引擎,故可完整单测。
|
||||||
// node: 一份布局配置(五型之一,可含 bySeat)
|
// node: 一份布局配置(五型之一,可含 bySeat)
|
||||||
// ctx : { seat, count, rects }
|
// ctx : { seat, count, rects, <运行时注入字段> }
|
||||||
// seat —— 'SELF'|'LEFT'|'RIGHT',仅 bySeat 需要
|
// seat —— 'SELF'|'LEFT'|'RIGHT',仅 bySeat 需要
|
||||||
// count —— 运行时项数(清单 §6.1:count 由数据给出、不进配置)
|
// count —— 运行时项数(清单 §6.1:count 由数据给出、不进配置)
|
||||||
// rects —— { 键名: {x,y,width,height} },仅 attach 需要
|
// rects —— { 键名: {x,y,width,height} },仅 attach 需要
|
||||||
// 返回恒为数组,point / attach 长度为 1。
|
// 返回恒为数组,point / attach 长度为 1。
|
||||||
//
|
//
|
||||||
|
// 【运行时注入】配置里写不出固定值的字段(动态文字宽高、贴哪张牌、随数据变化的行数/锚点…)
|
||||||
|
// 一律留空,由调用方在 ctx 里补上;可注入字段见 INJECT_KEYS,优先级恒为 ctx > bySeat > base。
|
||||||
|
// 布局节点用 runtime: ['target','w',…] 显式声明自己有哪些字段延后到运行时——那是给人和
|
||||||
|
// 机械守卫看的契约(区分「忘了写」与「故意留空」),求解器本身对所有 INJECT_KEYS 一视同仁。
|
||||||
|
//
|
||||||
// 参数名一律沿用框架 AlignmentUtils 的入参名(清单 §6.0),配置可原样喂入。
|
// 参数名一律沿用框架 AlignmentUtils 的入参名(清单 §6.0),配置可原样喂入。
|
||||||
// 精灵锚点恒在左上角,配置里的 anchorX/anchorY 是【对齐基准点】,不是精灵左上角。
|
// 精灵锚点恒在左上角,配置里的 anchorX/anchorY 是【对齐基准点】,不是精灵左上角。
|
||||||
// 出错一律抛异常,不返回 {x:0,y:0} 之类的兜底值(工程总则 §7 显式失败)。
|
// 出错一律抛异常,不返回 {x:0,y:0} 之类的兜底值(工程总则 §7 显式失败)。
|
||||||
var EQW_LayoutSolver = EQW_LayoutSolver || {
|
var EQW_LayoutSolver = EQW_LayoutSolver || {
|
||||||
|
|
||||||
|
//可由 ctx 运行时注入的配置字段(与布局节点 runtime 声明里允许出现的键名同一份清单)
|
||||||
|
INJECT_KEYS: ['target', 'x', 'y', 'w', 'h', 'anchorX', 'anchorY', 'rows', 'itemWidth', 'itemHeight'],
|
||||||
|
|
||||||
|
//各方向合法的对齐基准:框架 AlignmentUtils 的 switch 对不认识的 anchor 一律落到
|
||||||
|
//default(居中),写错方向的 anchor(如竖排写 'left')会被静默吞掉、界面偏到画外也不报错。
|
||||||
|
//故在此按方向白名单校验,把笔误挡在求解入口(工程总则 §7 显式失败)
|
||||||
|
ANCHORS_HORIZONTAL: ['left', 'right', 'center'],
|
||||||
|
ANCHORS_VERTICAL: ['top', 'bottom', 'center'],
|
||||||
|
|
||||||
//——— 对外:求解 ———
|
//——— 对外:求解 ———
|
||||||
solve: function (node, ctx) {
|
solve: function (node, ctx) {
|
||||||
if (!node) { throw new Error('[EQW_LayoutSolver] node 为空'); }
|
if (!node) { throw new Error('[EQW_LayoutSolver] node 为空'); }
|
||||||
ctx = ctx || {};
|
ctx = ctx || {};
|
||||||
var n = this._mergeBySeat(node, ctx.seat);
|
var n = this._mergeCtx(this._mergeBySeat(node, ctx.seat), ctx);
|
||||||
|
|
||||||
switch (n.kind) {
|
switch (n.kind) {
|
||||||
case 'point': return this._solvePoint(n);
|
case 'point': return this._solvePoint(n);
|
||||||
@@ -35,14 +49,69 @@ var EQW_LayoutSolver = EQW_LayoutSolver || {
|
|||||||
},
|
},
|
||||||
|
|
||||||
//——— 对外:求解并摆精灵(唯一触碰 SpriteManager 之处)———
|
//——— 对外:求解并摆精灵(唯一触碰 SpriteManager 之处)———
|
||||||
|
//精灵个数与求解出的矩形数必须一一对应:数量不等时显式抛错,【不静默截断】——
|
||||||
|
//少摆的那几个精灵会停在上一手牌的旧坐标上,是最难反查的一类错位(工程总则 §7 显式失败)
|
||||||
apply: function (node, spriteIds, ctx) {
|
apply: function (node, spriteIds, ctx) {
|
||||||
var rects = this.solve(node, ctx);
|
var rects = this.solve(node, ctx);
|
||||||
for (var i = 0; i < spriteIds.length && i < rects.length; i++) {
|
if (!spriteIds || spriteIds.length !== rects.length) {
|
||||||
|
throw new Error('[EQW_LayoutSolver] apply 精灵数与矩形数不一致: spriteIds=' +
|
||||||
|
(spriteIds ? spriteIds.length : 0) + ' rects=' + rects.length);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < spriteIds.length; i++) {
|
||||||
SpriteManager.setPosition(spriteIds[i], rects[i].x, rects[i].y);
|
SpriteManager.setPosition(spriteIds[i], rects[i].x, rects[i].y);
|
||||||
}
|
}
|
||||||
return rects;
|
return rects;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
//——— ctx 运行时注入:ctx 里给出的 INJECT_KEYS 字段覆盖配置同名字段 ———
|
||||||
|
_mergeCtx: function (node, ctx) {
|
||||||
|
var hit = false;
|
||||||
|
var k, i;
|
||||||
|
for (i = 0; i < this.INJECT_KEYS.length; i++) {
|
||||||
|
if (typeof ctx[this.INJECT_KEYS[i]] !== 'undefined') { hit = true; break; }
|
||||||
|
}
|
||||||
|
if (!hit) { return node; }
|
||||||
|
|
||||||
|
var merged = {};
|
||||||
|
for (k in node) {
|
||||||
|
if (node.hasOwnProperty(k)) { merged[k] = node[k]; }
|
||||||
|
}
|
||||||
|
for (i = 0; i < this.INJECT_KEYS.length; i++) {
|
||||||
|
k = this.INJECT_KEYS[i];
|
||||||
|
if (typeof ctx[k] !== 'undefined') { merged[k] = ctx[k]; }
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
},
|
||||||
|
|
||||||
|
//——— 对齐基准点校验:line/fan/grid 都要 anchorX/anchorY ———
|
||||||
|
//框架 AlignmentUtils 内部是 options.anchorY || 0,缺失会被静默当成 0(整排贴到画布顶边),
|
||||||
|
//故在此显式要求;写不出固定值的(如随类别行浮动的 anchorY)用 runtime 声明 + ctx 注入
|
||||||
|
_requireAnchorPoint: function (n) {
|
||||||
|
if (typeof n.anchorX !== 'number' || typeof n.anchorY !== 'number') {
|
||||||
|
throw new Error('[EQW_LayoutSolver] ' + n.kind + ' 需要数字 anchorX/anchorY:配置未写,' +
|
||||||
|
'且 ctx.anchorX/ctx.anchorY 未给出(anchorX=' + n.anchorX + ' anchorY=' + n.anchorY + ')');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//——— 方向 × 对齐基准校验 ———
|
||||||
|
_checkAnchor: function (direction, anchor) {
|
||||||
|
var allowed;
|
||||||
|
if (direction === 'vertical') {
|
||||||
|
allowed = this.ANCHORS_VERTICAL;
|
||||||
|
} else if (direction === 'horizontal' || typeof direction === 'undefined') {
|
||||||
|
allowed = this.ANCHORS_HORIZONTAL; //不写 direction 等同水平(框架默认)
|
||||||
|
} else {
|
||||||
|
throw new Error('[EQW_LayoutSolver] 未知 direction: ' + direction);
|
||||||
|
}
|
||||||
|
//不写 anchor 等同框架默认 center,两个方向都合法;写了就必须与方向匹配
|
||||||
|
if (typeof anchor === 'undefined') { return; }
|
||||||
|
for (var i = 0; i < allowed.length; i++) {
|
||||||
|
if (allowed[i] === anchor) { return; }
|
||||||
|
}
|
||||||
|
throw new Error('[EQW_LayoutSolver] direction=' + (direction || 'horizontal') +
|
||||||
|
' 不接受 anchor=' + anchor + ',只能是 ' + allowed.join('/'));
|
||||||
|
},
|
||||||
|
|
||||||
//——— bySeat 合并:bySeat[seat] 覆盖外层 base,未列出的字段继承(清单 §6.2)———
|
//——— bySeat 合并:bySeat[seat] 覆盖外层 base,未列出的字段继承(清单 §6.2)———
|
||||||
_mergeBySeat: function (node, seat) {
|
_mergeBySeat: function (node, seat) {
|
||||||
if (!node.bySeat) { return node; }
|
if (!node.bySeat) { return node; }
|
||||||
@@ -61,12 +130,20 @@ var EQW_LayoutSolver = EQW_LayoutSolver || {
|
|||||||
return merged;
|
return merged;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
//point:定点。x/y 缺失即无法定位,显式抛错——不返回 {x:undefined} 让 NaN 流到界面上
|
||||||
|
//(x/y 写不出固定值的节点,用 runtime 声明并由 ctx.x/ctx.y 注入)
|
||||||
_solvePoint: function (n) {
|
_solvePoint: function (n) {
|
||||||
|
if (typeof n.x !== 'number' || typeof n.y !== 'number') {
|
||||||
|
throw new Error('[EQW_LayoutSolver] point 需要数字 x/y:配置未写,且 ctx.x/ctx.y 未给出(x=' +
|
||||||
|
n.x + ' y=' + n.y + ')');
|
||||||
|
}
|
||||||
return [{ x: n.x, y: n.y, width: n.w, height: n.h }];
|
return [{ x: n.x, y: n.y, width: n.w, height: n.h }];
|
||||||
},
|
},
|
||||||
|
|
||||||
//line:等距排列。带 items 时逐项取宽(itemWidth 失效),此时不能走 distribute(它假设等宽)
|
//line:等距排列。带 items 时逐项取宽(itemWidth 失效),此时不能走 distribute(它假设等宽)
|
||||||
_solveLine: function (n, ctx) {
|
_solveLine: function (n, ctx) {
|
||||||
|
this._checkAnchor(n.direction, n.anchor);
|
||||||
|
this._requireAnchorPoint(n);
|
||||||
if (n.items) { return this._solveLineItems(n); }
|
if (n.items) { return this._solveLineItems(n); }
|
||||||
|
|
||||||
var count = ctx.count;
|
var count = ctx.count;
|
||||||
@@ -87,7 +164,11 @@ var EQW_LayoutSolver = EQW_LayoutSolver || {
|
|||||||
},
|
},
|
||||||
|
|
||||||
//line + items:逐项不等宽(底栏功能钮组、埋牌操作条等,清单 §6.1)
|
//line + items:逐项不等宽(底栏功能钮组、埋牌操作条等,清单 §6.1)
|
||||||
|
//只实现了水平方向(逐项取宽、沿 x 推进);竖排逐项不等高暂无用例,显式拒绝而不是按水平算
|
||||||
_solveLineItems: function (n) {
|
_solveLineItems: function (n) {
|
||||||
|
if (n.direction === 'vertical') {
|
||||||
|
throw new Error('[EQW_LayoutSolver] line + items 仅支持 direction=horizontal');
|
||||||
|
}
|
||||||
var items = n.items;
|
var items = n.items;
|
||||||
var spacing = n.spacing || 0;
|
var spacing = n.spacing || 0;
|
||||||
var i, total = 0;
|
var i, total = 0;
|
||||||
@@ -112,6 +193,8 @@ var EQW_LayoutSolver = EQW_LayoutSolver || {
|
|||||||
|
|
||||||
//fan:张数可变、总宽受限、动态压缩间距(清单 §6.1,间距算法【唯一实现】)
|
//fan:张数可变、总宽受限、动态压缩间距(清单 §6.1,间距算法【唯一实现】)
|
||||||
_solveFan: function (n, ctx) {
|
_solveFan: function (n, ctx) {
|
||||||
|
this._checkAnchor(n.direction, n.anchor);
|
||||||
|
this._requireAnchorPoint(n);
|
||||||
var count = ctx.count;
|
var count = ctx.count;
|
||||||
if (typeof count !== 'number') { throw new Error('[EQW_LayoutSolver] fan 需要 ctx.count'); }
|
if (typeof count !== 'number') { throw new Error('[EQW_LayoutSolver] fan 需要 ctx.count'); }
|
||||||
if (count <= 0) { return []; }
|
if (count <= 0) { return []; }
|
||||||
@@ -141,6 +224,15 @@ var EQW_LayoutSolver = EQW_LayoutSolver || {
|
|||||||
if (n.fillOrder && n.fillOrder !== 'row') {
|
if (n.fillOrder && n.fillOrder !== 'row') {
|
||||||
throw new Error('[EQW_LayoutSolver] grid 仅支持 fillOrder=row,暂不支持: ' + n.fillOrder);
|
throw new Error('[EQW_LayoutSolver] grid 仅支持 fillOrder=row,暂不支持: ' + n.fillOrder);
|
||||||
}
|
}
|
||||||
|
//cols/rows 缺一即容量为 NaN:超载守卫失效(count > NaN 恒为 false)、循环一次都不跑、
|
||||||
|
//静默返回空数组——摆不下与「一个都没摆」同样必须显式失败(工程总则 §7)。
|
||||||
|
//rows 允许由 ctx.rows 运行时注入(行数随实际项数而定的场景),此处校验的是合并后的有效值
|
||||||
|
if (typeof n.cols !== 'number' || typeof n.rows !== 'number') {
|
||||||
|
throw new Error('[EQW_LayoutSolver] grid 需要数字 cols/rows:配置未写,且 ctx.rows 未给出(cols=' +
|
||||||
|
n.cols + ' rows=' + n.rows + ')');
|
||||||
|
}
|
||||||
|
this._checkAnchor('horizontal', n.anchor); //grid 逐行走水平分布,anchor 同水平语义
|
||||||
|
this._requireAnchorPoint(n);
|
||||||
|
|
||||||
var capacity = n.cols * n.rows;
|
var capacity = n.cols * n.rows;
|
||||||
var count = (typeof ctx.count === 'number') ? ctx.count : capacity;
|
var count = (typeof ctx.count === 'number') ? ctx.count : capacity;
|
||||||
@@ -173,17 +265,16 @@ var EQW_LayoutSolver = EQW_LayoutSolver || {
|
|||||||
//attach:贴在另一个精灵上。corner 为空 = 在目标【内部】对齐;有值 = 与目标对应角重合
|
//attach:贴在另一个精灵上。corner 为空 = 在目标【内部】对齐;有值 = 与目标对应角重合
|
||||||
//target/w/h 都支持运行时注入:叫分档位贴自己的按钮、花色图标贴自己的按钮、牌角标贴"打出的那张牌",
|
//target/w/h 都支持运行时注入:叫分档位贴自己的按钮、花色图标贴自己的按钮、牌角标贴"打出的那张牌",
|
||||||
//这类目标配置里写不出固定值;动态文字标签的真实宽高要等文案渲染出来才知道,配置里也留空。
|
//这类目标配置里写不出固定值;动态文字标签的真实宽高要等文案渲染出来才知道,配置里也留空。
|
||||||
//这些节点写成模板,由 ctx.target/ctx.w/ctx.h 在运行时补上(ctx.* 优先于 n.* 里的同名字段)
|
//这些节点写成模板并用 runtime 声明留空字段,由 ctx 在运行时补上(已在 solve 里合并进 n)
|
||||||
_solveAttach: function (n, ctx) {
|
_solveAttach: function (n, ctx) {
|
||||||
if (!ctx.rects) { throw new Error('[EQW_LayoutSolver] attach 需要 ctx.rects'); }
|
if (!ctx.rects) { throw new Error('[EQW_LayoutSolver] attach 需要 ctx.rects'); }
|
||||||
var targetKey = ctx.target || n.target;
|
var targetKey = n.target;
|
||||||
if (!targetKey) { throw new Error('[EQW_LayoutSolver] attach 配置未写 target,且 ctx.target 未给出'); }
|
if (!targetKey) { throw new Error('[EQW_LayoutSolver] attach 配置未写 target,且 ctx.target 未给出'); }
|
||||||
var target = ctx.rects[targetKey];
|
var target = ctx.rects[targetKey];
|
||||||
if (!target) { throw new Error('[EQW_LayoutSolver] attach 的 target 未在 ctx.rects 中: ' + targetKey); }
|
if (!target) { throw new Error('[EQW_LayoutSolver] attach 的 target 未在 ctx.rects 中: ' + targetKey); }
|
||||||
|
|
||||||
//ctx.w/ctx.h 优先于配置里的 n.w/n.h;后续的校验、求解、返回值一律用合并后的值
|
var w = n.w;
|
||||||
var w = (typeof ctx.w === 'number') ? ctx.w : n.w;
|
var h = n.h;
|
||||||
var h = (typeof ctx.h === 'number') ? ctx.h : n.h;
|
|
||||||
|
|
||||||
var sprite = { width: w, height: h };
|
var sprite = { width: w, height: h };
|
||||||
var offset = { x: n.offsetX || 0, y: n.offsetY || 0 };
|
var offset = { x: n.offsetX || 0, y: n.offsetY || 0 };
|
||||||
|
|||||||
+103
-24
@@ -57,13 +57,17 @@ t.eq('图层 ID 无重复', dupsOf(layerVals), []);
|
|||||||
t.eq('群组 ID 无重复', dupsOf(groupVals), []);
|
t.eq('群组 ID 无重复', dupsOf(groupVals), []);
|
||||||
t.eq('图片资源 ID 无重复', dupsOf(imgVals), []);
|
t.eq('图片资源 ID 无重复', dupsOf(imgVals), []);
|
||||||
|
|
||||||
// ---- 布局配置里每个 attach.target 都能解析 ----
|
// ---- attach.target 的来源判别:targetKind 指明去哪个命名空间取矩形 ----
|
||||||
// target 可指向三类之一:子游戏精灵键名 / EQW_Layout 的布局节点名 / 平台精灵键名
|
// 'sprite' 子游戏精灵键名(EQW_Sprites 索引)
|
||||||
// (平台头像框落在框架保留段 1–1000,独立登记、不参与子游戏号段校验)
|
// 'layout' EQW_Layout 的布局节点名(矩形来自该节点的求解结果,可能是其中第 N 项)
|
||||||
|
// 'platform' 平台精灵键名(如平台头像框,落在框架保留段 1–1000,不参与子游戏号段校验)
|
||||||
|
// 只在【对应的那一个】命名空间里校验存在性——查三者并集范围太宽,几乎任何标识符都能"解析"。
|
||||||
|
const NAMESPACES = { sprite: index, layout: EQW_Layout, platform: EQW_PlatformSprites };
|
||||||
|
|
||||||
const resolvable = {};
|
const resolvable = {};
|
||||||
Object.keys(index).forEach(k => { resolvable[k] = true; });
|
Object.keys(NAMESPACES).forEach(kind => {
|
||||||
Object.keys(EQW_Layout).forEach(k => { resolvable[k] = true; });
|
Object.keys(NAMESPACES[kind]).forEach(k => { resolvable[k] = true; });
|
||||||
Object.keys(EQW_PlatformSprites).forEach(k => { resolvable[k] = true; });
|
});
|
||||||
|
|
||||||
// 收集所有布局节点,供解析检查与求解冒烟共用
|
// 收集所有布局节点,供解析检查与求解冒烟共用
|
||||||
const allNodes = []; // { path, node }
|
const allNodes = []; // { path, node }
|
||||||
@@ -78,8 +82,24 @@ function collectNodes(node, path) {
|
|||||||
}
|
}
|
||||||
Object.keys(EQW_Layout).forEach(k => collectNodes(EQW_Layout[k], k));
|
Object.keys(EQW_Layout).forEach(k => collectNodes(EQW_Layout[k], k));
|
||||||
|
|
||||||
|
// 每个 attach 节点必须声明合法 targetKind;非 attach 节点不得声明(避免复制粘贴留下的无意义字段)
|
||||||
|
const kindDecl = [];
|
||||||
|
allNodes.forEach(entry => {
|
||||||
|
const tk = entry.node.targetKind;
|
||||||
|
if (entry.node.kind === 'attach') {
|
||||||
|
if (!NAMESPACES.hasOwnProperty(tk)) { kindDecl.push(entry.path + ' -> targetKind=' + tk); }
|
||||||
|
} else if (typeof tk !== 'undefined') {
|
||||||
|
kindDecl.push(entry.path + ' -> 非 attach 却声明了 targetKind=' + tk);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (kindDecl.length) { console.log('targetKind 声明有问题的节点:\n ' + kindDecl.join('\n ')); }
|
||||||
|
t.eq('attach 节点均声明合法 targetKind', kindDecl, []);
|
||||||
|
|
||||||
|
// 配置里写死的 target 必须存在于 targetKind 指定的那个命名空间
|
||||||
const missing = [];
|
const missing = [];
|
||||||
allNodes.forEach(entry => {
|
allNodes.forEach(entry => {
|
||||||
|
const ns = NAMESPACES[entry.node.targetKind];
|
||||||
|
if (!ns) { return; } // targetKind 非法已由上一条断言报出
|
||||||
const targets = [];
|
const targets = [];
|
||||||
if (entry.node.target) { targets.push([entry.path, entry.node.target]); }
|
if (entry.node.target) { targets.push([entry.path, entry.node.target]); }
|
||||||
if (entry.node.bySeat) {
|
if (entry.node.bySeat) {
|
||||||
@@ -88,10 +108,14 @@ allNodes.forEach(entry => {
|
|||||||
if (v && v.target) { targets.push([entry.path + '.bySeat.' + s, v.target]); }
|
if (v && v.target) { targets.push([entry.path + '.bySeat.' + s, v.target]); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
targets.forEach(pair => { if (!resolvable[pair[1]]) { missing.push(pair[0] + ' -> ' + pair[1]); } });
|
targets.forEach(pair => {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(ns, pair[1])) {
|
||||||
|
missing.push(pair[0] + ' -> ' + pair[1] + '(不在 ' + entry.node.targetKind + ' 命名空间里)');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (missing.length) { console.log('无法解析的 attach.target:\n ' + missing.join('\n ')); }
|
if (missing.length) { console.log('无法解析的 attach.target:\n ' + missing.join('\n ')); }
|
||||||
t.eq('attach.target 全部可解析', missing, []);
|
t.eq('attach.target 在其 targetKind 命名空间内可解析', missing, []);
|
||||||
|
|
||||||
// ---- 求解冒烟:每个布局节点都真的能被求解器算出来 ----
|
// ---- 求解冒烟:每个布局节点都真的能被求解器算出来 ----
|
||||||
// 这是配置与求解器之间契约的最强守卫——字段名写错、缺 w/h、kind 不认识、
|
// 这是配置与求解器之间契约的最强守卫——字段名写错、缺 w/h、kind 不认识、
|
||||||
@@ -103,31 +127,83 @@ load('client/js/01_SubGame/codes/ui/LayoutSolver.js');
|
|||||||
const fakeRects = {};
|
const fakeRects = {};
|
||||||
Object.keys(resolvable).forEach(k => { fakeRects[k] = { x: 100, y: 100, width: 80, height: 40 }; });
|
Object.keys(resolvable).forEach(k => { fakeRects[k] = { x: 100, y: 100, width: 80, height: 40 }; });
|
||||||
|
|
||||||
|
// 运行时注入字段的假值表:【只对节点 runtime 里显式声明过的键】注入。
|
||||||
|
// 没声明却缺字段的节点,求解器照常抛错、本守卫照常变红——这正是 runtime 声明的意义:
|
||||||
|
// 区分「故意延后到运行时」与「忘了写」,守卫不再无差别地自动补齐缺失字段。
|
||||||
|
const RUNTIME_FAKE = {
|
||||||
|
target: 'TOP_INFO_BG', x: 10, y: 20, w: 60, h: 24,
|
||||||
|
anchorX: 30, anchorY: 40, rows: 2, itemWidth: 150, itemHeight: 32
|
||||||
|
};
|
||||||
|
// 求解器新开了注入通道却忘了给假值 → 这里立刻红
|
||||||
|
t.eq('运行时假值表覆盖全部可注入字段',
|
||||||
|
EQW_LayoutSolver.INJECT_KEYS.filter(k => !RUNTIME_FAKE.hasOwnProperty(k)), []);
|
||||||
|
t.eq('假值表里的 target 是真实可解析键', resolvable[RUNTIME_FAKE.target], true);
|
||||||
|
|
||||||
|
// bySeat 覆盖后的有效配置(bySeat 可覆盖包括 kind 在内的任意字段)
|
||||||
|
function effectiveNode(node, seat) {
|
||||||
|
if (!node.bySeat || !seat) { return node; }
|
||||||
|
const out = {};
|
||||||
|
Object.keys(node).forEach(k => { if (k !== 'bySeat') { out[k] = node[k]; } });
|
||||||
|
Object.keys(node.bySeat[seat]).forEach(k => { out[k] = node.bySeat[seat][k]; });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// runtime 声明本身要合法:必须是字符串数组、键名在求解器的可注入清单里、
|
||||||
|
// 且该字段在配置里确实留空(配置已经写了值还声明 runtime,说明声明是过期的)
|
||||||
|
const runtimeDecl = [];
|
||||||
|
allNodes.forEach(entry => {
|
||||||
|
const seats = entry.node.bySeat ? Object.keys(entry.node.bySeat) : [null];
|
||||||
|
seats.forEach(seat => {
|
||||||
|
const eff = effectiveNode(entry.node, seat);
|
||||||
|
const label = entry.path + (seat ? '[' + seat + ']' : '');
|
||||||
|
if (typeof eff.runtime === 'undefined') { return; }
|
||||||
|
if (!Array.isArray(eff.runtime)) { runtimeDecl.push(label + ' -> runtime 不是数组'); return; }
|
||||||
|
eff.runtime.forEach(k => {
|
||||||
|
if (EQW_LayoutSolver.INJECT_KEYS.indexOf(k) < 0) {
|
||||||
|
runtimeDecl.push(label + ' -> runtime 含不可注入的字段: ' + k);
|
||||||
|
} else if (typeof eff[k] !== 'undefined') {
|
||||||
|
runtimeDecl.push(label + ' -> runtime 声明了 ' + k + ',但配置里已写了值');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (runtimeDecl.length) { console.log('runtime 声明有问题的节点:\n ' + runtimeDecl.join('\n ')); }
|
||||||
|
t.eq('runtime 声明合法且确实留空', runtimeDecl, []);
|
||||||
|
|
||||||
|
const SMOKE_COUNT = 8;
|
||||||
const solveFailures = [];
|
const solveFailures = [];
|
||||||
allNodes.forEach(entry => {
|
allNodes.forEach(entry => {
|
||||||
// 有 bySeat 的按三个显示位各求一次,没有的求一次
|
// 有 bySeat 的按三个显示位各求一次,没有的求一次
|
||||||
const seats = entry.node.bySeat ? Object.keys(entry.node.bySeat) : [null];
|
const seats = entry.node.bySeat ? Object.keys(entry.node.bySeat) : [null];
|
||||||
seats.forEach(seat => {
|
seats.forEach(seat => {
|
||||||
const ctx = { rects: fakeRects, count: 8 };
|
const eff = effectiveNode(entry.node, seat);
|
||||||
|
const ctx = { rects: fakeRects };
|
||||||
if (seat) { ctx.seat = seat; }
|
if (seat) { ctx.seat = seat; }
|
||||||
// 无 target/w/h 的 attach 模板节点:运行时由调用方注入,这里补假值——
|
(eff.runtime || []).forEach(k => { ctx[k] = RUNTIME_FAKE[k]; });
|
||||||
// 只在配置(含 bySeat 覆盖后)确实没写时才补,不覆盖配置里已有的值,
|
|
||||||
// 否则冒烟就测不到「配置自带尺寸」那条真实路径
|
// 期望产出多少个矩形——只断言"是数组、坐标是数字"的话,返回空数组也照样通过,
|
||||||
const seatOverride = seat && entry.node.bySeat[seat] ? entry.node.bySeat[seat] : null;
|
// 「少画了几张牌」这类缺陷对守卫完全不可见(曾经 grid 缺 rows 就是这么漏过去的)
|
||||||
const seatTarget = seatOverride ? seatOverride.target : null;
|
let expected;
|
||||||
const seatW = seatOverride ? seatOverride.w : undefined;
|
if (eff.kind === 'grid') {
|
||||||
const seatH = seatOverride ? seatOverride.h : undefined;
|
const rows = (typeof ctx.rows === 'number') ? ctx.rows : eff.rows;
|
||||||
const effW = (seatW !== undefined) ? seatW : entry.node.w;
|
const capacity = eff.cols * rows;
|
||||||
const effH = (seatH !== undefined) ? seatH : entry.node.h;
|
ctx.count = Math.min(SMOKE_COUNT, capacity); // 不制造超载(超载抛错另有专门用例)
|
||||||
if (entry.node.kind === 'attach' && !entry.node.target && !seatTarget) {
|
expected = ctx.count;
|
||||||
ctx.target = Object.keys(fakeRects)[0];
|
} else if (eff.kind === 'line' || eff.kind === 'fan') {
|
||||||
|
ctx.count = SMOKE_COUNT;
|
||||||
|
expected = eff.items ? eff.items.length : SMOKE_COUNT;
|
||||||
|
} else {
|
||||||
|
expected = 1; // point / attach 恒为 1
|
||||||
}
|
}
|
||||||
if (entry.node.kind === 'attach' && typeof effW !== 'number') { ctx.w = 60; }
|
|
||||||
if (entry.node.kind === 'attach' && typeof effH !== 'number') { ctx.h = 24; }
|
|
||||||
const label = entry.path + (seat ? '[' + seat + ']' : '');
|
const label = entry.path + (seat ? '[' + seat + ']' : '');
|
||||||
try {
|
try {
|
||||||
const r = EQW_LayoutSolver.solve(entry.node, ctx);
|
const r = EQW_LayoutSolver.solve(entry.node, ctx);
|
||||||
if (!Array.isArray(r)) { solveFailures.push(label + ' -> 返回值不是数组'); return; }
|
if (!Array.isArray(r)) { solveFailures.push(label + ' -> 返回值不是数组'); return; }
|
||||||
|
if (r.length !== expected) {
|
||||||
|
solveFailures.push(label + ' -> 矩形数 ' + r.length + ',期望 ' + expected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
r.forEach((rect, i) => {
|
r.forEach((rect, i) => {
|
||||||
if (typeof rect.x !== 'number' || isNaN(rect.x) ||
|
if (typeof rect.x !== 'number' || isNaN(rect.x) ||
|
||||||
typeof rect.y !== 'number' || isNaN(rect.y)) {
|
typeof rect.y !== 'number' || isNaN(rect.y)) {
|
||||||
@@ -140,8 +216,11 @@ allNodes.forEach(entry => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (solveFailures.length) { console.log('求解失败的节点:\n ' + solveFailures.join('\n ')); }
|
if (solveFailures.length) { console.log('求解失败的节点:\n ' + solveFailures.join('\n ')); }
|
||||||
t.eq('全部布局节点可求解且坐标合法', solveFailures, []);
|
t.eq('全部布局节点可求解、矩形数与坐标都对', solveFailures, []);
|
||||||
t.eq('布局节点数量 > 40', allNodes.length > 40, true);
|
|
||||||
|
// 钉住节点总数:松下界(如 >40)删掉整个 View 都还能通过,任何意外的增删都要在这里可见。
|
||||||
|
// 增删布局节点时,连同本数字一起改——改动是有意的就顺手更新,不是有意的就红给你看
|
||||||
|
t.eq('布局节点总数', allNodes.length, 62);
|
||||||
|
|
||||||
// ---- 布局文件是纯数据:不含函数 ----
|
// ---- 布局文件是纯数据:不含函数 ----
|
||||||
const funcs = [];
|
const funcs = [];
|
||||||
|
|||||||
@@ -247,6 +247,92 @@ t.eq('bySeat 未知 seat 抛错', throws(() => EQW_LayoutSolver.solve(bySeat, {
|
|||||||
t.eq('node 为空抛错', throws(() => EQW_LayoutSolver.solve(null, {})), true);
|
t.eq('node 为空抛错', throws(() => EQW_LayoutSolver.solve(null, {})), true);
|
||||||
t.eq('未知 corner 抛错', throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: 'middle', w: 1, h: 1 }, { rects: rects })), true);
|
t.eq('未知 corner 抛错', throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: 'middle', w: 1, h: 1 }, { rects: rects })), true);
|
||||||
|
|
||||||
|
// ============ 方向 × 对齐基准:写错方向的 anchor 必须抛错,不被框架 default 静默吞掉 ============
|
||||||
|
// AlignmentUtils.distributeVertically 只认 top/bottom/center,'left' 会落到 default(居中)——
|
||||||
|
// 3 项 32 高 8 间距时算出 y=[-56,-16,24],前两行跑到画布外,界面上却看不出是配置写错了
|
||||||
|
const vLine = (anchor) => ({ kind: 'line', direction: 'vertical', anchorX: 0, anchorY: 0, itemWidth: 200, itemHeight: 32, spacing: 8, anchor: anchor });
|
||||||
|
const hLine = (anchor) => ({ kind: 'line', direction: 'horizontal', anchorX: 0, anchorY: 0, itemWidth: 200, itemHeight: 32, spacing: 8, anchor: anchor });
|
||||||
|
t.eq('竖排 anchor=left 抛错', throws(() => EQW_LayoutSolver.solve(vLine('left'), { count: 3 })), true);
|
||||||
|
t.eq('竖排 anchor=right 抛错', throws(() => EQW_LayoutSolver.solve(vLine('right'), { count: 3 })), true);
|
||||||
|
t.eq('竖排 anchor=top 正常', throws(() => EQW_LayoutSolver.solve(vLine('top'), { count: 3 })), false);
|
||||||
|
t.eq('竖排 anchor=bottom 正常', throws(() => EQW_LayoutSolver.solve(vLine('bottom'), { count: 3 })), false);
|
||||||
|
t.eq('竖排 anchor=center 正常', throws(() => EQW_LayoutSolver.solve(vLine('center'), { count: 3 })), false);
|
||||||
|
t.eq('竖排 不写 anchor 正常(框架默认 center)', throws(() => EQW_LayoutSolver.solve(vLine(undefined), { count: 3 })), false);
|
||||||
|
t.eq('横排 anchor=top 抛错', throws(() => EQW_LayoutSolver.solve(hLine('top'), { count: 3 })), true);
|
||||||
|
t.eq('横排 anchor=bottom 抛错', throws(() => EQW_LayoutSolver.solve(hLine('bottom'), { count: 3 })), true);
|
||||||
|
t.eq('横排 anchor=left 正常', throws(() => EQW_LayoutSolver.solve(hLine('left'), { count: 3 })), false);
|
||||||
|
t.eq('横排 anchor=right 正常', throws(() => EQW_LayoutSolver.solve(hLine('right'), { count: 3 })), false);
|
||||||
|
t.eq('未知 direction 抛错', throws(() => EQW_LayoutSolver.solve({ kind: 'line', direction: 'diagonal', anchorX: 0, anchorY: 0, itemWidth: 10, itemHeight: 10, anchor: 'left' }, { count: 3 })), true);
|
||||||
|
// fan 走同一条 distribute,校验一致
|
||||||
|
t.eq('fan 竖排 anchor=left 抛错', throws(() => EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'fan', direction: 'vertical', anchorX: 0, anchorY: 0, maxWidth: 100, itemWidth: 10, itemHeight: 10, spacingMax: 0, spacingMin: -5, anchor: 'left' }, { count: 3 })), true);
|
||||||
|
// grid 逐行走水平分布,竖排语义的 anchor 同样拒绝
|
||||||
|
t.eq('grid anchor=top 抛错', throws(() => EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'grid', anchorX: 0, anchorY: 0, cols: 2, rows: 2, itemWidth: 10, itemHeight: 10, anchor: 'top' }, { count: 4 })), true);
|
||||||
|
// line + items 只实现了水平:竖排显式拒绝,不按水平硬算
|
||||||
|
t.eq('line+items 竖排抛错', throws(() => EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'line', direction: 'vertical', anchorX: 0, anchorY: 0, itemHeight: 10, anchor: 'top', items: [{ key: 'A', width: 10 }] }, {})), true);
|
||||||
|
|
||||||
|
// ============ 对齐基准点必填:框架的 anchorY||0 会把缺失静默变成 0 ============
|
||||||
|
t.eq('line 缺 anchorY 抛错', throws(() => EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'line', direction: 'horizontal', anchorX: 0, itemWidth: 10, itemHeight: 10, anchor: 'left' }, { count: 2 })), true);
|
||||||
|
t.eq('line 由 ctx.anchorY 补上则正常', EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'line', direction: 'horizontal', anchorX: 0, itemWidth: 10, itemHeight: 10, anchor: 'left' },
|
||||||
|
{ count: 2, anchorY: 77 })[0].y, 77);
|
||||||
|
t.eq('ctx.anchorX 覆盖配置 anchorX', EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'line', direction: 'horizontal', anchorX: 0, anchorY: 0, itemWidth: 10, itemHeight: 10, anchor: 'left' },
|
||||||
|
{ count: 2, anchorX: 500 })[0].x, 500);
|
||||||
|
t.eq('fan 缺 anchorX 抛错', throws(() => EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'fan', direction: 'horizontal', anchorY: 0, maxWidth: 100, itemWidth: 10, itemHeight: 10, spacingMax: 0, spacingMin: -5, anchor: 'left' }, { count: 3 })), true);
|
||||||
|
|
||||||
|
// ============ grid:cols/rows 缺失必须抛错(曾静默返回空数组)============
|
||||||
|
// cols*undefined = NaN → count > NaN 恒 false(超载守卫失效)、for(row<undefined) 一次不跑 → 返回 []
|
||||||
|
const gridNoRows = { kind: 'grid', anchorX: 0, anchorY: 0, cols: 4, itemWidth: 10, itemHeight: 10, anchor: 'left' };
|
||||||
|
t.eq('grid 缺 rows 抛错', throws(() => EQW_LayoutSolver.solve(gridNoRows, { count: 3 })), true);
|
||||||
|
t.eq('grid 缺 cols 抛错', throws(() => EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'grid', anchorX: 0, anchorY: 0, rows: 2, itemWidth: 10, itemHeight: 10, anchor: 'left' }, { count: 3 })), true);
|
||||||
|
// ctx.rows 运行时注入:行数由实际项数决定的场景
|
||||||
|
t.eq('ctx.rows 注入后正常求解', EQW_LayoutSolver.solve(gridNoRows, { count: 6, rows: 2 }).length, 6);
|
||||||
|
t.eq('ctx.rows 参与容量计算(满格)', EQW_LayoutSolver.solve(gridNoRows, { count: 8, rows: 2 }).length, 8);
|
||||||
|
t.eq('ctx.rows 参与容量计算(超载仍抛错)', throws(() => EQW_LayoutSolver.solve(gridNoRows, { count: 9, rows: 2 })), true);
|
||||||
|
t.eq('ctx.rows 覆盖配置 rows', throws(() => EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'grid', anchorX: 0, anchorY: 0, cols: 4, rows: 4, itemWidth: 10, itemHeight: 10, anchor: 'left' },
|
||||||
|
{ count: 9, rows: 2 })), true);
|
||||||
|
|
||||||
|
// ============ point:x/y 缺失抛错 + 运行时注入 ============
|
||||||
|
t.eq('point 缺 x/y 抛错', throws(() => EQW_LayoutSolver.solve({ kind: 'point', w: 10, h: 10 }, {})), true);
|
||||||
|
t.eq('point 缺 y 抛错', throws(() => EQW_LayoutSolver.solve({ kind: 'point', x: 1, w: 10, h: 10 }, {})), true);
|
||||||
|
t.eq('point 由 ctx.x/ctx.y 注入', EQW_LayoutSolver.solve({ kind: 'point', w: 10, h: 10 }, { x: 5, y: 6 }),
|
||||||
|
[{ x: 5, y: 6, width: 10, height: 10 }]);
|
||||||
|
t.eq('point 的 ctx.w/ctx.h 也生效', EQW_LayoutSolver.solve({ kind: 'point', x: 1, y: 2 }, { w: 33, h: 44 }),
|
||||||
|
[{ x: 1, y: 2, width: 33, height: 44 }]);
|
||||||
|
|
||||||
|
// ============ 运行时注入通道清单 ============
|
||||||
|
t.eq('INJECT_KEYS 覆盖 target/x/y/w/h/anchorX/anchorY/rows/itemWidth/itemHeight',
|
||||||
|
EQW_LayoutSolver.INJECT_KEYS.slice().sort(),
|
||||||
|
['anchorX', 'anchorY', 'h', 'itemHeight', 'itemWidth', 'rows', 'target', 'w', 'x', 'y']);
|
||||||
|
t.eq('ctx.itemWidth 注入生效', EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'line', direction: 'vertical', anchorX: 0, anchorY: 0, itemHeight: 32, spacing: 8, anchor: 'top' },
|
||||||
|
{ count: 2, itemWidth: 240 })[0].width, 240);
|
||||||
|
// runtime 声明只是给人和守卫看的契约,不影响求解(求解器对所有 INJECT_KEYS 一视同仁)
|
||||||
|
t.eq('runtime 声明不影响求解', EQW_LayoutSolver.solve(
|
||||||
|
{ kind: 'point', runtime: ['x', 'y'] }, { x: 7, y: 8 }), [{ x: 7, y: 8, width: undefined, height: undefined }]);
|
||||||
|
|
||||||
|
// ============ apply:精灵数与矩形数不一致必须抛错,不静默截断 ============
|
||||||
|
// 少摆的精灵会停在上一手牌的旧坐标上(陈旧精灵 + 陈旧坐标),是最难反查的一类错位
|
||||||
|
const moved = [];
|
||||||
|
global.SpriteManager = { setPosition: function (id, x, y) { moved.push([id, x, y]); } };
|
||||||
|
const applyLine = { kind: 'line', direction: 'horizontal', anchorX: 0, anchorY: 0, itemWidth: 10, itemHeight: 10, spacing: 0, anchor: 'left' };
|
||||||
|
moved.length = 0;
|
||||||
|
EQW_LayoutSolver.apply(applyLine, [11, 12, 13], { count: 3 });
|
||||||
|
t.eq('apply 数量相等时逐个摆位', moved, [[11, 0, 0], [12, 10, 0], [13, 20, 0]]);
|
||||||
|
moved.length = 0;
|
||||||
|
t.eq('apply 精灵多于矩形抛错', throws(() => EQW_LayoutSolver.apply(applyLine, [11, 12, 13], { count: 2 })), true);
|
||||||
|
t.eq('apply 精灵少于矩形抛错', throws(() => EQW_LayoutSolver.apply(applyLine, [11, 12], { count: 3 })), true);
|
||||||
|
t.eq('apply 抛错时一个都没摆', moved, []);
|
||||||
|
t.eq('apply 未传 spriteIds 抛错', throws(() => EQW_LayoutSolver.apply(applyLine, null, { count: 3 })), true);
|
||||||
|
t.eq('apply 空数组 + 零矩形不抛错', throws(() => EQW_LayoutSolver.apply(applyLine, [], { count: 0 })), false);
|
||||||
|
|
||||||
// ============ 纯函数:不修改入参 ============
|
// ============ 纯函数:不修改入参 ============
|
||||||
const before = JSON.stringify(bySeat);
|
const before = JSON.stringify(bySeat);
|
||||||
EQW_LayoutSolver.solve(bySeat, { seat: 'SELF', count: 3 });
|
EQW_LayoutSolver.solve(bySeat, { seat: 'SELF', count: 3 });
|
||||||
|
|||||||
Reference in New Issue
Block a user