import { readFileSync } from 'node:fs'; import { DOMParser } from '@xmldom/xmldom'; /** * 读取 Game_Surface_3 风格的 Layer XML (UTF-8 编码), 解析 Layer + Spirit 字段. * * 协议来源: docs/protocol/ (服务器零改动). XML 结构与 save/Layer*.xml 一致: * * * * * ... * * * * * 数据源权威: 字段缺失/不存在一律返回 undefined (不上游兜底, 由下游显式判错). * * @param {string} path - 文件路径 (相对 cwd 或绝对) * @returns {{ layerId: string|undefined, layerName: string|undefined, spirits: Array<{ id, name, type, imgResID, x, y, w, h, group, indexOfLayer }> }} */ export function readLayerXml(path) { const text = readFileSync(path, 'utf8'); const doc = new DOMParser().parseFromString(text, 'text/xml'); const getProp = (el, name) => { const prop = Array.from(el.getElementsByTagName('Property')) .find(p => p.getAttribute('Name') === name); return prop?.getAttribute('Value'); }; const layerEl = doc.getElementsByTagName('Layer')[0]; const layerPropList = layerEl.getElementsByTagName('PropertyList')[0]; const layerId = getProp(layerPropList, 'ID'); const layerName = getProp(layerPropList, 'Name'); const spiritEls = doc.getElementsByTagName('Spirit'); const spirits = Array.from(spiritEls).map(sp => { const propList = sp.getElementsByTagName('PropertyList')[0]; return { id: getProp(propList, 'ID'), name: getProp(propList, 'Name'), type: getProp(propList, 'SpiritType'), imgResID: getProp(propList, 'ImgResID'), x: getProp(propList, 'X'), y: getProp(propList, 'Y'), w: getProp(propList, 'Width'), h: getProp(propList, 'Height'), group: getProp(propList, 'BelongGroupID'), indexOfLayer: getProp(propList, 'IndexOfLayer'), }; }); return { layerId, layerName, spirits }; } /** * 锚点转换: 原坐标系 (left-top, 1280×720 绝对) → Cocos 中心锚点 + Y-up. * * 公式 (实测验证 2026-09-02, Login_Layer 渠道logo + 游客登录双节点 _lpos 匹配): * cocos_lpos.x = original_X + original_W/2 - 640 # 1280/2 * cocos_lpos.y = 360 - (original_Y + original_H/2) # Y-up 翻转 + 720/2 * * @param {{ x?: string, y?: string, w?: string, h?: string }} spirit * @returns {{ x: number, y: number, z: 0 }} */ export function computeLpos(spirit) { const x = parseFloat(spirit.x || '0'); const y = parseFloat(spirit.y || '0'); const w = parseFloat(spirit.w || '0'); const h = parseFloat(spirit.h || '0'); // 锚点转换:原 X/Y 是左上角(1280×720 绝对坐标)→ Cocos 中心锚点 return { x: x + w / 2 - 640, // 原坐标系半宽 1280/2 y: 360 - (y + h / 2), // Y-up 翻转 + 半高 720/2 z: 0, }; } /** * 计算 Spirit 的 contentSize (Cocos UI 节点尺寸). * * 数据源权威: SizeCalcMode 缺失视作 0 (直接读 Width/Height); 非 0 抛错, * 由上游显式决定是否逆向 gameabc.min.js (Task 5 范围, 本任务不支持). * * @param {{ w?: string, h?: string, sizeCalcMode?: string, id?: string, name?: string }} spirit * @returns {{ width: number, height: number }} */ export function computeContentSize(spirit) { const sizeCalcMode = parseInt(spirit.sizeCalcMode || '0', 10); if (sizeCalcMode !== 0) { throw new Error(`SizeCalcMode=${sizeCalcMode} not yet supported for Spirit ${spirit.id} (${spirit.name}) - reverse gameabc.min.js first (Task 5)`); } return { width: parseFloat(spirit.w || '0'), height: parseFloat(spirit.h || '0'), }; }