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