coworking-agents 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
package/docs/building.png CHANGED
Binary file
package/docs/camera.png CHANGED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/docs/panel.png CHANGED
Binary file
package/docs/paper.png CHANGED
Binary file
package/docs/servers.png CHANGED
Binary file
package/docs/talk.png CHANGED
Binary file
package/docs/top-bar.png CHANGED
Binary file
Binary file
package/docs/usage.png CHANGED
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coworking-agents",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A pixel-art coworking office for your AI coding agents (Claude Code, Codex, …): see who's working, who needs you, and who's editing the same checkout.",
5
5
  "bin": {
6
6
  "coworking-agents": "bin/coworking-agents.js"
package/public/js/fun2.js CHANGED
@@ -91,21 +91,30 @@ function openArcade() {
91
91
  function drawArcadeTitle() {
92
92
  const c = arcadeEl.querySelector('.arcade-cv'); if (!c) return;
93
93
  const g = c.getContext('2d'); arcadeBg(g, performance.now());
94
- g.fillStyle = '#ffd84d'; g.font = 'bold 18px monospace'; g.textAlign = 'center'; g.fillText('BUG CATCHER', AW / 2, 62);
95
- g.fillStyle = '#c0cbdc'; g.font = '10px monospace'; g.fillText(T.arcade.press, AW / 2, 86); g.textAlign = 'left';
94
+ gText(g, AW / 2, 46, 'BUG CATCHER', '#ffd84d', 3, true);
95
+ gText(g, AW / 2, 78, lang === 'pt' ? 'ESPACO OU ENTER' : 'SPACE OR ENTER', '#c0cbdc', 1, true);
96
96
  drawBug(g, AW / 2 - 30, 100, false, 0); drawBug(g, AW / 2 - 4, 104, true, 1); drawBug(g, AW / 2 + 22, 100, false, 0);
97
97
  }
98
+ // crisp text: the office's pixel font, scaled by whole pixels
99
+ function gText(g, x, y, str, c, k = 1, center = false) {
100
+ const txt = String(str).toUpperCase(); if (center) x -= Math.round(txt.length * 4 * k / 2);
101
+ g.fillStyle = c; let cx = Math.round(x);
102
+ for (const ch of txt) { const gl = PIXFONT[ch]; if (gl) for (let i = 0; i < 15; i++) if (gl[i] === '1') g.fillRect(cx + (i % 3) * k, Math.round(y) + ((i / 3) | 0) * k, k, k); cx += 4 * k; }
103
+ }
98
104
  function arcadeBg(g, now) {
99
105
  g.fillStyle = '#141826'; g.fillRect(0, 0, AW, AH);
100
- g.fillStyle = '#1c2236'; for (let y = 0; y < AH; y += 8) { const sh = (y * 13) % 70; g.fillRect(6 + sh % 20, (y + now / 60) % AH, 30 + sh, 2); } // code scrolling behind
106
+ g.fillStyle = '#1c2236'; for (let y = 0; y < AH; y += 8) { const sh = (y * 13) % 70; g.fillRect(6 + sh % 20, Math.round((y + now / 60) % AH), 30 + sh, 2); } // code scrolling behind
101
107
  g.fillStyle = '#2a2f45'; g.fillRect(0, AH - 8, AW, 8);
102
108
  }
103
109
  function drawBug(g, x, y, gold, f) {
104
- const body = gold ? '#ffd84d' : '#63c74d', dark = gold ? '#b07d2a' : '#2f7a3b';
105
- g.fillStyle = '#1b1622'; for (const dx of [-2, 9]) for (const dy of [2, 5]) g.fillRect(x + dx + (f && dx < 0 ? -1 : 0), y + dy + (f ? 1 : 0), 3, 1);
106
- g.fillStyle = dark; g.fillRect(x, y, 10, 8); g.fillStyle = body; g.fillRect(x + 1, y + 1, 8, 6); g.fillStyle = dark; g.fillRect(x + 4, y + 1, 2, 6);
107
- g.fillStyle = '#1b1622'; g.fillRect(x + 2, y - 2, 6, 3); g.fillRect(x + 2, y - 4, 1, 2); g.fillRect(x + 7, y - 4, 1, 2);
108
- if (gold) { g.fillStyle = '#ffffff'; g.fillRect(x + 2, y + 2, 1, 1); }
110
+ x = Math.round(x); y = Math.round(y);
111
+ const body = gold ? '#ffd84d' : '#63c74d', dark = gold ? '#b07d2a' : '#2f7a3b', leg = '#c0cbdc';
112
+ g.fillStyle = leg; for (const [dx, dy] of [[-3, 3], [-3, 6], [11, 3], [11, 6]]) g.fillRect(x + dx + (f ? (dx < 0 ? -1 : 1) : 0), y + dy + (f ? 1 : 0), 4, 1); // legs wiggle
113
+ g.fillStyle = '#0b0a0e'; g.fillRect(x - 1, y - 1, 13, 11);
114
+ g.fillStyle = dark; g.fillRect(x, y, 11, 9); g.fillStyle = body; g.fillRect(x + 1, y + 1, 4, 7); g.fillRect(x + 6, y + 1, 4, 7); // shell, split down the middle
115
+ g.fillStyle = '#ffffff'; g.fillRect(x + 2, y + 2, 1, 2); g.fillRect(x + 7, y + 2, 1, 2);
116
+ g.fillStyle = '#3a4058'; g.fillRect(x + 2, y - 4, 7, 4); g.fillStyle = '#ffffff'; g.fillRect(x + 3, y - 3, 1, 1); g.fillRect(x + 7, y - 3, 1, 1); // head and eyes
117
+ g.fillStyle = leg; g.fillRect(x + 2, y - 7, 1, 3); g.fillRect(x + 8, y - 7, 1, 3); // antennae
109
118
  }
110
119
  function startArcade() {
111
120
  const cv2 = arcadeEl.querySelector('.arcade-cv'); if (!cv2) return;
@@ -129,7 +138,7 @@ function tickArcade(now) {
129
138
  } else if (b.y > AH - 8) { b.miss = true; G.lives--; G.combo = 0; }
130
139
  }
131
140
  G.bugs = G.bugs.filter(b => !b.hit && !b.miss);
132
- for (const s of G.sparks) { s.x += s.vx * dt; s.y += s.vy * dt; s.vy += 80 * dt; s.life -= dt; g.fillStyle = s.c; if (s.txt) { g.font = 'bold 9px monospace'; g.fillText(s.txt, s.x, s.y); } else g.fillRect(s.x, s.y, 2, 2); }
141
+ for (const s of G.sparks) { s.x += s.vx * dt; s.y += s.vy * dt; s.vy += 80 * dt; s.life -= dt; if (s.txt) gText(g, s.x, s.y - 6, s.txt, s.c); else { g.fillStyle = s.c; g.fillRect(Math.round(s.x), Math.round(s.y), 2, 2); } }
133
142
  G.sparks = G.sparks.filter(s => s.life > 0);
134
143
  // the laptop paddle
135
144
  const px = Math.round(G.x), py = AH - 18;
@@ -137,14 +146,14 @@ function tickArcade(now) {
137
146
  g.fillStyle = '#8b9bb4'; g.fillRect(px, py + 2, 28, 4); g.fillStyle = '#d97757'; g.fillRect(px + 11, py - 4, 6, 2);
138
147
  // HUD: hearts, score, combo
139
148
  for (let i = 0; i < 3; i++) { g.fillStyle = i < G.lives ? '#e43b44' : '#3a3448'; g.fillRect(6 + i * 10, 6, 3, 2); g.fillRect(10 + i * 10, 6, 3, 2); g.fillRect(6 + i * 10, 8, 7, 2); g.fillRect(7 + i * 10, 10, 5, 1); g.fillRect(8 + i * 10, 11, 3, 1); }
140
- g.fillStyle = '#ffffff'; g.font = 'bold 10px monospace'; g.textAlign = 'right'; g.fillText(String(G.score), AW - 6, 14); g.textAlign = 'left';
141
- if (G.combo >= 5) { g.fillStyle = '#ff6ec7'; g.font = 'bold 9px monospace'; g.fillText('COMBO x2', AW / 2 - 22, 14); }
149
+ gText(g, AW - 6 - String(G.score).length * 8, 5, G.score, '#ffffff', 2);
150
+ if (G.combo >= 5) gText(g, AW / 2, 6, 'COMBO X2', '#ff6ec7', 1, true);
142
151
  const sc = arcadeEl.querySelector('#arcade-score'); if (sc) sc.textContent = `${T.arcade.score}: ${G.score}`;
143
152
  if (G.lives <= 0) {
144
153
  const best = Number(store.get('arcadeBest', 0)) || 0;
145
154
  if (G.score > best) { store.set('arcadeBest', G.score); toast(`<b>${esc(T.arcade.record)}</b>${G.score}`, 'ok'); const be = arcadeEl.querySelector('#arcade-best'); if (be) be.textContent = G.score; }
146
155
  achRecord.arcadeBest = Math.max(achRecord.arcadeBest || 0, G.score); achBump('arcadeGames');
147
- g.fillStyle = 'rgba(0,0,0,.65)'; g.fillRect(0, 56, AW, 46); g.fillStyle = '#ffd84d'; g.font = 'bold 14px monospace'; g.textAlign = 'center'; g.fillText(T.arcade.over, AW / 2, 76); g.fillStyle = '#ffffff'; g.font = '10px monospace'; g.fillText(`${T.arcade.score}: ${G.score}`, AW / 2, 92); g.textAlign = 'left';
156
+ g.fillStyle = 'rgba(0,0,0,.7)'; g.fillRect(0, 56, AW, 46); gText(g, AW / 2, 64, 'GAME OVER', '#ffd84d', 2, true); gText(g, AW / 2, 84, `${G.score}`, '#ffffff', 2, true);
148
157
  game2 = null; return;
149
158
  }
150
159
  requestAnimationFrame(tickArcade);
@@ -2,71 +2,122 @@
2
2
  // ---------------- more places: the terrace, the server room, the elevator panel; weather indoors; photo frames ----------------
3
3
  let viewHits = [];
4
4
 
5
- // ---- the terrace: agents whose turn ended hang out on the roof, under the real sky ----
5
+ // ---- the rooftop: a garden terrace over the city; agents whose turn ended relax here ----
6
+ function skyStops(phase) {
7
+ return phase === 'night' ? ['#0b0f24', '#18204a', '#2a3466'] : phase === 'dawn' ? ['#3a3a7a', '#f77622', '#ffd27a'] : phase === 'dusk' ? ['#2a2a6a', '#b55088', '#f7a23a'] : ['#3a9ad9', '#7fc8f0', '#c8ecff'];
8
+ }
6
9
  function drawTerrace(t) {
7
- const Wt = TROPHY_W, Ht = TROPHY_H, hits = [];
8
- const hr = new Date().getHours() + new Date().getMinutes() / 60, sky = Art.skyFor(styleHour(qs.get('hour') ? Number(qs.get('hour')) : hr));
9
- // sky gradient and the skyline
10
- for (let y = 0; y < 170; y++) { const k = y / 170; r(0, y, Wt, 1, k < .5 ? sky.top : sky.bot); }
11
- if (sky.phase === 'night') for (let i = 0; i < 60; i++) { const h = hash('ts' + i); r(h % Wt, (h >>> 9) % 120, 1, 1, ((t / 800 + i) | 0) % 5 ? '#ffffff' : '#8b9bb4'); }
12
- else if (sky.phase === 'day') { r(Wt - 90, 26, 18, 18, '#ffe08a'); r(Wt - 86, 22, 10, 26, '#ffe08a'); }
13
- else { r(70, 120, 40, 20, '#ffd27a'); r(76, 114, 28, 6, '#ffd27a'); }
14
- for (let x = 0; x < Wt; x += 14) {
15
- const h = 30 + hash('sky' + x) % 70, c = sky.phase === 'night' ? '#141629' : sky.phase === 'day' ? '#7f93ad' : '#5a3a5a';
16
- r(x, 170 - h, 13, h, c);
17
- for (let yy = 170 - h + 4; yy < 166; yy += 6) for (let xx = 2; xx < 12; xx += 4) if (sky.phase !== 'day' && hash(x + ':' + yy + ':' + xx + ((t / 9000) | 0)) % 3 === 0) r(x + xx, yy, 2, 2, '#feae34');
18
- }
19
- // the deck: wooden planks, a railing, string lights, plants, a grill, a telescope, lounge chairs
20
- for (let y = 170; y < Ht; y += 8) { r(0, y, Wt, 8, (y / 8) % 2 ? '#9c6a44' : '#a8754c'); for (let x = (y * 7) % 40; x < Wt; x += 40) r(x, y, 1, 8, '#7a5236'); }
21
- r(0, 166, Wt, 6, '#5a3a28'); for (let x = 0; x < Wt; x += 12) r(x, 150, 2, 18, '#5a3a28'); r(0, 150, Wt, 3, '#6b4a33');
22
- for (let x = 6, i = 0; x < Wt; x += 16, i++) { const y = 14 + Math.round(Math.abs(Math.sin(x / 60)) * 12); r(x, y, 1, 1, '#2a1d16'); r(x, y + 1, 3, 3, ((t / 400 + i) | 0) % 4 ? ['#ffd84d', '#ff6ec7', '#2ce8f5'][i % 3] : '#5a4a3a'); }
23
- for (let x = 0; x < Wt; x++) r(x, 14 + Math.round(Math.abs(Math.sin(x / 60)) * 12), 1, 1, '#2a1d16');
24
- for (const px of [18, Wt - 34]) { Art.drawPlant(px, 186, true); Art.drawPlant(px, 300, true); }
25
- // grill with smoke
26
- r(Wt - 120, 196, 30, 16, PAL.ink); r(Wt - 119, 197, 28, 6, '#3a3a40'); r(Wt - 117, 198, 24, 1, '#e43b44'); r(Wt - 112, 212, 2, 14, PAL.ink); r(Wt - 100, 212, 2, 14, PAL.ink);
27
- for (let i = 0; i < 3; i++) { const k = ((t / 1400 + i / 3) % 1); r(Wt - 108 + Math.round(Math.sin(k * 6 + i) * 4), 192 - Math.round(k * 26), 3, 3, `rgba(220,220,230,${(.5 - k * .5).toFixed(2)})`); }
28
- // telescope
29
- r(110, 200, 2, 22, PAL.ink); r(104, 222, 14, 2, PAL.ink); r(104, 196, 18, 5, '#c0cbdc'); r(120, 195, 4, 7, '#8b9bb4');
30
- // the people: agents whose turn ended (measured), relaxing on lounge chairs
31
- const relaxing = (data ? data.people : []).filter(p => !p.leaving && p.state === 'idle').slice(0, 8);
10
+ const Wt = TROPHY_W, Ht = TROPHY_H, hits = [], HZ = 170;
11
+ const hr = new Date().getHours() + new Date().getMinutes() / 60, sky = Art.skyFor(styleHour(qs.get('hour') ? Number(qs.get('hour')) : hr)), night = sky.phase === 'night', day = sky.phase === 'day';
12
+ // sky: three-stop gradient, sun or moon, stars, clouds, a plane, birds
13
+ const st = skyStops(sky.phase);
14
+ for (let y = 0; y < HZ; y++) { const k = y / HZ, c = k < .55 ? st[0] : k < .85 ? st[1] : st[2]; r(0, y, Wt, 1, c); }
15
+ for (let y = 0; y < HZ; y += 2) { const k = y / HZ; ctx.fillStyle = `rgba(255,255,255,${(k * .06).toFixed(3)})`; ctx.fillRect(0, y, Wt, 2); }
16
+ if (night) { for (let i = 0; i < 90; i++) { const h = hash('ts' + i); r(h % Wt, (h >>> 9) % 130, 1, 1, ((t / 700 + i) | 0) % 6 ? '#ffffff' : '#6b7a9c'); }
17
+ const mx = Wt - 110, my = 34; ctx.save(); ctx.globalCompositeOperation = 'lighter'; const g = ctx.createRadialGradient(mx, my, 0, mx, my, 40); g.addColorStop(0, 'rgba(255,250,220,.25)'); g.addColorStop(1, 'rgba(255,250,220,0)'); ctx.fillStyle = g; ctx.fillRect(mx - 40, my - 40, 80, 80); ctx.restore();
18
+ r(mx - 8, my - 8, 16, 16, '#f4ecd8'); r(mx - 10, my - 5, 20, 10, '#f4ecd8'); r(mx - 3, my - 4, 3, 3, '#d8cfb8'); r(mx + 3, my + 2, 2, 2, '#d8cfb8'); }
19
+ else if (day) { const sx = Wt - 120, sy = 40; ctx.save(); ctx.globalCompositeOperation = 'lighter'; const g = ctx.createRadialGradient(sx, sy, 0, sx, sy, 60); g.addColorStop(0, 'rgba(255,240,180,.5)'); g.addColorStop(1, 'rgba(255,240,180,0)'); ctx.fillStyle = g; ctx.fillRect(sx - 60, sy - 60, 120, 120); ctx.restore(); r(sx - 10, sy - 10, 20, 20, '#fff2b0'); r(sx - 12, sy - 6, 24, 12, '#fff2b0'); }
20
+ else { const sx = sky.phase === 'dawn' ? 120 : Wt - 140; r(sx - 18, HZ - 26, 36, 16, '#ffd27a'); r(sx - 14, HZ - 32, 28, 6, '#ffd27a'); r(sx - 8, HZ - 36, 16, 4, '#ffd27a'); }
21
+ if (!night) for (let i = 0; i < 4; i++) { const cx = ((t / (90 + i * 30) + i * 180) % (Wt + 120)) - 60, cy = 20 + i * 22, c = day ? '#ffffff' : '#f4c0d0'; r(cx, cy, 40, 8, c); r(cx + 8, cy - 5, 22, 6, c); r(cx + 26, cy - 3, 12, 4, c); ctx.fillStyle = 'rgba(0,0,0,.06)'; ctx.fillRect(cx, cy + 6, 40, 2); }
22
+ { const px = ((t / 25) % (Wt + 80)) - 40, py = 52; r(px, py, 10, 2, night ? '#8b9bb4' : '#ffffff'); r(px + 3, py - 2, 3, 6, night ? '#8b9bb4' : '#ffffff'); if (((t / 400) | 0) % 2) r(px + 10, py, 1, 1, '#e43b44'); }
23
+ if (day) for (let i = 0; i < 3; i++) { const bx = ((t / 18 + i * 40) % (Wt + 40)) - 20, by = 70 + i * 6 + Math.round(Math.sin(t / 300 + i) * 2), w = ((t / 150 + i) | 0) % 2; r(bx, by, 1, 1, '#2a2a3a'); r(bx - 2, by - w, 2, 1, '#2a2a3a'); r(bx + 1, by - w, 2, 1, '#2a2a3a'); }
24
+ // the city in three layers: far (faded), middle, near (detailed: windows, water tanks, antennas)
25
+ const layer = (seed, base, minH, varH, color, win, step, detail) => {
26
+ for (let x = -6; x < Wt; x += step) {
27
+ const h = minH + hash(seed + x) % varH, w = step - 1 - hash(seed + 'w' + x) % 3, top = base - h;
28
+ r(x, top, w, h, color);
29
+ if (detail) { if (hash(seed + 'a' + x) % 4 === 0) { r(x + w / 2, top - 10, 1, 10, color); if (((t / 600 + x) | 0) % 2) r(x + w / 2, top - 11, 1, 1, '#ff4d57'); } if (hash(seed + 't' + x) % 5 === 0) { r(x + 3, top - 6, 7, 6, color); r(x + 4, top - 8, 5, 2, color); } }
30
+ if (win) for (let yy = top + 3; yy < base - 2; yy += 4) for (let xx = 2; xx < w - 2; xx += 3) { const lit = night ? hash(seed + x + ':' + yy + ':' + xx + ((t / 12000) | 0)) % 3 === 0 : day ? hash(seed + x + ':' + yy + xx) % 9 === 0 : hash(seed + x + yy + xx) % 4 === 0; if (lit) r(x + xx, yy, 1, 2, night ? '#feae34' : day ? '#bfe0ff' : '#ffd27a'); }
31
+ }
32
+ };
33
+ layer('far', HZ, 30, 60, night ? '#1a2045' : day ? '#a9c4dc' : '#7a4a6a', false, 11, false);
34
+ layer('mid', HZ, 20, 70, night ? '#141a38' : day ? '#8aa4bf' : '#5a3a5a', true, 15, true);
35
+ // the parapet and a glass railing
36
+ r(0, HZ, Wt, 10, '#6b5a4a'); r(0, HZ, Wt, 2, '#8a7a6a'); ctx.fillStyle = 'rgba(200,230,255,.18)'; ctx.fillRect(0, HZ - 18, Wt, 18); for (let x = 0; x < Wt; x += 40) r(x, HZ - 18, 2, 18, '#c0cbdc'); r(0, HZ - 19, Wt, 2, '#c0cbdc');
37
+ // the deck: planks with depth, a stone path
38
+ for (let y = HZ + 10; y < Ht; y += 7) { const k = (y - HZ) / (Ht - HZ); r(0, y, Wt, 7, (y / 7 | 0) % 2 ? '#a6754c' : '#b07f54'); r(0, y + 6, Wt, 1, '#8a5e3c'); for (let x = (y * 11) % 50; x < Wt; x += Math.round(50 + k * 20)) r(x, y, 1, 6, '#8a5e3c'); }
39
+ for (let i = 0; i < 10; i++) { const y = HZ + 24 + i * 22, w = 22 + i * 2; r(Wt / 2 - w / 2, y, w, 10, '#c9c0ae'); r(Wt / 2 - w / 2, y, w, 2, '#e0d8c6'); }
40
+ // a pergola with crossing string lights
41
+ for (const px of [40, Wt - 44]) { r(px, 30, 5, HZ + 70, '#6b4a33'); r(px + 1, 30, 1, HZ + 70, '#8a6a4a'); }
42
+ r(40, 28, Wt - 80, 5, '#6b4a33'); r(40, 28, Wt - 80, 1, '#8a6a4a');
43
+ for (const [x0, x1, sag] of [[44, Wt - 44, 30], [44, Wt / 2, 18], [Wt / 2, Wt - 44, 18]]) for (let x = x0, i = 0; x < x1; x += 2, i++) { const k = (x - x0) / (x1 - x0), y = 33 + Math.round(Math.sin(k * Math.PI) * sag); r(x, y, 1, 1, '#2a1d16'); if (i % 7 === 0) { const c = ['#ffd84d', '#ff6ec7', '#2ce8f5', '#ffffff'][(i / 7) % 4 | 0], on = ((t / 500 + i) | 0) % 5; r(x - 1, y + 1, 3, 3, on ? c : '#5a4a3a'); if (on && night) { ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.fillStyle = c + '30'; ctx.fillRect(x - 4, y - 2, 9, 9); ctx.restore(); } } }
44
+ // a plunge pool with shimmering water and loungers
45
+ const poolX = 70, poolY = HZ + 40;
46
+ r(poolX - 3, poolY - 3, 126, 66, '#e8e2d6'); r(poolX, poolY, 120, 60, '#2f8fd0'); r(poolX, poolY, 120, 4, '#1e6fa8');
47
+ for (let i = 0; i < 14; i++) { const h = hash('pw' + i), x = poolX + 4 + h % 112, y = poolY + 6 + (h >>> 8) % 50; if (((t / 300 + i) | 0) % 3) r(x, y, 6, 1, '#9fd3ff'); }
48
+ r(poolX + 110, poolY - 8, 2, 12, '#c0cbdc'); r(poolX + 104, poolY - 8, 8, 2, '#c0cbdc');
49
+ // the bar on the right: counter, bottles, stools, a neon sign
50
+ const bx = Wt - 200, by = HZ + 30;
51
+ r(bx, by + 18, 140, 24, '#5a3a28'); r(bx, by + 16, 140, 4, '#8a6a4a'); r(bx, by + 18, 140, 2, '#3a2418');
52
+ r(bx + 4, by - 14, 132, 26, '#3a2418'); for (let i = 0; i < 14; i++) r(bx + 8 + i * 9, by - 10 + (i % 2) * 2, 4, 10 - (i % 2) * 2, ['#63c74d', '#ffd84d', '#e43b44', '#2ce8f5', '#b55088'][i % 5]);
53
+ r(bx + 36, by - 32, 68, 14, '#1b1d2e'); const on = ((t / 140) | 0) % 37 !== 0; pixText(bx + 42, by - 28, 'ROOF BAR', on ? '#ff6ec7' : '#5a2a48'); if (on) { ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.fillStyle = 'rgba(255,110,199,.12)'; ctx.fillRect(bx + 30, by - 36, 80, 22); ctx.restore(); }
54
+ const stools = [0, 1, 2, 3, 4].map(i => ({ x: bx + 10 + i * 26, y: by + 44 }));
55
+ for (const s2 of stools) { r(s2.x + 2, s2.y + 8, 2, 10, PAL.ink); r(s2.x - 2, s2.y + 6, 10, 3, '#e43b44'); }
56
+ // umbrella tables in the middle
57
+ const tables = [{ x: 220, y: HZ + 110 }, { x: Wt - 290, y: HZ + 110 }, { x: 250, y: HZ + 180 }];
58
+ for (const tb of tables) { r(tb.x, tb.y + 16, 24, 4, '#e8e2d6'); r(tb.x + 11, tb.y - 30, 2, 46, '#c0cbdc'); r(tb.x - 14, tb.y - 36, 52, 8, '#e43b44'); r(tb.x - 8, tb.y - 40, 40, 4, '#e43b44'); for (let k = 0; k < 4; k++) r(tb.x - 14 + k * 13, tb.y - 36, 6, 8, '#ffffff'); }
59
+ // a fire pit (glows at night), potted palms, lanterns
60
+ const fx = Wt / 2 + 80, fy = HZ + 150; r(fx - 14, fy, 28, 10, '#6b6a66'); r(fx - 12, fy + 2, 24, 6, '#3a3a40');
61
+ for (let i = 0; i < 5; i++) { const fl = ((t / 90 + i) | 0) % 3; r(fx - 8 + i * 4, fy - 4 - fl * 2, 3, 6 + fl * 2, i % 2 ? '#feae34' : '#e43b44'); }
62
+ if (!day) { ctx.save(); ctx.globalCompositeOperation = 'lighter'; const g = ctx.createRadialGradient(fx, fy, 0, fx, fy, 50); g.addColorStop(0, 'rgba(255,150,60,.35)'); g.addColorStop(1, 'rgba(255,150,60,0)'); ctx.fillStyle = g; ctx.fillRect(fx - 50, fy - 50, 100, 100); ctx.restore(); }
63
+ for (const [px, py] of [[16, HZ + 20], [Wt - 30, HZ + 20], [16, Ht - 60], [Wt - 30, Ht - 60]]) { r(px, py + 20, 14, 12, '#c96f4a'); for (let k = 0; k < 5; k++) r(px + 6 - k * 3, py + 6 - k, 2, 16 + k, '#3a8f46'), r(px + 6 + k * 3, py + 6 - k, 2, 16 + k, '#3a8f46'); r(px + 6, py, 2, 22, '#6b4a33'); }
64
+ // the people: agents whose turn ended (measured), at the bar, the tables and the pool
65
+ const relaxing = (data ? data.people : []).filter(p => !p.leaving && p.state === 'idle').slice(0, 11);
66
+ const seats = [...stools.map(s2 => ({ x: s2.x - 4, y: s2.y - 14, pose: 'back' })), ...tables.flatMap(tb => [{ x: tb.x - 14, y: tb.y - 4, pose: 'front' }, { x: tb.x + 22, y: tb.y - 4, pose: 'front' }]), { x: poolX + 20, y: poolY + 66, pose: 'front' }, { x: poolX + 70, y: poolY + 66, pose: 'front' }];
32
67
  relaxing.forEach((p, i) => {
33
- const x = 170 + (i % 4) * 80, y = 230 + Math.floor(i / 4) * 70;
34
- r(x - 6, y + 16, 30, 8, '#e8e2d6'); r(x - 6, y + 16, 30, 2, '#ffffff'); r(x - 6, y + 24, 2, 6, PAL.ink); r(x + 22, y + 24, 2, 6, PAL.ink); // lounge chair
35
- Art.drawFront(x, y - 2, look(p.id), t, { legs: Art.LEGS_SIT, legsKey: 'sit' });
36
- r(x + 18, y + 8, 6, 6, PAL.ink); r(x + 19, y + 9, 4, 4, '#ff6ec7'); r(x + 20, y + 6, 1, 3, '#63c74d'); // a drink
37
- hits.push({ x: x - 6, y: y - 4, w: 30, h: 34, id: p.id, title: `${p.name} · ${p.title || ''}` });
68
+ const s2 = seats[i]; if (!s2) return;
69
+ if (s2.pose === 'back') Art.drawSeatedBack(s2.x, s2.y, look(p.id), t, {}); else Art.drawFront(s2.x, s2.y, look(p.id), t, { legs: Art.LEGS_SIT, legsKey: 'sit' });
70
+ hits.push({ x: s2.x - 2, y: s2.y - 4, w: 20, h: 32, id: p.id, title: `${p.name} · ${p.title || ''}` });
38
71
  });
39
- if (!relaxing.length) { r(Wt / 2 - 70, 250, 140, 14, '#00000044'); pixText(Wt / 2 - 52, 254, 'NOBODY ON BREAK', '#f4ecd8'); }
40
- r(Wt / 2 - 40, Ht - 22, 80, 14, PAL.ink); r(Wt / 2 - 39, Ht - 21, 78, 12, '#d9a441'); pixText(Wt / 2 - 26, Ht - 18, 'ROOFTOP', '#3b2418');
72
+ if (!relaxing.length) { r(Wt / 2 - 66, Ht - 40, 132, 14, '#00000055'); pixText(Wt / 2 - 58, Ht - 36, lang === 'pt' ? 'NINGUEM DE FOLGA' : 'NOBODY ON BREAK', '#f4ecd8'); }
73
+ if (!day) { ctx.fillStyle = 'rgba(10,12,40,.25)'; ctx.fillRect(0, HZ, Wt, Ht - HZ); } // evening light on the deck
41
74
  viewHits = hits;
42
75
  }
43
76
 
44
- // ---- the server room: one rack per open session, its LEDs blinking with the session's real state ----
77
+ // ---- the server room: two rows of glass racks, a NOC wall with live numbers, cold aisle, cable trays ----
45
78
  function drawServers(t) {
46
- const Wt = TROPHY_W, Ht = TROPHY_H, hits = [];
47
- r(0, 0, Wt, Ht, '#11131c');
48
- for (let y = 0; y < Ht; y += 20) for (let x = 0; x < Wt; x += 20) { r(x, y, 20, 1, '#1a1d2a'); r(x, y, 1, 20, '#1a1d2a'); } // raised floor tiles
49
- for (let x = 30; x < Wt; x += 120) { ctx.save(); ctx.globalCompositeOperation = 'lighter'; const g = ctx.createRadialGradient(x, 0, 0, x, 0, 120); g.addColorStop(0, 'rgba(60,140,255,.18)'); g.addColorStop(1, 'rgba(60,140,255,0)'); ctx.fillStyle = g; ctx.fillRect(x - 120, 0, 240, 140); ctx.restore(); }
50
- const people = (data ? data.people : []).filter(p => !p.leaving).slice(0, 18);
51
- const ledOf = s => s === 'needs_you' ? '#ff4d57' : s === 'waiting' ? '#feae34' : s === 'asleep' ? '#2a3050' : s === 'idle' ? '#3b5dc9' : '#62ff7a';
52
- people.forEach((p, i) => {
53
- const col = i % 6, row = Math.floor(i / 6), x = 30 + col * 100, y = 40 + row * 116;
54
- r(x - 2, y - 2, 64, 96, PAL.ink); r(x, y, 60, 92, '#2a2f45'); r(x, y, 60, 4, agentOf(p).color);
55
- const busy = !['idle', 'asleep'].includes(p.state), speed = p.state === 'asleep' ? 0 : busy ? 90 : 700;
56
- for (let u = 0; u < 8; u++) {
57
- const uy = y + 8 + u * 10; r(x + 4, uy, 52, 8, '#1b1d2e'); r(x + 4, uy, 52, 1, '#3a4058');
58
- for (let l = 0; l < 6; l++) { const on = speed && hash(p.id + u + ':' + l + ':' + ((t / speed) | 0)) % 3 !== 0; r(x + 8 + l * 4, uy + 3, 2, 2, on ? ledOf(p.state) : '#20243a'); }
59
- r(x + 38, uy + 2, 14, 4, '#11131c');
60
- }
61
- // a throughput bar: output tokens today (measured), relative to the busiest session
62
- const out = (p.today && p.today.outTokens) || 0, max = Math.max(1, ...people.map(q => (q.today && q.today.outTokens) || 0));
63
- r(x + 62, y + 92 - Math.round(out / max * 88), 3, Math.round(out / max * 88), ledOf(p.state));
64
- const nm = String(p.name).toUpperCase().replace(/[^A-Z0-9\-]/g, '').slice(0, 12);
65
- r(x - 2, y + 96, 64, 9, '#1b1d2e'); pixText(x + 30 - nm.length * 2, y + 98, nm, '#c0cbdc');
66
- hits.push({ x: x - 2, y: y - 2, w: 68, h: 108, id: p.id, title: `${p.name} · ${T.states[p.state] || p.state} · ${fmtK(out)} ${T.usage.outShort}` });
67
- });
68
- if (!people.length) pixText(Wt / 2 - 30, Ht / 2, 'NO SESSIONS', '#c0cbdc');
69
- r(Wt / 2 - 44, Ht - 20, 88, 14, PAL.ink); r(Wt / 2 - 43, Ht - 19, 86, 12, '#3b5dc9'); pixText(Wt / 2 - 30, Ht - 16, 'SERVER ROOM', '#ffffff');
79
+ const Wt = TROPHY_W, Ht = TROPHY_H, hits = [], people = (data ? data.people : []).filter(p => !p.leaving);
80
+ const ledOf = s => s === 'needs_you' ? '#ff4d57' : s === 'waiting' ? '#feae34' : s === 'asleep' ? '#2a3050' : s === 'idle' ? '#3b8bff' : '#62ff7a';
81
+ // room: dark walls with acoustic panels, a raised floor with vents, blue strip lights
82
+ r(0, 0, Wt, 140, '#151a28'); for (let x = 0; x < Wt; x += 32) { r(x + 2, 8, 28, 120, '#1a2032'); r(x + 2, 8, 28, 1, '#232a40'); }
83
+ r(0, 138, Wt, 3, '#2ce8f5'); ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.fillStyle = 'rgba(44,232,245,.08)'; ctx.fillRect(0, 128, Wt, 24); ctx.restore();
84
+ for (let y = 141; y < Ht; y += 18) for (let x = 0; x < Wt; x += 18) { r(x, y, 18, 18, (x + y) % 36 ? '#1c2132' : '#20263a'); r(x, y, 18, 1, '#2a3148'); r(x, y, 1, 18, '#2a3148'); }
85
+ for (let x = 96; x < Wt - 96; x += 36) { r(x, 268, 18, 18, '#11141f'); for (let k = 0; k < 4; k++) r(x + 2, 270 + k * 4, 14, 1, '#2a3148'); } // cold-aisle vents
86
+ for (let i = 0; i < 12; i++) { const h = hash('mist' + i), k = ((t / 2200 + i / 12) % 1); r(100 + h % (Wt - 200), 280 - Math.round(k * 40), 2, 1, `rgba(180,230,255,${(.35 - k * .35).toFixed(2)})`); }
87
+ // cable trays overhead with cables dropping to the racks
88
+ r(0, 22, Wt, 4, '#3a4058'); r(0, 22, Wt, 1, '#5a6280');
89
+ // racks: a back row (smaller) and a front row (bigger); each rack is a session
90
+ const rack = (p, x, y, w, h, i) => {
91
+ const busy = !['idle', 'asleep'].includes(p.state), speed = p.state === 'asleep' ? 0 : busy ? 80 : 600, c = ledOf(p.state);
92
+ r(x - 2, y - 2, w + 4, h + 4, '#0b0a0e'); r(x, y, w, h, '#232a40'); r(x, y, w, 3, agentOf(p).color);
93
+ const units = Math.floor((h - 10) / 7);
94
+ for (let u = 0; u < units; u++) { const uy = y + 6 + u * 7; r(x + 3, uy, w - 6, 5, '#141826'); r(x + 3, uy, w - 6, 1, '#2a3148'); for (let l = 0; l < Math.floor((w - 16) / 3); l++) { const lit = speed && hash(p.id + u + ':' + l + ':' + ((t / speed) | 0)) % 3 !== 0; r(x + 5 + l * 3, uy + 2, 2, 2, lit ? c : '#1e2236'); } }
95
+ ctx.fillStyle = 'rgba(200,230,255,.06)'; ctx.fillRect(x, y, w, h); ctx.fillStyle = 'rgba(255,255,255,.08)'; ctx.beginPath(); ctx.moveTo(x + w * .2, y); ctx.lineTo(x + w * .35, y); ctx.lineTo(x + w * .15, y + h); ctx.lineTo(x, y + h); ctx.fill(); // glass door
96
+ const blink = p.state === 'needs_you' ? ((t / 300) | 0) % 2 : 1; r(x + w / 2 - 2, y - 6, 4, 4, blink ? c : '#2a3050'); if (blink && p.state !== 'asleep') { ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.fillStyle = c + '33'; ctx.fillRect(x + w / 2 - 8, y - 12, 16, 16); ctx.restore(); }
97
+ r(x + w / 2, 26, 1, y - 32, ['#e43b44', '#ffd84d', '#3b5dc9', '#63c74d'][i % 4]); // cable from the tray
98
+ const nm = String(p.name).toUpperCase().replace(/[^A-Z0-9\-]/g, '').slice(0, Math.floor(w / 4));
99
+ r(x, y + h + 3, w, 8, '#11141f'); pixText(x + w / 2 - nm.length * 2, y + h + 5, nm, '#c0cbdc');
100
+ hits.push({ x: x - 2, y: y - 8, w: w + 4, h: h + 20, id: p.id, title: `${p.name} · ${T.states[p.state] || p.state} · ${fmtK((p.today && p.today.outTokens) || 0)} ${T.usage.outShort}` });
101
+ };
102
+ const back = people.slice(0, 8), front = people.slice(8, 14);
103
+ back.forEach((p, i) => rack(p, 40 + i * 72, 118, 44, 80, i));
104
+ front.forEach((p, i) => rack(p, 30 + i * 100, 238, 60, 110, i + 8));
105
+ // the NOC wall: three big screens with live numbers
106
+ const working = people.filter(p => !['idle', 'asleep', 'needs_you', 'waiting'].includes(p.state)).length, need = people.filter(p => p.state === 'needs_you' || p.state === 'waiting');
107
+ const scr = (x, w) => { r(x - 3, 34, w + 6, 66, '#0b0a0e'); r(x, 37, w, 60, '#071018'); ctx.fillStyle = 'rgba(44,232,245,.06)'; for (let y = 37; y < 97; y += 2) ctx.fillRect(x, y, w, 1); };
108
+ scr(170, 90); scr(275, 90); scr(380, 90);
109
+ pixText(176, 41, 'SESSIONS', '#2ce8f5'); gText(ctx, 176, 52, people.length, '#62ff7a', 4); pixText(176, 86, `${working} WORKING`, '#62ff7a');
110
+ pixText(281, 41, 'OUTPUT TODAY', '#2ce8f5');
111
+ { const top = people.slice().sort((a, b) => ((b.today && b.today.outTokens) || 0) - ((a.today && a.today.outTokens) || 0)).slice(0, 8), max = Math.max(1, ...top.map(p => (p.today && p.today.outTokens) || 0));
112
+ top.forEach((p, i) => { const h = Math.max(1, Math.round(((p.today && p.today.outTokens) || 0) / max * 38)); r(281 + i * 10, 92 - h, 7, h, agentOf(p).color); }); }
113
+ pixText(386, 41, 'ALERTS', need.length ? '#ff4d57' : '#2ce8f5');
114
+ if (need.length) need.slice(0, 5).forEach((p, i) => { if (((t / 500) | 0) % 2 || i) r(386, 52 + i * 8, 3, 3, '#ff4d57'); pixText(392, 52 + i * 8, String(p.name).toUpperCase().replace(/[^A-Z0-9\-]/g, '').slice(0, 13), '#ffb0b0'); });
115
+ else { pixText(386, 58, 'ALL SYSTEMS', '#62ff7a'); pixText(386, 66, 'NOMINAL', '#62ff7a'); }
116
+ // a sysadmin desk, a fire extinguisher, a warning stripe
117
+ r(Wt - 110, 300, 80, 6, '#3a4058'); r(Wt - 104, 306, 3, 30, '#2a3148'); r(Wt - 40, 306, 3, 30, '#2a3148'); r(Wt - 96, 286, 24, 14, '#0b0a0e'); r(Wt - 94, 288, 20, 10, '#2ce8f5'); r(Wt - 64, 294, 6, 6, '#e8e2d6');
118
+ r(14, 160, 8, 18, '#e43b44'); r(15, 156, 6, 4, '#2a2a30'); r(16, 164, 4, 6, '#ffffff');
119
+ for (let x = 0; x < Wt; x += 16) r(x, Ht - 6, 8, 6, '#ffd84d'), r(x + 8, Ht - 6, 8, 6, '#1b1622');
120
+ if (!people.length) pixText(Wt / 2 - 30, 200, 'NO SESSIONS', '#c0cbdc');
70
121
  viewHits = hits;
71
122
  }
72
123