二七王:Task 13 守卫加入「求解冒烟」

拿真实布局配置逐节点实际调用求解器,断言不抛错且坐标非 NaN。
字段名写错、缺 w/h、kind 不认识、bySeat 缺座位都会当场炸出来,
而不是等到界面上静默摆歪——这是配置与求解器之间契约的最强守卫。

attach.target 的解析范围同时改为三者并集(子游戏精灵 / 布局节点名 / 平台精灵)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 21:32:48 +08:00
co-authored by Claude Opus 5
parent 91f0eb116c
commit 949702dc90
@@ -2162,32 +2162,87 @@ 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 都能解析 ----
// (遍历 EQW_Layout 的所有节点,含 bySeat 变体) // target 可指向三类之一:子游戏精灵键名 / EQW_Layout 的布局节点名 / 平台精灵键名
const missing = []; // (平台头像框落在框架保留段 1–1000,独立登记、不参与子游戏号段校验)
function checkNode(node, path) { const resolvable = {};
if (!node || typeof node !== 'object') { return; } Object.keys(index).forEach(k => { resolvable[k] = true; });
if (node.kind === 'attach' && node.target && !index.hasOwnProperty(node.target)) { Object.keys(EQW_Layout).forEach(k => { resolvable[k] = true; });
missing.push(path + ' -> ' + node.target); Object.keys(EQW_PlatformSprites).forEach(k => { resolvable[k] = true; });
}
if (node.bySeat) { // 收集所有布局节点,供解析检查与求解冒烟共用
Object.keys(node.bySeat).forEach(s => { const allNodes = []; // { path, node }
const v = node.bySeat[s]; function collectNodes(node, path) {
if (v && v.target && !index.hasOwnProperty(v.target)) { if (!node || typeof node !== 'object' || Array.isArray(node)) { return; }
missing.push(path + '.bySeat.' + s + ' -> ' + v.target); if (node.kind) { allNodes.push({ path, node }); }
}
});
}
Object.keys(node).forEach(k => { Object.keys(node).forEach(k => {
if (k !== 'bySeat' && node[k] && typeof node[k] === 'object' && !Array.isArray(node[k])) { if (k !== 'bySeat' && node[k] && typeof node[k] === 'object' && !Array.isArray(node[k])) {
checkNode(node[k], path + '.' + k); collectNodes(node[k], path + '.' + k);
} }
}); });
} }
Object.keys(EQW_Layout).forEach(k => checkNode(EQW_Layout[k], k)); Object.keys(EQW_Layout).forEach(k => collectNodes(EQW_Layout[k], k));
if (missing.length) { console.log('无法解析的 attach.target:\n ' + missing.join('\n ')); }
const missing = [];
allNodes.forEach(entry => {
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 (!resolvable[pair[1]]) { missing.push(pair[0] + ' -> ' + pair[1]); } });
});
if (missing.length) { console.log('无法解析的 attach.target:
' + missing.join('
')); }
t.eq('attach.target 全部可解析', missing, []); t.eq('attach.target 全部可解析', 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 }; });
const solveFailures = [];
allNodes.forEach(entry => {
// 有 bySeat 的按三个显示位各求一次,没有的求一次
const seats = entry.node.bySeat ? Object.keys(entry.node.bySeat) : [null];
seats.forEach(seat => {
const ctx = { rects: fakeRects, count: 8 };
if (seat) { ctx.seat = seat; }
// 无 target 的 attach 模板节点:运行时由调用方注入目标,这里补一个假目标
const seatTarget = seat && entry.node.bySeat[seat] ? entry.node.bySeat[seat].target : null;
if (entry.node.kind === 'attach' && !entry.node.target && !seatTarget) {
ctx.target = Object.keys(fakeRects)[0];
}
const label = entry.path + (seat ? '[' + seat + ']' : '');
try {
const r = EQW_LayoutSolver.solve(entry.node, ctx);
if (!Array.isArray(r)) { solveFailures.push(label + ' -> 返回值不是数组'); 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('求解失败的节点:
' + solveFailures.join('
')); }
t.eq('全部布局节点可求解且坐标合法', solveFailures, []);
t.eq('布局节点数量 > 40', allNodes.length > 40, true);
// ---- 布局文件是纯数据:不含函数 ---- // ---- 布局文件是纯数据:不含函数 ----
const funcs = []; const funcs = [];
function checkPure(node, path) { function checkPure(node, path) {