Files

2180 lines
76 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;
}
/* Red Alert Banner */
#redAlertBanner {
display: none;
width: 100%;
max-width: 1180px;
background: #dc2626;
color: #fff;
font-family: 'Press Start 2P', monospace;
font-size: 11px;
padding: 8px 14px;
margin-bottom: 8px;
border-radius: 4px;
border: 2px solid #fecaca;
text-align: center;
animation: alertFlash 1s infinite alternate;
box-shadow: 0 0 20px rgba(220, 38, 38, 0.7);
}
@keyframes alertFlash {
0% { opacity: 1; transform: scale(1); }
100% { opacity: 0.85; transform: scale(1.005); }
}
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;
flex-wrap: wrap;
}
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: 10px;
}
.header-btn {
background: #1e2438;
border: 1px solid #3e4768;
color: #facc15;
padding: 4px 8px;
font-family: 'VT323';
font-size: 18px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
gap: 5px;
}
.header-btn:hover {
background: #2b3350;
border-color: #facc15;
}
/* Project Switcher Select */
.project-select-box {
display: flex;
align-items: center;
gap: 6px;
background: #141724;
border: 2px solid #38bdf8;
padding: 3px 8px;
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: 18px;
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%;
flex-wrap: wrap;
}
.infra-card {
flex: 1;
min-width: 240px;
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: 9px;
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'; }
.badge-err { background: #7f1d1d; color: #fca5a5; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-family:'Press Start 2P'; animation: pulse 1s infinite; }
.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: 440px;
}
.status-bar {
background: #090a10;
border-top: 2px solid #1e2438;
padding: 6px 12px;
font-size: 16px;
color: #94a3b8;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Bottom RPG Dialogue & Task Dispatcher */
.rpg-terminal-box {
margin-top: 4px;
background: #06070c;
border: 2px solid #334155;
border-radius: 4px;
padding: 8px 12px;
font-family: 'VT323', monospace;
font-size: 18px;
color: #a5f3fc;
max-height: 80px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
}
.rpg-term-prefix { color: #f59e0b; margin-right: 6px; }
.dispatcher-bar {
display: flex;
gap: 6px;
background: #0a0e1a;
border-top: 1px solid #1f293d;
padding: 6px 10px;
}
.dispatcher-input {
flex: 1;
background: #06070c;
border: 1px solid #3b82f6;
color: #67e8f9;
font-family: 'VT323', monospace;
font-size: 19px;
padding: 4px 10px;
border-radius: 3px;
outline: none;
}
.dispatcher-input::placeholder { color: #475569; }
.dispatcher-btn {
background: #1e3a8a;
border: 1px solid #3b82f6;
color: #fff;
font-family: 'Press Start 2P', monospace;
font-size: 9px;
padding: 0 12px;
cursor: pointer;
border-radius: 3px;
}
.dispatcher-btn:hover { background: #2563eb; }
/* 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: 44px;
height: 44px;
border: 2px solid #fff;
border-radius: 4px;
background: #1f2438;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
image-rendering: pixelated;
}
.rpg-rank-badge {
display: inline-block;
background: #312e81;
color: #c7d2fe;
border: 1px solid #6366f1;
border-radius: 3px;
font-family: 'Press Start 2P', monospace;
font-size: 8px;
padding: 2px 5px;
margin-top: 3px;
}
.xp-bar-container {
background: #090a10;
border: 1px solid #334155;
border-radius: 3px;
height: 8px;
width: 100%;
margin-top: 4px;
overflow: hidden;
}
.xp-bar-fill {
background: linear-gradient(90deg, #3b82f6, #10b981);
height: 100%;
width: 45%;
transition: width 0.3s;
}
.status-pill {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
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: 17px;
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: 85px;
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; }
/* Interactive Retro Modal (Kanban / Server Rack / Coffee) */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.75);
z-index: 999;
justify-content: center;
align-items: center;
}
.modal-card {
background: #0d111d;
border: 3px solid #38bdf8;
border-radius: 8px;
width: 90%;
max-width: 780px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 0 30px rgba(56, 189, 248, 0.4);
padding: 16px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #1e293b;
padding-bottom: 10px;
margin-bottom: 12px;
}
.modal-title {
font-family: 'Press Start 2P', monospace;
font-size: 12px;
color: #38bdf8;
}
.modal-close {
background: #dc2626;
color: #fff;
border: 1px solid #f87171;
font-family: 'Press Start 2P';
font-size: 10px;
padding: 4px 8px;
cursor: pointer;
}
.modal-content {
overflow-y: auto;
flex: 1;
font-family: 'VT323', monospace;
font-size: 18px;
}
/* Kanban Board Columns in Modal */
.kanban-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
}
.kanban-col {
background: #141927;
border: 1px solid #28314e;
border-radius: 4px;
padding: 8px;
min-height: 200px;
}
.kanban-col-title {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid #334155;
}
.kanban-card {
background: #1e2438;
border: 1px solid #3e486b;
border-radius: 3px;
padding: 6px;
margin-bottom: 6px;
font-size: 15px;
line-height: 1.2;
}
.kanban-card:hover { border-color: #38bdf8; }
</style>
</head>
<body>
<!-- CI Incident Red Alert Banner -->
<div id="redAlertBanner">
🚨 CI/CD INCIDENT DETECTED! Gitea Actions Build fehlgeschlagen. Coder untersucht Fehlerlogs! 🚨
</div>
<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>
<!-- Day/Night Ambient Switcher -->
<div class="project-select-box" style="border-color:#fbbf24;">
<span class="project-select-label" style="color:#fbbf24;">LICHT:</span>
<select id="ambientSelect" onchange="onAmbientChange(this.value)">
<option value="auto">🔄 Auto (Echtzeit)</option>
<option value="day">☀️ Tag (Daylight)</option>
<option value="sunset">🌅 Dämmerung</option>
<option value="night">🌙 Nacht (Cozy Neon)</option>
</select>
</div>
</div>
<div class="header-right">
<button class="header-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 / Token Velocity -->
<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 & CI</div>
<div>
<span class="infra-val" id="giteaPrs">0</span> <span style="font-size:13px; color:#94a3b8">PRs</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 class="infra-card">
<div class="infra-title">🚀 SPEEDOMETER</div>
<div>
<span class="infra-val" id="tokenSpeed" style="color:#c084fc">0</span> <span style="font-size:13px; color:#94a3b8">tok/s</span> |
<span class="badge-ok" id="systemHealthBadge">100% OK</span>
</div>
</div>
</div>
<div class="main-container">
<!-- Viewport / Canvas -->
<div class="viewport-box">
<canvas id="gameCanvas" width="800" height="440"></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>
<!-- Live Quest Dispatcher Bar -->
<div class="dispatcher-bar">
<input type="text" id="questInput" class="dispatcher-input" placeholder="✨ Neuer Prompt / Task für das Team... (z.B. 'Fix CSS Bug in Navigation')" onkeydown="if(event.key==='Enter') dispatchQuest();">
<button class="dispatcher-btn" onclick="dispatchQuest()">DISPATCH QUEST</button>
</div>
<div class="status-bar">
<span>🖱️ Klicke auf Agenten, Whiteboard, Kaffeemaschine oder Server-Racks!</span>
<span id="canvasStats">Aktiv: 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 & SIMULATION</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="background:#831843; border-color:#db2777; color:#fff;">💾 Lead übergibt Diskette</button>
<button onclick="triggerApprovalParty()" style="background:#065f46; border-color:#34d399; color:#fff;">🎉 PR Merge Party (+50 XP)</button>
<button onclick="simulateParallelCoders(3)" style="background:#1e3a8a; border-color:#3b82f6; color:#fff;">👥 3x Parallele Coder</button>
<button onclick="triggerCiIncident()" style="background:#7f1d1d; border-color:#ef4444; color:#fff;">🚨 CI Incident Alarm</button>
<button onclick="simulateEvent('reviewer', 'WORKING', 'Auditing Security PR #305')">🔍 QA Review</button>
<button onclick="simulateAllIdle()">☕ Kaffeepause</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 RPG Engine v2.0 bereit.</div>
</div>
</div>
</div>
</div>
<!-- Interactive Retro Modal -->
<div class="modal-overlay" id="interactiveModal" onclick="closeModal(event)">
<div class="modal-card" onclick="event.stopPropagation()">
<div class="modal-header">
<div class="modal-title" id="modalTitle">📋 WHITEBOARD KANBAN</div>
<button class="modal-close" onclick="closeModal()">✕ SCHLIESSEN</button>
</div>
<div class="modal-content" id="modalContent"></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, vol = 0.05) {
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(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.0001, 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(600, 'sawtooth', 0.15, 0.08);
setTimeout(() => play8BitTone(450, 'sawtooth', 0.2, 0.08), 120);
setTimeout(() => play8BitTone(600, 'sawtooth', 0.15, 0.08), 300);
setTimeout(() => play8BitTone(450, 'sawtooth', 0.2, 0.08), 420);
}
function soundKey() {
if (Math.random() < 0.3) {
play8BitTone(250 + Math.random()*200, 'triangle', 0.02, 0.02);
}
}
function soundCoffee() {
play8BitTone(180, 'sawtooth', 0.1, 0.03);
setTimeout(() => play8BitTone(220, 'triangle', 0.15, 0.03), 100);
setTimeout(() => play8BitTone(160, 'sine', 0.25, 0.04), 220);
}
function soundArcade() {
[523, 659, 784, 1046].forEach((f, i) => {
setTimeout(() => play8BitTone(f, 'square', 0.08, 0.06), i * 70);
});
}
function soundLevelUp() {
[440, 554, 659, 880, 1108].forEach((f, i) => {
setTimeout(() => play8BitTone(f, 'triangle', 0.12, 0.07), i * 80);
});
}
function toggleAudio() {
audioEnabled = !audioEnabled;
document.getElementById('soundToggle').innerText = audioEnabled ? '🔊 8-Bit Audio: ON' : '🔇 8-Bit Audio: OFF';
}
// Canvas Context & Time
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let currentProject = 'camper-knowledge-graph';
let ambientMode = 'auto';
let isLiveSync = true;
let selectedAgentKey = 'coder1';
let isIncidentActive = false;
// Confetti & Steam Particle System
let particles = [];
let steamParticles = [];
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();
}
function spawnSteam(x, y) {
for (let i = 0; i < 12; i++) {
steamParticles.push({
x: x + (Math.random() - 0.5) * 10,
y: y,
vx: (Math.random() - 0.5) * 0.8,
vy: - (Math.random() * 1.5 + 0.8),
size: Math.random() * 3 + 2,
life: 1.0,
color: 'rgba(255,255,255,'
});
}
soundCoffee();
}
// Dynamic Rooms
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: 135, name: "☕ AGENT LOUNGE & CAFE", floorColor1: "#382923", floorColor2: "#2d201a", wallColor: "#4f3b33", wallTop: "#6e5449" }
];
// Furniture Definitions
const FURNITURE = [
// Dev Lab
{ type: 'DESK_DUAL', x: 50, y: 70, id: 'desk_coder1', label: 'Workstation Alpha' },
{ type: 'DESK_DUAL', x: 190, y: 70, id: 'desk_coder2', label: 'Workstation Beta' },
{ type: 'DESK_DUAL', x: 120, y: 155, id: 'desk_coder3', label: 'Pair Desk Gamma' },
{ type: 'WHITEBOARD', x: 130, y: 35, label: 'Gitea Kanban' },
{ type: 'PLANT', x: 30, y: 220 },
// 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' }
];
// RPG Progression Ranks
const RPG_RANKS = [
{ minXp: 0, title: 'Junior Byte Crusher' },
{ minXp: 100, title: 'Async Artisan' },
{ minXp: 250, title: 'Senior Code Wizard' },
{ minXp: 500, title: 'Principal Architect' },
{ minXp: 1000, title: '10x Mythic Lead' }
];
function getAgentXp(id) {
try {
const val = localStorage.getItem(`rpg_xp_${id}`);
return val ? parseInt(val, 10) : 120;
} catch(e) { return 120; }
}
function addAgentXp(id, amount) {
const current = getAgentXp(id);
const next = current + amount;
try { localStorage.setItem(`rpg_xp_${id}`, next); } catch(e) {}
const oldRank = getRankForXp(current);
const newRank = getRankForXp(next);
if (newRank.title !== oldRank.title) {
soundLevelUp();
addLog(`🌟 <b>LEVEL UP!</b> ${agents[id]?.name || id} ist jetzt <b>${newRank.title}</b>!`);
appendTerminalLog('LEVEL-UP', `Glückwunsch! ${agents[id]?.name} stieg auf zu: ${newRank.title}`);
spawnConfetti(agents[id]?.x || 200, agents[id]?.y || 100);
}
updateInspector();
}
function getRankForXp(xp) {
let r = RPG_RANKS[0];
for (let item of RPG_RANKS) {
if (xp >= item.minXp) r = item;
}
return r;
}
// Agents Registry
const agents = {
coder1: {
id: 'coder1',
name: 'CKG Coder (Alpha)',
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..."],
speechBubble: null,
speechBubbleTime: 0
},
coder2: {
id: 'coder2',
name: 'CKG Tester (Beta)',
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"],
speechBubble: null,
speechBubbleTime: 0
},
coder3: {
id: 'coder3',
name: 'CKG Pair Coder (Gamma)',
role: 'Subagent Coder',
hairColor: '#10b981',
shirtColor: '#d97706',
pantsColor: '#1e293b',
skinColor: '#ffedd5',
state: 'IDLE',
currentTask: 'Standby für Parallele Turns',
lastAction: 'Standby',
x: 120, y: 360,
targetDesk: 'desk_coder3',
idleActivityIndex: 1,
lastActivitySwitch: 0,
liveLog: ["Subagent Pool bereit"],
speechBubble: null,
speechBubbleTime: 0
},
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"],
speechBubble: null,
speechBubbleTime: 0
},
reviewer: {
id: 'reviewer',
name: 'CKG Reviewer',
role: 'Security & QA',
hairColor: '#a855f7',
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"],
speechBubble: null,
speechBubbleTime: 0
}
};
// Live Kanban & Data cache
let kanbanState = { planned: 3, ready: 1, in_progress: 0, review: 2, done: 12 };
let kanbanItemsCache = { planned: [], ready: [], in_progress: [], review: [], done: [] };
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();
}
function onAmbientChange(val) {
ambientMode = val;
addLog(`💡 Beleuchtung umgestellt auf: <b>${val.toUpperCase()}</b>`);
}
// 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 wipElem = document.getElementById('giteaWip');
if (wipElem) wipElem.innerText = kanbanState.in_progress || 0;
}
if (data.gitea.kanban_items) {
kanbanItemsCache = data.gitea.kanban_items;
}
// 4. Update CI Status & Red Alert Mode
if (data.gitea.ci_status !== undefined) {
giteaCiState = {
active_jobs: data.gitea.active_ci_jobs || 0,
status: data.gitea.ci_status || 'PASSING'
};
if (data.gitea.ci_status === 'FAILURE' && !isIncidentActive) {
triggerCiIncident();
} else if (data.gitea.ci_status !== 'FAILURE' && isIncidentActive) {
resolveCiIncident();
}
}
// 5. Update Metrics (Speedometer)
if (data.metrics) {
document.getElementById('tokenSpeed').innerText = data.metrics.token_velocity || 0;
const hBadge = document.getElementById('systemHealthBadge');
hBadge.innerText = `${data.metrics.system_health}% OK`;
hBadge.className = (data.metrics.system_health < 100) ? 'badge-err' : 'badge-ok';
}
// 6. 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;
// Periodic speech bubbles from logs
const cleanLog = agentData.liveLog[0].replace(/^[🤖⚙️]\s*/, '');
agents[key].speechBubble = cleanLog.substring(0, 35) + '...';
}
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);
addAgentXp(key, 10);
}
}
}
}
// Handle multi-coder dynamic instances
const parallelCount = data.agents.coder1?.parallelCount || 0;
if (parallelCount >= 2) {
agents.coder2.state = 'WORKING';
agents.coder2.currentTask = 'Parallele Code-Session (Instance #2)';
agents.coder2.lastAction = 'Active Subagent Parallel Turn';
}
if (parallelCount >= 3) {
agents.coder3.state = 'WORKING';
agents.coder3.currentTask = 'Parallele Code-Session (Instance #3)';
agents.coder3.lastAction = 'Active Subagent Parallel Turn';
} else if (parallelCount < 3 && agents.coder3.state === 'WORKING' && !activeTeamSequence) {
agents.coder3.state = 'IDLE';
}
}
updateInspector();
} catch (err) {
console.warn('Live poll error:', err);
}
}
setInterval(fetchLiveStatus, 3000);
fetchLiveStatus();
// Dispatch Quest / Create Issue via API
async function dispatchQuest() {
const input = document.getElementById('questInput');
const text = input.value.trim();
if (!text) return;
appendTerminalLog('DISPATCH', `Sende Quest an ${currentProject}: "${text}"...`);
addLog(`🚀 <b>Quest dispatch:</b> "${text}"`);
play8BitTone(523, 'triangle', 0.1);
try {
const res = await fetch('/api/issue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project: currentProject, title: text, body: 'Erstellt via Agent RPG Dashboard Terminal.' })
});
if (res.ok) {
const respData = await res.json();
appendTerminalLog('GITEA', `✅ Issue #${respData.issue?.number || 'Neu'} erfolgreich erstellt! Lead übernimmt.`);
addLog(`✅ Issue angelegt: #${respData.issue?.number || ''} ${text}`);
input.value = '';
triggerDelegationAnimation();
addAgentXp('lead', 25);
} else {
appendTerminalLog('GITEA-ERR', 'Fehler beim Erstellen des Issues.');
}
} catch(e) {
appendTerminalLog('ERR', `Verbindungsfehler: ${e.message}`);
}
}
// Interactive Teamplay Sequences
let activeTeamSequence = null;
function triggerDelegationAnimation() {
isLiveSync = false;
addLog(`💾 <b>Teamplay:</b> Lead übergibt Diskette mit neuem Task an Coder Alpha...`);
appendTerminalLog('LEAD', 'Issue bereit: Delegiere Implementierung an Coder Alpha');
agents.lead.state = 'DELEGATING';
agents.lead.currentTask = 'Läuft zu Coder Alpha und übergibt Task-Diskette';
activeTeamSequence = {
type: 'DELEGATE',
startTime: Date.now(),
step: 1
};
play8BitTone(523, 'triangle', 0.15);
updateInspector();
}
function triggerApprovalParty() {
isLiveSync = false;
addLog(`🎉 <b>Reviewer:</b> PR Approved! Party im Büro! (+50 XP)`);
appendTerminalLog('QA', 'Alle CI & Security Checks bestanden! PR gemergt.');
agents.reviewer.state = 'WORKING';
agents.reviewer.currentTask = 'Feiert erfolgreichen PR Merge!';
// Award XP to all active agents
Object.keys(agents).forEach(k => addAgentXp(k, 50));
spawnConfetti(620, 100);
spawnConfetti(260, 100);
spawnConfetti(150, 180);
updateInspector();
}
function triggerCiIncident() {
isIncidentActive = true;
document.getElementById('redAlertBanner').style.display = 'block';
soundAlert();
addLog(`🚨 <b>RED ALERT:</b> CI Pipeline Failure in ${currentProject}!`);
appendTerminalLog('CI-ALARM', 'Workflow Run Failed! Coder & Reviewer schlagen Alarm!');
agents.coder1.state = 'ERROR';
agents.coder1.currentTask = 'CI Failed! Prüfe Logs und Stacktraces...';
updateInspector();
}
function resolveCiIncident() {
isIncidentActive = false;
document.getElementById('redAlertBanner').style.display = 'none';
addLog(`✅ <b>Incident behoben:</b> CI Pipeline läuft wieder grün.`);
appendTerminalLog('CI-OK', 'Alle Builds wieder grün.');
agents.coder1.state = 'WORKING';
updateInspector();
}
function simulateParallelCoders(count) {
isLiveSync = false;
addLog(`👥 [SIMULATION] ${count}x Parallele Coder-Instanzen gestartet!`);
appendTerminalLog('ORCHESTRATOR', `Spawne ${count} parallele Subagent-Coder an Workstations...`);
agents.coder1.state = 'WORKING';
agents.coder1.currentTask = 'Subagent #1: Implementiere Frontend & Canvas';
if (count >= 2) {
agents.coder2.state = 'WORKING';
agents.coder2.currentTask = 'Subagent #2: Refactoring Backend API & SQLite';
}
if (count >= 3) {
agents.coder3.state = 'WORKING';
agents.coder3.currentTask = 'Subagent #3: Running Playwright E2E Tests';
}
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'; addAgentXp(agentKey, 15); }
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;
if (isIncidentActive) resolveCiIncident();
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;
const xp = getAgentXp(agent.id);
const rank = getRankForXp(xp);
const nextXpTarget = (rank.minXp === 1000) ? 1500 : (RPG_RANKS.find(r => r.minXp > xp)?.minXp || 1000);
const xpPercent = Math.min(100, Math.round((xp / nextXpTarget) * 100));
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 style="flex:1">
<div style="font-family:'Press Start 2P'; font-size:10px; color:${agent.hairColor}">${agent.name}</div>
<div style="font-size:15px; color:#94a3b8">${agent.role}</div>
<div class="rpg-rank-badge">⭐ ${rank.title}</div>
</div>
</div>
<div style="margin-top:6px; font-size:15px; display:flex; justify-content:space-between; color:#94a3b8;">
<span>XP: <b style="color:#facc15">${xp}</b> / ${nextXpTarget}</span>
<span>${xpPercent}%</span>
</div>
<div class="xp-bar-container">
<div class="xp-bar-fill" style="width:${xpPercent}%"></div>
</div>
<div style="margin-top:6px;">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}`;
}
// Modal Manager
function openModal(title, htmlContent) {
document.getElementById('modalTitle').innerText = title;
document.getElementById('modalContent').innerHTML = htmlContent;
document.getElementById('interactiveModal').style.display = 'flex';
play8BitTone(700, 'triangle', 0.1);
}
function closeModal(e) {
document.getElementById('interactiveModal').style.display = 'none';
}
function showKanbanModal() {
let html = `<div class="kanban-grid">`;
const cols = [
{ id: 'planned', name: '📋 PLANNED', color: '#94a3b8' },
{ id: 'ready', name: '⚡ READY', color: '#facc15' },
{ id: 'in_progress', name: '💻 IN PROGRESS', color: '#38bdf8' },
{ id: 'review', name: '🔍 REVIEW / PR', color: '#ec4899' },
{ id: 'done', name: '✅ DONE', color: '#22c55e' }
];
cols.forEach(c => {
const items = kanbanItemsCache[c.id] || [];
html += `
<div class="kanban-col">
<div class="kanban-col-title" style="color:${c.color}">${c.name} (${items.length})</div>
${items.length === 0 ? '<div style="color:#475569; font-size:14px;">Keine Items</div>' : ''}
${items.map(i => `
<div class="kanban-card">
<b style="color:#38bdf8;">#${i.number}</b> ${i.title}
${i.is_pr ? '<span style="color:#ec4899; font-size:12px;"> [PR]</span>' : ''}
</div>
`).join('')}
</div>
`;
});
html += `</div>`;
openModal(`📋 GITEA KANBAN BOARD (${currentProject})`, html);
}
function showRackModal(type) {
let html = `
<div style="display:flex; flex-direction:column; gap:10px;">
<div style="background:#141724; padding:10px; border-radius:4px; border:1px solid #38bdf8;">
<b style="color:#38bdf8;">Station:</b> ${type === 'BULLMQ' ? '⚡ BullMQ Redis/Valkey Message Broker' : '🐙 Gitea Git & Actions CI/CD'}
</div>
<div><b>Projekt-Scope:</b> ${currentProject}</div>
<div><b>Status:</b> ${giteaCiState.status === 'FAILURE' ? '🚨 INCIDENT DETECTED' : '🟢 ALL SYSTEMS OPERATIONAL'}</div>
<div><b>Aktive CI Jobs:</b> ${giteaCiState.active_jobs}</div>
<div><b>Offene Pull Requests:</b> ${kanbanState.review || 0}</div>
</div>
`;
openModal(type === 'BULLMQ' ? '⚡ BULLMQ BROKER STATUS' : '🐙 GITEA REPOSITORY & CI/CD', html);
}
// ================= RENDERING ENGINE ================= //
function getAmbientSetting() {
if (ambientMode !== 'auto') return ambientMode;
const hour = new Date().getHours();
if (hour >= 7 && hour < 18) return 'day';
if (hour >= 18 && hour < 21) return 'sunset';
return 'night';
}
function drawRoom(r, time) {
// 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);
// Render Windows on Walls with Day/Sunset/Night View
if (r.id === 'dev_lab') {
drawWindow(r.x + 280, r.y + 2, 45, 12, time);
} else if (r.id === 'lead_room') {
drawWindow(r.x + 240, r.y + 2, 45, 12, time);
}
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 drawWindow(x, y, w, h, time) {
const amb = getAmbientSetting();
ctx.save();
ctx.fillStyle = (amb === 'day') ? '#38bdf8' : (amb === 'sunset' ? '#f97316' : '#0f172a');
ctx.fillRect(x, y, w, h);
ctx.strokeStyle = '#64748b';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, w, h);
if (amb === 'night') {
// Pixel stars in window
ctx.fillStyle = '#ffffff';
if (Math.sin(time * 0.005) > 0) ctx.fillRect(x + 10, y + 3, 2, 2);
if (Math.sin(time * 0.007 + 1) > 0) ctx.fillRect(x + 26, y + 6, 2, 2);
if (Math.sin(time * 0.004 + 2) > 0) ctx.fillRect(x + 36, y + 4, 1, 1);
} else if (amb === 'sunset') {
ctx.fillStyle = '#fde047';
ctx.fillRect(x + w/2 - 4, y + 4, 8, 8);
} else {
// Cloud
ctx.fillStyle = '#ffffff';
const cloudX = x + ((time * 0.01) % w);
ctx.fillRect(cloudX, y + 4, 8, 4);
}
ctx.restore();
}
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
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);
// 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
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
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
const w = 120, h = 54;
ctx.fillStyle = "#ffffff";
ctx.fillRect(f.x, f.y, w, h);
ctx.strokeStyle = "#38bdf8";
ctx.lineWidth = 2;
ctx.strokeRect(f.x, f.y, w, h);
ctx.fillStyle = "#1e293b";
ctx.fillRect(f.x, f.y, w, 12);
ctx.font = "8px 'Press Start 2P'";
ctx.fillStyle = "#facc15";
ctx.fillText("KANBAN (KLICK)", f.x + 4, f.y + 9);
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();
}
ctx.font = "7px 'Press Start 2P'";
ctx.fillStyle = "#475569";
ctx.fillText(colHeaders[c], cx + 4, f.y + 21);
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);
}
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 = isIncidentActive ? "#ef4444" : "#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();
// CI Indicator
const isCiActive = (giteaCiState.active_jobs > 0 || giteaCiState.status === 'BUILDING');
let ciColor = isIncidentActive ? "#ef4444" : ((isCiActive && Math.sin(time*0.015)>0) ? "#fbbf24" : "#22c55e");
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 = "#ffffff";
ctx.fillText("CI", f.x + 52, f.y + 27);
ctx.font = "11px 'Press Start 2P'";
ctx.fillStyle = isIncidentActive ? "#ef4444" : "#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);
ctx.font = "8px 'Press Start 2P'";
ctx.fillStyle = "#facc15";
ctx.fillText("🎮", f.x + 14, f.y + 26);
} 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;
// Teamplay Delegation sequence
if (activeTeamSequence && activeTeamSequence.type === 'DELEGATE' && agent.id === 'lead') {
targetX = 115;
targetY = 120;
isSeated = false;
if (Math.hypot(targetX - agent.x, targetY - agent.y) < 6) {
if (Date.now() - activeTeamSequence.startTime > 3000) {
agents.coder1.state = 'WORKING';
agents.coder1.currentTask = 'Task implementieren (Diskette empfangen)';
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;
soundKey();
} else if (agent.state === 'ERROR') {
// Hands on head in shock!
leftArm = -12;
rightArm = -12;
} 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);
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 = "13px 'VT323'";
ctx.fillStyle = "#ffffff";
ctx.textAlign = "center";
ctx.fillText(agent.name.split(' ')[0], px + 10, py + (isSeated ? 46 : 38));
ctx.textAlign = "left";
ctx.restore();
}
function drawEnhancedStatusBubble(agent, x, y, time, currentActivity) {
const isSpeechActive = agent.speechBubble && (Math.sin(time * 0.003) > -0.2);
if (agent.state === 'ERROR' || isIncidentActive) {
const shake = (Math.random() - 0.5) * 4;
ctx.fillStyle = "#7f1d1d";
ctx.fillRect(x - 8 + shake, y - 12, 36, 22);
ctx.strokeStyle = "#f87171";
ctx.lineWidth = 2;
ctx.strokeRect(x - 8 + shake, y - 12, 36, 22);
ctx.font = "11px 'Press Start 2P'";
ctx.fillStyle = "#fee2e2";
ctx.fillText("!CI", x - 2 + shake, y + 4);
} else 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') {
if (isSpeechActive && agent.speechBubble) {
// Thought bubble with real-time log text
ctx.font = "13px 'VT323'";
const w = ctx.measureText(agent.speechBubble).width + 14;
ctx.fillStyle = "#022c22";
ctx.fillRect(x - 10, y - 14, w, 22);
ctx.strokeStyle = "#34d399";
ctx.lineWidth = 1.5;
ctx.strokeRect(x - 10, y - 14, w, 22);
ctx.fillStyle = "#6ee7b7";
ctx.fillText(`💭 ${agent.speechBubble}`, x - 4, y + 1);
} else {
ctx.fillStyle = "#064e3b";
ctx.fillRect(x - 6, y - 10, 36, 20);
ctx.strokeStyle = "#34d399";
ctx.lineWidth = 1.5;
ctx.strokeRect(x - 6, y - 10, 36, 20);
ctx.font = "10px 'Press Start 2P'";
ctx.fillStyle = "#34d399";
ctx.fillText("</>", x, 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 === '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);
}
}
// Ambient Lighting Overlay Engine (Night / Dusk / Day)
function drawAmbientLighting(time) {
const amb = getAmbientSetting();
if (amb === 'day' && !isIncidentActive) return; // Daylight is full bright
ctx.save();
// Red Alert Emergency Sweep
if (isIncidentActive) {
const sweep = Math.sin(time * 0.008);
ctx.fillStyle = `rgba(220, 38, 38, ${0.25 + sweep * 0.15})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
} else if (amb === 'sunset') {
ctx.fillStyle = 'rgba(249, 115, 22, 0.18)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
} else if (amb === 'night') {
// Deep Cozy Night Shadow Mask
ctx.fillStyle = 'rgba(6, 10, 25, 0.55)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Warm radial light cones cutout around monitors & workstations
ctx.globalCompositeOperation = 'lighter';
const lights = [
{ x: 105, y: 105, r: 65, color: 'rgba(56, 189, 248, 0.25)' },
{ x: 245, y: 105, r: 65, color: 'rgba(52, 211, 153, 0.25)' },
{ x: 475, y: 105, r: 75, color: 'rgba(250, 204, 21, 0.25)' },
{ x: 625, y: 105, r: 75, color: 'rgba(244, 63, 94, 0.25)' },
{ x: 725, y: 75, r: 60, color: 'rgba(251, 191, 36, 0.3)' },
{ x: 495, y: 335, r: 50, color: 'rgba(6, 182, 212, 0.3)' }
];
lights.forEach(l => {
const grad = ctx.createRadialGradient(l.x, l.y, 10, l.x, l.y, l.r);
grad.addColorStop(0, l.color);
grad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(l.x, l.y, l.r, 0, Math.PI * 2);
ctx.fill();
});
}
ctx.restore();
}
// Click Detection on Agents & Furniture
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;
// 1. Check Agent clicks
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));
}
return;
}
}
// 2. Check Furniture clicks
FURNITURE.forEach(f => {
if (f.type === 'WHITEBOARD' && clickX >= f.x && clickX <= f.x + 120 && clickY >= f.y && clickY <= f.y + 60) {
showKanbanModal();
} else if (f.type === 'COFFEE_MACHINE' && clickX >= f.x && clickX <= f.x + 110 && clickY >= f.y && clickY <= f.y + 50) {
spawnSteam(f.x + 35, f.y - 10);
addLog(`☕ Frischer Espresso zubereitet! (+5 Energy für alle Agenten)`);
appendTerminalLog('COFFEE', 'Espresso-Bar aktiv: Koffeinschub verteilt!');
} else if (f.type === 'ARCADE_CAB' && clickX >= f.x && clickX <= f.x + 48 && clickY >= f.y && clickY <= f.y + 60) {
soundArcade();
spawnConfetti(f.x + 24, f.y + 10);
addLog(`🕹️ <b>Highscore geknackt!</b> Flappy Bird Agent Champion!`);
appendTerminalLog('ARCADE', 'Neuer Highscore auf dem Arcade-Automaten registriert!');
} else if ((f.type === 'BULLMQ_STATION' || f.type === 'GITEA_STATION') && clickX >= f.x && clickX <= f.x + 75 && clickY >= f.y && clickY <= f.y + 80) {
showRackModal(f.type === 'BULLMQ' ? 'BULLMQ' : 'GITEA');
}
});
});
// Main Animation Loop
function render(time) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ROOMS.forEach(r => drawRoom(r, time));
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 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;
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);
}
// Render Steam Particles
for (let i = steamParticles.length - 1; i >= 0; i--) {
const s = steamParticles[i];
s.x += s.vx;
s.y += s.vy;
s.life -= 0.025;
ctx.fillStyle = `${s.color}${s.life * 0.6})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
if (s.life <= 0) steamParticles.splice(i, 1);
}
// Render Ambient Lighting Overlay
drawAmbientLighting(time);
requestAnimationFrame(render);
}
// Clock
setInterval(() => {
document.getElementById('clock').innerText = new Date().toLocaleTimeString();
}, 1000);
// Initial Start
updateInspector();
requestAnimationFrame(render);
</script>
</body>
</html>