feat(web): 适配行动点系统

- 居民卡片显示行动点(AP 圆点指示器)
- 添加行动反馈 Toast 提示(成功/失败)
- 3 秒后自动消失的反馈动画

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2025-12-30 13:50:35 +08:00
parent 5ae63d9df9
commit f2978c9b66

View File

@@ -86,6 +86,20 @@
.emotion-calm { background: #1e3a5f; color: #93c5fd; } .emotion-calm { background: #1e3a5f; color: #93c5fd; }
.agent-info { font-size: 13px; color: #a1a1aa; } .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; } .actions-list { max-height: 300px; overflow-y: auto; }
.action-item { padding: 8px; background: #27272a; border-radius: 6px; margin-bottom: 6px; font-size: 13px; } .action-item { padding: 8px; background: #27272a; border-radius: 6px; margin-bottom: 6px; font-size: 13px; }
@@ -192,6 +206,9 @@
<!-- 主内容区 --> <!-- 主内容区 -->
<div class="main-grid"> <div class="main-grid">
<!-- 行动反馈提示 -->
<div id="action-feedback"></div>
<div> <div>
<div class="card" style="margin-bottom: 16px;"> <div class="card" style="margin-bottom: 16px;">
<div class="card-title">世界状态</div> <div class="card-title">世界状态</div>
@@ -270,6 +287,21 @@
const weatherMap = { sunny: '☀️', rainy: '🌧️' }; const weatherMap = { sunny: '☀️', rainy: '🌧️' };
const actionHistory = []; 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) { function renderSkills(skills, names) {
return Object.entries(skills).map(([id, s]) => { return Object.entries(skills).map(([id, s]) => {
@@ -284,13 +316,18 @@
} }
function updateUI(data) { function updateUI(data) {
const { world_state, actions, global_event, triggered_faction_event, story_event } = data; const { world_state, actions, global_event, triggered_faction_event, story_event, action_feedbacks } = data;
// 世界状态 // 世界状态
document.getElementById('tick').textContent = world_state.tick; document.getElementById('tick').textContent = world_state.tick;
document.getElementById('weather').textContent = weatherMap[world_state.weather] || world_state.weather; document.getElementById('weather').textContent = weatherMap[world_state.weather] || world_state.weather;
document.getElementById('mood').textContent = world_state.town_mood; document.getElementById('mood').textContent = world_state.town_mood;
// 行动反馈提示
if (action_feedbacks && action_feedbacks.length > 0) {
showActionFeedbacks(action_feedbacks);
}
// ① 能量条 // ① 能量条
if (world_state.global_meter) { if (world_state.global_meter) {
const meter = world_state.global_meter; const meter = world_state.global_meter;
@@ -412,12 +449,23 @@
const agentsEl = document.getElementById('agents'); const agentsEl = document.getElementById('agents');
agentsEl.innerHTML = ''; agentsEl.innerHTML = '';
for (const [id, agent] of Object.entries(world_state.agents)) { 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 += ` agentsEl.innerHTML += `
<div class="agent"> <div class="agent">
<div class="agent-header"> <div class="agent-header">
<span class="agent-name">${id}</span> <span class="agent-name">${id}</span>
<span class="agent-emotion emotion-${agent.emotion}">${agent.emotion}</span> <span class="agent-emotion emotion-${agent.emotion}">${agent.emotion}</span>
</div> </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 class="agent-info">记忆: ${agent.memory.slice(-2).join(' | ') || '-'}</div>
</div>`; </div>`;
} }