You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
275 lines
13 KiB
275 lines
13 KiB
|
3 weeks ago
|
import path from 'path';
|
||
|
|
|
||
|
|
const Fs = require('fs');
|
||
|
|
|
||
|
|
// ── 属性映射:标记 → MapGrid 组件全部 8 个属性的完整值 ──
|
||
|
|
const MARKER_PROPS: Record<string, Record<string, boolean>> = {
|
||
|
|
'': { _isCanWalk: true, _isCanPutGrid: true, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false },
|
||
|
|
'CanWalk': { _isCanWalk: true, _isCanPutGrid: true, _isBestPlace: true, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false },
|
||
|
|
'CanBeDig': { _isCanWalk: false, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: true, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false },
|
||
|
|
'CanPut': { _isCanWalk: false, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: true, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false },
|
||
|
|
'spawnPoint_1': { _isCanWalk: true, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false },
|
||
|
|
'spawnPoint_2': { _isCanWalk: true, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false },
|
||
|
|
'spawnPoint_3': { _isCanWalk: true, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false },
|
||
|
|
'wallTarget1': { _isCanWalk: true, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false },
|
||
|
|
'wallTarget2': { _isCanWalk: true, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false },
|
||
|
|
'isFreePlace': { _isCanWalk: true, _isCanPutGrid: true, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: true },
|
||
|
|
};
|
||
|
|
|
||
|
|
const SPAWN_MARKERS = ['spawnPoint_1', 'spawnPoint_2', 'spawnPoint_3'];
|
||
|
|
const MAPGRID_FILE_ID = 'a5Czjqnj1DIa/1qXv6uwzF';
|
||
|
|
|
||
|
|
const CELL_SIZE = 93;
|
||
|
|
const ORIGIN_X = -325.5;
|
||
|
|
const ORIGIN_Y = 418.5;
|
||
|
|
const COLS = 8;
|
||
|
|
const ROWS = 10;
|
||
|
|
|
||
|
|
function cellToWorld(index: number): { x: number; y: number } {
|
||
|
|
const col = index % COLS;
|
||
|
|
const row = Math.floor(index / COLS);
|
||
|
|
return { x: ORIGIN_X + col * CELL_SIZE, y: ORIGIN_Y - row * CELL_SIZE };
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 在 JSON 数组中按 _name 查找节点,返回索引 */
|
||
|
|
function findNodeIndex(data: any[], name: string): number {
|
||
|
|
for (let i = 0; i < data.length; i++) {
|
||
|
|
if (data[i] && data[i]._name === name) return i;
|
||
|
|
}
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 获取格子的 PrefabInstance 对象 */
|
||
|
|
function getGridInstance(data: any[], geziIdx: number, gridIndex: number): any | null {
|
||
|
|
const gezi = data[geziIdx];
|
||
|
|
if (!gezi || !gezi._children || gridIndex >= gezi._children.length) return null;
|
||
|
|
const gridRef = gezi._children[gridIndex];
|
||
|
|
const gridNode = data[gridRef.__id__];
|
||
|
|
if (!gridNode || !gridNode._prefab) return null;
|
||
|
|
const prefabInfo = data[gridNode._prefab.__id__];
|
||
|
|
if (!prefabInfo || !prefabInfo.instance) return null;
|
||
|
|
return data[prefabInfo.instance.__id__];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 清除指定格子中所有 MapGrid 组件的属性覆盖 */
|
||
|
|
function clearGridOverrides(data: any[], geziIdx: number, gridIndex: number): void {
|
||
|
|
const instance = getGridInstance(data, geziIdx, gridIndex);
|
||
|
|
if (!instance || !instance.propertyOverrides) return;
|
||
|
|
instance.propertyOverrides = instance.propertyOverrides.filter((ref: any) => {
|
||
|
|
const po = data[ref.__id__];
|
||
|
|
if (!po || !po.targetInfo) return true; // 保留非 targetInfo 的条目
|
||
|
|
const ti = data[po.targetInfo.__id__];
|
||
|
|
if (!ti || !ti.localID) return true;
|
||
|
|
// 删除 targetInfo.localID 匹配 MapGrid 组件的覆盖
|
||
|
|
return ti.localID[0] !== MAPGRID_FILE_ID;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 向指定格子的 PrefabInstance 追加一条 MapGrid 属性覆盖 */
|
||
|
|
function addGridOverride(data: any[], geziIdx: number, gridIndex: number, propName: string, value: any): void {
|
||
|
|
const instance = getGridInstance(data, geziIdx, gridIndex);
|
||
|
|
if (!instance || !instance.propertyOverrides) return;
|
||
|
|
|
||
|
|
const targetInfoId = data.length;
|
||
|
|
data.push({ __type__: 'cc.TargetInfo', localID: [MAPGRID_FILE_ID] });
|
||
|
|
|
||
|
|
const overrideId = data.length;
|
||
|
|
data.push({
|
||
|
|
__type__: 'CCPropertyOverrideInfo',
|
||
|
|
targetInfo: { __id__: targetInfoId },
|
||
|
|
propertyPath: [propName],
|
||
|
|
value: value,
|
||
|
|
});
|
||
|
|
instance.propertyOverrides.push({ __id__: overrideId });
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 移动/设置指定父节点下某个子节点的坐标 */
|
||
|
|
function setChildPos(data: any[], parentName: string, childName: string, x: number, y: number): boolean {
|
||
|
|
const parentIdx = findNodeIndex(data, parentName);
|
||
|
|
if (parentIdx < 0) return false;
|
||
|
|
const parent = data[parentIdx];
|
||
|
|
if (!parent._children) return false;
|
||
|
|
for (const ref of parent._children) {
|
||
|
|
const child = data[ref.__id__];
|
||
|
|
if (child && child._name === childName) {
|
||
|
|
child._lpos = { __type__: 'cc.Vec3', x, y, z: 0 };
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 从父节点中删除指定名称的子节点 */
|
||
|
|
function removeChildByName(data: any[], parentName: string, childName: string): void {
|
||
|
|
const parentIdx = findNodeIndex(data, parentName);
|
||
|
|
if (parentIdx < 0) return;
|
||
|
|
const parent = data[parentIdx];
|
||
|
|
if (!parent._children) return;
|
||
|
|
parent._children = parent._children.filter((ref: any) => {
|
||
|
|
const child = data[ref.__id__];
|
||
|
|
return !(child && child._name === childName);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 向父节点添加一个浅拷贝自模板的子节点(用于补充缺失的 spawnPoint) */
|
||
|
|
function copyChildFromTemplate(data: any[], template: any[], parentName: string, childName: string): void {
|
||
|
|
const parentIdx = findNodeIndex(data, parentName);
|
||
|
|
if (parentIdx < 0) return;
|
||
|
|
// 从模板中找到同名子节点引用
|
||
|
|
const tplParentIdx = findNodeIndex(template, parentName);
|
||
|
|
if (tplParentIdx < 0) return;
|
||
|
|
const tplParent = template[tplParentIdx];
|
||
|
|
for (const ref of tplParent._children || []) {
|
||
|
|
const child = template[ref.__id__];
|
||
|
|
if (child && child._name === childName) {
|
||
|
|
// 将子节点及其组件深克隆追加到 data 末尾,并在父节点 _children 中引用
|
||
|
|
const cloneStartIdx = data.length;
|
||
|
|
const cloned = JSON.parse(JSON.stringify(child));
|
||
|
|
data.push(cloned);
|
||
|
|
data[parentIdx]._children.push({ __id__: cloneStartIdx });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 应用格子数据到预制体 JSON(新增和更新共用)
|
||
|
|
*/
|
||
|
|
function applyGridData(data: any[], template: any[], grids: string[]): void {
|
||
|
|
const geziIdx = findNodeIndex(data, '格子');
|
||
|
|
if (geziIdx < 0) return;
|
||
|
|
|
||
|
|
// 1. 收集 spawn 位置映射 + 遍历格子清除旧覆盖 / 添加新覆盖
|
||
|
|
const spawnPositions = new Map<string, { x: number; y: number }>();
|
||
|
|
for (let i = 0; i < 80; i++) {
|
||
|
|
clearGridOverrides(data, geziIdx, i);
|
||
|
|
|
||
|
|
const marker = grids[i] || '';
|
||
|
|
const props = MARKER_PROPS[marker];
|
||
|
|
if (props) {
|
||
|
|
for (const [prop, value] of Object.entries(props)) {
|
||
|
|
addGridOverride(data, geziIdx, i, prop, value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (SPAWN_MARKERS.includes(marker)) {
|
||
|
|
spawnPositions.set(marker, cellToWorld(i));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. wallTarget1/wallTarget2 配对
|
||
|
|
const t1 = grids.indexOf('wallTarget1');
|
||
|
|
const t2 = grids.indexOf('wallTarget2');
|
||
|
|
if (t1 >= 0 && t2 >= 0) {
|
||
|
|
const p1 = cellToWorld(t1), p2 = cellToWorld(t2);
|
||
|
|
const r1 = Math.floor(t1 / COLS), c1 = t1 % COLS;
|
||
|
|
const r2 = Math.floor(t2 / COLS), c2 = t2 % COLS;
|
||
|
|
let mx: number, my: number;
|
||
|
|
if (r1 === r2) { mx = (p1.x + p2.x) / 2; my = p1.y; } // 横连
|
||
|
|
else if (c1 === c2) { mx = p1.x; my = (p1.y + p2.y) / 2; } // 竖连
|
||
|
|
else { mx = (p1.x + p2.x) / 2; my = (p1.y + p2.y) / 2; } // 斜角
|
||
|
|
setChildPos(data, '目标点', 'wallTarget1', mx, my);
|
||
|
|
} else if (t1 >= 0) {
|
||
|
|
const pos = cellToWorld(t1);
|
||
|
|
setChildPos(data, '目标点', 'wallTarget1', pos.x, pos.y);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 同步 spawn 节点:删除多余 → 补回缺失 → 设置坐标
|
||
|
|
const allSpawns = ['spawnPoint_1', 'spawnPoint_2', 'spawnPoint_3', 'spawnPoint_4', 'spawnPoint_5', 'spawnPoint_6', 'spawnPoint_7'];
|
||
|
|
for (const spName of allSpawns) {
|
||
|
|
if (spawnPositions.has(spName)) continue;
|
||
|
|
removeChildByName(data, '出怪口', spName);
|
||
|
|
}
|
||
|
|
for (const [spName, pos] of spawnPositions) {
|
||
|
|
const parentIdx = findNodeIndex(data, '出怪口');
|
||
|
|
const exists = parentIdx >= 0 && (data[parentIdx]._children || []).some((ref: any) => {
|
||
|
|
const child = data[ref.__id__];
|
||
|
|
return child && child._name === spName;
|
||
|
|
});
|
||
|
|
if (!exists) {
|
||
|
|
copyChildFromTemplate(data, template, '出怪口', spName);
|
||
|
|
}
|
||
|
|
setChildPos(data, '出怪口', spName, pos.x, pos.y);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 扩展入口 ──
|
||
|
|
export const methods: { [key: string]: (...args: any[]) => any } = {
|
||
|
|
async generateMapLayers() {
|
||
|
|
try {
|
||
|
|
const projectPath = Editor.Project.path;
|
||
|
|
|
||
|
|
// 1. 读取 Excel
|
||
|
|
const excelPath = path.join(projectPath, 'assets/res/地图/地图.xls');
|
||
|
|
if (!Fs.existsSync(excelPath)) {
|
||
|
|
console.error('[地图生成] Excel 文件不存在:', excelPath);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const xlsxPath = path.join(__dirname, '../../excel-to-json-ts/lib/xlsx.js');
|
||
|
|
const XLSX = require(xlsxPath);
|
||
|
|
const buffer = Fs.readFileSync(excelPath);
|
||
|
|
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
||
|
|
|
||
|
|
// 2. 读取模板
|
||
|
|
const templatePath = path.join(projectPath, 'assets/bundles/battleMap/prefab/地图/mapLayerTemplate.prefab');
|
||
|
|
if (!Fs.existsSync(templatePath)) {
|
||
|
|
console.error('[地图生成] 模板预制体不存在:', templatePath);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const templateJson = Fs.readFileSync(templatePath, 'utf-8');
|
||
|
|
const templateData = JSON.parse(templateJson);
|
||
|
|
|
||
|
|
const prefabDir = path.join(projectPath, 'assets/bundles/battleMap/prefab/地图');
|
||
|
|
|
||
|
|
console.log(`[地图生成] 开始处理 ${workbook.SheetNames.length} 个工作表...`);
|
||
|
|
|
||
|
|
// 3. 遍历每个工作表
|
||
|
|
for (const sheetName of workbook.SheetNames) {
|
||
|
|
const worksheet = workbook.Sheets[sheetName];
|
||
|
|
const rows: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||
|
|
|
||
|
|
const grids: string[] = [];
|
||
|
|
for (let r = 0; r < ROWS; r++) {
|
||
|
|
for (let c = 0; c < COLS; c++) {
|
||
|
|
const raw = (rows[r] || [])[c];
|
||
|
|
grids.push((raw !== undefined && raw !== null && raw !== '') ? String(raw).trim() : '');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const prefabPath = path.join(prefabDir, `${sheetName}.prefab`);
|
||
|
|
const exists = Fs.existsSync(prefabPath);
|
||
|
|
|
||
|
|
let prefabData: any[];
|
||
|
|
if (exists) {
|
||
|
|
// 已有预制体 → 读取后原地修改
|
||
|
|
const existingJson = Fs.readFileSync(prefabPath, 'utf-8');
|
||
|
|
prefabData = JSON.parse(existingJson);
|
||
|
|
applyGridData(prefabData, templateData, grids);
|
||
|
|
} else {
|
||
|
|
// 新预制体 → 从模板克隆生成
|
||
|
|
prefabData = JSON.parse(JSON.stringify(templateData));
|
||
|
|
prefabData[0]._name = sheetName;
|
||
|
|
prefabData[1]._name = sheetName;
|
||
|
|
applyGridData(prefabData, templateData, grids);
|
||
|
|
}
|
||
|
|
|
||
|
|
const jsonStr = JSON.stringify(prefabData, null, 2);
|
||
|
|
const url = `db://assets/bundles/battleMap/prefab/地图/${sheetName}.prefab`;
|
||
|
|
await Editor.Message.request('asset-db', 'create-asset', url, jsonStr, { overwrite: true });
|
||
|
|
|
||
|
|
const stats: Record<string, number> = {};
|
||
|
|
for (const m of grids) { const k = m || '(空白)'; stats[k] = (stats[k] || 0) + 1; }
|
||
|
|
const statStr = Object.entries(stats).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}:${v}`).join(', ');
|
||
|
|
console.log(`[地图生成] ${exists ? '~' : '+'} ${sheetName}.prefab (${statStr})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('[地图生成] 全部完成!');
|
||
|
|
} catch (err) {
|
||
|
|
console.error('[地图生成] 错误:', err);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
export function load() { }
|
||
|
|
export function unload() { }
|