二七王: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:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user