Files
cocos/extensions/theme-controller/dist/theme-db.js
刘宇飞 d393302388 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>
2026-06-16 15:30:58 +08:00

444 lines
15 KiB
JavaScript
Raw 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 THEMES_DB_REL = 'assets/resources/theme/themes-database.json';
const PALETTES_INDEX_REL = 'assets/resources/map-tiles/palettes/_index.json';
const LEVELS_DB_REL = 'assets/level-data/levels-database.json';
const ENTITY_FIELDS = [
{ key: 'playerFront', label: '角色正面' },
{ key: 'playerBack', label: '角色背面' },
{ key: 'vehicleNorth', label: '载具北 (上)' },
{ key: 'vehicleEast', label: '载具东 (右)' },
{ key: 'vehicleSouth', label: '载具南 (下)' },
{ key: 'vehicleWest', label: '载具西 (左)' },
{ key: 'prop', label: '可拾取物' },
{ key: 'propGround', label: '可拾取物(空地 nProp)' },
];
const TILE_FIELDS = [
{ key: 'Baseblock', label: 'Baseblock地面' },
{ key: 'JumpBlock', label: 'JumpBlock跳跃块' },
{ key: 'WallBlock', label: 'WallBlock' },
{ key: 'borderDecor', label: '装饰墙砖第4块' },
];
/** Unity GameManager.changeIcon / UIMain 右侧 HUD 按钮 */
const HUD_FIELDS = [
{ key: 'navigation', label: '导航' },
{ key: 'revert', label: '重置' },
{ key: 'speed1', label: '1倍速' },
{ key: 'speed2', label: '2倍速' },
{ key: 'speed4', label: '4倍速' },
{ key: 'zoomIn', label: '放大' },
{ key: 'zoomOut', label: '缩小' },
{ key: 'audioOn', label: '声音开' },
{ key: 'audioOff', label: '声音关' },
];
const CELL_PIXEL = 100;
const HALF_H = CELL_PIXEL * 0.25;
const DEFAULT_PROP_BLOCK_Y_OFFSET = 14;
const DEFAULT_PROP_GROUND_Y_OFFSET = -11;
const DEFAULT_MOVER_EMPTY_CELL_Y_OFFSET = HALF_H;
const DEFAULT_PLAYER_RIDE_Y_OFFSET = HALF_H * 0.88;
const DEFAULT_PLAYER_STAND_Y_OFFSET = 0;
const ENTITY_DISPLAY_BASE = {
player: { w: 0.68, h: 0.9 },
vehicle: { w: 0.96, h: 0.88 },
prop: { w: 0.52, h: 0.69 },
propGround: { w: 0.52, h: 0.69 },
};
const DEFAULT_ENTITY_DISPLAY = {
player: { scale: 1 },
vehicle: { scale: 1 },
prop: { scale: 1 },
propGround: { scale: 1 },
propBlockYOffset: DEFAULT_PROP_BLOCK_Y_OFFSET,
propGroundYOffset: DEFAULT_PROP_GROUND_Y_OFFSET,
moverEmptyCellYOffset: DEFAULT_MOVER_EMPTY_CELL_Y_OFFSET,
playerRideYOffset: DEFAULT_PLAYER_RIDE_Y_OFFSET,
playerStandYOffset: DEFAULT_PLAYER_STAND_Y_OFFSET,
};
const ENTITY_DISPLAY_FIELDS = [
{ key: 'player', label: '角色' },
{ key: 'vehicle', label: '载具' },
{ key: 'prop', label: '可拾取物' },
{ key: 'propGround', label: '可拾取物(空地)' },
];
const ENTITY_DISPLAY_OFFSET_FIELDS = [
{ key: 'propBlockYOffset', label: '砖块可拾取物 Y 偏移 (px)', fallback: DEFAULT_PROP_BLOCK_Y_OFFSET },
{ key: 'propGroundYOffset', label: '空地可拾取物 Y 偏移 (px)', fallback: DEFAULT_PROP_GROUND_Y_OFFSET },
{ key: 'moverEmptyCellYOffset', label: '空地/载具格 角色 Y 补偿 (px)', fallback: DEFAULT_MOVER_EMPTY_CELL_Y_OFFSET },
{ key: 'playerRideYOffset', label: '骑乘载具 角色 Y 偏移 (px)', fallback: DEFAULT_PLAYER_RIDE_Y_OFFSET },
{ key: 'playerStandYOffset', label: '角色站立 Y 偏移 (px)', fallback: DEFAULT_PLAYER_STAND_Y_OFFSET },
];
function clampScale(v, fallback = 1) {
const n = typeof v === 'number' ? v : parseFloat(String(v));
if (Number.isNaN(n)) return fallback;
return Math.max(0.1, Math.min(2, n));
}
function readScale(raw, base) {
if (raw && raw.scale != null) return clampScale(raw.scale);
if (raw?.w != null && raw?.h != null) {
return clampScale(Math.min(raw.w / base.w, raw.h / base.h));
}
if (raw?.w != null) return clampScale(raw.w / base.w);
if (raw?.h != null) return clampScale(raw.h / base.h);
return 1;
}
function readYOffset(raw, fallback) {
const n = typeof raw === 'number' ? raw : parseFloat(String(raw));
if (Number.isNaN(n)) return fallback;
return n;
}
function mergeEntityDisplay(raw) {
const propScale = readScale(raw?.prop, ENTITY_DISPLAY_BASE.prop);
const out = {
player: { scale: readScale(raw?.player, ENTITY_DISPLAY_BASE.player) },
vehicle: { scale: readScale(raw?.vehicle, ENTITY_DISPLAY_BASE.vehicle) },
prop: { scale: propScale },
propGround: { scale: readScale(raw?.propGround ?? raw?.prop, ENTITY_DISPLAY_BASE.propGround) },
propBlockYOffset: readYOffset(raw?.propBlockYOffset, DEFAULT_PROP_BLOCK_Y_OFFSET),
propGroundYOffset: readYOffset(raw?.propGroundYOffset, DEFAULT_PROP_GROUND_Y_OFFSET),
moverEmptyCellYOffset: readYOffset(raw?.moverEmptyCellYOffset, DEFAULT_MOVER_EMPTY_CELL_Y_OFFSET),
playerRideYOffset: readYOffset(raw?.playerRideYOffset, DEFAULT_PLAYER_RIDE_Y_OFFSET),
playerStandYOffset: readYOffset(raw?.playerStandYOffset, DEFAULT_PLAYER_STAND_Y_OFFSET),
};
return out;
}
function entityDisplayCellBoxes(scales) {
const out = {};
for (const f of ENTITY_DISPLAY_FIELDS) {
const s = scales[f.key].scale;
const base = ENTITY_DISPLAY_BASE[f.key];
out[f.key] = { w: base.w * s, h: base.h * s };
}
return out;
}
function readEntityDisplayFromDb(db) {
return mergeEntityDisplay(db?.entityDisplay);
}
function readEntityDisplayFromForm(root) {
if (!root) return mergeEntityDisplay(undefined);
const get = (id) => {
const el = root.querySelector(`#${id}`);
if (!el) return NaN;
return parseFloat(String(el.value ?? ''));
};
const raw = {};
for (const f of ENTITY_DISPLAY_FIELDS) {
raw[f.key] = { scale: get(`tc-ed-${f.key}-scale`) };
}
for (const f of ENTITY_DISPLAY_OFFSET_FIELDS) {
raw[f.key] = get(`tc-ed-${f.key}`);
}
return mergeEntityDisplay(raw);
}
function writeEntityDisplayToForm(root, entityDisplay) {
const scales = mergeEntityDisplay(entityDisplay);
const boxes = entityDisplayCellBoxes(scales);
const set = (id, val) => {
const el = root.querySelector(`#${id}`);
if (el) el.value = String(val);
};
const setHint = (id, base, box) => {
const el = root.querySelector(`#${id}`);
if (!el) return;
const bw = Math.round(base.w * CELL_PIXEL);
const bh = Math.round(base.h * CELL_PIXEL);
const w = Math.round(box.w * CELL_PIXEL);
const h = Math.round(box.h * CELL_PIXEL);
el.textContent = `默认 ${bw}×${bh} → 现 ≈ ${w}×${h} px`;
};
for (const f of ENTITY_DISPLAY_FIELDS) {
const scale = scales[f.key].scale;
set(`tc-ed-${f.key}-scale`, scale);
setHint(`tc-ed-${f.key}-px`, ENTITY_DISPLAY_BASE[f.key], boxes[f.key]);
}
for (const f of ENTITY_DISPLAY_OFFSET_FIELDS) {
set(`tc-ed-${f.key}`, scales[f.key]);
}
}
function bindEntityDisplayInputs(root, onChange) {
for (const f of ENTITY_DISPLAY_FIELDS) {
const el = root.querySelector(`#tc-ed-${f.key}-scale`);
if (el) el.addEventListener('change', () => onChange?.());
}
for (const f of ENTITY_DISPLAY_OFFSET_FIELDS) {
const el = root.querySelector(`#tc-ed-${f.key}`);
if (el) el.addEventListener('change', () => onChange?.());
}
}
function themesDbPath(projectPath) {
return path.join(projectPath, THEMES_DB_REL);
}
function palettesIndexPath(projectPath) {
return path.join(projectPath, PALETTES_INDEX_REL);
}
function levelsDbPath(projectPath) {
return path.join(projectPath, LEVELS_DB_REL);
}
function normalizePath(raw) {
if (!raw) return '';
let p = String(raw).trim().replace(/\\/g, '/');
if (p.startsWith('assets/resources/')) p = p.slice('assets/resources/'.length);
if (p.startsWith('resources/')) p = p.slice('resources/'.length);
if (p.startsWith('/')) p = p.slice(1);
if (p.endsWith('.png')) p = p.slice(0, -4);
return p;
}
function clampRatio(v, fallback) {
const n = typeof v === 'number' ? v : parseFloat(String(v));
if (Number.isNaN(n)) return fallback;
return Math.max(0.1, Math.min(2, n));
}
function readThemesDb(projectPath) {
const fp = themesDbPath(projectPath);
if (!fs.existsSync(fp)) {
return { version: 1, themes: {} };
}
return JSON.parse(fs.readFileSync(fp, 'utf8'));
}
function writeThemesDb(projectPath, data) {
data.updatedAt = new Date().toISOString();
if (data.entityDisplay && data.themes) {
for (const id of Object.keys(data.themes)) {
if (!data.themes[id].entityDisplay) {
data.themes[id].entityDisplay = mergeEntityDisplay(data.entityDisplay);
}
}
delete data.entityDisplay;
}
const fp = themesDbPath(projectPath);
fs.mkdirSync(path.dirname(fp), { recursive: true });
fs.writeFileSync(fp, JSON.stringify(data, null, 2), 'utf8');
}
function themeToPaletteEntry(themeId, theme) {
const tiles = [];
const t = theme.tiles || {};
const decorKey = theme.borderDecorKey || 'kuai11';
const entries = [
['Baseblock', t.Baseblock, 'ground'],
['JumpBlock', t.JumpBlock, 'ground'],
['WallBlock', t.WallBlock, 'border'],
[decorKey, t.borderDecor, 'border'],
];
let idx = 0;
for (const [tileKey, texPath, layer] of entries) {
if (!texPath) continue;
tiles.push({
display: tileKey,
layer,
tileKey,
texture: texPath,
unityIndex: idx++,
});
}
return {
displayName: theme.displayName || themeId,
tiles,
};
}
function themesToPalettesMap(db) {
const out = {};
for (const [id, theme] of Object.entries(db.themes || {})) {
out[id] = themeToPaletteEntry(id, theme);
}
return out;
}
function buildThemeLabels(db) {
const labels = {};
for (const [id, theme] of Object.entries(db.themes || {})) {
labels[id] = theme.displayName || id;
}
return labels;
}
function syncPalettesIndex(projectPath, db) {
const payload = { themes: themesToPalettesMap(db) };
const fp = palettesIndexPath(projectPath);
fs.mkdirSync(path.dirname(fp), { recursive: true });
fs.writeFileSync(fp, JSON.stringify(payload, null, 2), 'utf8');
}
function defaultHudForFolder(folder) {
return {
navigation: `textures/${folder}/anniu_03`,
revert: `textures/${folder}/anniu_06`,
speed1: `textures/${folder}/anniu_08`,
speed2: `textures/${folder}/anniu_10`,
speed4: `textures/${folder}/anniu_12`,
zoomIn: `textures/${folder}/anniu_17`,
zoomOut: `textures/${folder}/anniu_19`,
audioOn: `textures/${folder}/anniu_22`,
audioOff: `textures/${folder}/anniu_21`,
};
}
function createEmptyTheme(themeId, textureFolder) {
const folder = textureFolder || themeId;
return {
displayName: themeId,
textureFolder: folder,
background: '',
hud: defaultHudForFolder(folder),
entities: {
playerFront: `textures/${folder}/player_F`,
playerBack: `textures/${folder}/player_B`,
vehicleFront: `textures/${folder}/ship_F`,
vehicleBack: `textures/${folder}/ship_B`,
prop: `textures/${folder}/Prop`,
propGround: `textures/${folder}/nProp`,
},
tiles: {
Baseblock: `textures/${folder}/Baseblock`,
JumpBlock: `textures/${folder}/JumpBlock`,
WallBlock: `textures/${folder}/WallBlock`,
borderDecor: `textures/${folder}/kuai11`,
},
borderDecorKey: 'kuai11',
entityDisplay: { ...DEFAULT_ENTITY_DISPLAY },
};
}
function countLevelsUsingTheme(projectPath, themeId) {
const fp = levelsDbPath(projectPath);
if (!fs.existsSync(fp)) return 0;
const db = JSON.parse(fs.readFileSync(fp, 'utf8'));
let n = 0;
for (const cfg of Object.values(db.levels || {})) {
if (cfg && cfg.theme === themeId) n += 1;
}
return n;
}
function readHudFromForm(root) {
const get = (id) => root.querySelector(`#${id}`)?.value ?? '';
const hud = {};
for (const f of HUD_FIELDS) {
const v = normalizePath(get(`tc-hud-${f.key}`));
if (v) hud[f.key] = v;
}
const sx = parseFloat(String(get('tc-hud-iconScaleX')));
const sy = parseFloat(String(get('tc-hud-iconScaleY')));
if (!Number.isNaN(sx)) hud.iconScaleX = sx;
if (!Number.isNaN(sy)) hud.iconScaleY = sy;
return hud;
}
function writeHudToForm(root, hud) {
const set = (id, val) => {
const el = root.querySelector(`#${id}`);
if (el) el.value = val ?? '';
};
for (const f of HUD_FIELDS) {
set(`tc-hud-${f.key}`, hud?.[f.key] || '');
}
set('tc-hud-iconScaleX', hud?.iconScaleX ?? 1);
set('tc-hud-iconScaleY', hud?.iconScaleY ?? 1);
}
function readThemeFromForm(root) {
const get = (id) => root.querySelector(`#${id}`)?.value ?? '';
const theme = {
displayName: get('tc-display-name'),
textureFolder: get('tc-texture-folder'),
background: normalizePath(get('tc-background')),
borderDecorKey: get('tc-border-key') || 'kuai11',
entities: {},
tiles: {},
hud: readHudFromForm(root),
entityDisplay: readEntityDisplayFromForm(root.querySelector('#tc-entity-display')),
};
for (const f of ENTITY_FIELDS) {
const v = normalizePath(get(`tc-ent-${f.key}`));
if (v) theme.entities[f.key] = v;
}
for (const f of TILE_FIELDS) {
const v = normalizePath(get(`tc-tile-${f.key}`));
if (v) theme.tiles[f.key] = v;
}
if (!theme.background) delete theme.background;
return theme;
}
function writeThemeToForm(root, themeId, theme) {
const set = (id, val) => {
const el = root.querySelector(`#${id}`);
if (el) el.value = val ?? '';
};
set('tc-theme-id', themeId || '');
set('tc-display-name', theme?.displayName || '');
set('tc-texture-folder', theme?.textureFolder || themeId || '');
set('tc-background', theme?.background || '');
set('tc-border-key', theme?.borderDecorKey || 'kuai11');
for (const f of ENTITY_FIELDS) {
set(`tc-ent-${f.key}`, theme?.entities?.[f.key] || '');
}
for (const f of TILE_FIELDS) {
set(`tc-tile-${f.key}`, theme?.tiles?.[f.key] || '');
}
writeHudToForm(root, theme?.hud);
const edWrap = root.querySelector('#tc-entity-display');
if (edWrap) writeEntityDisplayToForm(edWrap, theme?.entityDisplay);
}
module.exports = {
THEMES_DB_REL,
PALETTES_INDEX_REL,
ENTITY_FIELDS,
TILE_FIELDS,
HUD_FIELDS,
ENTITY_DISPLAY_FIELDS,
ENTITY_DISPLAY_OFFSET_FIELDS,
ENTITY_DISPLAY_BASE,
DEFAULT_ENTITY_DISPLAY,
DEFAULT_PROP_BLOCK_Y_OFFSET,
DEFAULT_PROP_GROUND_Y_OFFSET,
CELL_PIXEL,
themesDbPath,
readThemesDb,
writeThemesDb,
themesToPalettesMap,
buildThemeLabels,
syncPalettesIndex,
createEmptyTheme,
countLevelsUsingTheme,
readThemeFromForm,
writeThemeToForm,
normalizePath,
mergeEntityDisplay,
entityDisplayCellBoxes,
readEntityDisplayFromDb,
readEntityDisplayFromForm,
writeEntityDisplayToForm,
bindEntityDisplayInputs,
};