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'),
};
}
/**
* 将 Spirit 记录按 BelongGroupID 归类, 生成 group-X 父节点集合 + Layer 根直挂子节点.
*
* 规则:
* - BelongGroupID === '0' (或缺失) → 直挂 Layer 根, 不进任何 group 父节点
* - BelongGroupID > 0 → 归入对应 group- 父节点
* - group 父节点按 ID 升序排列; 同 group 内子节点按 IndexOfLayer 升序
* - 命名保留原 BelongGroupID 值, 不重排
*
* 数据源权威: IndexOfLayer 缺失视作 0 (XML 中该字段由 readLayerXml 提供, 必有值).
*
* @param {Array<{ id?: string, group?: string, indexOfLayer?: string }>} spirits
* @returns {{ groupParents: Array<{ name: string, childSpiritIds: string[] }>, rootChildSpiritIds: string[] }}
*/
export function buildGroupParents(spirits) {
const groupMap = new Map(); // group_id -> [{ spiritId, index }]
const rootSpirits = [];
for (const s of spirits) {
if (s.group === '0' || !s.group) {
rootSpirits.push({ spiritId: s.id, index: parseInt(s.indexOfLayer || '0', 10) });
} else {
if (!groupMap.has(s.group)) groupMap.set(s.group, []);
groupMap.get(s.group).push({ spiritId: s.id, index: parseInt(s.indexOfLayer || '0', 10) });
}
}
const groupParents = Array.from(groupMap.entries())
.sort(([a], [b]) => parseInt(a, 10) - parseInt(b, 10))
.map(([gid, items]) => ({
name: `group-${gid}`,
childSpiritIds: items.sort((a, b) => a.index - b.index).map(x => x.spiritId),
}));
const rootChildSpiritIds = rootSpirits
.sort((a, b) => a.index - b.index)
.map(x => x.spiritId);
return { groupParents, rootChildSpiritIds };
}