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:
2026-06-16 15:30:58 +08:00
parent cba5105908
commit d393302388
6248 changed files with 17322729 additions and 11036 deletions

View 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;
},
};