Files
ai-town/web/index.html
empty 87007d9b43 feat(web): 适配阵营博弈和剧情线系统
- 修复派系显示逻辑适配新数据结构
- 添加阵营能量条显示 (power/threshold)
- 添加阵营技能触发提示 (festival/panic)
- 添加剧情线进度显示
- 添加剧情事件触发提示
- 移除中立派显示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 12:33:23 +08:00

361 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Town</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #e4e4e7;
min-height: 100vh;
padding: 20px;
}
.container { max-width: 900px; margin: 0 auto; }
header { text-align: center; margin-bottom: 20px; }
header h1 {
font-size: 28px;
background: linear-gradient(90deg, #60a5fa, #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-bar { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 8px; font-size: 13px; color: #71717a; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #ef4444; }
.status-dot.connected { background: #22c55e; }
/* 输入区域 */
.input-section { background: rgba(255,255,255,0.05); border-radius: 12px; padding: 16px; margin-bottom: 16px; }
.input-row { display: flex; gap: 8px; margin-bottom: 10px; }
.input-row input { flex: 1; padding: 10px 14px; border: 1px solid #3f3f46; border-radius: 8px; background: #27272a; color: #e4e4e7; font-size: 14px; outline: none; }
.input-row input:focus { border-color: #60a5fa; }
button { padding: 10px 18px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; }
.btn-primary { background: #3b82f6; color: white; }
.btn-primary:hover { background: #2563eb; }
.quick-btns { display: flex; gap: 8px; }
.btn-quick { background: #3f3f46; color: #a1a1aa; }
.btn-quick:hover { background: #52525b; color: #e4e4e7; }
/* 能量条 - 极简 */
#global-meter { width: 100%; height: 16px; background: #27272a; border-radius: 8px; margin-bottom: 16px; overflow: hidden; }
#meter-bar { height: 100%; width: 0%; background: linear-gradient(90deg, #00ffcc, #ff0066); transition: width 0.3s ease; }
/* 世界事件提示 */
#world-event { background: #422006; border: 1px solid #f59e0b; border-radius: 8px; padding: 12px; margin-bottom: 16px; display: none; font-size: 14px; }
/* 持续影响 */
#world-effects { margin-bottom: 16px; }
.effect-item { display: inline-block; background: #1e3a5f; border-radius: 6px; padding: 6px 10px; margin-right: 8px; margin-bottom: 8px; font-size: 12px; }
.effect-item .remaining { color: #f59e0b; margin-left: 4px; }
/* 主内容区 */
.main-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 700px) { .main-grid { grid-template-columns: 1fr; } }
.card { background: rgba(255,255,255,0.05); border-radius: 12px; padding: 16px; }
.card-title { font-size: 12px; font-weight: 600; color: #71717a; text-transform: uppercase; margin-bottom: 12px; }
/* 世界状态 */
.world-stats { display: flex; gap: 20px; }
.stat-value { font-size: 24px; font-weight: 600; color: #60a5fa; }
.stat-label { font-size: 12px; color: #71717a; }
/* 角色 */
.agent { background: #27272a; border-radius: 8px; padding: 12px; margin-bottom: 8px; }
.agent:last-child { margin-bottom: 0; }
.agent-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.agent-name { font-weight: 600; }
.agent-emotion { font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #3f3f46; }
.emotion-happy { background: #166534; color: #86efac; }
.emotion-anxious { background: #7f1d1d; color: #fca5a5; }
.emotion-calm { background: #1e3a5f; color: #93c5fd; }
.agent-info { font-size: 13px; color: #a1a1aa; }
/* 行动日志 */
.actions-list { max-height: 300px; overflow-y: auto; }
.action-item { padding: 8px; background: #27272a; border-radius: 6px; margin-bottom: 6px; font-size: 13px; }
.action-agent { font-weight: 600; color: #a78bfa; }
.action-say { color: #fbbf24; }
.action-do { color: #34d399; }
/* 派系分布 */
.factions-bar { display: flex; height: 24px; border-radius: 6px; overflow: hidden; margin-top: 12px; }
.faction-segment { display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; transition: width 0.3s ease; }
.faction-optimists { background: #22c55e; color: #052e16; }
.faction-neutral { background: #71717a; color: #fafafa; }
.faction-fearful { background: #ef4444; color: #450a0a; }
.factions-legend { display: flex; gap: 12px; margin-top: 8px; font-size: 11px; color: #a1a1aa; }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px; }
/* 阵营能量条 */
.faction-power { display: flex; gap: 12px; margin-top: 12px; }
.power-item { flex: 1; }
.power-label { font-size: 11px; color: #a1a1aa; margin-bottom: 4px; display: flex; justify-content: space-between; }
.power-bar { height: 8px; background: #27272a; border-radius: 4px; overflow: hidden; }
.power-fill { height: 100%; transition: width 0.3s ease; }
.power-fill.optimist { background: linear-gradient(90deg, #22c55e, #86efac); }
.power-fill.fearful { background: linear-gradient(90deg, #ef4444, #fca5a5); }
/* 阵营技能提示 */
#faction-event { background: #1e3a5f; border: 1px solid #60a5fa; border-radius: 8px; padding: 12px; margin-bottom: 16px; display: none; font-size: 14px; }
/* 剧情线 */
.story-arcs { margin-top: 16px; padding-top: 12px; border-top: 1px solid #3f3f46; }
.arc-item { margin-bottom: 10px; }
.arc-header { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px; }
.arc-name { color: #a1a1aa; }
.arc-stage { color: #f59e0b; }
.arc-bar { height: 6px; background: #27272a; border-radius: 3px; overflow: hidden; }
.arc-fill { height: 100%; background: linear-gradient(90deg, #a78bfa, #f472b6); transition: width 0.3s ease; }
/* 剧情事件提示 */
#story-event { background: #3b0764; border: 1px solid #a78bfa; border-radius: 8px; padding: 12px; margin-bottom: 16px; display: none; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>AI Town</h1>
<div class="status-bar">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">未连接</span>
</div>
</header>
<!-- 输入区域 -->
<section class="input-section">
<div class="input-row">
<input type="text" id="msgInput" placeholder="输入消息..." />
<button class="btn-primary" onclick="sendMessage()">发送</button>
</div>
<div class="quick-btns">
<button class="btn-quick" onclick="sendQuick('支持')">支持</button>
<button class="btn-quick" onclick="sendQuick('混乱')">混乱</button>
<button class="btn-quick" onclick="sendQuick('下雨')">下雨</button>
</div>
</section>
<!-- ① 能量条 -->
<div id="global-meter"><div id="meter-bar"></div></div>
<!-- ② 世界事件提示 -->
<div id="world-event"></div>
<!-- ② 阵营技能提示 -->
<div id="faction-event"></div>
<!-- ② 剧情事件提示 -->
<div id="story-event"></div>
<!-- ③ 持续影响 -->
<div id="world-effects"></div>
<!-- 主内容区 -->
<div class="main-grid">
<div>
<div class="card" style="margin-bottom: 16px;">
<div class="card-title">世界状态</div>
<div class="world-stats">
<div><div class="stat-value" id="tick">0</div><div class="stat-label">Tick</div></div>
<div><div class="stat-value" id="weather">-</div><div class="stat-label">天气</div></div>
<div><div class="stat-value" id="mood">0</div><div class="stat-label">情绪值</div></div>
</div>
<div class="factions-bar" id="factions-bar"></div>
<div class="factions-legend">
<span><span class="legend-dot" style="background:#22c55e"></span>乐观派</span>
<span><span class="legend-dot" style="background:#ef4444"></span>恐惧派</span>
</div>
<!-- 阵营能量条 -->
<div class="faction-power" id="faction-power"></div>
<!-- 剧情线 -->
<div class="story-arcs" id="story-arcs"></div>
</div>
<div class="card">
<div class="card-title">居民</div>
<div id="agents"></div>
</div>
</div>
<div class="card">
<div class="card-title">行动日志</div>
<div class="actions-list" id="actions"></div>
</div>
</div>
</div>
<script>
// WebSocket
let ws = null;
let reconnectDelay = 1000;
function connect() {
ws = new WebSocket('ws://localhost:3000');
ws.onopen = () => {
document.getElementById('statusDot').classList.add('connected');
document.getElementById('statusText').textContent = '已连接';
reconnectDelay = 1000;
};
ws.onclose = () => {
document.getElementById('statusDot').classList.remove('connected');
document.getElementById('statusText').textContent = '重连中...';
setTimeout(connect, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
};
ws.onerror = () => ws.close();
ws.onmessage = (e) => updateUI(JSON.parse(e.data));
}
// 发送消息
function sendMessage() {
const input = document.getElementById('msgInput');
const text = input.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({ type: 'comment', text, user: 'viewer' }));
input.value = '';
}
function sendQuick(text) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({ type: 'comment', text, user: 'viewer' }));
}
// 更新 UI
const weatherMap = { sunny: '☀️', rainy: '🌧️' };
const actionHistory = [];
function updateUI(data) {
const { world_state, actions, global_event, triggered_faction_event, story_event } = data;
// 世界状态
document.getElementById('tick').textContent = world_state.tick;
document.getElementById('weather').textContent = weatherMap[world_state.weather] || world_state.weather;
document.getElementById('mood').textContent = world_state.town_mood;
// ① 能量条
if (world_state.global_meter) {
const meter = world_state.global_meter;
const percent = Math.min(100, meter.value / meter.threshold * 100);
document.getElementById('meter-bar').style.width = percent + '%';
}
// ② 世界事件
const eventEl = document.getElementById('world-event');
if (global_event?.triggered && global_event.event) {
eventEl.textContent = '🌍 世界事件:' + global_event.event.description;
eventEl.style.display = 'block';
} else {
eventEl.style.display = 'none';
}
// ② 阵营技能提示
const factionEventEl = document.getElementById('faction-event');
if (triggered_faction_event?.type) {
const msg = triggered_faction_event.type === 'festival'
? '🎉 乐观派发动了节日庆典!'
: '😱 恐惧派引发了恐慌!';
factionEventEl.textContent = msg;
factionEventEl.style.display = 'block';
} else {
factionEventEl.style.display = 'none';
}
// ② 剧情事件提示
const storyEventEl = document.getElementById('story-event');
if (story_event?.triggered) {
storyEventEl.textContent = '📖 ' + story_event.event_name + '' + story_event.description;
storyEventEl.style.display = 'block';
} else {
storyEventEl.style.display = 'none';
}
// ③ 持续影响
const effectsEl = document.getElementById('world-effects');
if (world_state.world_effects?.length > 0) {
effectsEl.innerHTML = world_state.world_effects.map(e =>
`<span class="effect-item">${e.name}<span class="remaining">(${e.remaining_ticks})</span></span>`
).join('');
} else {
effectsEl.innerHTML = '';
}
// ④ 派系分布
if (world_state.factions) {
const f = world_state.factions;
const optimistCount = f.optimists.members.length;
const fearfulCount = f.fearful.members.length;
const total = optimistCount + fearfulCount || 1;
const bar = document.getElementById('factions-bar');
bar.innerHTML = '';
if (optimistCount > 0) bar.innerHTML += `<div class="faction-segment faction-optimists" style="width:${optimistCount/total*100}%">${optimistCount}</div>`;
if (fearfulCount > 0) bar.innerHTML += `<div class="faction-segment faction-fearful" style="width:${fearfulCount/total*100}%">${fearfulCount}</div>`;
// ⑤ 阵营能量条
const powerEl = document.getElementById('faction-power');
const optPct = Math.min(100, f.optimists.power / f.optimists.threshold * 100);
const fearPct = Math.min(100, f.fearful.power / f.fearful.threshold * 100);
powerEl.innerHTML = `
<div class="power-item">
<div class="power-label"><span>乐观派能量</span><span>${f.optimists.power}/${f.optimists.threshold}</span></div>
<div class="power-bar"><div class="power-fill optimist" style="width:${optPct}%"></div></div>
</div>
<div class="power-item">
<div class="power-label"><span>恐惧派能量</span><span>${f.fearful.power}/${f.fearful.threshold}</span></div>
<div class="power-bar"><div class="power-fill fearful" style="width:${fearPct}%"></div></div>
</div>`;
}
// ⑥ 剧情线进度
if (world_state.story_arcs) {
const arcsEl = document.getElementById('story-arcs');
const arcNames = { civil_unrest: '民众骚乱', golden_age: '黄金时代' };
arcsEl.innerHTML = Object.entries(world_state.story_arcs).map(([id, arc]) => {
const pct = Math.min(100, arc.progress / arc.threshold * 100);
return `<div class="arc-item">
<div class="arc-header">
<span class="arc-name">${arcNames[id] || id}</span>
<span class="arc-stage">阶段 ${arc.stage}</span>
</div>
<div class="arc-bar"><div class="arc-fill" style="width:${pct}%"></div></div>
</div>`;
}).join('');
}
// 角色
const agentsEl = document.getElementById('agents');
agentsEl.innerHTML = '';
for (const [id, agent] of Object.entries(world_state.agents)) {
agentsEl.innerHTML += `
<div class="agent">
<div class="agent-header">
<span class="agent-name">${id}</span>
<span class="agent-emotion emotion-${agent.emotion}">${agent.emotion}</span>
</div>
<div class="agent-info">记忆: ${agent.memory.slice(-2).join(' | ') || '-'}</div>
</div>`;
}
// 行动日志(倒序:新的在上)
const now = new Date().toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).replace(/\//g, '-');
for (const a of actions) {
a.time = now;
actionHistory.unshift(a);
}
if (actionHistory.length > 50) actionHistory.length = 50;
document.getElementById('actions').innerHTML = actionHistory.map(a =>
`<div class="action-item"><span style="color:#71717a;font-size:11px;">${a.time}</span> <span class="action-agent">${a.agent_id}</span> <span class="action-say">「${a.say}」</span> <span class="action-do">→ ${a.do}</span></div>`
).join('');
}
// 回车发送
document.getElementById('msgInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendMessage();
});
// 启动
connect();
</script>
</body>
</html>