Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
271
extensions/level-map-editor/dist/prefab-sync.js
vendored
Normal file
271
extensions/level-map-editor/dist/prefab-sync.js
vendored
Normal file
@@ -0,0 +1,271 @@
|
||||
'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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user