2180 lines
76 KiB
HTML
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>
|