Files
openclaw-agent-rpg/index.html
T

1563 lines
54 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Agent Office RPG - Ultimate Live HQ</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap" rel="stylesheet">
<style>
:root {
--bg-dark: #090a10;
--card-bg: #141724;
--card-border: #262c42;
--accent: #10b981;
--accent-warn: #f59e0b;
--accent-err: #ef4444;
--accent-info: #3b82f6;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
background-color: var(--bg-dark);
color: #e2e8f0;
font-family: 'VT323', monospace;
font-size: 20px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 14px;
overflow-x: hidden;
}
header {
width: 100%;
max-width: 1180px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
border-bottom: 3px solid #262c42;
padding-bottom: 8px;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
h1 {
font-family: 'Press Start 2P', monospace;
font-size: 14px;
color: #34d399;
text-shadow: 2px 2px #064e3b;
display: flex;
align-items: center;
gap: 10px;
}
.badge-live {
background: #dc2626;
color: #fff;
font-size: 9px;
padding: 2px 6px;
border-radius: 3px;
animation: pulse 1.5s infinite;
font-family: 'Press Start 2P', monospace;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.header-right {
display: flex;
align-items: center;
gap: 14px;
}
.sound-btn {
background: #1e2438;
border: 1px solid #3e4768;
color: #facc15;
padding: 4px 8px;
font-family: 'VT323';
font-size: 18px;
cursor: pointer;
border-radius: 4px;
}
.sound-btn:hover {
background: #2b3350;
border-color: #facc15;
}
/* Project Switcher Select */
.project-select-box {
display: flex;
align-items: center;
gap: 8px;
background: #141724;
border: 2px solid #38bdf8;
padding: 4px 10px;
border-radius: 4px;
}
.project-select-label {
font-family: 'Press Start 2P';
font-size: 9px;
color: #38bdf8;
}
select {
background: #090a10;
color: #f1f5f9;
border: 1px solid #334155;
font-family: 'VT323', monospace;
font-size: 19px;
padding: 2px 6px;
cursor: pointer;
border-radius: 3px;
outline: none;
}
.time-display {
font-family: 'Press Start 2P', monospace;
font-size: 10px;
color: #94a3b8;
}
/* Infra Badges */
.infra-bar {
display: flex;
gap: 10px;
margin-bottom: 10px;
max-width: 1180px;
width: 100%;
}
.infra-card {
flex: 1;
background: var(--card-bg);
border: 2px solid var(--card-border);
border-radius: 6px;
padding: 6px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.infra-title {
font-family: 'Press Start 2P', monospace;
font-size: 10px;
display: flex;
align-items: center;
gap: 6px;
}
.infra-val {
font-size: 18px;
color: #38bdf8;
}
.badge-ok { background: #064e3b; color: #6ee7b7; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-family:'Press Start 2P'; }
.badge-busy { background: #854d0e; color: #fef08a; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-family:'Press Start 2P'; }
.main-container {
display: flex;
gap: 14px;
max-width: 1180px;
width: 100%;
}
/* Canvas / Game Area */
.viewport-box {
flex: 1;
background: #090b12;
border: 3px solid var(--card-border);
border-radius: 8px;
box-shadow: 0 12px 30px rgba(0,0,0,0.7);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
canvas {
display: block;
image-rendering: pixelated;
cursor: crosshair;
background: #0e111a;
width: 100%;
height: 520px;
}
.status-bar {
background: #090a10;
border-top: 2px solid #1e2438;
padding: 6px 12px;
font-size: 17px;
color: #94a3b8;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Bottom RPG Dialogue / Terminal Box */
.rpg-terminal-box {
margin-top: 8px;
background: #06070c;
border: 2px solid #334155;
border-radius: 4px;
padding: 8px 12px;
font-family: 'VT323', monospace;
font-size: 18px;
color: #a5f3fc;
max-height: 85px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
}
.rpg-term-prefix { color: #f59e0b; margin-right: 6px; }
/* Sidebar / Inspector */
.sidebar {
width: 350px;
display: flex;
flex-direction: column;
gap: 10px;
}
.panel {
background: var(--card-bg);
border: 2px solid var(--card-border);
border-radius: 6px;
padding: 10px;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
.panel-title {
font-family: 'Press Start 2P', monospace;
font-size: 10px;
color: #60a5fa;
margin-bottom: 8px;
border-bottom: 1px dashed #2d334d;
padding-bottom: 5px;
display: flex;
justify-content: space-between;
}
/* Agent Profile */
.agent-card {
font-size: 18px;
line-height: 1.25;
}
.agent-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.avatar-preview {
width: 42px;
height: 42px;
border: 2px solid #fff;
border-radius: 4px;
background: #1f2438;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
image-rendering: pixelated;
}
.status-pill {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: bold;
font-family: 'Press Start 2P', monospace;
margin-top: 2px;
}
.status-WORKING { background: #064e3b; color: #6ee7b7; border: 1px solid #059669; }
.status-THINKING { background: #713f12; color: #fde047; border: 1px solid #ca8a04; }
.status-DISCORD { background: #3730a3; color: #c7d2fe; border: 1px solid #6366f1; }
.status-DELEGATING { background: #831843; color: #fbcfe8; border: 1px solid #db2777; }
.status-ERROR { background: #7f1d1d; color: #fca5a5; border: 1px solid #dc2626; }
.status-IDLE { background: #1e293b; color: #94a3b8; border: 1px solid #475569; }
.stat-row {
margin-top: 4px;
color: #cbd5e1;
}
.stat-row span {
color: #94a3b8;
}
.task-desc {
background: #090b12;
padding: 6px 8px;
border-radius: 4px;
border-left: 3px solid #34d399;
margin-top: 6px;
font-size: 16px;
word-break: break-word;
color: #f1f5f9;
line-height: 1.2;
}
/* Control Sandbox */
.controls-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
margin-top: 4px;
}
button {
background: #1c2236;
color: #f1f5f9;
border: 2px solid #333d59;
border-radius: 4px;
padding: 5px;
font-family: 'VT323', monospace;
font-size: 18px;
cursor: pointer;
transition: all 0.1s;
}
button:hover {
background: #28314e;
border-color: #34d399;
color: #34d399;
}
button:active {
transform: translateY(1px);
}
/* Mini Activity Log */
.log-box {
height: 95px;
overflow-y: auto;
background: #090b12;
padding: 6px;
border-radius: 4px;
font-size: 15px;
display: flex;
flex-direction: column;
gap: 3px;
}
.log-entry {
border-bottom: 1px solid #161b2a;
padding-bottom: 2px;
}
.log-time { color: #64748b; margin-right: 4px; }
</style>
</head>
<body>
<header>
<div class="header-left">
<h1>🏢 OPENCLAW RETRO HQ <span class="badge-live">LIVE</span></h1>
<div class="project-select-box">
<span class="project-select-label">PROJEKT:</span>
<select id="projectSelect" onchange="onProjectChange(this.value)">
<option value="camper-knowledge-graph">🚐 Camper Knowledge Graph</option>
<option value="appsoluteweb-relaunch">🌐 Appsoluteweb Relaunch</option>
<option value="cinnamon-compass">🗺️ Cinnamon Compass</option>
<option value="mainscreen-party-framework">🎉 Mainscreen Party</option>
</select>
</div>
</div>
<div class="header-right">
<button class="sound-btn" onclick="toggleAudio()" id="soundToggle">🔊 8-Bit Audio: ON</button>
<div class="time-display" id="clock">00:00:00</div>
</div>
</header>
<!-- Realtime Infra Health / Queues / Lead Heartbeat -->
<div class="infra-bar">
<div class="infra-card">
<div class="infra-title">⚡ BULLMQ QUEUE</div>
<div>
<span class="infra-val" id="bullmqWaiting">0</span> <span style="font-size:14px; color:#94a3b8">waiting</span> |
<span class="infra-val" id="bullmqActive" style="color:#34d399">0</span> <span style="font-size:14px; color:#94a3b8">active</span> |
<span class="badge-ok" id="bullmqStatus">IDLE</span>
</div>
</div>
<div class="infra-card">
<div class="infra-title">💓 LEAD HEARTBEAT</div>
<div>
<span class="infra-val" id="leadProject" style="color:#facc15">camper-knowledge-graph</span> |
<span class="infra-val" id="leadAge" style="color:#34d399">0.0s</span> |
<span class="badge-ok" id="leadPulse">STANDBY</span>
</div>
</div>
<div class="infra-card">
<div class="infra-title">🐙 GITEA KANBAN & PRS</div>
<div>
<span class="infra-val" id="giteaPrs">0</span> <span style="font-size:13px; color:#94a3b8">PRs</span> |
<span class="infra-val" id="giteaReview" style="color:#ec4899">0</span> <span style="font-size:13px; color:#94a3b8">rev</span> |
<span class="infra-val" id="giteaWip" style="color:#38bdf8">0</span> <span style="font-size:13px; color:#94a3b8">wip</span> |
<span class="badge-ok" id="giteaStatus">CONNECTED</span>
</div>
</div>
</div>
<div class="main-container">
<!-- Viewport / Canvas -->
<div class="viewport-box">
<canvas id="gameCanvas" width="800" height="430"></canvas>
<!-- Live Quest / Shell Terminal Box -->
<div class="rpg-terminal-box" id="rpgTerminal">
<div><span class="rpg-term-prefix">[QUEST-LOG]</span> Initialisiere OpenClaw Live-HQ...</div>
</div>
<div class="status-bar">
<span>🖱️ Klicke auf Agenten für Detailinspektion</span>
<span id="canvasStats">Active: 0 | Idle: 4</span>
</div>
</div>
<!-- Sidebar / Inspector -->
<div class="sidebar">
<!-- Selected Agent Panel -->
<div class="panel">
<div class="panel-title">
<span>AGENTEN-INSPEKTOR</span>
<span id="inspAgentId">#01</span>
</div>
<div class="agent-card" id="agentInspector"></div>
</div>
<!-- Quick Control / Simulation Sandbox -->
<div class="panel">
<div class="panel-title">LIVE SYNC & STEUERUNG</div>
<div class="controls-grid">
<button onclick="resumeLiveSync()" style="grid-column: span 2; background:#065f46; border-color:#34d399; color:#fff;">🔴 LIVE-SYNC REAKTIVIEREN</button>
<button onclick="triggerDelegationAnimation()" style="grid-column: span 2; background:#831843; border-color:#db2777; color:#fff;">💾 Teamplay: Lead übergibt Diskette</button>
<button onclick="triggerApprovalParty()" style="background:#065f46; border-color:#34d399; color:#fff;">🎉 PR Approved Party</button>
<button onclick="simulateEvent('lead', 'DISCORD', 'Beantwortet Discord-Frage von @Christoph')" style="background:#3730a3; border-color:#818cf8; color:#fff;">💬 Discord DM</button>
<button onclick="simulateEvent('coder1', 'WORKING', 'Refactoring knowledge-graph auth')">💻 Coder Coden</button>
<button onclick="simulateEvent('reviewer', 'WORKING', 'Auditing Security PR #305')">🔍 QA Review</button>
<button onclick="simulateAllIdle()" style="grid-column: span 2;">☕ Alle in Lounge schicken</button>
</div>
</div>
<!-- Mini Log -->
<div class="panel">
<div class="panel-title">LIVE EVENT TICKER</div>
<div class="log-box" id="eventLog">
<div class="log-entry"><span class="log-time">[HQ]</span> Pixel Agent Grid geladen.</div>
</div>
</div>
</div>
</div>
<script>
// Audio Synthesizer (8-Bit Chiptune WebAudio)
let audioEnabled = true;
let audioCtx = null;
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
}
function play8BitTone(freq, type, duration) {
if (!audioEnabled) return;
try {
initAudio();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type || 'square';
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(0.05, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
} catch(e) {}
}
function soundCoin() {
play8BitTone(987.77, 'square', 0.08);
setTimeout(() => play8BitTone(1318.51, 'square', 0.18), 80);
}
function soundAlert() {
play8BitTone(440, 'sawtooth', 0.1);
setTimeout(() => play8BitTone(330, 'sawtooth', 0.15), 100);
}
function soundKey() {
play8BitTone(300 + Math.random()*200, 'triangle', 0.03);
}
function toggleAudio() {
audioEnabled = !audioEnabled;
document.getElementById('soundToggle').innerText = audioEnabled ? '🔊 8-Bit Audio: ON' : '🔇 8-Bit Audio: OFF';
}
// Canvas Context
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let currentProject = 'camper-knowledge-graph';
let isLiveSync = true;
let selectedAgentKey = 'coder1';
// Particle / Confetti System for PR Merges / Celebrations
let particles = [];
function spawnConfetti(x, y) {
for (let i = 0; i < 35; i++) {
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 1.2) * 5,
color: ['#f43f5e', '#3b82f6', '#10b981', '#fbbf24', '#a855f7'][Math.floor(Math.random()*5)],
size: Math.random() * 4 + 2,
life: 1.0
});
}
soundCoin();
}
// Dynamic Rooms & Windows (Day / Night lighting)
const ROOMS = [
{ id: 'dev_lab', x: 20, y: 30, w: 360, h: 240, name: "💻 DEV LAB", floorColor1: "#242d42", floorColor2: "#1e2638", wallColor: "#3b4461", wallTop: "#566288" },
{ id: 'lead_room', x: 400, y: 30, w: 380, h: 240, name: "🏛️ WAR ROOM & KANBAN", floorColor1: "#282a36", floorColor2: "#21232e", wallColor: "#3e4257", wallTop: "#5c627f" },
{ id: 'break_room', x: 20, y: 290, w: 760, h: 125, name: "☕ AGENT LOUNGE & CAFE", floorColor1: "#382923", floorColor2: "#2d201a", wallColor: "#4f3b33", wallTop: "#6e5449" }
];
// Furniture Definitions
const FURNITURE = [
// Dev Lab
{ type: 'DESK_DUAL', x: 60, y: 70, id: 'desk_coder1', label: 'Workstation Alpha' },
{ type: 'DESK_DUAL', x: 220, y: 70, id: 'desk_coder2', label: 'Workstation Beta' },
{ type: 'WHITEBOARD', x: 130, y: 35, label: 'Gitea Kanban' },
{ type: 'PLANT', x: 30, y: 210 },
// War Room
{ type: 'LEAD_DESK', x: 420, y: 70, id: 'desk_lead', label: 'Lead Console' },
{ type: 'QA_DESK', x: 570, y: 70, id: 'desk_reviewer', label: 'QA / Review Desk' },
{ type: 'BULLMQ_STATION', x: 690, y: 38, label: '⚡ BULLMQ' },
{ type: 'GITEA_STATION', x: 690, y: 135, label: '🐙 GITEA' },
// Lounge
{ type: 'COFFEE_MACHINE', x: 50, y: 325 },
{ type: 'RETRO_SOFA', x: 240, y: 330 },
{ type: 'ARCADE_CAB', x: 470, y: 305 },
{ type: 'VENDING_MACHINE', x: 550, y: 305 },
{ type: 'PLANT', x: 720, y: 350 },
{ type: 'RUG', x: 220, y: 315, w: 180, h: 90 }
];
// Idle Spots
const IDLE_ACTIVITIES = [
{ id: 'arcade', name: '🕹️ Street Fighter', x: 470, y: 350, bubble: '🕹️ KO!', anim: 'gaming' },
{ id: 'coffee', name: '☕ Espresso trinken', x: 90, y: 360, bubble: '☕ *slurp*', anim: 'coffee' },
{ id: 'plant', name: '🌿 Bonsai gießen', x: 720, y: 355, bubble: '💧 🌱', anim: 'watering' },
{ id: 'vending', name: '🥤 Club-Mate', x: 550, y: 350, bubble: '🥤 *ahhh*', anim: 'mate' },
{ id: 'couch', name: '🛋️ Powernap', x: 300, y: 345, bubble: '😴 zZz', anim: 'couch' }
];
// Agents
const agents = {
coder1: {
id: 'coder1',
name: 'CKG Coder',
role: 'Fullstack Dev',
hairColor: '#eab308',
shirtColor: '#0284c7',
pantsColor: '#1e293b',
skinColor: '#fed7aa',
state: 'IDLE',
currentTask: 'Bereit für neue Aufgaben',
lastAction: 'Standby',
x: 90, y: 360,
targetDesk: 'desk_coder1',
idleActivityIndex: 0,
lastActivitySwitch: 0,
liveLog: ["Wartet auf BullMQ-Jobs..."]
},
coder2: {
id: 'coder2',
name: 'CKG Tester',
role: 'E2E & Staging',
hairColor: '#ef4444',
shirtColor: '#7c3aed',
pantsColor: '#0f172a',
skinColor: '#fcd34d',
state: 'IDLE',
currentTask: 'Staging Environment Healthy',
lastAction: 'Healthy',
x: 300, y: 345,
targetDesk: 'desk_coder2',
idleActivityIndex: 4,
lastActivitySwitch: 0,
liveLog: ["Staging Check: 200 OK"]
},
lead: {
id: 'lead',
name: 'CKG Lead',
role: 'Orchestrator',
hairColor: '#38bdf8',
shirtColor: '#eab308',
pantsColor: '#334155',
skinColor: '#fed7aa',
state: 'IDLE',
currentTask: 'Überwacht Repositories & Heartbeats',
lastAction: 'Standby',
x: 550, y: 350,
targetDesk: 'desk_lead',
idleActivityIndex: 3,
lastActivitySwitch: 0,
liveLog: ["Heartbeat Standby"]
},
reviewer: {
id: 'reviewer',
name: 'CKG Reviewer',
role: 'Security & QA',
hairColor: '#10b981',
shirtColor: '#dc2626',
pantsColor: '#1e293b',
skinColor: '#ffedd5',
state: 'IDLE',
currentTask: 'Wartet auf Pull-Requests in Gitea',
lastAction: 'Standby',
x: 470, y: 350,
targetDesk: 'desk_reviewer',
idleActivityIndex: 0,
lastActivitySwitch: 0,
liveLog: ["Watcher aktiv"]
}
};
// Live Kanban Counts & CI State
let kanbanState = { planned: 3, ready: 1, in_progress: 0, review: 2, done: 12 };
let giteaCiState = { active_jobs: 0, status: 'PASSING' };
function addLog(text) {
const logBox = document.getElementById('eventLog');
const time = new Date().toLocaleTimeString();
const div = document.createElement('div');
div.className = 'log-entry';
div.innerHTML = `<span class="log-time">[${time}]</span> ${text}`;
logBox.insertBefore(div, logBox.firstChild);
}
function appendTerminalLog(prefix, text) {
const term = document.getElementById('rpgTerminal');
const row = document.createElement('div');
row.innerHTML = `<span class="rpg-term-prefix">[${prefix}]</span> ${text}`;
term.appendChild(row);
term.scrollTop = term.scrollHeight;
}
function onProjectChange(val) {
currentProject = val;
addLog(`📁 Projekt gewechselt zu: <b>${val}</b>`);
appendTerminalLog('HQ', `Lade Kontext & Queues für: ${val}...`);
fetchLiveStatus();
}
// Live Server Sync
async function fetchLiveStatus() {
if (!isLiveSync) return;
try {
const res = await fetch(`/api/status?project=${currentProject}`);
if (!res.ok) return;
const data = await res.json();
// 1. Update BullMQ HUD
document.getElementById('bullmqWaiting').innerText = data.bullmq.total_waiting;
document.getElementById('bullmqActive').innerText = data.bullmq.total_active;
const bStatus = document.getElementById('bullmqStatus');
bStatus.innerText = data.bullmq.status;
bStatus.className = (data.bullmq.status === 'PROCESSING') ? 'badge-busy' : 'badge-ok';
// 2. Update Lead Heartbeat HUD
if (data.lead_heartbeat) {
document.getElementById('leadProject').innerText = data.lead_heartbeat.project || currentProject;
document.getElementById('leadAge').innerText = `${data.lead_heartbeat.last_pulse_seconds_ago}s`;
const lPulse = document.getElementById('leadPulse');
lPulse.innerText = data.lead_heartbeat.is_beating ? 'BEATING 💓' : 'STANDBY';
lPulse.className = data.lead_heartbeat.is_beating ? 'badge-busy' : 'badge-ok';
}
// 3. Update Gitea HUD & Kanban
document.getElementById('giteaPrs').innerText = data.gitea.open_prs_count || 0;
const gStatus = document.getElementById('giteaStatus');
gStatus.innerText = data.gitea.connected ? 'CONNECTED' : 'DISCONNECTED';
gStatus.className = data.gitea.connected ? 'badge-ok' : 'badge-busy';
if (data.gitea.kanban_columns) {
kanbanState = data.gitea.kanban_columns;
const revElem = document.getElementById('giteaReview');
if (revElem) revElem.innerText = kanbanState.review || 0;
const wipElem = document.getElementById('giteaWip');
if (wipElem) wipElem.innerText = kanbanState.in_progress || 0;
}
if (data.gitea.ci_status !== undefined) {
giteaCiState = {
active_jobs: data.gitea.active_ci_jobs || 0,
status: data.gitea.ci_status || 'PASSING'
};
}
// 4. Update Agents
if (data.agents) {
for (const [key, agentData] of Object.entries(data.agents)) {
if (agents[key]) {
const oldState = agents[key].state;
agents[key].name = agentData.name;
agents[key].state = agentData.state;
agents[key].currentTask = agentData.currentTask;
agents[key].lastAction = agentData.lastAction;
agents[key].parallelCount = agentData.parallelCount || 0;
if (agentData.liveLog && agentData.liveLog.length > 0) {
agents[key].liveLog = agentData.liveLog;
}
if (oldState !== agentData.state) {
addLog(`⚡ Live-Update: <b>${agents[key].name}</b> -> <span class="status-pill status-${agentData.state}">${agentData.state}</span>`);
if (agentData.state === 'WORKING') play8BitTone(600, 'square', 0.1);
}
}
}
}
updateInspector();
} catch (err) {
console.warn('Live poll error:', err);
}
}
setInterval(fetchLiveStatus, 3000);
fetchLiveStatus();
// Interactive Teamplay Sequences
let activeTeamSequence = null;
function triggerDelegationAnimation() {
isLiveSync = false;
addLog(`💾 <b>Teamplay:</b> Lead übergibt Diskette mit neuem Task an Coder 1...`);
appendTerminalLog('LEAD', 'Issue #306 bereit: Delegiere Implementierung an Coder Alpha');
agents.lead.state = 'DELEGATING';
agents.lead.currentTask = 'Läuft zu Coder 1 und übergibt Task-Diskette';
// Move Lead to Coder 1 Desk
activeTeamSequence = {
type: 'DELEGATE',
startTime: Date.now(),
step: 1
};
play8BitTone(523, 'triangle', 0.15);
updateInspector();
}
function triggerApprovalParty() {
isLiveSync = false;
addLog(`🎉 <b>Reviewer:</b> PR #305 Approved! Party im Büro!`);
appendTerminalLog('QA', 'Alle CI & Security Checks bestanden! PR #305 gemergt.');
agents.reviewer.state = 'WORKING';
agents.reviewer.currentTask = 'Feiert erfolgreichen PR Merge!';
spawnConfetti(620, 100);
spawnConfetti(260, 100);
updateInspector();
}
function simulateEvent(agentKey, state, task) {
isLiveSync = false;
const agent = agents[agentKey];
if (!agent) return;
agent.state = state;
if (task) agent.currentTask = task;
if (state === 'WORKING') agent.lastAction = 'Active subprocess tool execution';
else if (state === 'ERROR') { agent.lastAction = 'Exception caught'; soundAlert(); }
addLog(`[MANUELL] <b>${agent.name}</b>: <span class="status-pill status-${state}">${state}</span> ${task || ''}`);
appendTerminalLog(agent.name.toUpperCase(), task || state);
updateInspector();
}
function resumeLiveSync() {
isLiveSync = true;
activeTeamSequence = null;
addLog('🔄 <b>Live-Synchronisation reaktiviert!</b>');
appendTerminalLog('HQ', 'Echtzeit-Synchronisation aktiv.');
fetchLiveStatus();
}
function simulateAllIdle() {
isLiveSync = false;
activeTeamSequence = null;
Object.keys(agents).forEach(k => {
agents[k].state = 'IDLE';
agents[k].currentTask = 'Relaxing in the lounge / drinking coffee';
});
addLog('☕ [MANUELL] Alle Agenten in den Pausenraum geschickt.');
appendTerminalLog('LOUNGE', 'Alle Agenten machen Kaffeepause.');
updateInspector();
}
function updateInspector() {
const agent = agents[selectedAgentKey];
if (!agent) return;
document.getElementById('inspAgentId').innerText = `#${agent.id.toUpperCase()}`;
const container = document.getElementById('agentInspector');
container.innerHTML = `
<div class="agent-header">
<div class="avatar-preview" style="border-color:${agent.shirtColor}; box-shadow: 0 0 10px ${agent.shirtColor}44;">
<div style="width:20px; height:24px; background:${agent.shirtColor}; border-radius:2px; position:relative;">
<div style="position:absolute; top:-8px; left:2px; width:16px; height:10px; background:${agent.skinColor};"></div>
<div style="position:absolute; top:-12px; left:1px; width:18px; height:6px; background:${agent.hairColor};"></div>
</div>
</div>
<div>
<div style="font-family:'Press Start 2P'; font-size:11px; color:${agent.hairColor}">${agent.name}</div>
<div style="font-size:16px; color:#94a3b8">${agent.role}</div>
</div>
</div>
<div>Status: <span class="status-pill status-${agent.state}">${agent.state}</span></div>
<div class="stat-row"><span>Letzte Aktion:</span><br>${agent.lastAction}</div>
<div class="stat-row"><span>Aktuelle Aufgabe:</span></div>
<div class="task-desc">${agent.currentTask}</div>
`;
let activeCount = Object.values(agents).filter(a => a.state !== 'IDLE').length;
let idleCount = Object.values(agents).filter(a => a.state === 'IDLE').length;
document.getElementById('canvasStats').innerText = `Aktiv: ${activeCount} | Idle: ${idleCount}`;
}
// ================= RENDERING ENGINE ================= //
function drawRoom(r) {
// Room Wall Shadow
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(r.x + 4, r.y + 4, r.w, r.h);
// Room Floor with Tiled Planks
const tileSize = 24;
for (let y = r.y; y < r.y + r.h; y += tileSize) {
for (let x = r.x; x < r.x + r.w; x += tileSize) {
const isAlt = ((x / tileSize) ^ (y / tileSize)) & 1;
ctx.fillStyle = isAlt ? r.floorColor1 : r.floorColor2;
ctx.fillRect(x, y, tileSize, tileSize);
ctx.strokeStyle = "rgba(0,0,0,0.15)";
ctx.lineWidth = 1;
ctx.strokeRect(x, y, tileSize, tileSize);
}
}
// 3D Wall Border
ctx.fillStyle = r.wallColor;
ctx.fillRect(r.x, r.y, r.w, 14);
ctx.fillStyle = r.wallTop;
ctx.fillRect(r.x, r.y, r.w, 4);
ctx.strokeStyle = "#1b2030";
ctx.lineWidth = 3;
ctx.strokeRect(r.x, r.y, r.w, r.h);
// Label Plaque
ctx.fillStyle = "#0c0e17";
const txtWidth = ctx.measureText(r.name).width;
ctx.fillRect(r.x + 12, r.y - 12, txtWidth + 24, 22);
ctx.strokeStyle = "#38bdf8";
ctx.lineWidth = 1.5;
ctx.strokeRect(r.x + 12, r.y - 12, txtWidth + 24, 22);
ctx.font = "11px 'Press Start 2P', monospace";
ctx.fillStyle = "#38bdf8";
ctx.fillText(r.name, r.x + 22, r.y + 3);
}
function drawFurniture(f, time) {
ctx.save();
if (f.type === 'RUG') {
ctx.fillStyle = "#991b1b";
ctx.fillRect(f.x, f.y, f.w, f.h);
ctx.strokeStyle = "#fbbf24";
ctx.lineWidth = 3;
ctx.strokeRect(f.x + 4, f.y + 4, f.w - 8, f.h - 8);
} else if (f.type === 'DESK_DUAL' || f.type === 'LEAD_DESK' || f.type === 'QA_DESK') {
const w = 114, h = 68;
// Desk shadow
ctx.fillStyle = "rgba(0,0,0,0.45)";
ctx.beginPath();
ctx.roundRect(f.x + 4, f.y + 10, w, h + 4, 6);
ctx.fill();
// Tabletop Wood
ctx.fillStyle = "#2d1f18";
ctx.fillRect(f.x, f.y + 4, w, h);
ctx.fillStyle = "#453229";
ctx.fillRect(f.x + 2, f.y + 2, w - 4, h - 4);
ctx.strokeStyle = "#694c3d";
ctx.lineWidth = 1.5;
ctx.strokeRect(f.x + 1, f.y + 1, w - 2, h - 2);
// Desk Mat on bottom half
ctx.fillStyle = "#111827";
ctx.fillRect(f.x + 10, f.y + 26, w - 20, 36);
ctx.strokeStyle = (f.type === 'QA_DESK') ? "#ef4444" : ((f.type === 'LEAD_DESK') ? "#eab308" : "#3b82f6");
ctx.lineWidth = 1;
ctx.strokeRect(f.x + 10, f.y + 26, w - 20, 36);
// TOP SECTION: MONITORS
ctx.fillStyle = "#64748b";
ctx.fillRect(f.x + w/2 - 4, f.y + 4, 8, 8);
// Screen 1 (Left)
ctx.fillStyle = "#030712";
ctx.fillRect(f.x + 14, f.y + 6, 38, 24);
ctx.strokeStyle = "#38bdf8";
ctx.lineWidth = 1;
ctx.strokeRect(f.x + 14, f.y + 6, 38, 24);
ctx.fillStyle = "#1e1e2e";
ctx.fillRect(f.x + 16, f.y + 8, 34, 20);
const codePulse = (Math.sin(time * 0.006) > 0);
ctx.fillStyle = "#c084fc"; ctx.fillRect(f.x + 18, f.y + 10, 8, 2);
ctx.fillStyle = "#38bdf8"; ctx.fillRect(f.x + 28, f.y + 10, 16, 2);
ctx.fillStyle = "#4ade80"; ctx.fillRect(f.x + 20, f.y + 15, 24, 2);
ctx.fillStyle = "#facc15"; ctx.fillRect(f.x + 20, f.y + 20, 18, 2);
if (codePulse) {
ctx.fillStyle = "#ffffff";
ctx.fillRect(f.x + 40, f.y + 20, 2, 3);
}
// Screen 2 (Right)
ctx.fillStyle = "#030712";
ctx.fillRect(f.x + 56, f.y + 6, 38, 24);
ctx.strokeStyle = (f.type === 'QA_DESK') ? "#f43f5e" : ((f.type === 'LEAD_DESK') ? "#fbbf24" : "#34d399");
ctx.lineWidth = 1;
ctx.strokeRect(f.x + 56, f.y + 6, 38, 24);
if (f.type === 'QA_DESK') {
ctx.fillStyle = "#1e1b2e";
ctx.fillRect(f.x + 58, f.y + 8, 34, 20);
ctx.fillStyle = "#22c55e"; ctx.fillRect(f.x + 60, f.y + 11, 4, 3);
ctx.fillStyle = "#94a3b8"; ctx.fillRect(f.x + 66, f.y + 11, 20, 2);
ctx.fillStyle = "#ef4444"; ctx.fillRect(f.x + 60, f.y + 18, 4, 3);
ctx.fillStyle = "#fca5a5"; ctx.fillRect(f.x + 66, f.y + 18, 18, 2);
} else if (f.type === 'LEAD_DESK') {
ctx.fillStyle = "#181824";
ctx.fillRect(f.x + 58, f.y + 8, 34, 20);
ctx.fillStyle = "#f59e0b"; ctx.fillRect(f.x + 62, f.y + 12, 6, 6);
ctx.fillStyle = "#38bdf8"; ctx.fillRect(f.x + 80, f.y + 12, 6, 6);
ctx.strokeStyle = "#475569"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(f.x + 68, f.y + 15); ctx.lineTo(f.x + 80, f.y + 15); ctx.stroke();
} else {
ctx.fillStyle = "#0c1017";
ctx.fillRect(f.x + 58, f.y + 8, 34, 20);
ctx.fillStyle = "#34d399";
ctx.fillRect(f.x + 60, f.y + 10, 26, 2);
ctx.fillRect(f.x + 60, f.y + 15, 18, 2);
}
// Mini PC Tower
ctx.fillStyle = "#0f172a";
ctx.fillRect(f.x + w - 16, f.y + 4, 12, 24);
const pcRgb = (Math.sin(time * 0.01) > 0) ? "#ec4899" : "#06b6d4";
ctx.fillStyle = pcRgb;
ctx.fillRect(f.x + w - 14, f.y + 10, 8, 8);
// Keyboard & Mouse on bottom
ctx.fillStyle = "#1e2433";
ctx.fillRect(f.x + w/2 - 20, f.y + 36, 40, 12);
for (let k = 0; k < 6; k++) {
ctx.fillStyle = (k % 2 === 0) ? "#475569" : "#64748b";
ctx.fillRect(f.x + w/2 - 18 + k * 6, f.y + 38, 4, 3);
}
ctx.fillStyle = "#1e293b";
ctx.fillRect(f.x + w/2 + 25, f.y + 38, 7, 10);
// Coffee Mug
ctx.fillStyle = "#ffffff";
ctx.fillRect(f.x + 14, f.y + 36, 9, 11);
ctx.fillStyle = "#78350f";
ctx.fillRect(f.x + 15, f.y + 37, 7, 3);
// Chair cushion in front
ctx.fillStyle = "#1e293b";
ctx.fillRect(f.x + w/2 - 18, f.y + h - 6, 36, 14);
ctx.font = "14px 'VT323'";
ctx.fillStyle = "#cbd5e1";
ctx.fillText(f.label, f.x + 12, f.y + h + 24);
} else if (f.type === 'WHITEBOARD') {
// Realtime Gitea Kanban Board on the Wall!
const w = 120, h = 54;
ctx.fillStyle = "#ffffff";
ctx.fillRect(f.x, f.y, w, h);
ctx.strokeStyle = "#64748b";
ctx.lineWidth = 2;
ctx.strokeRect(f.x, f.y, w, h);
// Header Bar
ctx.fillStyle = "#1e293b";
ctx.fillRect(f.x, f.y, w, 12);
ctx.font = "8px 'Press Start 2P'";
ctx.fillStyle = "#facc15";
ctx.fillText("KANBAN BOARD", f.x + 8, f.y + 9);
// 4 Columns: Plan | WIP | Review | Done
const colW = w / 4;
const colColors = ["#94a3b8", "#38bdf8", "#ec4899", "#22c55e"];
const colHeaders = ["PLN", "WIP", "REV", "OK"];
const colCounts = [
kanbanState.planned || 0,
kanbanState.in_progress || 0,
kanbanState.review || 0,
kanbanState.done || 0
];
for (let c = 0; c < 4; c++) {
const cx = f.x + c * colW;
if (c > 0) {
ctx.strokeStyle = "#e2e8f0";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx, f.y + 12); ctx.lineTo(cx, f.y + h);
ctx.stroke();
}
// Column Header Label
ctx.font = "7px 'Press Start 2P'";
ctx.fillStyle = "#475569";
ctx.fillText(colHeaders[c], cx + 4, f.y + 21);
// Draw Mini Sticky Notes
const count = colCounts[c];
const stickiesToShow = Math.min(count, 3);
for (let s = 0; s < stickiesToShow; s++) {
ctx.fillStyle = colColors[c];
ctx.fillRect(cx + 4, f.y + 24 + s * 9, colW - 8, 7);
ctx.strokeStyle = "rgba(0,0,0,0.3)";
ctx.strokeRect(cx + 4, f.y + 24 + s * 9, colW - 8, 7);
}
// Counter Badge if count > 0
if (count > 0) {
ctx.font = "10px 'VT323'";
ctx.fillStyle = "#0f172a";
ctx.fillText(count.toString(), cx + colW - 10, f.y + 21);
}
}
} else if (f.type === 'BULLMQ_STATION') {
const w = 75, h = 80;
ctx.fillStyle = "#0c101c";
ctx.fillRect(f.x, f.y, w, h);
ctx.strokeStyle = "#f59e0b";
ctx.lineWidth = 2;
ctx.strokeRect(f.x, f.y, w, h);
ctx.fillStyle = "#1e2438";
ctx.fillRect(f.x + 6, f.y + 8, w - 12, 16);
ctx.fillRect(f.x + 6, f.y + 28, w - 12, 16);
ctx.fillRect(f.x + 6, f.y + 48, w - 12, 16);
const jobShift = (time * 0.04) % 40;
ctx.fillStyle = "#fbbf24";
ctx.fillRect(f.x + 8 + (jobShift % 35), f.y + 11, 10, 10);
ctx.fillStyle = "#38bdf8";
ctx.fillRect(f.x + 8 + ((jobShift + 18) % 35), f.y + 31, 10, 10);
ctx.fillStyle = "#34d399";
ctx.fillRect(f.x + 8 + ((jobShift + 8) % 35), f.y + 51, 10, 10);
ctx.font = "11px 'Press Start 2P'";
ctx.fillStyle = "#fbbf24";
ctx.fillText("⚡BULLMQ", f.x + 2, f.y + h + 14);
} else if (f.type === 'GITEA_STATION') {
const w = 75, h = 80;
ctx.fillStyle = "#09121a";
ctx.fillRect(f.x, f.y, w, h);
ctx.strokeStyle = "#38bdf8";
ctx.lineWidth = 2;
ctx.strokeRect(f.x, f.y, w, h);
ctx.strokeStyle = "#60a5fa";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(f.x + 15, f.y + 14); ctx.lineTo(f.x + 15, f.y + 66);
ctx.moveTo(f.x + 15, f.y + 28); ctx.lineTo(f.x + 35, f.y + 42); ctx.lineTo(f.x + 35, f.y + 58);
ctx.stroke();
ctx.fillStyle = "#34d399";
ctx.beginPath(); ctx.arc(f.x + 15, f.y + 18, 4, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(f.x + 15, f.y + 40, 4, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(f.x + 35, f.y + 46, 4, 0, Math.PI*2); ctx.fill();
// Gitea CI Indicator
const isCiActive = (giteaCiState.active_jobs > 0 || giteaCiState.status === 'BUILDING');
let ciColor = "#22c55e"; // Solid Green when idle/passing
if (isCiActive) {
// Blink/pulsate only while a workflow is actively running
const ciBlink = Math.sin(time * 0.015) > 0;
ciColor = ciBlink ? "#fbbf24" : "#d97706";
} else if (giteaCiState.status === 'FAILURE') {
ciColor = "#ef4444";
}
ctx.fillStyle = ciColor;
ctx.fillRect(f.x + 48, f.y + 18, 20, 11);
ctx.strokeStyle = "rgba(0,0,0,0.4)";
ctx.lineWidth = 1;
ctx.strokeRect(f.x + 48, f.y + 18, 20, 11);
ctx.font = "8px 'Press Start 2P'";
ctx.fillStyle = isCiActive ? "#0f172a" : "#ffffff";
ctx.fillText("CI", f.x + 52, f.y + 27);
// Optional mini job counter indicator if building
if (isCiActive && giteaCiState.active_jobs > 0) {
ctx.font = "9px 'VT323'";
ctx.fillStyle = "#fbbf24";
ctx.fillText(`⚡${giteaCiState.active_jobs}`, f.x + 48, f.y + 40);
}
ctx.font = "11px 'Press Start 2P'";
ctx.fillStyle = "#38bdf8";
ctx.fillText("🐙GITEA", f.x + 4, f.y + h + 14);
} else if (f.type === 'COFFEE_MACHINE') {
ctx.fillStyle = "#3f1c08";
ctx.fillRect(f.x, f.y, 110, 45);
ctx.strokeStyle = "#78350f";
ctx.strokeRect(f.x, f.y, 110, 45);
ctx.fillStyle = "#94a3b8";
ctx.fillRect(f.x + 15, f.y - 16, 40, 20);
ctx.font = "15px 'VT323'";
ctx.fillStyle = "#fef3c7";
ctx.fillText("☕ ESPRESSO BAR", f.x + 12, f.y + 26);
} else if (f.type === 'RETRO_SOFA') {
ctx.fillStyle = "#7c2d12";
ctx.fillRect(f.x, f.y, 150, 50);
ctx.fillStyle = "#9a3412";
ctx.fillRect(f.x + 6, f.y + 6, 138, 38);
} else if (f.type === 'ARCADE_CAB') {
ctx.fillStyle = "#4338ca";
ctx.fillRect(f.x, f.y, 48, 60);
ctx.fillStyle = "#06b6d4";
ctx.fillRect(f.x + 6, f.y + 10, 36, 24);
} else if (f.type === 'VENDING_MACHINE') {
ctx.fillStyle = "#1e3a8a";
ctx.fillRect(f.x, f.y, 45, 60);
ctx.fillStyle = "#38bdf8";
ctx.fillRect(f.x + 6, f.y + 8, 33, 30);
} else if (f.type === 'PLANT') {
ctx.fillStyle = "#9a3412";
ctx.fillRect(f.x + 4, f.y + 14, 18, 16);
ctx.fillStyle = "#15803d";
ctx.beginPath();
ctx.arc(f.x + 13, f.y + 6, 12, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
// ================= DETAILED CHARACTER SPRITES & ACTIONS ================= //
function drawHumanoidAgent(agent, time) {
let targetX, targetY;
let currentActivity = null;
let isSeated = false;
// Check if teamplay sequence is directing this agent
if (activeTeamSequence && activeTeamSequence.type === 'DELEGATE' && agent.id === 'lead') {
// Lead walks to Coder 1 Desk
targetX = 135;
targetY = 120;
isSeated = false;
if (Math.hypot(targetX - agent.x, targetY - agent.y) < 6) {
// Handover animation
if (Date.now() - activeTeamSequence.startTime > 3000) {
agents.coder1.state = 'WORKING';
agents.coder1.currentTask = 'Task #306: Implementierung gestartet';
spawnConfetti(targetX, targetY);
activeTeamSequence = null;
agents.lead.state = 'IDLE';
appendTerminalLog('CODER', 'Diskette empfangen! Beginne mit der Umsetzung...');
}
}
} else if (agent.state === 'IDLE') {
if (!agent.lastActivitySwitch || time - agent.lastActivitySwitch > 12000) {
agent.idleActivityIndex = Math.floor(Math.random() * IDLE_ACTIVITIES.length);
agent.lastActivitySwitch = time;
}
currentActivity = IDLE_ACTIVITIES[agent.idleActivityIndex];
targetX = currentActivity.x;
targetY = currentActivity.y;
} else {
const desk = FURNITURE.find(f => f.id === agent.targetDesk);
targetX = desk ? desk.x + 38 : agent.x;
targetY = desk ? desk.y + 44 : agent.y;
isSeated = true;
}
agent.x += (targetX - agent.x) * 0.05;
agent.y += (targetY - agent.y) * 0.05;
const isWalking = Math.hypot(targetX - agent.x, targetY - agent.y) > 2;
let walkBob = 0;
let legOffset = 0;
let leftArm = 0;
let rightArm = 0;
if (isWalking) {
walkBob = Math.sin(time * 0.02) * 3;
legOffset = Math.sin(time * 0.02) * 4;
} else if (agent.state === 'WORKING' && isSeated) {
leftArm = Math.sin(time * 0.035) * 3;
rightArm = -Math.sin(time * 0.035) * 3;
} else if (agent.state === 'THINKING' && isSeated) {
rightArm = -8 + Math.sin(time * 0.01) * 2;
} else if (agent.state === 'IDLE' && currentActivity) {
if (currentActivity.anim === 'gaming') {
leftArm = Math.sin(time * 0.02) * 4;
rightArm = Math.sin(time * 0.04) * 3;
} else if (currentActivity.anim === 'coffee' || currentActivity.anim === 'mate') {
rightArm = -6 + Math.sin(time * 0.005) * 2;
} else if (currentActivity.anim === 'watering') {
rightArm = -4 + Math.sin(time * 0.01) * 3;
}
}
const px = Math.round(agent.x);
const py = Math.round(agent.y + (isSeated && !isWalking ? 0 : walkBob));
ctx.save();
// Selection Marker
if (selectedAgentKey === agent.id) {
ctx.strokeStyle = "#34d399";
ctx.lineWidth = 2;
ctx.setLineDash([4, 4]);
ctx.strokeRect(px - 14, py - 36, 48, 68);
ctx.setLineDash([]);
const bounce = Math.sin(time * 0.01) * 3;
ctx.fillStyle = "#34d399";
ctx.beginPath();
ctx.moveTo(px + 10, py - 42 + bounce);
ctx.lineTo(px + 4, py - 50 + bounce);
ctx.lineTo(px + 16, py - 50 + bounce);
ctx.fill();
}
// Soft Floor Drop Shadow
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.beginPath();
ctx.ellipse(px + 10, py + 26, 12, 5, 0, 0, Math.PI * 2);
ctx.fill();
// Draw FRONT or BACK view
if (!isSeated || isWalking) {
// Front View
ctx.fillStyle = agent.pantsColor;
ctx.fillRect(px + 3, py + 14, 6, 12 + legOffset);
ctx.fillRect(px + 11, py + 14, 6, 12 - legOffset);
ctx.fillStyle = "#0f172a";
ctx.fillRect(px + 2, py + 24 + legOffset, 7, 4);
ctx.fillRect(px + 11, py + 24 - legOffset, 7, 4);
ctx.fillStyle = agent.shirtColor;
ctx.fillRect(px + 1, py, 18, 15);
ctx.strokeStyle = "#00000044";
ctx.strokeRect(px + 1, py, 18, 15);
ctx.fillStyle = agent.shirtColor;
ctx.fillRect(px - 4, py + 2 + leftArm, 5, 10);
ctx.fillRect(px + 19, py + 2 + rightArm, 5, 10);
ctx.fillStyle = agent.skinColor;
ctx.fillRect(px - 4, py + 11 + leftArm, 5, 4);
ctx.fillRect(px + 19, py + 11 + rightArm, 5, 4);
// Holding floppy disk during delegation
if (agent.state === 'DELEGATING') {
ctx.fillStyle = "#38bdf8"; // Floppy Disk
ctx.fillRect(px + 21, py + 4, 8, 8);
ctx.fillStyle = "#ffffff";
ctx.fillRect(px + 23, py + 5, 4, 4);
} else if (agent.state === 'IDLE' && currentActivity) {
if (currentActivity.anim === 'coffee') {
ctx.fillStyle = "#ffffff"; ctx.fillRect(px + 20, py + 5 + rightArm, 6, 6);
ctx.fillStyle = "#78350f"; ctx.fillRect(px + 21, py + 5 + rightArm, 4, 2);
} else if (currentActivity.anim === 'mate') {
ctx.fillStyle = "#38bdf8"; ctx.fillRect(px + 21, py + 4 + rightArm, 5, 8);
ctx.fillStyle = "#fbbf24"; ctx.fillRect(px + 22, py + 3 + rightArm, 3, 2);
} else if (currentActivity.anim === 'watering') {
ctx.fillStyle = "#22c55e"; ctx.fillRect(px + 20, py + 6 + rightArm, 7, 5);
ctx.fillStyle = "#38bdf8"; ctx.fillRect(px + 28, py + 12 + ((time * 0.05) % 8), 2, 2);
}
}
ctx.fillStyle = agent.skinColor;
ctx.fillRect(px + 6, py - 3, 8, 4);
ctx.fillRect(px + 2, py - 18, 16, 15);
ctx.strokeStyle = "#00000033";
ctx.strokeRect(px + 2, py - 18, 16, 15);
ctx.fillStyle = agent.hairColor;
ctx.fillRect(px + 1, py - 22, 18, 7);
ctx.fillRect(px, py - 18, 4, 8);
ctx.fillRect(px + 16, py - 18, 4, 8);
if (agent.state === 'ERROR') {
ctx.fillStyle = "#dc2626"; ctx.font = "10px 'Press Start 2P'";
ctx.fillText("x", px + 4, py - 8); ctx.fillText("x", px + 11, py - 8);
} else if (currentActivity && currentActivity.anim === 'couch') {
ctx.fillStyle = "#334155";
ctx.fillRect(px + 4, py - 9, 4, 2); ctx.fillRect(px + 12, py - 9, 4, 2);
} else {
ctx.fillStyle = "#0f172a";
ctx.fillRect(px + 4, py - 10, 3, 4); ctx.fillRect(px + 12, py - 10, 3, 4);
ctx.fillStyle = "#ffffff";
ctx.fillRect(px + 4, py - 10, 1, 2); ctx.fillRect(px + 12, py - 10, 1, 2);
}
} else {
// Back View (Seated at Desk facing monitors)
ctx.fillStyle = "#1e293b";
ctx.fillRect(px - 10, py - 8, 40, 26);
ctx.strokeStyle = "#334155";
ctx.lineWidth = 1.5;
ctx.strokeRect(px - 10, py - 8, 40, 26);
ctx.fillStyle = "#334155";
ctx.fillRect(px - 4, py - 18, 28, 8);
ctx.fillStyle = agent.shirtColor;
ctx.fillRect(px + 1, py - 2, 18, 16);
ctx.strokeStyle = "#00000044";
ctx.strokeRect(px + 1, py - 2, 18, 16);
ctx.strokeStyle = "rgba(0,0,0,0.25)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(px + 10, py); ctx.lineTo(px + 10, py + 12);
ctx.stroke();
ctx.fillStyle = agent.shirtColor;
ctx.fillRect(px - 4, py - 4 + leftArm, 5, 12);
ctx.fillRect(px + 19, py - 4 + rightArm, 5, 12);
ctx.fillStyle = agent.skinColor;
ctx.fillRect(px - 4, py - 8 + leftArm, 5, 4);
ctx.fillRect(px + 19, py - 8 + rightArm, 5, 4);
ctx.fillStyle = agent.hairColor;
ctx.fillRect(px + 1, py - 22, 18, 18);
ctx.strokeStyle = "#00000033";
ctx.lineWidth = 1;
ctx.strokeRect(px + 1, py - 22, 18, 18);
ctx.fillStyle = "#0f172a";
ctx.fillRect(px, py - 24, 20, 3);
ctx.fillStyle = "#38bdf8";
ctx.fillRect(px - 2, py - 17, 4, 8);
ctx.fillRect(px + 18, py - 17, 4, 8);
}
drawEnhancedStatusBubble(agent, px, py - 28, time, currentActivity);
ctx.font = "14px 'VT323'";
ctx.fillStyle = "#ffffff";
ctx.textAlign = "center";
ctx.fillText(agent.name, px + 10, py + (isSeated ? 46 : 38));
ctx.textAlign = "left";
ctx.restore();
}
function drawEnhancedStatusBubble(agent, x, y, time, currentActivity) {
if (agent.state === 'DISCORD') {
const bounce = Math.sin(time * 0.012) * 3;
ctx.fillStyle = "#4338ca";
ctx.fillRect(x - 14, y - 12 + bounce, 48, 22);
ctx.strokeStyle = "#818cf8";
ctx.lineWidth = 2;
ctx.strokeRect(x - 14, y - 12 + bounce, 48, 22);
ctx.font = "12px 'VT323'";
ctx.fillStyle = "#ffffff";
ctx.fillText("💬 DISCORD", x - 10, y + 3 + bounce);
} else if (agent.state === 'DELEGATING') {
const bounce = Math.sin(time * 0.012) * 3;
ctx.fillStyle = "#831843";
ctx.fillRect(x - 10, y - 12 + bounce, 42, 22);
ctx.strokeStyle = "#f472b6";
ctx.lineWidth = 1.5;
ctx.strokeRect(x - 10, y - 12 + bounce, 42, 22);
ctx.font = "12px 'VT323'";
ctx.fillStyle = "#ffffff";
ctx.fillText("💾 DELEGATE", x - 6, y + 3 + bounce);
} else if (agent.state === 'WORKING') {
const count = agent.parallelCount || 1;
const bubbleW = (count > 1) ? 54 : 40;
ctx.fillStyle = "#064e3b";
ctx.fillRect(x - 10, y - 10, bubbleW, 20);
ctx.strokeStyle = (count > 1) ? "#facc15" : "#34d399";
ctx.lineWidth = 1.5;
ctx.strokeRect(x - 10, y - 10, bubbleW, 20);
ctx.font = "10px 'Press Start 2P'";
ctx.fillStyle = "#34d399";
ctx.fillText("</>", x - 4, y + 4);
if (count > 1) {
ctx.font = "10px 'Press Start 2P'";
ctx.fillStyle = "#facc15";
ctx.fillText(`x${count}`, x + 24, y + 4);
}
} else if (agent.state === 'THINKING') {
const bob = Math.sin(time * 0.01) * 2;
ctx.fillStyle = "#713f12";
ctx.fillRect(x - 6, y - 10 + bob, 34, 20);
ctx.strokeStyle = "#facc15";
ctx.lineWidth = 1.5;
ctx.strokeRect(x - 6, y - 10 + bob, 34, 20);
ctx.font = "14px 'VT323'";
ctx.fillStyle = "#fef08a";
ctx.fillText("💭 LLM", x - 2, y + 4 + bob);
} else if (agent.state === 'ERROR') {
const shake = (Math.random() - 0.5) * 3;
ctx.fillStyle = "#7f1d1d";
ctx.fillRect(x - 2 + shake, y - 12, 24, 22);
ctx.strokeStyle = "#f87171";
ctx.lineWidth = 2;
ctx.strokeRect(x - 2 + shake, y - 12, 24, 22);
ctx.font = "14px 'Press Start 2P'";
ctx.fillStyle = "#fee2e2";
ctx.fillText("!", x + 5 + shake, y + 4);
} else if (agent.state === 'IDLE') {
const bob = Math.sin(time * 0.008) * 2;
const bubbleTxt = (currentActivity && currentActivity.bubble) ? currentActivity.bubble : '☕ zZz';
ctx.fillStyle = "#1e293b";
const w = ctx.measureText(bubbleTxt).width + 16;
ctx.fillRect(x - 6, y - 10 + bob, w, 20);
ctx.strokeStyle = "#64748b";
ctx.lineWidth = 1.5;
ctx.strokeRect(x - 6, y - 10 + bob, w, 20);
ctx.font = "14px 'VT323'";
ctx.fillStyle = "#e2e8f0";
ctx.fillText(bubbleTxt, x - 2, y + 4 + bob);
}
}
// Click Detection
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const clickX = (e.clientX - rect.left) * scaleX;
const clickY = (e.clientY - rect.top) * scaleY;
for (const [key, agent] of Object.entries(agents)) {
if (
clickX >= agent.x - 15 &&
clickX <= agent.x + 35 &&
clickY >= agent.y - 35 &&
clickY <= agent.y + 40
) {
selectedAgentKey = key;
updateInspector();
addLog(`Inspektor aktiviert für: <b>${agent.name}</b>`);
if (agent.liveLog) {
agent.liveLog.forEach(l => appendTerminalLog(agent.name.toUpperCase(), l));
}
break;
}
}
});
// Main Animation Loop with Depth / Y-Sorting & Confetti
function render(time) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ROOMS.forEach(drawRoom);
const renderList = [];
FURNITURE.forEach(f => {
let sortY = f.y;
if (f.type.startsWith('DESK') || f.type.startsWith('LEAD') || f.type.startsWith('QA')) {
sortY = f.y + 30;
} else if (f.type === 'RUG') {
sortY = f.y - 100;
} else {
sortY = f.y + 40;
}
renderList.push({ type: 'FURNITURE', sortY: sortY, data: f });
});
Object.values(agents).forEach(agent => {
let isSeated = (agent.state !== 'IDLE' && agent.state !== 'DELEGATING');
let sortY = agent.y + (isSeated ? 20 : 25);
renderList.push({ type: 'AGENT', sortY: sortY, data: agent });
});
renderList.sort((a, b) => a.sortY - b.sortY);
renderList.forEach(item => {
if (item.type === 'FURNITURE') {
drawFurniture(item.data, time);
} else if (item.type === 'AGENT') {
drawHumanoidAgent(item.data, time);
}
});
// Render & Update Confetti Particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15; // gravity
p.life -= 0.02;
ctx.fillStyle = p.color;
ctx.fillRect(p.x, p.y, p.size, p.size);
if (p.life <= 0) particles.splice(i, 1);
}
requestAnimationFrame(render);
}
// Clock
setInterval(() => {
document.getElementById('clock').innerText = new Date().toLocaleTimeString();
}, 1000);
// Initial Start
updateInspector();
requestAnimationFrame(render);
</script>
</body>
</html>