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
+
00
+
0
+
0
+
+ 危机感: + 0 +
+
+
+ 军事 +
+
+
+ 精神 +
+
+
+ 政治 +
+
+
+
+ + +
+ + + + + + + +
+ + +
+
+
+
+
+
+ + +
+
人生日志
+
+
[0年0天]等待开始...
+
+
+ + +
+
词条
+
+ 暂无词条 +
+
+ + +
+
属性
+
+
+
体质
+
0
+
+
+
悟性
+
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}
+
+
+
${exp}/${need}
+
+
+${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 ? '继续' : '暂停'; + } +}