'use strict'; const fs = require('fs'); const path = require('path'); const levelIdUtil = require('./level-id'); const LEVEL_MAP_TYPE = 'd4e5fanuMlNDh8qO0xdbn+K'; const DB_REL = 'assets/level-data/levels-database.json'; const PREFAB_DIR_REL = 'assets/resources/level-prefabs'; const PREFAB_NAME_RE = /^Level(\d+)\.prefab$/i; const TILE_NODE_RE = /^([gb])_(-?\d+)_(-?\d+)$/; let spriteUuidCache = null; function projectRoot(projectPath) { return projectPath || Editor.Project.path; } function dbPath(projectPath) { return path.join(projectRoot(projectPath), DB_REL); } function prefabDir(projectPath) { return path.join(projectRoot(projectPath), PREFAB_DIR_REL); } function prefabPath(levelId, projectPath) { return path.join(prefabDir(projectPath), `Level${levelId}.prefab`); } function readJsonFile(fp, fallback) { try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return fallback; } } function writeJsonFile(fp, data) { fs.mkdirSync(path.dirname(fp), { recursive: true }); fs.writeFileSync(fp, `${JSON.stringify(data, null, 2)}\n`, 'utf8'); } function buildSpriteUuidCache(projectPath) { if (spriteUuidCache) return spriteUuidCache; const cache = new Map(); const texRoot = path.join(projectRoot(projectPath), 'assets/resources/textures'); const walk = (dir) => { if (!fs.existsSync(dir)) return; for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, ent.name); if (ent.isDirectory()) { walk(full); continue; } if (!/\.(png|jpg)\.meta$/i.test(ent.name)) continue; const tileKey = ent.name.replace(/\.(png|jpg)\.meta$/i, ''); try { const meta = JSON.parse(fs.readFileSync(full, 'utf8')); if (meta.subMetas) { for (const sub of Object.values(meta.subMetas)) { if (sub && sub.uuid) cache.set(sub.uuid, tileKey); } } if (meta.uuid) { cache.set(meta.uuid, tileKey); cache.set(`${meta.uuid}@f9941`, tileKey); } } catch { /* ignore broken meta */ } } }; walk(texRoot); spriteUuidCache = cache; return cache; } function tileKeyFromSpriteUuid(uuid, projectPath) { if (!uuid) return null; const cache = buildSpriteUuidCache(projectPath); const direct = cache.get(uuid); if (direct) return direct; const base = String(uuid).split('@')[0]; for (const [key, val] of cache.entries()) { if (key.startsWith(base)) return val; } return null; } function parseJsonRecord(raw) { try { const parsed = JSON.parse(raw || '{}'); return parsed && typeof parsed === 'object' ? parsed : {}; } catch { return {}; } } function parseLevelMapData(objs) { for (const obj of objs) { if (!obj || typeof obj !== 'object') continue; if (obj.__type__ !== LEVEL_MAP_TYPE && obj.groundJson === undefined) continue; if (obj.groundJson === undefined && obj.levelID === undefined) continue; return { levelID: parseInt(obj.levelID, 10) || 0, ground: parseJsonRecord(obj.groundJson), border: parseJsonRecord(obj.borderJson), theme: String(obj.theme || '').trim() || null, }; } return null; } function spriteUuidOnNode(objs, node) { if (!node || !Array.isArray(node._components)) return null; for (const ref of node._components) { const comp = objs[(ref && ref.__id__) - 1]; if (!comp || comp.__type__ !== 'cc.Sprite') continue; const frame = comp._spriteFrame; if (frame && frame.__uuid__) return frame.__uuid__; } return null; } function parseTilesFromNodes(objs, projectPath) { const ground = {}; const border = {}; for (const obj of objs) { if (!obj || obj.__type__ !== 'cc.Node' || typeof obj._name !== 'string') continue; const m = obj._name.match(TILE_NODE_RE); if (!m) continue; const x = parseInt(m[2], 10); const y = parseInt(m[3], 10); if (Number.isNaN(x) || Number.isNaN(y)) continue; const key = `${x},${y}`; const layer = m[1] === 'g' ? 'ground' : 'border'; const tileKey = tileKeyFromSpriteUuid(spriteUuidOnNode(objs, obj), projectPath); if (layer === 'ground') { ground[key] = tileKey || 'Baseblock'; } else { border[key] = tileKey || 'WallBlock'; } } return { ground, border }; } function mergeMapSources(mapData, fromNodes) { const md = mapData || { ground: {}, border: {}, theme: null, levelID: 0 }; const nodes = fromNodes || { ground: {}, border: {} }; const ground = Object.keys(nodes.ground).length ? nodes.ground : md.ground || {}; const border = Object.keys(nodes.border).length ? nodes.border : md.border || {}; return { levelID: md.levelID || 0, ground, border, theme: md.theme || null, }; } function mergeEntry(levelId, mapData, prev) { const extId = levelIdUtil.isExternalLevelId(levelId) ? levelId : levelIdUtil.externalLevelId(levelId); const out = { levelID: extId, boundary: (prev && prev.boundary) || { x: 20, y: 20 }, spawns: Array.isArray(prev && prev.spawns) ? [...prev.spawns] : [], cocosPrefab: levelIdUtil.prefabResourcePath(extId), }; if (prev && prev.unityPrefab) out.unityPrefab = prev.unityPrefab; if (mapData) { if (mapData.ground && Object.keys(mapData.ground).length) out.ground = mapData.ground; if (mapData.border && Object.keys(mapData.border).length) out.border = mapData.border; if (mapData.theme) out.theme = mapData.theme; if (mapData.levelID > 0) out.levelID = levelIdUtil.isExternalLevelId(mapData.levelID) ? mapData.levelID : levelIdUtil.externalLevelId(mapData.levelID); } else if (prev) { for (const k of ['ground', 'border', 'theme', 'entityTextures']) { if (prev[k] !== undefined) out[k] = prev[k]; } } return out; } function parsePrefabFile(prefabFile, projectPath) { const objs = readJsonFile(prefabFile, null); if (!Array.isArray(objs)) return null; const mapData = parseLevelMapData(objs); const fromNodes = parseTilesFromNodes(objs, projectPath); return mergeMapSources(mapData, fromNodes); } function loadDatabase(projectPath) { const fp = dbPath(projectPath); if (!fs.existsSync(fp)) { return levelIdUtil.normalizeDb({ levels: {} }); } return levelIdUtil.normalizeDb(readJsonFile(fp, { levels: {} })); } function saveDatabase(db, projectPath) { levelIdUtil.normalizeDb(db); levelIdUtil.updateDbStats(db); db.generatedAt = new Date().toISOString(); if (!db.version) db.version = 2; if (!db.source) db.source = 'Cocos level-prefabs LevelMapData + levels-database spawns'; writeJsonFile(dbPath(projectPath), db); } async function refreshDatabaseAsset() { const assetUrl = `db://${DB_REL}`; try { await Editor.Message.request('asset-db', 'refresh-asset', assetUrl); const uuid = await Editor.Message.request('asset-db', 'query-uuid', assetUrl); if (uuid) await Editor.Message.request('asset-db', 'refresh-asset', uuid); } catch (e) { console.warn('[level-map-editor] levels-database refresh failed', e); } } function importLevelFromPrefab(levelId, options = {}) { const projectPath = projectRoot(options.projectPath); const fp = prefabPath(levelId, projectPath); if (!fs.existsSync(fp)) { return { ok: false, reason: 'prefab missing', levelId }; } const mapData = parsePrefabFile(fp, projectPath); if (!mapData) { return { ok: false, reason: 'parse failed', levelId }; } const db = loadDatabase(projectPath); const key = String(levelId); const prev = db.levels[key]; db.levels[key] = mergeEntry(levelId, mapData, prev); levelIdUtil.touchDatabase(db, levelId); saveDatabase(db, projectPath); return { ok: true, levelId, ground: Object.keys(mapData.ground || {}).length, border: Object.keys(mapData.border || {}).length, theme: mapData.theme, }; } function extractLevelIdFromUrl(url) { if (!url || typeof url !== 'string') return null; const m = url.match(/level-prefabs\/Level(\d+)\.prefab/i); return m ? parseInt(m[1], 10) : null; } function extractLevelIdFromFilename(name) { if (!name || typeof name !== 'string') return null; const m = name.match(PREFAB_NAME_RE); return m ? parseInt(m[1], 10) : null; } module.exports = { DB_REL, PREFAB_DIR_REL, prefabPath, dbPath, parsePrefabFile, importLevelFromPrefab, refreshDatabaseAsset, extractLevelIdFromUrl, extractLevelIdFromFilename, clearSpriteUuidCache() { spriteUuidCache = null; }, };