- CD_LEFT/CD_RIGHT/CD_SELF(1150-1152)由文字精灵改为多帧图数字精灵: 资源 NUM_COUNTDOWN,纯秒数无后缀;对应布局节点 P_COUNTDOWN 的 h 改取 NUM_STYLE.COUNTDOWN.charHeight,w 留到运行时注入。 - 结算得分 RESULT_SCORE_LEFT/RIGHT/SELF(1805-1807)同理由文字精灵改为 数字图精灵(NUM_RESULT_WIN/NUM_RESULT_LOSE),与大局结算已有的 ACC_TPL_SCORE_NUM 保持一致;布局节点随之调整,移除不再使用的 TEXT_STYLE.SCORE_BIG。 - NUM_CALL_SCORE 第16帧由"待美术与规则确认"改为明确的"无后缀"(叫分档位 面板只显示纯数字,"N子"角标是独立文字精灵);NUM_MAIN_SUIT_COUNT / NUM_RESULT_WIN / NUM_RESULT_LOSE 第16帧仍待确认,不改。 - Layout_Action.js 的 CALL_BTN_SCORE_TEXT、Layout_Result.js 的 RESULT_SCORE_TEXT 改名为 CALL_BTN_SCORE_NUM / RESULT_SCORE_NUM: 两者驱动的都是数字图精灵,_TEXT 后缀误导;同步测试引用。 - docs/client/development-guide/02:订正"SpriteManager 第1154行"这一失效 行号引用(回退后该行号落在 setTextWithWidth 体内),改为不写死行号。 - docs_dev 清单同步:§2.4/§5.4/§5.6/§6.6/§6.9 倒计时与结算得分的精灵类型 描述、§6.6/§6.9 对应布局参数。 - test_constants.js 的 NUM_STYLE 同源守卫表新增 COUNTDOWN、RESULT_WIN、 RESULT_LOSE 三对(连同改名后的 CALL_BTN_SCORE_NUM),共 6 条新断言。 精灵总数、布局节点总数均未变(378 / 61),只改类型与命名,不增删。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
359 lines
19 KiB
JavaScript
359 lines
19 KiB
JavaScript
// 常量层机械守卫:号段合规、ID 不重复、布局引用可解析
|
||
const { load } = require('./_load');
|
||
const t = require('./_assert')();
|
||
|
||
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/ImageResources.js');
|
||
load('client/js/01_SubGame/codes/config/SoundResources.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');
|
||
load('client/js/01_SubGame/codes/config/LayoutConstants.js');
|
||
load('client/js/01_SubGame/codes/config/Layout_Table.js');
|
||
load('client/js/01_SubGame/codes/config/Layout_Cards.js');
|
||
load('client/js/01_SubGame/codes/config/Layout_Action.js');
|
||
load('client/js/01_SubGame/codes/config/Layout_Result.js');
|
||
load('client/js/01_SubGame/codes/config/Layout_CreateRoom.js');
|
||
load('client/js/01_SubGame/codes/config/AnimConstants.js');
|
||
load('client/js/01_SubGame/codes/core/SpriteIndex.js');
|
||
|
||
// ---- 图层号段:101–200 / 301–400 / 501–600 / 701+(client 02 §1)----
|
||
// 平台图层 27(建房)是引用而非子游戏自建,单独排除
|
||
const layerVals = Object.keys(EQW_Layers).filter(k => k !== 'PLATFORM_CREATE_ROOM').map(k => EQW_Layers[k]);
|
||
const layerOk = layerVals.every(v =>
|
||
(v >= 101 && v <= 200) || (v >= 301 && v <= 400) || (v >= 501 && v <= 600) || v >= 701);
|
||
t.eq('图层落在子游戏段', layerOk, true);
|
||
t.eq('图层 27 是平台的建房界面', EQW_Layers.PLATFORM_CREATE_ROOM, 27);
|
||
|
||
// ---- 群组 ≥201 ----
|
||
const groupVals = Object.keys(EQW_Groups).map(k => EQW_Groups[k]);
|
||
t.eq('群组均 >= 201', groupVals.every(v => v >= 201), true);
|
||
|
||
// ---- 图片资源 ≥501;声音 ≥101 ----
|
||
const imgVals = Object.keys(EQW_Images).map(k => EQW_Images[k]);
|
||
t.eq('图片资源均 >= 501', imgVals.every(v => v >= 501), true);
|
||
const soundVals = Object.keys(EQW_Sounds).map(k => EQW_Sounds[k]);
|
||
t.eq('声音资源均 >= 101', soundVals.every(v => v >= 101), true); // T-20 未定时为空数组,恒真
|
||
|
||
// ---- 精灵 ID 1001–2999(段内 3000 已被平台占用,清单 §0.3)----
|
||
const index = EQW_SpriteIndex.build(EQW_Sprites);
|
||
const spriteVals = Object.keys(index).map(k => index[k]);
|
||
// 钉住精确数量:松下界(如 >200)删掉整个 View 都还能通过。
|
||
// 增删精灵时连同本数字一起改——有意的顺手更新,不是有意的就红给你看
|
||
t.eq('精灵总数', spriteVals.length, 378);
|
||
const spriteOk = spriteVals.every(v => v >= 1001 && v <= 2999);
|
||
if (!spriteOk) { console.log('越界精灵: ' + JSON.stringify(spriteVals.filter(v => v < 1001 || v > 2999))); }
|
||
t.eq('精灵落在 1001-2999', spriteOk, true);
|
||
|
||
// ---- 手牌标记与手牌一一对应(清单 §1.6:三种标记互斥、【每张牌】至多一个)----
|
||
// core/CardMark.js 也是按每张牌返回标记的:一手 28 张常带 13 个以上标记、36 张埋牌手更多,
|
||
// 标记精灵必须与手牌等量,否则渲染时必然有牌的标记画不出来
|
||
const groupMap = EQW_SpriteIndex.buildGroupMap(EQW_Sprites);
|
||
const handPairing = [];
|
||
for (let i = 1; i <= 36; i++) {
|
||
if (!index.hasOwnProperty('HAND_CARD_' + i)) { handPairing.push('缺 HAND_CARD_' + i); }
|
||
if (!index.hasOwnProperty('HAND_MARK_' + i)) { handPairing.push('缺 HAND_MARK_' + i); }
|
||
else if (groupMap['HAND_MARK_' + i] !== groupMap['HAND_CARD_' + i]) {
|
||
handPairing.push('HAND_MARK_' + i + ' 与手牌不同群组');
|
||
}
|
||
}
|
||
if (handPairing.length) { console.log('手牌标记配对问题:\n ' + handPairing.join('\n ')); }
|
||
t.eq('手牌标记与手牌 1..36 一一对应', handPairing, []);
|
||
t.eq('手牌标记恰好 36 个(无第 37 个)', index.hasOwnProperty('HAND_MARK_37'), false);
|
||
|
||
// ---- 各类 ID 全局不重复 ----
|
||
function dupsOf(vals) {
|
||
const seen = {}, dups = [];
|
||
vals.forEach(v => { if (seen[v]) { dups.push(v); } seen[v] = true; });
|
||
return dups;
|
||
}
|
||
t.eq('精灵 ID 无重复', dupsOf(spriteVals), []);
|
||
t.eq('图层 ID 无重复', dupsOf(layerVals), []);
|
||
t.eq('群组 ID 无重复', dupsOf(groupVals), []);
|
||
t.eq('图片资源 ID 无重复', dupsOf(imgVals), []);
|
||
|
||
// ---- attach.target 的来源判别:targetKind 指明去哪个命名空间取矩形 ----
|
||
// 'sprite' 子游戏精灵键名(EQW_Sprites 索引)
|
||
// 'layout' EQW_Layout 的布局节点名(矩形来自该节点的求解结果,可能是其中第 N 项)
|
||
// 'platform' 平台精灵键名(如平台头像框,落在框架保留段 1–1000,不参与子游戏号段校验)
|
||
// 只在【对应的那一个】命名空间里校验存在性——查三者并集范围太宽,几乎任何标识符都能"解析"。
|
||
const NAMESPACES = { sprite: index, layout: EQW_Layout, platform: EQW_PlatformSprites };
|
||
|
||
const resolvable = {};
|
||
Object.keys(NAMESPACES).forEach(kind => {
|
||
Object.keys(NAMESPACES[kind]).forEach(k => { resolvable[k] = true; });
|
||
});
|
||
|
||
// 收集所有布局节点,供解析检查与求解冒烟共用
|
||
const allNodes = []; // { path, node }
|
||
function collectNodes(node, path) {
|
||
if (!node || typeof node !== 'object' || Array.isArray(node)) { return; }
|
||
if (node.kind) { allNodes.push({ path, node }); }
|
||
Object.keys(node).forEach(k => {
|
||
if (k !== 'bySeat' && node[k] && typeof node[k] === 'object' && !Array.isArray(node[k])) {
|
||
collectNodes(node[k], path + '.' + 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 = [];
|
||
allNodes.forEach(entry => {
|
||
const ns = NAMESPACES[entry.node.targetKind];
|
||
if (!ns) { return; } // targetKind 非法已由上一条断言报出
|
||
const targets = [];
|
||
if (entry.node.target) { targets.push([entry.path, entry.node.target]); }
|
||
if (entry.node.bySeat) {
|
||
Object.keys(entry.node.bySeat).forEach(s => {
|
||
const v = entry.node.bySeat[s];
|
||
if (v && v.target) { targets.push([entry.path + '.bySeat.' + s, v.target]); }
|
||
});
|
||
}
|
||
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 ')); }
|
||
t.eq('attach.target 在其 targetKind 命名空间内可解析', missing, []);
|
||
|
||
// ---- 求解冒烟:每个布局节点都真的能被求解器算出来 ----
|
||
// 这是配置与求解器之间契约的最强守卫——字段名写错、缺 w/h、kind 不认识、
|
||
// bySeat 缺座位,全都会在这里当场抛错,而不是等到界面上静默摆歪。
|
||
load('client/js/gameabc-framework/ui/AlignmentUtils.js');
|
||
load('client/js/01_SubGame/codes/ui/LayoutSolver.js');
|
||
|
||
// 合成一个万能 ctx:所有可解析的键都给一个矩形,count 给一个合理值
|
||
const fakeRects = {};
|
||
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 = [];
|
||
allNodes.forEach(entry => {
|
||
// 有 bySeat 的按三个显示位各求一次,没有的求一次
|
||
const seats = entry.node.bySeat ? Object.keys(entry.node.bySeat) : [null];
|
||
seats.forEach(seat => {
|
||
const eff = effectiveNode(entry.node, seat);
|
||
const ctx = { rects: fakeRects };
|
||
if (seat) { ctx.seat = seat; }
|
||
(eff.runtime || []).forEach(k => { ctx[k] = RUNTIME_FAKE[k]; });
|
||
|
||
// 期望产出多少个矩形——只断言"是数组、坐标是数字"的话,返回空数组也照样通过,
|
||
// 「少画了几张牌」这类缺陷对守卫完全不可见(曾经 grid 缺 rows 就是这么漏过去的)
|
||
let expected;
|
||
if (eff.kind === 'grid') {
|
||
const rows = (typeof ctx.rows === 'number') ? ctx.rows : eff.rows;
|
||
const capacity = eff.cols * rows;
|
||
ctx.count = Math.min(SMOKE_COUNT, capacity); // 不制造超载(超载抛错另有专门用例)
|
||
expected = ctx.count;
|
||
} 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
|
||
}
|
||
|
||
const label = entry.path + (seat ? '[' + seat + ']' : '');
|
||
try {
|
||
const r = EQW_LayoutSolver.solve(entry.node, ctx);
|
||
if (!Array.isArray(r)) { solveFailures.push(label + ' -> 返回值不是数组'); return; }
|
||
if (r.length !== expected) {
|
||
solveFailures.push(label + ' -> 矩形数 ' + r.length + ',期望 ' + expected);
|
||
return;
|
||
}
|
||
r.forEach((rect, i) => {
|
||
if (typeof rect.x !== 'number' || isNaN(rect.x) ||
|
||
typeof rect.y !== 'number' || isNaN(rect.y)) {
|
||
solveFailures.push(label + ' -> 第' + i + '项坐标非法: ' + JSON.stringify(rect));
|
||
}
|
||
});
|
||
} catch (e) {
|
||
solveFailures.push(label + ' -> ' + e.message);
|
||
}
|
||
});
|
||
});
|
||
if (solveFailures.length) { console.log('求解失败的节点:\n ' + solveFailures.join('\n ')); }
|
||
t.eq('全部布局节点可求解、矩形数与坐标都对', solveFailures, []);
|
||
|
||
// 钉住节点总数:松下界(如 >40)删掉整个 View 都还能通过,任何意外的增删都要在这里可见。
|
||
// 增删布局节点时,连同本数字一起改——改动是有意的就顺手更新,不是有意的就红给你看
|
||
t.eq('布局节点总数', allNodes.length, 61);
|
||
|
||
// ---- 布局文件是纯数据:不含函数 ----
|
||
const funcs = [];
|
||
function checkPure(node, path) {
|
||
if (!node || typeof node !== 'object') { return; }
|
||
Object.keys(node).forEach(k => {
|
||
const v = node[k];
|
||
if (typeof v === 'function') { funcs.push(path + '.' + k); }
|
||
else if (v && typeof v === 'object') { checkPure(v, path + '.' + k); }
|
||
});
|
||
}
|
||
Object.keys(EQW_Layout).forEach(k => checkPure(EQW_Layout[k], k));
|
||
t.eq('布局配置不含函数', funcs, []);
|
||
|
||
// ---- 每个布局节点的 kind 合法 ----
|
||
const badKinds = [];
|
||
function checkKind(node, path) {
|
||
if (!node || typeof node !== 'object') { return; }
|
||
if (node.kind && ['point', 'line', 'fan', 'grid', 'attach'].indexOf(node.kind) < 0) {
|
||
badKinds.push(path + ' -> ' + node.kind);
|
||
}
|
||
Object.keys(node).forEach(k => {
|
||
if (node[k] && typeof node[k] === 'object' && !Array.isArray(node[k])) { checkKind(node[k], path + '.' + k); }
|
||
});
|
||
}
|
||
Object.keys(EQW_Layout).forEach(k => checkKind(EQW_Layout[k], k));
|
||
t.eq('布局 kind 全部合法', badKinds, []);
|
||
|
||
// ---- 建房选项配置:精灵键由配置给出,渲染器不按下标拼字符串 ----
|
||
const optProblems = [];
|
||
const usedOptSprites = {};
|
||
function useSprite(where, key) {
|
||
if (!key) { optProblems.push(where + ' 未声明精灵键'); return; }
|
||
if (!index.hasOwnProperty(key)) { optProblems.push(where + ' -> 精灵键不存在: ' + key); return; }
|
||
if (usedOptSprites[key]) { optProblems.push(where + ' -> 精灵键被重复引用: ' + key); }
|
||
usedOptSprites[key] = true;
|
||
}
|
||
const bitsSeen = {};
|
||
EQW_RoomOptions.categories.forEach((cat, ci) => {
|
||
useSprite('类别' + ci + ' titleSprite', cat.titleSprite);
|
||
cat.groups.forEach(g => {
|
||
if (g.hasOwnProperty('bit')) { optProblems.push('组 ' + g.key + ' 仍把 bit 挂在组上(应统一到 item)'); }
|
||
if (g.note) { useSprite('组 ' + g.key + ' noteSprite', g.noteSprite); }
|
||
if (['radio', 'checkbox', 'radioOptional'].indexOf(g.type) < 0) {
|
||
optProblems.push('组 ' + g.key + ' -> 未知 type: ' + g.type);
|
||
}
|
||
g.items.forEach((it, ii) => {
|
||
const where = '组 ' + g.key + ' 第' + ii + '项';
|
||
useSprite(where + ' box', it.box);
|
||
useSprite(where + ' text', it.text);
|
||
if (typeof it.bit !== 'number') { optProblems.push(where + ' 缺 bit'); }
|
||
if (it.value !== '0' && it.value !== '1') { optProblems.push(where + ' value 非 0/1: ' + it.value); }
|
||
bitsSeen[it.bit] = true;
|
||
// 单选组内各项必须共用同一个 bit(同一位互斥取值)
|
||
if (g.type !== 'checkbox' && it.bit !== g.items[0].bit) {
|
||
optProblems.push(where + ' 与同组首项 bit 不一致');
|
||
}
|
||
});
|
||
// 多选组各项必须占用互不相同的位
|
||
if (g.type === 'checkbox') {
|
||
const bs = g.items.map(it => it.bit);
|
||
if (new Set(bs).size !== bs.length) { optProblems.push('组 ' + g.key + ' 多选各项 bit 重复'); }
|
||
}
|
||
});
|
||
});
|
||
// 配置声明的精灵必须恰好覆盖建房 View 里预置的全部选项精灵(多一个少一个都说明两边不同步)
|
||
Object.keys(EQW_Sprites.CreateRoomView.CreateRoom).forEach(k => {
|
||
if (k === 'id') { return; }
|
||
if (!usedOptSprites[k]) { optProblems.push('精灵 ' + k + ' 没有任何配置项引用它'); }
|
||
});
|
||
if (optProblems.length) { console.log('建房选项配置问题:\n ' + optProblems.join('\n ')); }
|
||
t.eq('建房选项精灵键与 bit 声明完整', optProblems, []);
|
||
// roomtype 是 5 位串(服务端 class.config.js IDX_ASET..IDX_NOCHECK = 0..4),位号必须刚好铺满
|
||
t.eq('roomtype 位号铺满 0-4', Object.keys(bitsSeen).map(Number).sort((a, b) => a - b), [0, 1, 2, 3, 4]);
|
||
|
||
// ---- 牌尺寸档引用的资源键存在 ----
|
||
['L', 'M', 'S'].forEach(size => {
|
||
t.eq('CARD_SIZE.' + size + ' 的资源存在', EQW_Images.hasOwnProperty(EQW_Layout.CARD_SIZE[size].res), true);
|
||
});
|
||
|
||
// ---- 多帧图数字样式(配合 SpriteManager.setNumberImage):资源键存在、字段齐全合法 ----
|
||
// 与 CARD_SIZE 同一约定:纯数据配置只存资源【键名】,调用方用 EQW_Images[res] 取真实 ID
|
||
const numStyleProblems = [];
|
||
Object.keys(EQW_Layout.NUM_STYLE).forEach(key => {
|
||
const s = EQW_Layout.NUM_STYLE[key];
|
||
if (!EQW_Images.hasOwnProperty(s.res)) { numStyleProblems.push(key + ' 的资源键不存在: ' + s.res); }
|
||
// charWidth 是 setNumberImage 的必需入参(该接口对非正数直接返回 false)
|
||
['charWidth', 'charHeight'].forEach(f => {
|
||
if (typeof s[f] !== 'number' || s[f] <= 0) { numStyleProblems.push(key + '.' + f + ' 非正数: ' + s[f]); }
|
||
});
|
||
// 后缀落在资源第 16 帧,只能是 0 或 1 个字符;且必须是 setNumberImage 认识的那三个
|
||
// (编码表 /[分倍张]/ → 'g' → 帧16;写别的字会被原样丢给引擎,按未知字符渲染)
|
||
if (typeof s.suffix !== 'string') { numStyleProblems.push(key + '.suffix 必须是字符串: ' + s.suffix); }
|
||
else if (s.suffix !== '' && ['分', '倍', '张'].indexOf(s.suffix) < 0) {
|
||
numStyleProblems.push(key + '.suffix 不是 setNumberImage 支持的后缀(分/倍/张): ' + s.suffix);
|
||
}
|
||
});
|
||
if (numStyleProblems.length) { console.log('数字样式问题:\n ' + numStyleProblems.join('\n ')); }
|
||
t.eq('NUM_STYLE 资源键存在且字段合法', numStyleProblems, []);
|
||
|
||
// 引用 NUM_STYLE 的布局节点必须与样式同源(SSOT:数值只在 LayoutConstants 定义一处,
|
||
// 有人在 Layout_Action.js 里手抄一个不同的数就在这里变红)。
|
||
// 注意 w 不在此列:setNumberImage 会按【字符数 × charWidth】自动改精灵宽,故 w 由运行时注入
|
||
[
|
||
['CALL_BTN_SCORE_NUM', 'CALL_SCORE'],
|
||
['MAIN_SUIT_COUNT', 'MAIN_SUIT_COUNT'],
|
||
['P_COUNTDOWN', 'COUNTDOWN'],
|
||
['RESULT_SCORE_NUM', 'RESULT_WIN'],
|
||
['RESULT_SCORE_NUM', 'RESULT_LOSE']
|
||
].forEach(pair => {
|
||
const node = EQW_Layout[pair[0]];
|
||
const st = EQW_Layout.NUM_STYLE[pair[1]];
|
||
t.eq(pair[0] + '.h 与 NUM_STYLE.' + pair[1] + '.charHeight 同源', node.h, st.charHeight);
|
||
t.eq(pair[0] + '.w 留到运行时注入', node.runtime.indexOf('w') >= 0 && typeof node.w === 'undefined', true);
|
||
});
|
||
|
||
process.exit(t.done('constants') ? 0 : 1);
|