feat: rewrite renderer with 6 stats, shop panel, artifact display
This commit is contained in:
+548
@@ -0,0 +1,548 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>轮回录</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #f0c040;
|
||||
margin-bottom: 20px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
/* 顶部信息栏 */
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #16213e;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.top-bar .info-item {
|
||||
font-size: 14px;
|
||||
}
|
||||
.top-bar .info-item span {
|
||||
color: #f0c040;
|
||||
font-weight: bold;
|
||||
}
|
||||
.crisis-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.crisis-indicator span {
|
||||
font-size: 12px;
|
||||
}
|
||||
.crisis-value {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 外敌进度 */
|
||||
.enemy-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 200px;
|
||||
}
|
||||
.enemy-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.enemy-label {
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
.enemy-progress {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.enemy-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
.enemy-military { background: linear-gradient(90deg, #c0392b, #e74c3c); }
|
||||
.enemy-spiritual { background: linear-gradient(90deg, #8e44ad, #9b59b6); }
|
||||
.enemy-political { background: linear-gradient(90deg, #2980b9, #3498db); }
|
||||
|
||||
/* 控制按钮 */
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.speed-btn, .control-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #2a5580;
|
||||
background: #1e3a5f;
|
||||
color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.speed-btn:hover, .control-btn:hover {
|
||||
background: #2a5580;
|
||||
}
|
||||
.speed-btn.active {
|
||||
background: #f0c040;
|
||||
color: #1a1a2e;
|
||||
border-color: #f0c040;
|
||||
}
|
||||
|
||||
/* 面板通用样式 */
|
||||
.panel {
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.panel-title {
|
||||
color: #f0c040;
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 事件面板 */
|
||||
.event-area {
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.event-title {
|
||||
font-size: 18px;
|
||||
color: #f0c040;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.event-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #ccc;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.event-timer {
|
||||
font-size: 12px;
|
||||
color: #e74c3c;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.event-choices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.choice-btn {
|
||||
background: #1e3a5f;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #2a5580;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.choice-btn:hover:not(:disabled) {
|
||||
background: #2a5580;
|
||||
border-color: #3a75b0;
|
||||
}
|
||||
.choice-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 日志流 */
|
||||
.log-area {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.log-entry {
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
.log-time {
|
||||
color: #888;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.log-normal { color: #ccc; }
|
||||
.log-impact { color: #a8d8ea; }
|
||||
.log-major { color: #f0c040; }
|
||||
.log-death { color: #e74c3c; }
|
||||
.log-ability { color: #2ecc71; }
|
||||
.log-promote { color: #9b59b6; }
|
||||
.log-crisis { color: #e67e22; }
|
||||
|
||||
/* 词条 */
|
||||
.talent-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.talent-tag {
|
||||
padding: 4px 12px;
|
||||
border-radius: 15px;
|
||||
font-size: 13px;
|
||||
border: 1px solid;
|
||||
}
|
||||
.talent-tag.identity { border-color: #f39c12; color: #f39c12; }
|
||||
.talent-tag.talent { border-color: #2ecc71; color: #2ecc71; }
|
||||
.talent-tag.item { border-color: #3498db; color: #3498db; }
|
||||
.talent-tag.curse { border-color: #e74c3c; color: #e74c3c; }
|
||||
|
||||
/* 属性 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
.stat-name {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
color: #f0c040;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 职业 */
|
||||
.career-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.career-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #0f1535;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.career-active {
|
||||
border: 1px solid #f0c040;
|
||||
}
|
||||
.career-name {
|
||||
width: 100px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.career-level {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
color: #f0c040;
|
||||
font-weight: bold;
|
||||
}
|
||||
.career-bar-wrap {
|
||||
flex: 1;
|
||||
height: 16px;
|
||||
background: #333;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.career-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #27ae60, #2ecc71);
|
||||
border-radius: 8px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.career-exp-text {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 2px #000;
|
||||
}
|
||||
|
||||
/* 记忆和元经验 */
|
||||
.memory-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.memory-tag {
|
||||
background: #3d2b1f;
|
||||
color: #d4a574;
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.meta-exp-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
.meta-exp-item {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
.meta-exp-item span {
|
||||
color: #e0c060;
|
||||
}
|
||||
|
||||
/* 死亡结算 */
|
||||
.death-screen {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.death-screen.hidden {
|
||||
display: none;
|
||||
}
|
||||
.death-content {
|
||||
background: #16213e;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.death-content h2 {
|
||||
color: #e74c3c;
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.death-content h4 {
|
||||
color: #f0c040;
|
||||
margin: 15px 0 10px;
|
||||
}
|
||||
.death-content p {
|
||||
line-height: 1.8;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.career-achievements {
|
||||
margin: 10px 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.new-entries {
|
||||
color: #2ecc71;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.history-compare {
|
||||
background: #0f1535;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.meta-gains {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.action-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-top: 20px;
|
||||
background: #f39c12;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background: #e67e22;
|
||||
}
|
||||
|
||||
/* 空文本 */
|
||||
.empty-text {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 隐藏 */
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 600px) {
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.career-name { width: 80px; }
|
||||
.enemy-bar { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>轮回录</h1>
|
||||
|
||||
<!-- 顶部信息 -->
|
||||
<div class="top-bar">
|
||||
<div class="info-item">第 <span id="reincarnation">1</span> 世</div>
|
||||
<div class="info-item">第 <span id="year">0</span> 年 <span id="day">0</span> 天</div>
|
||||
<div class="info-item"><span id="age">0</span> 岁</div>
|
||||
<div class="info-item"><span id="money">0</span> 两</div>
|
||||
<div class="crisis-indicator">
|
||||
<span>危机感:</span>
|
||||
<span class="crisis-value" id="crisis">0</span>
|
||||
</div>
|
||||
<div class="enemy-bar">
|
||||
<div class="enemy-row">
|
||||
<span class="enemy-label">军事</span>
|
||||
<div class="enemy-progress"><div class="enemy-fill enemy-military" id="enemy-military" style="width: 0%"></div></div>
|
||||
</div>
|
||||
<div class="enemy-row">
|
||||
<span class="enemy-label">精神</span>
|
||||
<div class="enemy-progress"><div class="enemy-fill enemy-spiritual" id="enemy-spiritual" style="width: 0%"></div></div>
|
||||
</div>
|
||||
<div class="enemy-row">
|
||||
<span class="enemy-label">政治</span>
|
||||
<div class="enemy-progress"><div class="enemy-fill enemy-political" id="enemy-political" style="width: 0%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="controls">
|
||||
<button class="speed-btn active" data-speed="0">1x</button>
|
||||
<button class="speed-btn" data-speed="1">10x</button>
|
||||
<button class="speed-btn" data-speed="2">100x</button>
|
||||
<button class="speed-btn" data-speed="3">MAX</button>
|
||||
<button class="control-btn" id="pause-btn">暂停</button>
|
||||
<button class="control-btn" id="save-btn">存档</button>
|
||||
<button class="control-btn" id="load-btn">读档</button>
|
||||
</div>
|
||||
|
||||
<!-- 事件面板 -->
|
||||
<div class="panel event-area" id="event-panel">
|
||||
<div class="event-title" id="event-title"></div>
|
||||
<div class="event-text" id="event-text"></div>
|
||||
<div class="event-timer" id="event-timer"></div>
|
||||
<div class="event-choices" id="event-choices"></div>
|
||||
</div>
|
||||
|
||||
<!-- 日志流 -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">人生日志</div>
|
||||
<div class="log-area" id="log-stream">
|
||||
<div class="log-entry"><span class="log-time">[0年0天]</span><span class="log-normal">等待开始...</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 词条 -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">词条</div>
|
||||
<div class="talent-list" id="talent-list">
|
||||
<span class="empty-text">暂无词条</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 属性 -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">属性</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-name">体质</div>
|
||||
<div class="stat-value" id="stat-body">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-name">悟性</div>
|
||||
<div class="stat-value" id="stat-wisdom">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-name">魅力</div>
|
||||
<div class="stat-value" id="stat-charm">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-name">气运</div>
|
||||
<div class="stat-value" id="stat-destiny">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-name">经商</div>
|
||||
<div class="stat-value" id="stat-business">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-name">智谋</div>
|
||||
<div class="stat-value" id="stat-intelligence">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 职业 -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">职业</div>
|
||||
<div class="career-list" id="career-list">
|
||||
<span class="empty-text">尚无职业</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 神器 -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">神器</div>
|
||||
<div id="artifact-list">
|
||||
<span class="empty-text">暂无神器</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商铺 -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">商铺</div>
|
||||
<div class="shop-tabs" id="shop-tabs"></div>
|
||||
<div id="shop-panel">
|
||||
<span class="empty-text">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 记忆与元经验 -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">前世记忆</div>
|
||||
<div class="memory-list" id="memory-list">
|
||||
<span class="empty-text">暂无记忆</span>
|
||||
</div>
|
||||
<div class="panel-title" style="margin-top:10px;border:none;padding:0;font-size:14px;">元经验加成</div>
|
||||
<div class="meta-exp-list" id="meta-exp-list">
|
||||
<span class="empty-text">尚无积累</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 死亡结算界面 -->
|
||||
<div class="death-screen hidden" id="death-screen">
|
||||
<div class="death-content" id="death-content">
|
||||
<!-- 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 = `
|
||||
<button class="shop-tab active" data-tab="items">物品</button>
|
||||
<button class="shop-tab" data-tab="buffs">增益</button>
|
||||
<button class="shop-tab" data-tab="artifacts">神器</button>
|
||||
`;
|
||||
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 = '<span class="empty-text">尚无职业</span>';
|
||||
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 `
|
||||
<div class="career-item">
|
||||
<div class="career-name">${c.name}</div>
|
||||
<div class="career-level">Lv.${level}</div>
|
||||
<div class="career-bar-wrap">
|
||||
<div class="career-bar" style="width: ${pct}%"></div>
|
||||
<div class="career-exp-text">${exp}/${need}</div>
|
||||
</div>
|
||||
<div style="width: 80px; text-align: right; font-size: 12px; color: #2ecc71;">+${income}两/天</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
export function renderTalents(state) {
|
||||
if (!els.talentList) return;
|
||||
|
||||
const talents = state.talents || [];
|
||||
if (talents.length === 0) {
|
||||
els.talentList.innerHTML = '<span class="empty-text">暂无词条</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
els.talentList.innerHTML = talents.map(t => {
|
||||
const entry = getEntry(t.id);
|
||||
if (!entry) return '';
|
||||
const typeClass = entry.type || 'talent';
|
||||
return `<span class="talent-tag ${typeClass}" title="${entry.desc || ''}">${entry.name}</span>`;
|
||||
}).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 = '<span class="empty-text">暂无神器</span>';
|
||||
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 `
|
||||
<div class="artifact-item">
|
||||
<span class="artifact-name">${name}</span>
|
||||
<span class="artifact-level">Lv.${level}</span>
|
||||
</div>
|
||||
`;
|
||||
}).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 = '<span class="empty-text">暂无商品</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
els.shopPanel.innerHTML = `<div class="shop-list">${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 `
|
||||
<div class="shop-item ${isLocked ? 'locked' : ''}">
|
||||
<div class="shop-item-name">${item.name}${levelText}</div>
|
||||
<div class="shop-item-cost">${costText}</div>
|
||||
<div class="shop-item-desc">${desc || '无描述'}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
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 = '<div class="log-entry"><span class="log-time">[0年0天]</span><span class="log-normal">等待开始...</span></div>';
|
||||
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 `<div class="log-entry ${typeClass}">
|
||||
<span class="log-time">[${year}年${day}天]</span>
|
||||
<span class="log-text">${log.text || ''}</span>
|
||||
</div>`;
|
||||
}).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 `<button class="choice-btn" data-index="${idx}" ${disabled ? 'disabled' : ''}>
|
||||
${choice.text}${disabled ? ' (条件不足)' : ''}
|
||||
</button>`;
|
||||
}).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 = `
|
||||
<div class="death-summary">
|
||||
<h2>身死道消</h2>
|
||||
<p>死因:${reason}</p>
|
||||
<p>享年:${age} 岁 ${day % 365} 天</p>
|
||||
<h4>职业成就</h4>
|
||||
<div class="career-achievements">${careerAchievements.length > 0 ? careerAchievements.join('<br>') : '碌碌无为'}</div>
|
||||
</div>
|
||||
<button id="rebirth-btn" class="action-btn">进入轮回</button>
|
||||
`;
|
||||
|
||||
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 ? '继续' : '暂停';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user