Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
272 lines
8.9 KiB
JavaScript
272 lines
8.9 KiB
JavaScript
'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;
|
|
},
|
|
};
|