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:
20
extensions/theme-controller/dist/main.js
vendored
Normal file
20
extensions/theme-controller/dist/main.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
exports.methods = {
|
||||
openPanel() {
|
||||
try {
|
||||
Editor.Panel.open('theme-controller');
|
||||
console.log('[theme-controller] Panel.open');
|
||||
} catch (e) {
|
||||
console.error('[theme-controller] open failed:', e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
exports.load = function () {
|
||||
console.log('[theme-controller] extension loaded');
|
||||
};
|
||||
|
||||
exports.unload = function () {
|
||||
console.log('[theme-controller] extension unloaded');
|
||||
};
|
||||
510
extensions/theme-controller/dist/panels/theme-controller.js
vendored
Normal file
510
extensions/theme-controller/dist/panels/theme-controller.js
vendored
Normal 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 直写,勿 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;
|
||||
};
|
||||
96
extensions/theme-controller/dist/texture-preview.js
vendored
Normal file
96
extensions/theme-controller/dist/texture-preview.js
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { pathToFileURL } = require('url');
|
||||
|
||||
/** 需要贴图预览的表单字段 id */
|
||||
const HUD_ICON_KEYS = [
|
||||
'navigation', 'revert', 'speed1', 'speed2', 'speed4',
|
||||
'zoomIn', 'zoomOut', 'audioOn', 'audioOff',
|
||||
];
|
||||
|
||||
const PREVIEW_INPUT_IDS = [
|
||||
'tc-background',
|
||||
...['playerFront', 'playerBack', 'vehicleNorth', 'vehicleEast', 'vehicleSouth', 'vehicleWest', 'prop', 'propGround'].map((k) => `tc-ent-${k}`),
|
||||
...['Baseblock', 'JumpBlock', 'WallBlock', 'borderDecor'].map((k) => `tc-tile-${k}`),
|
||||
...HUD_ICON_KEYS.map((k) => `tc-hud-${k}`),
|
||||
];
|
||||
|
||||
function textureFsPath(projectRoot, texRel) {
|
||||
const normalized = String(texRel || '').trim();
|
||||
if (!normalized) return null;
|
||||
let base = normalized.replace(/\\/g, '/');
|
||||
if (base.startsWith('assets/resources/')) base = base.slice('assets/resources/'.length);
|
||||
if (base.endsWith('.png')) base = base.slice(0, -4);
|
||||
if (!base.startsWith('textures/')) base = `textures/${base}`;
|
||||
return path.join(projectRoot, 'assets/resources', `${base}.png`);
|
||||
}
|
||||
|
||||
function updatePreviewBox(previewEl, texRel, projectRoot) {
|
||||
if (!previewEl) return;
|
||||
let img = previewEl.querySelector('img');
|
||||
let empty = previewEl.querySelector('.tc-preview-empty');
|
||||
const fsPath = textureFsPath(projectRoot, texRel);
|
||||
if (!fsPath || !fs.existsSync(fsPath)) {
|
||||
previewEl.classList.add('empty');
|
||||
if (img) {
|
||||
img.style.display = 'none';
|
||||
img.removeAttribute('src');
|
||||
}
|
||||
if (empty) empty.style.display = 'flex';
|
||||
previewEl.title = texRel ? `未找到: ${fsPath || texRel}` : '';
|
||||
return;
|
||||
}
|
||||
previewEl.classList.remove('empty');
|
||||
if (!img) {
|
||||
img = document.createElement('img');
|
||||
img.alt = '';
|
||||
previewEl.appendChild(img);
|
||||
}
|
||||
if (!empty) {
|
||||
empty = document.createElement('span');
|
||||
empty.className = 'tc-preview-empty';
|
||||
empty.textContent = '无图';
|
||||
previewEl.appendChild(empty);
|
||||
}
|
||||
empty.style.display = 'none';
|
||||
img.style.display = 'block';
|
||||
img.src = `${pathToFileURL(fsPath).href}?v=${fs.statSync(fsPath).mtimeMs}`;
|
||||
previewEl.title = fsPath;
|
||||
}
|
||||
|
||||
function refreshAllPreviews(formRoot, projectRoot) {
|
||||
if (!formRoot) return;
|
||||
for (const id of PREVIEW_INPUT_IDS) {
|
||||
const input = formRoot.querySelector(`#${id}`);
|
||||
const preview = formRoot.querySelector(`#${id}-preview`);
|
||||
if (preview && input) {
|
||||
updatePreviewBox(preview, input.value, projectRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bindPreviewInputs(formRoot, projectRoot, onRefresh) {
|
||||
if (!formRoot) return;
|
||||
for (const id of PREVIEW_INPUT_IDS) {
|
||||
const input = formRoot.querySelector(`#${id}`);
|
||||
if (!input || input._tcPreviewBound) continue;
|
||||
input._tcPreviewBound = true;
|
||||
const handler = () => {
|
||||
const preview = formRoot.querySelector(`#${id}-preview`);
|
||||
updatePreviewBox(preview, input.value, projectRoot);
|
||||
if (onRefresh) onRefresh();
|
||||
};
|
||||
input.addEventListener('change', handler);
|
||||
input.addEventListener('blur', handler);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PREVIEW_INPUT_IDS,
|
||||
textureFsPath,
|
||||
updatePreviewBox,
|
||||
refreshAllPreviews,
|
||||
bindPreviewInputs,
|
||||
};
|
||||
443
extensions/theme-controller/dist/theme-db.js
vendored
Normal file
443
extensions/theme-controller/dist/theme-db.js
vendored
Normal file
@@ -0,0 +1,443 @@
|
||||
'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,
|
||||
};
|
||||
Reference in New Issue
Block a user