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,510 @@
'use strict';
const themeDb = require('../theme-db');
const texPreview = require('../texture-preview');
exports.template = `
<div class="tc-root">
<div class="tc-toolbar">
<ui-label value="主题 ID"></ui-label>
<ui-input id="tc-theme-id" placeholder="如 sanxing" style="width:120px"></ui-input>
<ui-button id="tc-btn-select">选中</ui-button>
<ui-button id="tc-btn-new">新建</ui-button>
<ui-button id="tc-btn-delete">删除</ui-button>
<span style="width:8px"></span>
<ui-button id="tc-btn-save">保存主题</ui-button>
<ui-button id="tc-btn-sync">同步调色板</ui-button>
<ui-button id="tc-btn-refresh-previews">刷新预览</ui-button>
</div>
<div class="tc-body">
<div class="tc-list-wrap">
<h4>主题列表</h4>
<div id="tc-theme-list" class="tc-list"></div>
</div>
<div class="tc-form-wrap" id="tc-form-wrap">
<h4>主题配置</h4>
<div id="tc-form-fields"></div>
</div>
</div>
<div class="tc-status" id="tc-status">加载 themes-database.json</div>
</div>
`;
exports.style = `
.tc-root { display:flex; flex-direction:column; height:100%; font-size:12px; }
.tc-toolbar { display:flex; gap:6px; align-items:center; padding:8px; border-bottom:1px solid var(--color-normal-border); flex-wrap:wrap; }
.tc-ed-section { margin-bottom: 10px; padding: 8px; background: var(--color-normal-fill-emphasis); border-radius: 4px; }
.tc-ed-section h5 { margin: 0 0 8px; font-size: 11px; font-weight: bold; }
.tc-ed-grid { display: flex; flex-direction: column; gap: 6px; }
.tc-ed-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.tc-ed-row ui-label { min-width: 72px; font-size: 11px; }
.tc-ed-row ui-num-input { width: 88px; }
.tc-ed-px { font-size: 10px; color: var(--color-normal-contrast-weaken); min-width: 160px; }
.tc-ed-actions { display: flex; gap: 8px; margin-top: 8px; }
.tc-body { display:flex; flex:1; min-height:0; }
.tc-list-wrap { width:200px; border-right:1px solid var(--color-normal-border); padding:8px; overflow-y:auto; }
.tc-list-wrap h4, .tc-form-wrap h4 { margin:0 0 8px; font-size:12px; }
.tc-list { display:flex; flex-direction:column; gap:4px; }
.tc-list ui-button { justify-content:flex-start; text-align:left; }
.tc-list ui-button.active { font-weight:bold; background:var(--color-primary-fill); }
.tc-form-wrap { flex:1; padding:8px; overflow-y:auto; }
.tc-row { display:flex; align-items:center; gap:8px; margin-bottom:6px; flex-wrap:wrap; }
.tc-row ui-label { min-width:108px; font-size:11px; }
.tc-row ui-input { flex:1; min-width:200px; }
.tc-preview-row { display:flex; align-items:flex-start; gap:10px; margin-bottom:10px; }
.tc-preview-row ui-label { min-width:108px; font-size:11px; padding-top:22px; flex-shrink:0; }
.tc-preview-col { display:flex; flex-direction:column; align-items:center; gap:4px; flex-shrink:0; }
.tc-preview {
width:72px; height:72px; background:#2a2a2a; border-radius:4px;
border:1px solid var(--color-normal-border); display:flex; align-items:center;
justify-content:center; overflow:hidden; position:relative;
}
.tc-preview.large { width:120px; height:72px; }
.tc-preview img { max-width:100%; max-height:100%; object-fit:contain; display:block; }
.tc-preview.empty .tc-preview-empty { display:flex; }
.tc-preview-empty {
display:none; color:#888; font-size:10px; text-align:center; padding:4px;
align-items:center; justify-content:center; width:100%; height:100%;
}
.tc-preview-caption { font-size:9px; color:var(--color-normal-contrast-weaken); max-width:72px; text-align:center; word-break:break-all; }
.tc-preview-row ui-input { flex:1; min-width:180px; margin-top:18px; }
.tc-preview-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(200px, 1fr)); gap:10px; margin-bottom:8px; }
.tc-preview-card {
display:flex; flex-direction:column; gap:4px; padding:8px; background:var(--color-normal-fill);
border-radius:4px; border:1px solid var(--color-normal-border);
}
.tc-preview-card ui-label { font-size:11px; }
.tc-preview-card ui-input { width:100%; }
.tc-preview-card .tc-preview { width:100%; height:80px; }
.tc-section { font-weight:bold; margin:12px 0 8px; font-size:11px; color:var(--color-normal-contrast-weaken); }
.tc-status { padding:6px 8px; border-top:1px solid var(--color-normal-border); font-size:11px; color:var(--color-normal-contrast-weaken); }
`;
exports.$ = {
themeId: '#tc-theme-id',
btnSelect: '#tc-btn-select',
btnNew: '#tc-btn-new',
btnDelete: '#tc-btn-delete',
btnSave: '#tc-btn-save',
btnSync: '#tc-btn-sync',
btnRefreshPreviews: '#tc-btn-refresh-previews',
themeList: '#tc-theme-list',
formFields: '#tc-form-fields',
formWrap: '#tc-form-wrap',
status: '#tc-status',
};
function projectPath() {
return Editor.Project.path;
}
function createPreviewBox(id, large = false) {
const box = document.createElement('div');
box.className = 'tc-preview' + (large ? ' large' : '');
box.id = `${id}-preview`;
const empty = document.createElement('span');
empty.className = 'tc-preview-empty';
empty.textContent = '无图';
box.appendChild(empty);
return box;
}
function buildEntityDisplaySection(wrap) {
const section = document.createElement('div');
section.className = 'tc-ed-section';
const title = document.createElement('h5');
title.textContent = '实体尺寸(等比缩放,随本主题保存)';
section.appendChild(title);
const hint = document.createElement('p');
hint.style.cssText = 'margin:0 0 8px;font-size:10px;color:var(--color-normal-contrast-weaken);line-height:1.4';
hint.textContent = '「缩放」只改贴图大小下方「Y 偏移」改站立/道具高度。保存主题后重载关卡即可看到 Y 变化。';
section.appendChild(hint);
const grid = document.createElement('div');
grid.id = 'tc-entity-display';
grid.className = 'tc-ed-grid';
for (const f of themeDb.ENTITY_DISPLAY_FIELDS) {
const row = document.createElement('div');
row.className = 'tc-ed-row';
const lab = document.createElement('ui-label');
lab.textContent = f.label;
const scaleIn = document.createElement('ui-num-input');
scaleIn.id = `tc-ed-${f.key}-scale`;
scaleIn.step = '0.05';
scaleIn.min = '0.1';
scaleIn.max = '2';
const scaleLab = document.createElement('ui-label');
scaleLab.textContent = '缩放';
scaleLab.style.minWidth = '28px';
const px = document.createElement('span');
px.id = `tc-ed-${f.key}-px`;
px.className = 'tc-ed-px';
row.appendChild(lab);
row.appendChild(scaleLab);
row.appendChild(scaleIn);
row.appendChild(px);
grid.appendChild(row);
}
for (const f of themeDb.ENTITY_DISPLAY_OFFSET_FIELDS) {
const row = document.createElement('div');
row.className = 'tc-ed-row';
const lab = document.createElement('ui-label');
lab.textContent = f.label;
const offsetIn = document.createElement('ui-num-input');
offsetIn.id = `tc-ed-${f.key}`;
offsetIn.step = '1';
const pxLab = document.createElement('ui-label');
pxLab.textContent = 'px';
pxLab.style.minWidth = '20px';
const pxHint = document.createElement('span');
pxHint.id = `tc-ed-${f.key}-hint`;
pxHint.className = 'tc-ed-px';
row.appendChild(lab);
row.appendChild(offsetIn);
row.appendChild(pxLab);
row.appendChild(pxHint);
grid.appendChild(row);
}
section.appendChild(grid);
const actions = document.createElement('div');
actions.className = 'tc-ed-actions';
const btnReset = document.createElement('ui-button');
btnReset.id = 'tc-btn-reset-entity-display';
btnReset.textContent = '恢复默认';
actions.appendChild(btnReset);
section.appendChild(actions);
wrap.appendChild(section);
for (const f of themeDb.ENTITY_DISPLAY_FIELDS) {
const el = grid.querySelector(`#tc-ed-${f.key}-scale`);
if (el) el.addEventListener('change', () => refreshEntityDisplayHints(grid));
}
for (const f of themeDb.ENTITY_DISPLAY_OFFSET_FIELDS) {
const el = grid.querySelector(`#tc-ed-${f.key}`);
if (el) el.addEventListener('change', () => refreshEntityDisplayHints(grid));
}
btnReset.addEventListener('click', () => {
themeDb.writeEntityDisplayToForm(grid, themeDb.DEFAULT_ENTITY_DISPLAY);
refreshEntityDisplayHints(grid);
});
}
function refreshEntityDisplayHints(wrap) {
if (!wrap) return;
const scales = themeDb.readEntityDisplayFromForm(wrap);
const boxes = themeDb.entityDisplayCellBoxes(scales);
for (const f of themeDb.ENTITY_DISPLAY_FIELDS) {
const el = wrap.querySelector(`#tc-ed-${f.key}-px`);
if (!el) continue;
const base = themeDb.ENTITY_DISPLAY_BASE[f.key];
const box = boxes[f.key];
const bw = Math.round(base.w * themeDb.CELL_PIXEL);
const bh = Math.round(base.h * themeDb.CELL_PIXEL);
const w = Math.round(box.w * themeDb.CELL_PIXEL);
const h = Math.round(box.h * themeDb.CELL_PIXEL);
el.textContent = `默认 ${bw}×${bh} → 现 ≈ ${w}×${h} px`;
}
for (const f of themeDb.ENTITY_DISPLAY_OFFSET_FIELDS) {
const el = wrap.querySelector(`#tc-ed-${f.key}-hint`);
if (!el) continue;
const v = scales[f.key];
el.textContent = `当前 ${v} px正值抬高负值降低`;
}
}
function buildForm(panel) {
const wrap = panel.$.formFields;
if (!wrap) return;
wrap.innerHTML = '';
const addSection = (title) => {
const s = document.createElement('div');
s.className = 'tc-section';
s.textContent = title;
wrap.appendChild(s);
};
const addPlainRow = (id, label, placeholder) => {
const row = document.createElement('div');
row.className = 'tc-row';
const lab = document.createElement('ui-label');
lab.textContent = label;
const inp = document.createElement('ui-input');
inp.id = id;
inp.placeholder = placeholder || '';
row.appendChild(lab);
row.appendChild(inp);
wrap.appendChild(row);
};
const addPreviewRow = (id, label, placeholder, large = false) => {
const row = document.createElement('div');
row.className = 'tc-preview-row';
const lab = document.createElement('ui-label');
lab.textContent = label;
const col = document.createElement('div');
col.className = 'tc-preview-col';
col.appendChild(createPreviewBox(id, large));
const inp = document.createElement('ui-input');
inp.id = id;
inp.placeholder = placeholder || '';
row.appendChild(lab);
row.appendChild(col);
row.appendChild(inp);
wrap.appendChild(row);
};
const addPreviewCard = (id, label, placeholder) => {
const card = document.createElement('div');
card.className = 'tc-preview-card';
const lab = document.createElement('ui-label');
lab.textContent = label;
card.appendChild(lab);
card.appendChild(createPreviewBox(id, false));
const inp = document.createElement('ui-input');
inp.id = id;
inp.placeholder = placeholder || '';
card.appendChild(inp);
return card;
};
addSection('基础');
addPlainRow('tc-display-name', '显示名称', '三星堆 sanxing');
addPlainRow('tc-texture-folder', '贴图文件夹', 'sanxing');
addPreviewRow('tc-background', '背景图', 'textures/sanxing/bg', true);
addSection('实体贴图(相对 assets/resources');
const entGrid = document.createElement('div');
entGrid.className = 'tc-preview-grid';
for (const f of themeDb.ENTITY_FIELDS) {
entGrid.appendChild(addPreviewCard(`tc-ent-${f.key}`, f.label, 'textures/...'));
}
wrap.appendChild(entGrid);
buildEntityDisplaySection(wrap);
addSection('四种砖块');
const tileGrid = document.createElement('div');
tileGrid.className = 'tc-preview-grid';
for (const f of themeDb.TILE_FIELDS) {
tileGrid.appendChild(addPreviewCard(`tc-tile-${f.key}`, f.label, 'textures/.../Baseblock'));
}
wrap.appendChild(tileGrid);
addPlainRow('tc-border-key', '装饰砖 tileKey', 'kuai11');
addSection('HUD 按钮Unity UIMain 右侧,相对 assets/resources');
const hudGrid = document.createElement('div');
hudGrid.className = 'tc-preview-grid';
for (const f of themeDb.HUD_FIELDS) {
hudGrid.appendChild(addPreviewCard(`tc-hud-${f.key}`, f.label, 'textures/.../anniu_03'));
}
wrap.appendChild(hudGrid);
const hudScaleRow = document.createElement('div');
hudScaleRow.className = 'tc-row';
const scaleLab = document.createElement('ui-label');
scaleLab.textContent = '按钮图标缩放';
const sx = document.createElement('ui-num-input');
sx.id = 'tc-hud-iconScaleX';
sx.step = '0.01';
sx.min = '0.5';
sx.max = '2';
const sy = document.createElement('ui-num-input');
sy.id = 'tc-hud-iconScaleY';
sy.step = '0.01';
sy.min = '0.5';
sy.max = '2';
const sxLab = document.createElement('ui-label');
sxLab.textContent = 'X';
sxLab.style.minWidth = '16px';
const syLab = document.createElement('ui-label');
syLab.textContent = 'Y';
syLab.style.minWidth = '16px';
hudScaleRow.appendChild(scaleLab);
hudScaleRow.appendChild(sxLab);
hudScaleRow.appendChild(sx);
hudScaleRow.appendChild(syLab);
hudScaleRow.appendChild(sy);
wrap.appendChild(hudScaleRow);
texPreview.bindPreviewInputs(panel.$.formWrap, projectPath());
}
function refreshPreviews(panel) {
texPreview.refreshAllPreviews(panel.$.formWrap, projectPath());
}
function writeFormAndRefresh(panel, themeId, theme) {
themeDb.writeThemeToForm(panel.$.formWrap, themeId, theme);
const edWrap = panel.$.formWrap?.querySelector('#tc-entity-display');
if (edWrap) refreshEntityDisplayHints(edWrap);
refreshPreviews(panel);
}
function renderThemeList(panel, state) {
const list = panel.$.themeList;
if (!list) return;
list.innerHTML = '';
const ids = Object.keys(state.db.themes || {}).sort();
for (const id of ids) {
const btn = document.createElement('ui-button');
const t = state.db.themes[id];
btn.textContent = t.displayName ? `${t.displayName} (${id})` : id;
if (state.selectedId === id) btn.classList.add('active');
btn.addEventListener('click', () => {
state.selectedId = id;
panel.$.themeId.value = id;
writeFormAndRefresh(panel, id, t);
renderThemeList(panel, state);
if (panel.$.status) panel.$.status.textContent = `编辑主题:${id}`;
});
list.appendChild(btn);
}
}
function loadDb(state) {
state.db = themeDb.readThemesDb(projectPath());
if (state.db.entityDisplay && state.db.themes) {
for (const id of Object.keys(state.db.themes)) {
if (!state.db.themes[id].entityDisplay) {
state.db.themes[id].entityDisplay = themeDb.mergeEntityDisplay(state.db.entityDisplay);
}
}
}
for (const id of Object.keys(state.db.themes || {})) {
if (!state.db.themes[id].entityDisplay) {
state.db.themes[id].entityDisplay = themeDb.mergeEntityDisplay(undefined);
}
}
if (!state.selectedId || !state.db.themes[state.selectedId]) {
const ids = Object.keys(state.db.themes || {}).sort();
state.selectedId = ids[0] || null;
}
}
async function refreshAssetDb(urlOrUuid) {
try {
await Editor.Message.request('asset-db', 'refresh-asset', urlOrUuid);
} catch (e) {
console.warn('[theme-controller] refresh failed:', urlOrUuid, e?.message || e);
}
}
/** 仅刷新 themes-database运行时 resources.load 需要。palettes/_index.json 由 fs 直写,勿 refreshCocos 会报 Can not change asset。 */
async function refreshAssets() {
const themesUrl = `db://${themeDb.THEMES_DB_REL}`;
await refreshAssetDb(themesUrl);
try {
const uuid = await Editor.Message.request('asset-db', 'query-uuid', themesUrl);
if (uuid) await refreshAssetDb(uuid);
} catch (_) { /* query-uuid 不可用时忽略 */ }
}
function saveCurrent(panel, state) {
const id = String(panel.$.themeId?.value || '').trim();
if (!id) {
if (panel.$.status) panel.$.status.textContent = '请输入主题 ID';
return false;
}
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(id)) {
if (panel.$.status) panel.$.status.textContent = '主题 ID 仅允许字母数字下划线,且以字母开头';
return false;
}
const theme = themeDb.readThemeFromForm(panel.$.formWrap);
if (!state.db.themes) state.db.themes = {};
state.db.themes[id] = theme;
state.selectedId = id;
themeDb.writeThemesDb(projectPath(), state.db);
themeDb.syncPalettesIndex(projectPath(), state.db);
void refreshAssets();
renderThemeList(panel, state);
refreshPreviews(panel);
if (panel.$.status) panel.$.status.textContent = `已保存主题「${id}」并同步调色板`;
return true;
}
exports.ready = function () {
if (this._tcReady) return;
this._tcReady = true;
const state = { db: { themes: {} }, selectedId: null };
this._tcState = state;
buildForm(this);
loadDb(state);
if (state.selectedId) {
this.$.themeId.value = state.selectedId;
writeFormAndRefresh(this, state.selectedId, state.db.themes[state.selectedId]);
}
renderThemeList(this, state);
if (this.$.status) {
this.$.status.textContent = `已加载 ${Object.keys(state.db.themes || {}).length} 个主题`;
}
this.$.btnSelect?.addEventListener('click', () => {
const id = String(this.$.themeId?.value || '').trim();
if (!id || !state.db.themes[id]) {
if (this.$.status) this.$.status.textContent = `主题不存在:${id}`;
return;
}
state.selectedId = id;
writeFormAndRefresh(this, id, state.db.themes[id]);
renderThemeList(this, state);
});
this.$.btnNew?.addEventListener('click', () => {
const id = String(this.$.themeId?.value || '').trim();
if (!id) {
if (this.$.status) this.$.status.textContent = '请输入新主题 ID';
return;
}
if (state.db.themes[id]) {
if (this.$.status) this.$.status.textContent = `主题已存在:${id}`;
return;
}
state.db.themes[id] = themeDb.createEmptyTheme(id);
state.selectedId = id;
writeFormAndRefresh(this, id, state.db.themes[id]);
renderThemeList(this, state);
if (this.$.status) this.$.status.textContent = `已创建空主题「${id}」,填写后点保存`;
});
this.$.btnDelete?.addEventListener('click', () => {
const id = state.selectedId || String(this.$.themeId?.value || '').trim();
if (!id || !state.db.themes[id]) {
if (this.$.status) this.$.status.textContent = '无选中主题';
return;
}
const used = themeDb.countLevelsUsingTheme(projectPath(), id);
if (used > 0) {
if (this.$.status) this.$.status.textContent = `无法删除:${used} 个关卡使用「${id}`;
return;
}
delete state.db.themes[id];
themeDb.writeThemesDb(projectPath(), state.db);
themeDb.syncPalettesIndex(projectPath(), state.db);
loadDb(state);
if (state.selectedId) {
this.$.themeId.value = state.selectedId;
writeFormAndRefresh(this, state.selectedId, state.db.themes[state.selectedId]);
}
renderThemeList(this, state);
if (this.$.status) this.$.status.textContent = `已删除主题「${id}`;
});
this.$.btnSave?.addEventListener('click', () => saveCurrent(this, state));
this.$.btnSync?.addEventListener('click', () => {
themeDb.syncPalettesIndex(projectPath(), state.db);
void refreshAssets();
refreshPreviews(this);
if (this.$.status) this.$.status.textContent = '已同步 palettes/_index.json';
});
this.$.btnRefreshPreviews?.addEventListener('click', () => {
refreshPreviews(this);
if (this.$.status) this.$.status.textContent = '已刷新贴图预览';
});
};
exports.close = function () {
this._tcReady = false;
};