From a05225b5143d8efe6351378e44ed2aa8ed9972d8 Mon Sep 17 00:00:00 2001
From: congsh <2452821485@qq.com>
Date: Wed, 13 May 2026 04:29:30 +0000
Subject: [PATCH] feat: rewrite renderer with 6 stats, shop panel, artifact
display
---
index.html | 548 +++++++++++++++++++++++++++++++++++++++++++++
src/ui/renderer.js | 434 +++++++++++++++++++++++++++++++++++
2 files changed, 982 insertions(+)
create mode 100644 index.html
create mode 100644 src/ui/renderer.js
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..1c1080e
--- /dev/null
+++ b/index.html
@@ -0,0 +1,548 @@
+
+
+
+
+
+ 轮回录
+
+
+
+
+
轮回录
+
+
+
+
第 1 世
+
第 0 年 0 天
+
0 岁
+
0 两
+
+ 危机感:
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
前世记忆
+
+ 暂无记忆
+
+
元经验加成
+
+ 尚无积累
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ui/renderer.js b/src/ui/renderer.js
new file mode 100644
index 0000000..fe450ed
--- /dev/null
+++ b/src/ui/renderer.js
@@ -0,0 +1,434 @@
+// ==================== UI 渲染器 ====================
+
+import { getEntry } from '../content/talents.js';
+
+// DOM 元素缓存
+let els = {};
+
+// 商铺当前 Tab
+let shopActiveTab = 'items';
+
+export function initRenderer() {
+ els = {
+ reincarnation: document.getElementById('reincarnation'),
+ year: document.getElementById('year'),
+ day: document.getElementById('day'),
+ age: document.getElementById('age'),
+ money: document.getElementById('money'),
+ crisis: document.getElementById('crisis'),
+ stats: {
+ body: document.getElementById('stat-body'),
+ wisdom: document.getElementById('stat-wisdom'),
+ charm: document.getElementById('stat-charm'),
+ destiny: document.getElementById('stat-destiny'),
+ business: document.getElementById('stat-business'),
+ intelligence: document.getElementById('stat-intelligence'),
+ },
+ eventPanel: document.getElementById('event-panel'),
+ eventTitle: document.getElementById('event-title'),
+ eventText: document.getElementById('event-text'),
+ eventTimer: document.getElementById('event-timer'),
+ eventChoices: document.getElementById('event-choices'),
+ logStream: document.getElementById('log-stream'),
+ careerList: document.getElementById('career-list'),
+ talentList: document.getElementById('talent-list'),
+ artifactList: document.getElementById('artifact-list'),
+ shopPanel: document.getElementById('shop-panel'),
+ shopTabs: document.getElementById('shop-tabs'),
+ deathScreen: document.getElementById('death-screen'),
+ deathContent: document.getElementById('death-content'),
+ };
+
+ injectShopStyles();
+ initShopTabs();
+}
+
+function injectShopStyles() {
+ if (document.getElementById('renderer-shop-styles')) return;
+ const style = document.createElement('style');
+ style.id = 'renderer-shop-styles';
+ style.textContent = `
+ .shop-tabs { display: flex; gap: 10px; margin-bottom: 10px; }
+ .shop-tab { padding: 5px 15px; cursor: pointer; border: none; background: #1e3a5f; color: #ccc; border-radius: 4px; font-size: 13px; }
+ .shop-tab.active { background: #f0c040; color: #1a1a2e; }
+ .shop-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
+ .shop-item { background: #0f1535; padding: 10px; border-radius: 6px; }
+ .shop-item.locked { opacity: 0.5; }
+ .shop-item-name { font-size: 14px; color: #f0c040; margin-bottom: 4px; }
+ .shop-item-cost { font-size: 12px; color: #e0c060; margin-bottom: 4px; }
+ .shop-item-desc { font-size: 12px; color: #888; }
+ .artifact-item { background: #0f1535; padding: 8px 12px; border-radius: 6px; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; }
+ .artifact-name { color: #f0c040; font-size: 14px; }
+ .artifact-level { color: #e0c060; font-size: 13px; }
+ `;
+ document.head.appendChild(style);
+}
+
+function initShopTabs() {
+ if (!els.shopTabs) return;
+ els.shopTabs.innerHTML = `
+
+
+
+ `;
+ els.shopTabs.addEventListener('click', (e) => {
+ const tabBtn = e.target.closest('.shop-tab');
+ if (!tabBtn) return;
+ shopActiveTab = tabBtn.dataset.tab;
+ els.shopTabs.querySelectorAll('.shop-tab').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.tab === shopActiveTab);
+ });
+ // Trigger re-render if state is available via a stored reference
+ if (els._lastState && els._lastCareerConfig && els._lastShopConfig) {
+ renderShop(els._lastState, els._lastShopConfig);
+ }
+ });
+}
+
+export function renderAll(state, careerConfig, shopConfig) {
+ // Store references for tab switching
+ els._lastState = state;
+ els._lastCareerConfig = careerConfig;
+ els._lastShopConfig = shopConfig;
+
+ renderTopBar(state);
+ renderStats(state);
+ renderCareers(state, careerConfig);
+ renderTalents(state);
+ renderArtifacts(state, shopConfig);
+ renderShop(state, shopConfig);
+ renderLogs(state);
+ renderEventPanel(state);
+}
+
+export function renderTopBar(state) {
+ if (els.reincarnation) els.reincarnation.textContent = state.reincarnation || 0;
+ if (els.year) els.year.textContent = state.year || 0;
+ if (els.day) els.day.textContent = state.day || 0;
+ if (els.age) els.age.textContent = state.age || 0;
+ if (els.money) els.money.textContent = state.money || 0;
+}
+
+export function renderStats(state) {
+ const stats = state.stats || {};
+ if (els.stats.body) els.stats.body.textContent = formatStat(stats.body);
+ if (els.stats.wisdom) els.stats.wisdom.textContent = formatStat(stats.wisdom);
+ if (els.stats.charm) els.stats.charm.textContent = formatStat(stats.charm);
+ if (els.stats.destiny) els.stats.destiny.textContent = formatStat(stats.destiny);
+ if (els.stats.business) els.stats.business.textContent = formatStat(stats.business);
+ if (els.stats.intelligence) els.stats.intelligence.textContent = formatStat(stats.intelligence);
+}
+
+function formatStat(value) {
+ if (value === undefined || value === null) return '0';
+ return Number(value).toFixed(1);
+}
+
+export function renderCareers(state, careerConfig) {
+ if (!els.careerList) return;
+
+ const careers = careerConfig?.careers || [];
+ const stateCareers = state.careers || {};
+
+ const activeCareers = careers.filter(c => stateCareers[c.id]);
+
+ if (activeCareers.length === 0) {
+ els.careerList.innerHTML = '尚无职业';
+ return;
+ }
+
+ els.careerList.innerHTML = activeCareers.map(c => {
+ const careerState = stateCareers[c.id];
+ const level = careerState.level || 0;
+ const exp = careerState.exp || 0;
+ const base = c.expCurve?.base || 10;
+ const factor = c.expCurve?.factor || 1.15;
+ const need = Math.floor(base * Math.pow(factor, level));
+ const pct = Math.min(100, (exp / need) * 100);
+ const income = c.dailyIncome || 0;
+
+ return `
+
+
${c.name}
+
Lv.${level}
+
+
+${income}两/天
+
+ `;
+ }).join('');
+}
+
+export function renderTalents(state) {
+ if (!els.talentList) return;
+
+ const talents = state.talents || [];
+ if (talents.length === 0) {
+ els.talentList.innerHTML = '暂无词条';
+ return;
+ }
+
+ els.talentList.innerHTML = talents.map(t => {
+ const entry = getEntry(t.id);
+ if (!entry) return '';
+ const typeClass = entry.type || 'talent';
+ return `${entry.name}`;
+ }).join('');
+}
+
+export function renderArtifacts(state, shopConfig) {
+ if (!els.artifactList) return;
+
+ const artifacts = state.artifacts || {};
+ const artifactIds = Object.keys(artifacts).filter(id => artifacts[id] > 0);
+
+ if (artifactIds.length === 0) {
+ els.artifactList.innerHTML = '暂无神器';
+ return;
+ }
+
+ const configArtifacts = shopConfig?.artifacts || [];
+
+ els.artifactList.innerHTML = artifactIds.map(id => {
+ const level = artifacts[id];
+ const cfg = configArtifacts.find(a => a.id === id);
+ const name = cfg?.name || id;
+ return `
+
+ ${name}
+ Lv.${level}
+
+ `;
+ }).join('');
+}
+
+export function renderShop(state, shopConfig) {
+ if (!els.shopPanel) return;
+
+ const shopData = shopConfig || {};
+ let items = [];
+
+ if (shopActiveTab === 'items') {
+ items = (shopData.items || []).map(item => ({
+ ...item,
+ section: 'item',
+ displayCost: item.cost,
+ }));
+ } else if (shopActiveTab === 'buffs') {
+ items = (shopData.buffs || []).map(buff => ({
+ ...buff,
+ section: 'buff',
+ displayCost: buff.cost,
+ }));
+ } else if (shopActiveTab === 'artifacts') {
+ items = (shopData.artifacts || []).map(artifact => {
+ const currentLevel = state.artifacts?.[artifact.id] || 0;
+ const cost = artifact.baseCost != null && artifact.costGrowth != null
+ ? Math.floor(artifact.baseCost * Math.pow(artifact.costGrowth, currentLevel))
+ : null;
+ return {
+ ...artifact,
+ section: 'artifact',
+ displayCost: cost,
+ currentLevel,
+ };
+ });
+ }
+
+ if (items.length === 0) {
+ els.shopPanel.innerHTML = '暂无商品';
+ return;
+ }
+
+ els.shopPanel.innerHTML = `${items.map(item => {
+ const isLocked = isShopItemLocked(item, state);
+ const desc = buildEffectDesc(item.effects);
+ const costText = item.displayCost != null ? `${item.displayCost} 两` : '已 max';
+ const levelText = item.section === 'artifact' && item.currentLevel != null
+ ? ` (当前 Lv.${item.currentLevel})`
+ : '';
+
+ return `
+
+
${item.name}${levelText}
+
${costText}
+
${desc || '无描述'}
+
+ `;
+ }).join('')}
`;
+}
+
+function isShopItemLocked(item, state) {
+ // Simple unlock check: if has unlockConditions and none met, it's locked
+ if (!item.unlockConditions || item.unlockConditions.length === 0) return false;
+ // We can't easily call evaluateCondition from here without importing it,
+ // so we do a basic check for common conditions
+ for (const cond of item.unlockConditions) {
+ if (cond.type === 'age' && cond.op === '>=' && state.age < cond.value) return true;
+ if (cond.type === 'careerLevel') {
+ const career = state.careers?.[cond.careerId];
+ const level = career?.level || 0;
+ if (cond.op === '>=' && level < cond.value) return true;
+ }
+ if (cond.type === 'hasItem') {
+ const hasItem = state.shopItems?.[cond.itemId] > 0;
+ if (!hasItem) return true;
+ }
+ if (cond.type === 'worldFlag') {
+ const flagValue = state.worldFlags?.[cond.flag];
+ if (flagValue !== cond.value) return true;
+ }
+ if (cond.type === 'identity') {
+ if (state.identity !== cond.value) return true;
+ }
+ if (cond.type === 'or') {
+ // For OR conditions, if any sub-condition passes, it's unlocked
+ const subConditions = cond.conditions || [];
+ const anyMet = subConditions.some(sub => !isShopItemLocked({ unlockConditions: [sub] }, state));
+ if (!anyMet) return true;
+ }
+ if (cond.type === 'and') {
+ const subConditions = cond.conditions || [];
+ const allMet = subConditions.every(sub => !isShopItemLocked({ unlockConditions: [sub] }, state));
+ if (!allMet) return true;
+ }
+ }
+ return false;
+}
+
+function buildEffectDesc(effects) {
+ if (!Array.isArray(effects)) return '';
+ return effects.map(e => {
+ switch (e.type) {
+ case 'addStat': return `${e.stat} +${e.value}`;
+ case 'setStat': return `${e.stat} = ${e.value}`;
+ case 'unlockCareer': return `解锁职业`;
+ case 'setFlag': return `激活标记`;
+ case 'addItem': return `获得物品`;
+ case 'addBuff': return `获得增益`;
+ case 'upgradeArtifact': return `升级神器`;
+ default: return e.type;
+ }
+ }).join(',');
+}
+
+export function renderLogs(state) {
+ if (!els.logStream) return;
+
+ const logs = state.logs || [];
+ const recentLogs = logs.slice(0, 50);
+
+ if (recentLogs.length === 0) {
+ els.logStream.innerHTML = '[0年0天]等待开始...
';
+ return;
+ }
+
+ els.logStream.innerHTML = recentLogs.map(log => {
+ const typeClass = log.type === 'death' ? 'log-death' :
+ log.type === 'major' ? 'log-major' :
+ log.type === 'ability' ? 'log-ability' :
+ log.type === 'promote' ? 'log-promote' :
+ log.type === 'crisis' ? 'log-crisis' :
+ log.type === 'impact' ? 'log-impact' : 'log-normal';
+ const year = log.year != null ? log.year : 0;
+ const day = log.day != null ? log.day % 365 : 0;
+ return `
+ [${year}年${day}天]
+ ${log.text || ''}
+
`;
+ }).join('');
+
+ els.logStream.scrollTop = 0;
+}
+
+export function renderEventPanel(state) {
+ const ev = state.currentEvent;
+ if (!ev) {
+ if (els.eventTitle) els.eventTitle.textContent = '';
+ if (els.eventText) els.eventText.textContent = '';
+ if (els.eventTimer) els.eventTimer.textContent = '';
+ if (els.eventChoices) els.eventChoices.innerHTML = '';
+ return;
+ }
+
+ if (els.eventTitle) els.eventTitle.textContent = ev.title || '';
+ if (els.eventText) els.eventText.textContent = ev.text || '';
+
+ if (els.eventChoices) {
+ if (ev.isChoice && Array.isArray(ev.choices)) {
+ els.eventChoices.innerHTML = ev.choices.map((choice, idx) => {
+ const disabled = choice.requirement && !choice.requirement(state);
+ return ``;
+ }).join('');
+ } else {
+ els.eventChoices.innerHTML = '';
+ }
+ }
+}
+
+export function showDeathScreen(state, careerConfig) {
+ if (!els.deathScreen || !els.deathContent) return;
+
+ const reason = state.deathReason || '未知原因';
+ const age = state.age || 0;
+ const day = state.day || 0;
+
+ // Career achievements
+ const careers = state.careers || {};
+ const configCareers = careerConfig?.careers || [];
+ const careerAchievements = Object.entries(careers)
+ .map(([id, c]) => {
+ const cfg = configCareers.find(cc => cc.id === id);
+ return cfg ? `${cfg.name} Lv.${c.level || 0}` : null;
+ })
+ .filter(Boolean);
+
+ els.deathContent.innerHTML = `
+
+
身死道消
+
死因:${reason}
+
享年:${age} 岁 ${day % 365} 天
+
职业成就
+
${careerAchievements.length > 0 ? careerAchievements.join('
') : '碌碌无为'}
+
+
+ `;
+
+ els.deathScreen.classList.remove('hidden');
+}
+
+export function hideChoiceEvent() {
+ if (els.eventTitle) els.eventTitle.textContent = '';
+ if (els.eventText) els.eventText.textContent = '';
+ if (els.eventTimer) els.eventTimer.textContent = '';
+ if (els.eventChoices) els.eventChoices.innerHTML = '';
+}
+
+export function hideDeathScreen() {
+ if (els.deathScreen) {
+ els.deathScreen.classList.add('hidden');
+ }
+}
+
+export function updateTimer(seconds) {
+ if (els.eventTimer) {
+ els.eventTimer.textContent = seconds > 0 ? `${seconds}秒` : '';
+ }
+}
+
+export function updateSpeedButton(speedLevel) {
+ document.querySelectorAll('.speed-btn').forEach((btn, idx) => {
+ btn.classList.toggle('active', idx === speedLevel);
+ });
+}
+
+export function updatePauseButton(paused) {
+ const pauseBtn = document.getElementById('pause-btn');
+ if (pauseBtn) {
+ pauseBtn.textContent = paused ? '继续' : '暂停';
+ }
+}