Files
cocos/extensions/theme-controller/dist/panels/theme-controller.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

511 lines
20 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 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;
};