feat: add LLM integration and enhance game engine

- Add OpenAI-compatible LLM integration for agent dialogue
- Enhance survival mechanics with energy decay and feeding system
- Update frontend debug client with improved UI
- Add .gitignore rules for Unity and Serena

🤖 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
2026-01-01 12:15:13 +08:00
parent cf1739b7f8
commit e96948e8a4
7 changed files with 504 additions and 12 deletions

View File

@@ -96,6 +96,9 @@ function handleGameEvent(event) {
userGoldDisplay.textContent = userGold;
}
break;
case 'agent_speak':
showSpeechBubble(data.agent_id, data.agent_name, data.text);
break;
}
logEvent(event);
@@ -201,6 +204,75 @@ function feedAgent(agentName) {
ws.send(JSON.stringify(payload));
}
/**
* Show speech bubble above an agent card
*/
function showSpeechBubble(agentId, agentName, text) {
const card = document.getElementById(`agent-${agentId}`);
const overlay = document.getElementById('speechBubblesOverlay');
if (!card || !overlay) {
console.warn(`Agent card or overlay not found: agent-${agentId}`);
return;
}
// Remove existing bubble for this agent if any
const existingBubble = document.getElementById(`bubble-${agentId}`);
if (existingBubble) {
existingBubble.remove();
}
// Get card position relative to overlay
const cardRect = card.getBoundingClientRect();
const overlayRect = overlay.parentElement.getBoundingClientRect();
// Create new speech bubble
const bubble = document.createElement('div');
bubble.className = 'speech-bubble';
bubble.id = `bubble-${agentId}`;
bubble.innerHTML = `
<div class="bubble-name">${agentName}</div>
<div>${text}</div>
`;
// Position bubble above the card
const left = (cardRect.left - overlayRect.left) + (cardRect.width / 2);
const top = (cardRect.top - overlayRect.top) - 10;
bubble.style.left = `${left}px`;
bubble.style.top = `${top}px`;
overlay.appendChild(bubble);
// Auto-hide after 5 seconds
setTimeout(() => {
bubble.classList.add('fade-out');
setTimeout(() => {
if (bubble.parentNode) {
bubble.remove();
}
}, 300); // Wait for fade animation
}, 5000);
}
/**
* Reset game - revive all agents
*/
function resetGame() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('未连接到服务器');
return;
}
const user = getCurrentUser();
const payload = {
action: 'send_comment',
payload: { user, message: 'reset' }
};
ws.send(JSON.stringify(payload));
}
/**
* Send a comment/command to the server
*/
@@ -250,6 +322,8 @@ function formatEventData(eventType, data) {
case 'agent_died':
case 'check':
return data.message;
case 'agent_speak':
return `💬 ${data.agent_name}: "${data.text}"`;
case 'agents_update':
return `角色状态已更新`;
case 'user_update':

View File

@@ -92,6 +92,7 @@
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 15px;
padding-top: 50px; /* Space for speech bubbles */
}
.agent-card {
background: rgba(255, 255, 255, 0.05);
@@ -180,6 +181,63 @@
cursor: not-allowed;
}
/* Speech Bubble */
.speech-bubbles-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
pointer-events: none;
z-index: 100;
}
.speech-bubble {
position: absolute;
background: rgba(255, 255, 255, 0.95);
color: #333;
padding: 10px 15px;
border-radius: 12px;
font-size: 0.9rem;
max-width: 250px;
text-align: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
transform: translateX(-50%);
animation: bubbleIn 0.3s ease-out;
pointer-events: auto;
}
.speech-bubble::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid rgba(255, 255, 255, 0.95);
}
.speech-bubble .bubble-name {
font-weight: bold;
color: #88cc88;
margin-bottom: 5px;
font-size: 0.8rem;
}
.speech-bubble.fade-out {
animation: bubbleOut 0.3s ease-in forwards;
}
@keyframes bubbleIn {
from { opacity: 0; transform: translateX(-50%) translateY(10px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes bubbleOut {
from { opacity: 1; transform: translateX(-50%) translateY(0); }
to { opacity: 0; transform: translateX(-50%) translateY(-10px); }
}
.agents-section {
position: relative;
}
.agent-card {
position: relative;
}
/* Panels */
.panels {
display: flex;
@@ -267,6 +325,7 @@
.event.error { border-color: #ff4444; background: rgba(255, 68, 68, 0.1); }
.event.feed { border-color: #ffaa00; background: rgba(255, 170, 0, 0.1); color: #ffcc66; }
.event.agent_died { border-color: #ff4444; background: rgba(255, 68, 68, 0.15); color: #ff8888; }
.event.agent_speak { border-color: #88ccff; background: rgba(136, 204, 255, 0.1); color: #aaddff; }
.event.check { border-color: #88cc88; background: rgba(136, 204, 136, 0.1); }
.event-time { color: #888; font-size: 11px; }
.event-type { font-weight: bold; text-transform: uppercase; font-size: 11px; }
@@ -299,7 +358,11 @@
<!-- Agents Section -->
<div class="agents-section">
<h2 class="section-title">岛上幸存者</h2>
<div class="speech-bubbles-overlay" id="speechBubblesOverlay"></div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h2 class="section-title" style="margin-bottom: 0;">岛上幸存者</h2>
<button onclick="resetGame()" style="background: #ff6666; padding: 8px 16px; font-size: 13px;">🔄 重新开始</button>
</div>
<div class="agents-grid" id="agentsGrid">
<!-- Agent cards will be dynamically generated -->
<div class="agent-card" id="agent-loading">
@@ -316,7 +379,7 @@
<button onclick="sendComment()">发送</button>
</div>
<p style="margin-top: 10px; font-size: 0.85rem; color: #888;">
指令: <code>feed [名字]</code> - 投喂角色 (消耗10金币) | <code>check</code> - 查询状态
指令: <code>feed [名字]</code> - 投喂 | <code>check</code> - 查询 | <code>reset</code> - 重新开始
</p>
</div>