Files

292 lines
9.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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_BUNDLE_REL = 'assets/bundle-level-prefabs/level-prefabs';
const PREFAB_LEGACY_REL = 'assets/resources/level-prefabs';
/** @deprecated 使用 resolvePrefabDirRel */
const PREFAB_DIR_REL = PREFAB_LEGACY_REL;
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 resolvePrefabDirRel(projectPath) {
const root = projectRoot(projectPath);
if (fs.existsSync(path.join(root, PREFAB_BUNDLE_REL))) return PREFAB_BUNDLE_REL;
return PREFAB_LEGACY_REL;
}
function prefabDir(projectPath) {
return path.join(projectRoot(projectPath), resolvePrefabDirRel(projectPath));
}
function prefabDbUrl(levelId, projectPath) {
return `db://${resolvePrefabDirRel(projectPath)}/Level${levelId}.prefab`;
}
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 === 'JumpBlock' ? 'JumpBlock' : 'Baseblock';
} else {
const s = tileKey || 'WallBlock';
border[key] = (s === 'Baseblock' || s === 'JumpBlock') ? 'WallBlock' : s;
}
}
return { ground, border };
}
function mergeMapSources(mapData, fromNodes) {
const md = mapData || { ground: {}, border: {}, theme: null, levelID: 0 };
const nodes = fromNodes || { ground: {}, border: {} };
// LevelMapData 是烘焙权威(含 JumpBlock / kuai11。节点贴图 UUID 对不上时
// 会被收成 Baseblock / WallBlock不能盖过 JSON。
const ground = Object.keys(md.ground || {}).length ? md.ground : (nodes.ground || {});
const border = Object.keys(md.border || {}).length ? md.border : (nodes.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,
PREFAB_BUNDLE_REL,
PREFAB_LEGACY_REL,
resolvePrefabDirRel,
prefabDbUrl,
prefabPath,
dbPath,
parsePrefabFile,
importLevelFromPrefab,
refreshDatabaseAsset,
extractLevelIdFromUrl,
extractLevelIdFromFilename,
clearSpriteUuidCache() {
spriteUuidCache = null;
},
};