'use strict'; const themeDb = require('../theme-db'); const texPreview = require('../texture-preview'); exports.template = `
选中 新建 删除 保存主题 同步调色板 刷新预览

主题列表

主题配置

加载 themes-database.json
`; 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 直写,勿 refresh(Cocos 会报 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; };