From 949702dc90532c9086a0807c2bce1889e5531877 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 26 Aug 2026 21:32:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E4=B8=83=E7=8E=8B=EF=BC=9ATask=2013?= =?UTF-8?q?=20=E5=AE=88=E5=8D=AB=E5=8A=A0=E5=85=A5=E3=80=8C=E6=B1=82?= =?UTF-8?q?=E8=A7=A3=E5=86=92=E7=83=9F=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 拿真实布局配置逐节点实际调用求解器,断言不抛错且坐标非 NaN。 字段名写错、缺 w/h、kind 不认识、bySeat 缺座位都会当场炸出来, 而不是等到界面上静默摆歪——这是配置与求解器之间契约的最强守卫。 attach.target 的解析范围同时改为三者并集(子游戏精灵 / 布局节点名 / 平台精灵)。 Co-Authored-By: Claude Opus 5 (1M context) --- .../2026-08-26-二七王前端A-地基与逻辑层.md | 93 +++++++++++++++---- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/docs/superpowers/plans/2026-08-26-二七王前端A-地基与逻辑层.md b/docs/superpowers/plans/2026-08-26-二七王前端A-地基与逻辑层.md index d6b397e..2654973 100644 --- a/docs/superpowers/plans/2026-08-26-二七王前端A-地基与逻辑层.md +++ b/docs/superpowers/plans/2026-08-26-二七王前端A-地基与逻辑层.md @@ -2162,32 +2162,87 @@ t.eq('图层 ID 无重复', dupsOf(layerVals), []); t.eq('群组 ID 无重复', dupsOf(groupVals), []); t.eq('图片资源 ID 无重复', dupsOf(imgVals), []); -// ---- 布局配置里每个 attach.target 都能在精灵索引里查到 ---- -// (遍历 EQW_Layout 的所有节点,含 bySeat 变体) -const missing = []; -function checkNode(node, path) { - if (!node || typeof node !== 'object') { return; } - if (node.kind === 'attach' && node.target && !index.hasOwnProperty(node.target)) { - missing.push(path + ' -> ' + node.target); - } - if (node.bySeat) { - Object.keys(node.bySeat).forEach(s => { - const v = node.bySeat[s]; - if (v && v.target && !index.hasOwnProperty(v.target)) { - missing.push(path + '.bySeat.' + s + ' -> ' + v.target); - } - }); - } +// ---- 布局配置里每个 attach.target 都能解析 ---- +// target 可指向三类之一:子游戏精灵键名 / EQW_Layout 的布局节点名 / 平台精灵键名 +// (平台头像框落在框架保留段 1–1000,独立登记、不参与子游戏号段校验) +const resolvable = {}; +Object.keys(index).forEach(k => { resolvable[k] = true; }); +Object.keys(EQW_Layout).forEach(k => { resolvable[k] = true; }); +Object.keys(EQW_PlatformSprites).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])) { - checkNode(node[k], path + '.' + k); + collectNodes(node[k], path + '.' + k); } }); } -Object.keys(EQW_Layout).forEach(k => checkNode(EQW_Layout[k], k)); -if (missing.length) { console.log('无法解析的 attach.target:\n ' + missing.join('\n ')); } +Object.keys(EQW_Layout).forEach(k => collectNodes(EQW_Layout[k], k)); + +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, []); +// ---- 求解冒烟:每个布局节点都真的能被求解器算出来 ---- +// 这是配置与求解器之间契约的最强守卫——字段名写错、缺 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 = []; function checkPure(node, path) {