- 居民卡片显示行动点(AP 圆点指示器) - 添加行动反馈 Toast 提示(成功/失败) - 3 秒后自动消失的反馈动画 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
498 lines
22 KiB
HTML
498 lines
22 KiB
HTML
<!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; }
|
||
|
||
/* 投票按钮 */
|
||
.vote-section { display: flex; gap: 8px; margin-top: 10px; align-items: center; }
|
||
.vote-label { font-size: 12px; color: #71717a; }
|
||
.btn-vote { padding: 8px 16px; font-weight: 600; }
|
||
.btn-vote-opt { background: #166534; color: #86efac; }
|
||
.btn-vote-opt:hover { background: #15803d; }
|
||
.btn-vote-fear { background: #7f1d1d; color: #fca5a5; }
|
||
.btn-vote-fear:hover { background: #991b1b; }
|
||
|
||
/* 投票统计 */
|
||
.votes-display { display: flex; gap: 16px; margin-top: 8px; font-size: 13px; }
|
||
.vote-count { display: flex; align-items: center; gap: 4px; }
|
||
.vote-count.opt { color: #86efac; }
|
||
.vote-count.fear { color: #fca5a5; }
|
||
|
||
/* 能量条 - 极简 */
|
||
#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; }
|
||
|
||
/* 行动点 */
|
||
.agent-ap { display: flex; align-items: center; gap: 4px; margin-top: 6px; }
|
||
.ap-dots { display: flex; gap: 3px; }
|
||
.ap-dot { width: 8px; height: 8px; border-radius: 50%; background: #3f3f46; }
|
||
.ap-dot.filled { background: #60a5fa; }
|
||
.ap-label { font-size: 11px; color: #71717a; }
|
||
|
||
/* 行动反馈提示 */
|
||
#action-feedback { position: fixed; bottom: 20px; right: 20px; z-index: 100; }
|
||
.feedback-toast { padding: 10px 16px; border-radius: 8px; margin-top: 8px; font-size: 13px; animation: fadeIn 0.3s ease; }
|
||
.feedback-toast.success { background: #166534; color: #86efac; border: 1px solid #22c55e; }
|
||
.feedback-toast.fail { background: #7f1d1d; color: #fca5a5; border: 1px solid #ef4444; }
|
||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||
|
||
/* 行动日志 */
|
||
.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; }
|
||
|
||
/* 技能树 */
|
||
.skill-trees { margin-top: 16px; padding-top: 12px; border-top: 1px solid #3f3f46; }
|
||
.skill-trees-title { font-size: 11px; color: #71717a; text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.05em; }
|
||
.skill-trees-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
.skill-tree { background: #27272a; border-radius: 6px; padding: 10px; }
|
||
.skill-tree-header { font-size: 11px; font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; gap: 4px; }
|
||
.skill-tree-header.opt { color: #86efac; }
|
||
.skill-tree-header.fear { color: #fca5a5; }
|
||
.skill-item { display: flex; align-items: center; gap: 6px; padding: 6px 8px; margin-bottom: 4px; border-radius: 4px; font-size: 11px; background: #1a1a2e; }
|
||
.skill-item:last-child { margin-bottom: 0; }
|
||
.skill-item.unlocked { border-left: 2px solid #22c55e; }
|
||
.skill-item.locked { border-left: 2px solid #3f3f46; opacity: 0.7; }
|
||
.skill-icon { font-size: 12px; }
|
||
.skill-name { flex: 1; color: #e4e4e7; }
|
||
.skill-cost { font-size: 10px; color: #71717a; background: #3f3f46; padding: 2px 6px; border-radius: 3px; }
|
||
</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>
|
||
<div class="vote-section">
|
||
<span class="vote-label">投票:</span>
|
||
<button class="btn-vote btn-vote-opt" onclick="sendVote('optimists')">👍 乐观派</button>
|
||
<button class="btn-vote btn-vote-fear" onclick="sendVote('fearful')">👎 恐惧派</button>
|
||
<div class="votes-display">
|
||
<span class="vote-count opt" id="vote-opt">乐观: 0</span>
|
||
<span class="vote-count fear" id="vote-fear">恐惧: 0</span>
|
||
</div>
|
||
</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 id="action-feedback"></div>
|
||
<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 class="skill-trees" id="skill-trees"></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' }));
|
||
}
|
||
|
||
// 发送投票
|
||
function sendVote(faction) {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||
ws.send(JSON.stringify({ type: 'vote', faction, text: '', user: 'viewer' }));
|
||
}
|
||
|
||
// 更新 UI
|
||
const weatherMap = { sunny: '☀️', rainy: '🌧️' };
|
||
const actionHistory = [];
|
||
|
||
// 显示行动反馈提示
|
||
function showActionFeedbacks(feedbacks) {
|
||
const container = document.getElementById('action-feedback');
|
||
feedbacks.forEach(fb => {
|
||
if (!fb.user) return;
|
||
const cls = fb.success ? 'success' : 'fail';
|
||
const icon = fb.success ? '✓' : '✗';
|
||
const toast = document.createElement('div');
|
||
toast.className = `feedback-toast ${cls}`;
|
||
toast.textContent = `${icon} ${fb.user}: ${fb.reason} (AP: ${fb.remaining_ap})`;
|
||
container.appendChild(toast);
|
||
setTimeout(() => toast.remove(), 3000);
|
||
});
|
||
}
|
||
|
||
// 渲染技能列表
|
||
function renderSkills(skills, names) {
|
||
return Object.entries(skills).map(([id, s]) => {
|
||
const cls = s.unlocked ? 'unlocked' : 'locked';
|
||
const icon = s.unlocked ? '✅' : '🔒';
|
||
return `<div class="skill-item ${cls}">
|
||
<span class="skill-icon">${icon}</span>
|
||
<span class="skill-name">${names[id] || id}</span>
|
||
<span class="skill-cost">${s.cost}</span>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function updateUI(data) {
|
||
const { world_state, actions, global_event, triggered_faction_event, story_event, action_feedbacks } = 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 (action_feedbacks && action_feedbacks.length > 0) {
|
||
showActionFeedbacks(action_feedbacks);
|
||
}
|
||
|
||
// ① 能量条
|
||
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 + '%';
|
||
}
|
||
|
||
// ① 投票统计
|
||
if (world_state.votes) {
|
||
document.getElementById('vote-opt').textContent = '乐观: ' + world_state.votes.optimists;
|
||
document.getElementById('vote-fear').textContent = '恐惧: ' + world_state.votes.fearful;
|
||
}
|
||
|
||
// ② 世界事件
|
||
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('');
|
||
}
|
||
|
||
// ⑦ 技能树
|
||
if (world_state.faction_skills) {
|
||
const skillsEl = document.getElementById('skill-trees');
|
||
const fs = world_state.faction_skills;
|
||
const skillNames = {
|
||
festival_boost: '节日增幅', unity: '团结一心',
|
||
panic_spread: '恐慌蔓延', control: '压制控制'
|
||
};
|
||
skillsEl.innerHTML = `
|
||
<div class="skill-trees-title">技能树</div>
|
||
<div class="skill-trees-grid">
|
||
<div class="skill-tree">
|
||
<div class="skill-tree-header opt">🌟 乐观派</div>
|
||
${renderSkills(fs.optimists.skills, skillNames)}
|
||
</div>
|
||
<div class="skill-tree">
|
||
<div class="skill-tree-header fear">💀 恐惧派</div>
|
||
${renderSkills(fs.fearful.skills, skillNames)}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// 角色
|
||
const agentsEl = document.getElementById('agents');
|
||
agentsEl.innerHTML = '';
|
||
for (const [id, agent] of Object.entries(world_state.agents)) {
|
||
const ap = agent.action_points || 0;
|
||
const maxAp = agent.max_action_points || 3;
|
||
const apDots = Array(maxAp).fill(0).map((_, i) =>
|
||
`<span class="ap-dot ${i < ap ? 'filled' : ''}"></span>`
|
||
).join('');
|
||
|
||
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-ap">
|
||
<span class="ap-label">AP:</span>
|
||
<div class="ap-dots">${apDots}</div>
|
||
<span class="ap-label">${ap}/${maxAp}</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>
|