ai-control-center 1.15.2

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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +584 -0
  3. package/bin/aicc.js +772 -0
  4. package/lib/actions/approve.js +71 -0
  5. package/lib/actions/assign-project.js +132 -0
  6. package/lib/actions/browser-test.js +64 -0
  7. package/lib/actions/cleanup.js +174 -0
  8. package/lib/actions/debug.js +298 -0
  9. package/lib/actions/deploy.js +1229 -0
  10. package/lib/actions/fix-bug.js +134 -0
  11. package/lib/actions/new-feature.js +255 -0
  12. package/lib/actions/reject.js +307 -0
  13. package/lib/actions/review.js +706 -0
  14. package/lib/actions/status.js +47 -0
  15. package/lib/agents/browser-qa-agent.js +611 -0
  16. package/lib/agents/payment-agent.js +116 -0
  17. package/lib/agents/suggestion-agent.js +88 -0
  18. package/lib/cli.js +303 -0
  19. package/lib/config.js +243 -0
  20. package/lib/hub/hub-server.js +440 -0
  21. package/lib/hub/project-poller.js +75 -0
  22. package/lib/hub/skill-registry.js +89 -0
  23. package/lib/hub/state-aggregator.js +204 -0
  24. package/lib/index.js +471 -0
  25. package/lib/init/doctor.js +523 -0
  26. package/lib/init/presets.js +222 -0
  27. package/lib/init/skill-fetcher.js +77 -0
  28. package/lib/init/wizard.js +973 -0
  29. package/lib/integrations/codex-runner.js +128 -0
  30. package/lib/integrations/github-actions.js +248 -0
  31. package/lib/integrations/github-reporter.js +229 -0
  32. package/lib/integrations/screenshot-store.js +102 -0
  33. package/lib/openclaw/bridge.js +650 -0
  34. package/lib/openclaw/generate-skill.js +235 -0
  35. package/lib/openclaw/openclaw.json +64 -0
  36. package/lib/orchestrator/autonomous-loop.js +429 -0
  37. package/lib/orchestrator/thread-triggers.js +63 -0
  38. package/lib/roleplay/agent-messenger.js +75 -0
  39. package/lib/roleplay/discussion-threads.js +303 -0
  40. package/lib/roleplay/health-monitor.js +121 -0
  41. package/lib/roleplay/pm-agent.js +513 -0
  42. package/lib/roleplay/roleplay-config.js +25 -0
  43. package/lib/roleplay/room.js +164 -0
  44. package/lib/shared/action-runner.js +2330 -0
  45. package/lib/shared/event-bus.js +185 -0
  46. package/lib/slack/bot.js +378 -0
  47. package/lib/telegram/bot.js +416 -0
  48. package/lib/telegram/commands.js +1267 -0
  49. package/lib/telegram/keyboards.js +113 -0
  50. package/lib/telegram/notifications.js +247 -0
  51. package/lib/twitch/bot.js +354 -0
  52. package/lib/twitch/commands.js +302 -0
  53. package/lib/twitch/notifications.js +63 -0
  54. package/lib/utils/achievements.js +191 -0
  55. package/lib/utils/activity-log.js +182 -0
  56. package/lib/utils/agent-leaderboard.js +119 -0
  57. package/lib/utils/audit-logger.js +232 -0
  58. package/lib/utils/codebase-context.js +288 -0
  59. package/lib/utils/codebase-indexer.js +381 -0
  60. package/lib/utils/config-schema.js +230 -0
  61. package/lib/utils/context-compressor.js +172 -0
  62. package/lib/utils/correlation.js +63 -0
  63. package/lib/utils/cost-tracker.js +423 -0
  64. package/lib/utils/cron-scheduler.js +53 -0
  65. package/lib/utils/db-adapter.js +293 -0
  66. package/lib/utils/display.js +272 -0
  67. package/lib/utils/errors.js +116 -0
  68. package/lib/utils/format.js +134 -0
  69. package/lib/utils/intent-engine.js +464 -0
  70. package/lib/utils/mcp-client.js +238 -0
  71. package/lib/utils/model-ab-test.js +164 -0
  72. package/lib/utils/notify.js +122 -0
  73. package/lib/utils/persona-loader.js +80 -0
  74. package/lib/utils/pipeline-lock.js +73 -0
  75. package/lib/utils/pipeline.js +214 -0
  76. package/lib/utils/plugin-runner.js +234 -0
  77. package/lib/utils/rate-limiter.js +84 -0
  78. package/lib/utils/rbac.js +74 -0
  79. package/lib/utils/runner.js +1809 -0
  80. package/lib/utils/security.js +191 -0
  81. package/lib/utils/self-healer.js +144 -0
  82. package/lib/utils/skill-loader.js +255 -0
  83. package/lib/utils/spinner.js +132 -0
  84. package/lib/utils/stage-queue.js +50 -0
  85. package/lib/utils/state-machine.js +89 -0
  86. package/lib/utils/status-bar.js +327 -0
  87. package/lib/utils/token-estimator.js +101 -0
  88. package/lib/utils/ux-analyzer.js +101 -0
  89. package/lib/utils/webhook-emitter.js +83 -0
  90. package/lib/web/public/css/styles.css +417 -0
  91. package/lib/web/public/dark-mode.js +44 -0
  92. package/lib/web/public/hub/kanban.html +206 -0
  93. package/lib/web/public/index.html +45 -0
  94. package/lib/web/public/js/app.js +71 -0
  95. package/lib/web/public/js/ask.js +110 -0
  96. package/lib/web/public/js/dashboard.js +165 -0
  97. package/lib/web/public/js/deploy.js +72 -0
  98. package/lib/web/public/js/feature.js +79 -0
  99. package/lib/web/public/js/health.js +65 -0
  100. package/lib/web/public/js/logs.js +93 -0
  101. package/lib/web/public/js/review.js +123 -0
  102. package/lib/web/public/js/ws-client.js +82 -0
  103. package/lib/web/public/office/css/office.css +678 -0
  104. package/lib/web/public/office/index.html +148 -0
  105. package/lib/web/public/office/js/achievements-ui.js +117 -0
  106. package/lib/web/public/office/js/character.js +1056 -0
  107. package/lib/web/public/office/js/chat-bubbles.js +177 -0
  108. package/lib/web/public/office/js/cost-overlay.js +123 -0
  109. package/lib/web/public/office/js/day-night.js +68 -0
  110. package/lib/web/public/office/js/effects.js +632 -0
  111. package/lib/web/public/office/js/engine.js +146 -0
  112. package/lib/web/public/office/js/feature-ticket.js +216 -0
  113. package/lib/web/public/office/js/hub-client.js +60 -0
  114. package/lib/web/public/office/js/main.js +1757 -0
  115. package/lib/web/public/office/js/office-layout.js +1524 -0
  116. package/lib/web/public/office/js/pathfinding.js +144 -0
  117. package/lib/web/public/office/js/pixel-sprites.js +1454 -0
  118. package/lib/web/public/office/js/progress-bars.js +117 -0
  119. package/lib/web/public/office/js/replay.js +191 -0
  120. package/lib/web/public/office/js/sound-effects.js +91 -0
  121. package/lib/web/public/office/js/sprite-renderer.js +211 -0
  122. package/lib/web/public/office/js/stamina-system.js +89 -0
  123. package/lib/web/public/office/js/ui.js +107 -0
  124. package/lib/web/public/onboarding/index.html +243 -0
  125. package/lib/web/public/timeline/index.html +195 -0
  126. package/lib/web/routes/api.js +499 -0
  127. package/lib/web/routes/logs.js +20 -0
  128. package/lib/web/routes/metrics.js +99 -0
  129. package/lib/web/server.js +183 -0
  130. package/lib/web/ws/handler.js +65 -0
  131. package/package.json +67 -0
  132. package/templates/agent-architect.md +69 -0
  133. package/templates/agent-gemini-pm.md +49 -0
  134. package/templates/agent-gemini-reviewer.md +52 -0
  135. package/templates/copilot-instructions.md +36 -0
  136. package/templates/pipelines/mobile.json +27 -0
  137. package/templates/pipelines/nodejs-api.json +27 -0
  138. package/templates/pipelines/python.json +27 -0
  139. package/templates/pipelines/react.json +27 -0
  140. package/templates/pipelines/salesforce.json +27 -0
  141. package/templates/role-gemini.md +97 -0
  142. package/templates/skill-architect.md +114 -0
  143. package/templates/skill-browser-qa.md +50 -0
  144. package/templates/skill-bug-from-qa.md +58 -0
  145. package/templates/skill-chatbot.md +93 -0
  146. package/templates/skill-implement.md +78 -0
  147. package/templates/skill-openclaw.md +174 -0
  148. package/templates/skill-payment.md +110 -0
  149. package/templates/skill-pm-spec.md +77 -0
  150. package/templates/skill-requirement-capture.md +97 -0
  151. package/templates/skill-review.md +108 -0
  152. package/templates/skill-reviewer-qa.md +44 -0
  153. package/templates/skill-suggestion.md +45 -0
  154. package/templates/skill-template.md +142 -0
@@ -0,0 +1,1524 @@
1
+ /**
2
+ * Office Layout — pixel art RPG style team zones with furniture.
3
+ * Expanded with warehouse, conveyor belt, break room, gym, and status board.
4
+ */
5
+
6
+ import { getPathfinder } from './pathfinding.js';
7
+ import { renderPixelText, PIXEL_SIZE } from './sprite-renderer.js';
8
+
9
+ const TILE = 32;
10
+ const TEAM_WIDTH = 22; // tiles (wider for new rooms)
11
+ const TEAM_HEIGHT = 22; // tiles (taller for warehouse area)
12
+ const TEAM_GAP = 3;
13
+
14
+ // Workstation positions within a team zone (in tiles)
15
+ const STATIONS = {
16
+ // ── Work area (top section) ──
17
+ statusBoard:{ x: 1, y: 2, w: 6, h: 3, label: 'Status Board' },
18
+ pmDesk: { x: 8, y: 2, w: 3, h: 2, label: 'PM Desk' },
19
+ whiteboard: { x: 12, y: 2, w: 5, h: 3, label: 'Whiteboard' },
20
+ coffee: { x: 18, y: 2, w: 2, h: 2, label: 'Coffee' },
21
+ codingDesk: { x: 1, y: 7, w: 6, h: 2, label: 'Coding Station' },
22
+ reviewTable:{ x: 8, y: 7, w: 3, h: 2, label: 'Review Table' },
23
+ // ── Social area (middle section) ──
24
+ breakRoom: { x: 12, y: 7, w: 4, h: 3, label: 'Break Room' },
25
+ gym: { x: 17, y: 7, w: 4, h: 3, label: 'Gym' },
26
+ // ── Deploy & warehouse (bottom section) ──
27
+ deployDock: { x: 1, y: 11, w: 4, h: 2, label: 'Deploy Dock' },
28
+ conveyor: { x: 5, y: 11, w: 5, h: 2, label: 'Conveyor Belt' },
29
+ warehouse: { x: 10, y: 10, w: 11, h: 5, label: 'Warehouse' },
30
+ // Gossip spots (open floor)
31
+ lounge1: { x: 1, y: 16, w: 3, h: 2, label: 'Lounge' },
32
+ lounge2: { x: 5, y: 16, w: 3, h: 2, label: 'Lounge' },
33
+ };
34
+
35
+ // RPG Pixel Palette
36
+ const PAL = {
37
+ floor1: '#3a3a5a', floor2: '#343454',
38
+ wall: '#4a4a7a', wallDk: '#38385a', wallTop: '#5a5a8a',
39
+ desk: '#8b6914', deskDk: '#6a5010', deskLt: '#a07820',
40
+ monitor: '#1a1a2e', screen: '#40a8f0', screenG: '#40d870',
41
+ wbBg: '#e8e8d8', wbFrame: '#8a8070',
42
+ dockBg: '#4a4a4a', dockYel: '#e8a020',
43
+ whGray: '#5a5a62', whGrayLt: '#6a6a72', whGrayDk: '#4a4a52',
44
+ convBelt: '#606068', convRoller: '#888890',
45
+ pkgTan: '#c8a050', pkgTan2: '#a88030',
46
+ prodGrn: '#2a7a3a', prodDk: '#1a5a2a', prodRf: '#3a9a4a',
47
+ road: '#505058', roadLn: '#707078',
48
+ carpet: '#3a2a4a', shadow: '#2a2a3a',
49
+ breakFloor: '#4a3a2a', breakTable: '#6a5030',
50
+ gymFloor: '#2a3a4a', gymMat: '#3050a0',
51
+ statusBg: '#1a2a1a', statusGreen: '#40d060', statusRed: '#d04040',
52
+ statusYellow: '#d8c020', statusBlue: '#4080d0',
53
+ };
54
+
55
+ export class OfficeLayout {
56
+ constructor() {
57
+ this._teamOrigins = new Map();
58
+ this._projectData = new Map(); // project → {feature, stage, packages}
59
+ this._conveyorOffset = 0; // animated conveyor belt offset
60
+ this._time = 0;
61
+ this._trucks = new Map(); // projectName → truck animation state
62
+ this._exhaustParticles = new Map(); // projectName → [{x,y,life,maxLife,vx,vy}]
63
+ }
64
+
65
+ setupTeams(projectConfigs) {
66
+ this._teamOrigins.clear();
67
+ const startX = 2;
68
+ const startY = 2;
69
+
70
+ projectConfigs.forEach((config, i) => {
71
+ const ox = (startX + i * (TEAM_WIDTH + TEAM_GAP)) * TILE;
72
+ const oy = startY * TILE;
73
+ this._teamOrigins.set(config.name, { x: ox, y: oy, config });
74
+ if (!this._projectData.has(config.name)) {
75
+ this._projectData.set(config.name, {
76
+ feature: '', stage: 'idle',
77
+ packages: [], // shipped features stored in warehouse
78
+ isDeploying: false,
79
+ conveyorOffset: 0,
80
+ conveyorTime: 0,
81
+ });
82
+ }
83
+ });
84
+
85
+ // Build pathfinding grids
86
+ const pf = getPathfinder();
87
+ for (const [name, origin] of this._teamOrigins) {
88
+ const obstacles = [];
89
+ for (const [key, s] of Object.entries(STATIONS)) {
90
+ if (key !== 'lounge1' && key !== 'lounge2') {
91
+ // Pad obstacles by 1 tile so characters walk around furniture, not through it
92
+ obstacles.push({ x: Math.max(0, s.x - 1), y: Math.max(0, s.y - 1), w: s.w + 2, h: s.h + 2 });
93
+ }
94
+ }
95
+ pf.buildGrid(name, { x: origin.x, y: origin.y }, TEAM_WIDTH, TEAM_HEIGHT, obstacles);
96
+ }
97
+ }
98
+
99
+ /** Update project state data for rendering */
100
+ setProjectState(projectName, feature, stage, isDeploying) {
101
+ const data = this._projectData.get(projectName);
102
+ if (!data) return;
103
+ data.feature = feature || '';
104
+ data.stage = stage || 'idle';
105
+ data.isDeploying = isDeploying || false;
106
+ }
107
+
108
+ /** Start the deployer truck driving animation for a project */
109
+ startTruckAnimation(projectName) {
110
+ // Don't start if already running
111
+ if (this._trucks.has(projectName)) return;
112
+ const origin = this._teamOrigins.get(projectName);
113
+ if (!origin) return;
114
+
115
+ // Truck start position: center of deploy dock (in pixels, relative to team origin)
116
+ const dockX = STATIONS.deployDock.x * TILE + (STATIONS.deployDock.w * TILE) / 2 - 16;
117
+ const dockY = STATIONS.deployDock.y * TILE + 8;
118
+ // Warehouse entrance (right edge of conveyor / left edge of warehouse)
119
+ const warehouseX = STATIONS.warehouse.x * TILE - 4;
120
+
121
+ this._trucks.set(projectName, {
122
+ x: dockX,
123
+ y: dockY,
124
+ startX: dockX,
125
+ targetX: warehouseX,
126
+ phase: 'loading',
127
+ timer: 0,
128
+ featureName: this._projectData.get(projectName)?.feature || 'feature',
129
+ packageY: 0, // for unloading animation
130
+ packageAlpha: 1,
131
+ });
132
+ this._exhaustParticles.set(projectName, []);
133
+ }
134
+
135
+ /** Check if a truck animation is active for a project */
136
+ hasTruckAnimation(projectName) {
137
+ return this._trucks.has(projectName);
138
+ }
139
+
140
+ /** Update all active truck animations */
141
+ _updateTrucks(dt) {
142
+ for (const [name, truck] of this._trucks) {
143
+ truck.timer += dt;
144
+ const particles = this._exhaustParticles.get(name) || [];
145
+
146
+ switch (truck.phase) {
147
+ case 'loading':
148
+ // Truck vibrates at dock for 2s
149
+ if (truck.timer >= 2) {
150
+ truck.phase = 'driving';
151
+ truck.timer = 0;
152
+ }
153
+ break;
154
+
155
+ case 'driving': {
156
+ // Drive from dock to warehouse entrance over 4s
157
+ const progress = Math.min(truck.timer / 4, 1);
158
+ // Ease-in-out for smooth movement
159
+ const eased = progress < 0.5
160
+ ? 2 * progress * progress
161
+ : 1 - Math.pow(-2 * progress + 2, 2) / 2;
162
+ truck.x = truck.startX + (truck.targetX - truck.startX) * eased;
163
+
164
+ // Spawn exhaust particles behind the truck (from the left/back side)
165
+ if (Math.random() < 0.4) {
166
+ particles.push({
167
+ x: truck.x - 4,
168
+ y: truck.y + 10 + (Math.random() - 0.5) * 6,
169
+ life: 0,
170
+ maxLife: 0.6 + Math.random() * 0.4,
171
+ vx: -8 - Math.random() * 6,
172
+ vy: -4 - Math.random() * 4,
173
+ size: 2 + Math.random() * 3,
174
+ });
175
+ }
176
+
177
+ if (truck.timer >= 4) {
178
+ truck.phase = 'unloading';
179
+ truck.timer = 0;
180
+ truck.x = truck.targetX;
181
+ }
182
+ break;
183
+ }
184
+
185
+ case 'unloading':
186
+ // Package slides off into warehouse over 2s
187
+ truck.packageY = Math.min(truck.timer / 1.5, 1) * 20;
188
+ truck.packageAlpha = Math.max(1 - truck.timer / 2, 0);
189
+ if (truck.timer >= 2) {
190
+ truck.phase = 'returning';
191
+ truck.timer = 0;
192
+ }
193
+ break;
194
+
195
+ case 'returning': {
196
+ // Drive back to dock over 3s
197
+ const retProgress = Math.min(truck.timer / 3, 1);
198
+ const retEased = retProgress < 0.5
199
+ ? 2 * retProgress * retProgress
200
+ : 1 - Math.pow(-2 * retProgress + 2, 2) / 2;
201
+ truck.x = truck.targetX + (truck.startX - truck.targetX) * retEased;
202
+
203
+ if (truck.timer >= 3) {
204
+ truck.phase = 'done';
205
+ truck.timer = 0;
206
+ }
207
+ break;
208
+ }
209
+
210
+ case 'done':
211
+ this._trucks.delete(name);
212
+ this._exhaustParticles.delete(name);
213
+ continue;
214
+ }
215
+
216
+ // Update exhaust particles
217
+ for (let i = particles.length - 1; i >= 0; i--) {
218
+ const p = particles[i];
219
+ p.life += dt;
220
+ p.x += p.vx * dt;
221
+ p.y += p.vy * dt;
222
+ p.vx *= 0.96;
223
+ p.vy *= 0.96;
224
+ if (p.life >= p.maxLife) {
225
+ particles.splice(i, 1);
226
+ }
227
+ }
228
+ this._exhaustParticles.set(name, particles);
229
+ }
230
+ }
231
+
232
+ /** Draw animated truck and effects for a project */
233
+ _drawAnimatedTruck(ctx, name, ox, oy) {
234
+ const truck = this._trucks.get(name);
235
+ if (!truck) return;
236
+
237
+ const tx = ox + truck.x;
238
+ const ty = ox !== undefined ? oy + truck.y : truck.y;
239
+
240
+ // Draw exhaust smoke particles (behind truck, so draw first)
241
+ const particles = this._exhaustParticles.get(name) || [];
242
+ for (const p of particles) {
243
+ const alpha = 0.5 * (1 - p.life / p.maxLife);
244
+ ctx.save();
245
+ ctx.globalAlpha = alpha;
246
+ ctx.fillStyle = '#888';
247
+ ctx.fillRect(ox + p.x, oy + p.y, p.size, p.size);
248
+ ctx.restore();
249
+ }
250
+
251
+ // Draw the truck at animated position
252
+ const vibrate = truck.phase === 'loading'
253
+ ? Math.sin(this._time * 20) * 1.5
254
+ : 0;
255
+ this._drawPixelTruck(ctx, tx + vibrate, ty, true, this._time);
256
+
257
+ // Draw unloading package
258
+ if (truck.phase === 'unloading') {
259
+ ctx.save();
260
+ ctx.globalAlpha = truck.packageAlpha;
261
+ // Small package box sliding right and down
262
+ const pkgX = tx + 24 + truck.packageY * 0.8;
263
+ const pkgY = ty + 4 + truck.packageY;
264
+ ctx.fillStyle = '#c8a050';
265
+ ctx.fillRect(pkgX, pkgY, 12, 10);
266
+ ctx.fillStyle = '#a88030';
267
+ ctx.fillRect(pkgX, pkgY, 12, 2);
268
+ ctx.fillRect(pkgX + 5, pkgY, 2, 10);
269
+ ctx.fillRect(pkgX, pkgY + 4, 12, 2);
270
+ ctx.restore();
271
+ }
272
+
273
+ // "VROOM" text while driving
274
+ if (truck.phase === 'driving') {
275
+ ctx.save();
276
+ ctx.globalAlpha = 0.7 + Math.sin(this._time * 5) * 0.3;
277
+ renderPixelText(ctx, 'VROOM!', tx + 10, ty - 6, '#ffcc00', 1);
278
+ ctx.restore();
279
+ }
280
+ }
281
+
282
+ /** Add a shipped package to the warehouse */
283
+ addShippedPackage(projectName, featureName) {
284
+ const data = this._projectData.get(projectName);
285
+ if (!data) return;
286
+ data.packages.push({
287
+ name: featureName,
288
+ time: Date.now(),
289
+ });
290
+ if (data.packages.length > 12) data.packages.shift(); // max 12 visible
291
+ }
292
+
293
+ getCharacterPositions(projectName) {
294
+ const origin = this._teamOrigins.get(projectName);
295
+ if (!origin) return {};
296
+ const o = origin;
297
+ return {
298
+ pm: { x: o.x + (STATIONS.pmDesk.x + 1.5) * TILE, y: o.y + (STATIONS.pmDesk.y + 2.2) * TILE },
299
+ arch: { x: o.x + (STATIONS.whiteboard.x + 2.5) * TILE, y: o.y + (STATIONS.whiteboard.y + 3.5) * TILE },
300
+ coder: { x: o.x + (STATIONS.codingDesk.x + 3) * TILE, y: o.y + (STATIONS.codingDesk.y + 2.5) * TILE },
301
+ deployer: { x: o.x + (STATIONS.deployDock.x + 2) * TILE, y: o.y + (STATIONS.deployDock.y + 2.5) * TILE },
302
+ };
303
+ }
304
+
305
+ /** Get positions for idle activities */
306
+ getIdlePositions(projectName) {
307
+ const origin = this._teamOrigins.get(projectName);
308
+ if (!origin) return {};
309
+ const o = origin;
310
+ return {
311
+ breakRoom: { x: o.x + (STATIONS.breakRoom.x + 2) * TILE, y: o.y + (STATIONS.breakRoom.y + 2.5) * TILE },
312
+ gym: { x: o.x + (STATIONS.gym.x + 2) * TILE, y: o.y + (STATIONS.gym.y + 2.5) * TILE },
313
+ coffee: { x: o.x + (STATIONS.coffee.x + 1) * TILE, y: o.y + (STATIONS.coffee.y + 2.2) * TILE },
314
+ lounge1: { x: o.x + (STATIONS.lounge1.x + 1.5) * TILE, y: o.y + (STATIONS.lounge1.y + 1.5) * TILE },
315
+ lounge2: { x: o.x + (STATIONS.lounge2.x + 1.5) * TILE, y: o.y + (STATIONS.lounge2.y + 1.5) * TILE },
316
+ };
317
+ }
318
+
319
+ getWorkstation(projectName, role) {
320
+ const positions = this.getCharacterPositions(projectName);
321
+ return positions[role] || { x: 0, y: 0 };
322
+ }
323
+
324
+ render(ctx, dt) {
325
+ this._time += (dt || 0.016);
326
+ // Update per-project conveyor offset independently
327
+ for (const data of this._projectData.values()) {
328
+ if (data.isDeploying) {
329
+ data.conveyorOffset = (data.conveyorOffset + (dt || 0.016) * 30) % 8;
330
+ data.conveyorTime = (data.conveyorTime || 0) + (dt || 0.016);
331
+ }
332
+ }
333
+
334
+ // Update truck animations
335
+ this._updateTrucks(dt || 0.016);
336
+
337
+ for (const [name, origin] of this._teamOrigins) {
338
+ this._renderTeamZone(ctx, name, origin);
339
+ }
340
+ }
341
+
342
+ _renderTeamZone(ctx, name, origin) {
343
+ const ox = origin.x;
344
+ const oy = origin.y;
345
+ const w = TEAM_WIDTH * TILE;
346
+ const h = TEAM_HEIGHT * TILE;
347
+ const color = origin.config.color || '#4A90D9';
348
+ const data = this._projectData.get(name) || {};
349
+
350
+ // ── Floor ─────────────────────────────────────────────────
351
+ for (let ty = 0; ty < TEAM_HEIGHT; ty++) {
352
+ for (let tx = 0; tx < TEAM_WIDTH; tx++) {
353
+ const isLight = (tx + ty) % 2 === 0;
354
+ ctx.fillStyle = isLight ? PAL.floor1 : PAL.floor2;
355
+ ctx.fillRect(ox + tx * TILE, oy + ty * TILE, TILE, TILE);
356
+ ctx.strokeStyle = '#2e2e4e';
357
+ ctx.lineWidth = 0.5;
358
+ ctx.strokeRect(ox + tx * TILE, oy + ty * TILE, TILE, TILE);
359
+ }
360
+ }
361
+
362
+ // ── Room floors ───────────────────────────────────────────
363
+ // Break room floor
364
+ this._fillArea(ctx, ox, oy, STATIONS.breakRoom, PAL.breakFloor);
365
+ // Gym floor
366
+ this._fillArea(ctx, ox, oy, STATIONS.gym, PAL.gymFloor);
367
+ // Carpet under work area
368
+ this._drawCarpet(ctx, ox + 0.5 * TILE, oy + 5.5 * TILE, 7 * TILE, 3 * TILE);
369
+
370
+ // ── Walls ─────────────────────────────────────────────────
371
+ ctx.fillStyle = PAL.wallTop;
372
+ ctx.fillRect(ox, oy, w, TILE * 0.8);
373
+ ctx.fillStyle = PAL.wall;
374
+ ctx.fillRect(ox, oy + TILE * 0.8, w, TILE * 0.4);
375
+ ctx.fillStyle = PAL.wallDk;
376
+ ctx.fillRect(ox, oy + TILE * 1.2, w, 3);
377
+ ctx.fillStyle = PAL.wall;
378
+ ctx.fillRect(ox, oy, TILE * 0.4, h);
379
+ ctx.fillRect(ox + w - TILE * 0.4, oy, TILE * 0.4, h);
380
+ ctx.fillStyle = PAL.wallDk;
381
+ ctx.fillRect(ox + TILE * 0.4, oy, 2, h);
382
+ ctx.fillRect(ox + w - TILE * 0.4 - 2, oy, 2, h);
383
+ ctx.fillRect(ox, oy + h - 3, w, 3);
384
+
385
+ // ── Team banner ───────────────────────────────────────────
386
+ ctx.fillStyle = color;
387
+ ctx.fillRect(ox + TILE * 0.4, oy, w - TILE * 0.8, 22);
388
+ ctx.fillStyle = '#fff';
389
+ ctx.fillRect(ox + TILE * 0.4, oy + 21, w - TILE * 0.8, 1);
390
+ renderPixelText(ctx, name, ox + w / 2, oy + 6, '#fff', 2);
391
+
392
+ // ── Status Board (big LED-style) ──────────────────────────
393
+ this._drawStatusBoard(ctx, ox, oy, data);
394
+
395
+ // ── PM Desk ───────────────────────────────────────────────
396
+ this._drawPixelDesk(ctx,
397
+ ox + STATIONS.pmDesk.x * TILE, oy + STATIONS.pmDesk.y * TILE,
398
+ STATIONS.pmDesk.w * TILE, STATIONS.pmDesk.h * TILE);
399
+ this._drawPapers(ctx,
400
+ ox + (STATIONS.pmDesk.x + 0.5) * TILE, oy + (STATIONS.pmDesk.y + 0.3) * TILE);
401
+ // Phase 4: PM Desk Lamp
402
+ this._drawDeskLamp(ctx,
403
+ ox + (STATIONS.pmDesk.x + 2.2) * TILE, oy + (STATIONS.pmDesk.y + 0.1) * TILE,
404
+ data.stage, this._time);
405
+
406
+ // ── Whiteboard (shows current feature) ────────────────────
407
+ this._drawWhiteboardWithFeature(ctx,
408
+ ox + STATIONS.whiteboard.x * TILE, oy + STATIONS.whiteboard.y * TILE,
409
+ STATIONS.whiteboard.w * TILE, STATIONS.whiteboard.h * TILE,
410
+ data.feature, data.stage, this._time);
411
+
412
+ // ── Coffee ────────────────────────────────────────────────
413
+ this._drawCoffeeMachine(ctx, ox + STATIONS.coffee.x * TILE, oy + STATIONS.coffee.y * TILE);
414
+
415
+ // ── Coding Station ────────────────────────────────────────
416
+ this._drawCodingStation(ctx,
417
+ ox + STATIONS.codingDesk.x * TILE, oy + STATIONS.codingDesk.y * TILE,
418
+ STATIONS.codingDesk.w * TILE, STATIONS.codingDesk.h * TILE,
419
+ data.stage, this._time);
420
+
421
+ // ── Review Table ──────────────────────────────────────────
422
+ this._drawPixelDesk(ctx,
423
+ ox + STATIONS.reviewTable.x * TILE, oy + STATIONS.reviewTable.y * TILE,
424
+ STATIONS.reviewTable.w * TILE, STATIONS.reviewTable.h * TILE);
425
+ this._drawMagnifier(ctx,
426
+ ox + (STATIONS.reviewTable.x + 1) * TILE, oy + (STATIONS.reviewTable.y + 0.3) * TILE);
427
+ this._drawReviewTableAnim(ctx,
428
+ ox + STATIONS.reviewTable.x * TILE, oy + STATIONS.reviewTable.y * TILE,
429
+ STATIONS.reviewTable.w * TILE, STATIONS.reviewTable.h * TILE,
430
+ data.stage, this._time);
431
+
432
+ // ── Break Room ────────────────────────────────────────────
433
+ this._drawBreakRoom(ctx, ox + STATIONS.breakRoom.x * TILE, oy + STATIONS.breakRoom.y * TILE,
434
+ STATIONS.breakRoom.w * TILE, STATIONS.breakRoom.h * TILE);
435
+
436
+ // ── Gym ───────────────────────────────────────────────────
437
+ this._drawGym(ctx, ox + STATIONS.gym.x * TILE, oy + STATIONS.gym.y * TILE,
438
+ STATIONS.gym.w * TILE, STATIONS.gym.h * TILE);
439
+
440
+ // ── Deploy Dock ───────────────────────────────────────────
441
+ this._drawDeployDock(ctx,
442
+ ox + STATIONS.deployDock.x * TILE, oy + STATIONS.deployDock.y * TILE,
443
+ STATIONS.deployDock.w * TILE, STATIONS.deployDock.h * TILE,
444
+ data.stage, this._time, this._trucks.has(name));
445
+
446
+ // ── Conveyor Belt (animated) ──────────────────────────────
447
+ this._drawConveyorBelt(ctx,
448
+ ox + STATIONS.conveyor.x * TILE, oy + STATIONS.conveyor.y * TILE,
449
+ STATIONS.conveyor.w * TILE, STATIONS.conveyor.h * TILE,
450
+ data.isDeploying, data.conveyorOffset || 0, data.conveyorTime || 0);
451
+
452
+ // ── Warehouse ─────────────────────────────────────────────
453
+ this._drawWarehouse(ctx,
454
+ ox + STATIONS.warehouse.x * TILE, oy + STATIONS.warehouse.y * TILE,
455
+ STATIONS.warehouse.w * TILE, STATIONS.warehouse.h * TILE,
456
+ data.packages);
457
+
458
+ // ── Animated Deploy Truck ─────────────────────────────────
459
+ this._drawAnimatedTruck(ctx, name, ox, oy);
460
+
461
+ // ── Lounge areas (plants, benches) ────────────────────────
462
+ this._drawLounge(ctx, ox + STATIONS.lounge1.x * TILE, oy + STATIONS.lounge1.y * TILE);
463
+ this._drawLounge(ctx, ox + STATIONS.lounge2.x * TILE, oy + STATIONS.lounge2.y * TILE);
464
+
465
+ // ── Chairs ────────────────────────────────────────────────
466
+ this._drawChair(ctx, ox + (STATIONS.pmDesk.x + 1.5) * TILE, oy + (STATIONS.pmDesk.y + 1.8) * TILE);
467
+ this._drawChair(ctx, ox + (STATIONS.codingDesk.x + 1) * TILE, oy + (STATIONS.codingDesk.y + 1.8) * TILE);
468
+ this._drawChair(ctx, ox + (STATIONS.codingDesk.x + 3) * TILE, oy + (STATIONS.codingDesk.y + 1.8) * TILE);
469
+ this._drawChair(ctx, ox + (STATIONS.codingDesk.x + 5) * TILE, oy + (STATIONS.codingDesk.y + 1.8) * TILE);
470
+ this._drawChair(ctx, ox + (STATIONS.reviewTable.x + 1) * TILE, oy + (STATIONS.reviewTable.y + 1.8) * TILE);
471
+ this._drawChair(ctx, ox + (STATIONS.reviewTable.x + 2.5) * TILE, oy + (STATIONS.reviewTable.y + 1.8) * TILE);
472
+ this._drawChair(ctx, ox + (STATIONS.deployDock.x + 2) * TILE, oy + (STATIONS.deployDock.y + 1.8) * TILE);
473
+ this._drawChair(ctx, ox + (STATIONS.whiteboard.x + 2.5) * TILE, oy + (STATIONS.whiteboard.y + 3) * TILE);
474
+
475
+ // Room divider lines
476
+ ctx.fillStyle = PAL.wallDk;
477
+ // Between work and social
478
+ ctx.fillRect(ox + 11.5 * TILE, oy + 5.5 * TILE, 2, 4 * TILE);
479
+ // Between break room and gym
480
+ ctx.fillRect(ox + 16.5 * TILE, oy + 5.5 * TILE, 2, 3.5 * TILE);
481
+ }
482
+
483
+ // ── Helper ──────────────────────────────────────────────────
484
+
485
+ _fillArea(ctx, ox, oy, station, color) {
486
+ ctx.fillStyle = color;
487
+ ctx.fillRect(ox + station.x * TILE, oy + station.y * TILE, station.w * TILE, station.h * TILE);
488
+ }
489
+
490
+ // ── Status Board ────────────────────────────────────────────
491
+
492
+ _drawStatusBoard(ctx, ox, oy, data) {
493
+ const s = STATIONS.statusBoard;
494
+ const x = ox + s.x * TILE;
495
+ const y = oy + s.y * TILE;
496
+ const w = s.w * TILE;
497
+ const h = s.h * TILE;
498
+
499
+ // Board background
500
+ ctx.fillStyle = PAL.statusBg;
501
+ ctx.fillRect(x, y, w, h);
502
+ ctx.strokeStyle = '#3a5a3a';
503
+ ctx.lineWidth = 2;
504
+ ctx.strokeRect(x, y, w, h);
505
+
506
+ // Scanline effect
507
+ ctx.fillStyle = '#ffffff08';
508
+ for (let sy = 0; sy < h; sy += 3) {
509
+ ctx.fillRect(x, y + sy, w, 1);
510
+ }
511
+
512
+ // Title
513
+ renderPixelText(ctx, 'STATUS', x + w / 2, y + 4, '#40d060', 1);
514
+
515
+ // Status indicator
516
+ const stage = data.stage || 'idle';
517
+ const isWorking = stage && stage !== 'idle' && stage !== 'inbox' && stage !== '';
518
+ const statusColor = isWorking ? PAL.statusGreen : PAL.statusYellow;
519
+ const statusText = isWorking ? 'WORKING' : 'IDLE';
520
+
521
+ // Blinking dot
522
+ const blink = Math.sin(this._time * (isWorking ? 4 : 1.5)) > 0;
523
+ if (blink) {
524
+ ctx.fillStyle = statusColor;
525
+ ctx.fillRect(x + 6, y + 16, 6, 6);
526
+ }
527
+
528
+ renderPixelText(ctx, statusText, x + w / 2, y + 16, statusColor, 1);
529
+
530
+ // Current stage
531
+ const stageShort = stage.replace(/_/g, ' ').toUpperCase().slice(0, 18);
532
+ if (isWorking) {
533
+ renderPixelText(ctx, stageShort, x + w / 2, y + 28, '#80c0f0', 1);
534
+ }
535
+
536
+ // Feature name — use native canvas text for wider display
537
+ if (data.feature) {
538
+ const maxChars = Math.floor(w / 5);
539
+ const featLine1 = data.feature.slice(0, maxChars).toUpperCase();
540
+ ctx.fillStyle = '#e8d040';
541
+ ctx.font = 'bold 8px monospace';
542
+ ctx.textAlign = 'center';
543
+ ctx.fillText(featLine1, x + w / 2, y + 48);
544
+ // Second line if feature name is longer
545
+ if (data.feature.length > maxChars) {
546
+ const featLine2 = data.feature.slice(maxChars, maxChars * 2).toUpperCase();
547
+ ctx.fillStyle = '#c8b030';
548
+ ctx.fillText(featLine2, x + w / 2, y + 58);
549
+ }
550
+ // Short description below
551
+ ctx.fillStyle = '#708070';
552
+ ctx.font = '7px monospace';
553
+ ctx.fillText(stage.replace(/_/g, ' '), x + w / 2, y + 68);
554
+ ctx.textAlign = 'start';
555
+ }
556
+
557
+ // Shipped count
558
+ const shipped = (data.packages || []).length;
559
+ renderPixelText(ctx, `SHIPPED:${shipped}`, x + w / 2, y + h - 12, '#808080', 1);
560
+ }
561
+
562
+ // ── Whiteboard with Feature ─────────────────────────────────
563
+
564
+ _drawWhiteboardWithFeature(ctx, x, y, w, h, feature, stage = '', t = 0) {
565
+ // Frame
566
+ ctx.fillStyle = PAL.wbFrame;
567
+ ctx.fillRect(x + 2, y, w - 4, 3);
568
+ ctx.fillRect(x + 2, y + h - 10, w - 4, 3);
569
+ ctx.fillRect(x + 2, y, 3, h - 8);
570
+ ctx.fillRect(x + w - 5, y, 3, h - 8);
571
+
572
+ // White surface
573
+ ctx.fillStyle = PAL.wbBg;
574
+ ctx.fillRect(x + 5, y + 3, w - 10, h - 14);
575
+
576
+ // Title area
577
+ ctx.fillStyle = '#4060b0';
578
+ ctx.fillRect(x + 8, y + 6, w - 16, 12);
579
+ renderPixelText(ctx, 'ARCHITECTURE', x + w / 2, y + 8, '#fff', 1);
580
+
581
+ // Feature name (if any)
582
+ if (feature) {
583
+ const lines = feature.toUpperCase().match(/.{1,16}/g) || [];
584
+ for (let i = 0; i < Math.min(lines.length, 3); i++) {
585
+ renderPixelText(ctx, lines[i], x + w / 2, y + 22 + i * 10, '#d04040', 1);
586
+ }
587
+ }
588
+
589
+ // Diagrams
590
+ ctx.fillStyle = '#4060b0';
591
+ ctx.fillRect(x + 10, y + 52, 16, 10);
592
+ ctx.fillStyle = '#e8e8d8';
593
+ ctx.fillRect(x + 12, y + 54, 12, 6);
594
+ ctx.fillStyle = '#d04040';
595
+ ctx.fillRect(x + 34, y + 52, 16, 10);
596
+ ctx.fillStyle = '#e8e8d8';
597
+ ctx.fillRect(x + 36, y + 54, 12, 6);
598
+ // Arrow
599
+ ctx.fillStyle = '#4060b0';
600
+ ctx.fillRect(x + 26, y + 56, 8, 2);
601
+ ctx.fillRect(x + 32, y + 54, 2, 6);
602
+
603
+ // Marker tray
604
+ ctx.fillStyle = PAL.wbFrame;
605
+ ctx.fillRect(x + 8, y + h - 8, w - 16, 4);
606
+ ctx.fillStyle = '#d04040';
607
+ ctx.fillRect(x + 12, y + h - 10, 3, 6);
608
+ ctx.fillStyle = '#4060b0';
609
+ ctx.fillRect(x + 18, y + h - 10, 3, 6);
610
+ ctx.fillStyle = '#40a040';
611
+ ctx.fillRect(x + 24, y + h - 10, 3, 6);
612
+
613
+ // Phase 5: Animated diagram drawing when architect is designing
614
+ if (stage === 'architecture' || stage === 'arch_complete') {
615
+ const boardX = x + 8;
616
+ const boardY = y + 38;
617
+ const boardW = w - 16;
618
+ const boardH = h - 52;
619
+
620
+ // Clear diagram area for animated content
621
+ ctx.fillStyle = PAL.wbBg;
622
+ ctx.fillRect(boardX, boardY, boardW, 38);
623
+
624
+ // Cycle through diagram layouts every ~8 seconds
625
+ const layoutPhase = Math.floor(t % 8);
626
+ const cycleT = t % 8; // time within current cycle
627
+
628
+ // Marker colors cycling
629
+ const markerColors = ['#4060b0', '#d04040', '#40a040'];
630
+ const activeMarker = markerColors[Math.floor(t) % 3];
631
+
632
+ // --- Diagram layout definitions ---
633
+ const layouts = [
634
+ // Layout 0-2: Three boxes in a row with arrows
635
+ () => {
636
+ const boxes = [
637
+ { bx: boardX + 4, by: boardY + 8, bw: 22, bh: 12, label: 'API' },
638
+ { bx: boardX + 38, by: boardY + 8, bw: 22, bh: 12, label: 'SVC' },
639
+ { bx: boardX + 72, by: boardY + 8, bw: 22, bh: 12, label: 'DB' },
640
+ ];
641
+ const progress = Math.min(cycleT / 2, 1); // grow over 2 seconds
642
+ for (let bi = 0; bi < boxes.length; bi++) {
643
+ const b = boxes[bi];
644
+ const drawW = Math.floor(b.bw * progress);
645
+ const drawH = Math.floor(b.bh * progress);
646
+ if (drawW > 2 && drawH > 2) {
647
+ ctx.strokeStyle = markerColors[bi % 3];
648
+ ctx.lineWidth = 1;
649
+ ctx.strokeRect(b.bx, b.by, drawW, drawH);
650
+ if (progress > 0.6) {
651
+ ctx.fillStyle = markerColors[bi % 3];
652
+ ctx.font = 'bold 5px monospace';
653
+ ctx.textAlign = 'center';
654
+ ctx.fillText(b.label, b.bx + b.bw / 2, b.by + b.bh / 2 + 2);
655
+ ctx.textAlign = 'start';
656
+ }
657
+ }
658
+ }
659
+ // Connection arrows (appear after boxes)
660
+ if (cycleT > 1.5) {
661
+ const arrowProg = Math.min((cycleT - 1.5) / 1, 1);
662
+ const arrowLen = Math.floor(12 * arrowProg);
663
+ ctx.fillStyle = activeMarker;
664
+ ctx.fillRect(boardX + 26, boardY + 13, arrowLen, 2);
665
+ if (arrowLen > 4) { ctx.fillRect(boardX + 26 + arrowLen - 2, boardY + 11, 2, 6); }
666
+ ctx.fillRect(boardX + 60, boardY + 13, arrowLen, 2);
667
+ if (arrowLen > 4) { ctx.fillRect(boardX + 60 + arrowLen - 2, boardY + 11, 2, 6); }
668
+ }
669
+ },
670
+ // Layout 3-5: Vertical flow chart
671
+ () => {
672
+ const boxes = [
673
+ { bx: boardX + 30, by: boardY + 2, bw: 30, bh: 10, label: 'INPUT' },
674
+ { bx: boardX + 30, by: boardY + 18, bw: 30, bh: 10, label: 'PROC' },
675
+ { bx: boardX + 30, by: boardY + 34, bw: 30, bh: 10, label: 'OUT' },
676
+ ];
677
+ const progress = Math.min(cycleT / 2, 1);
678
+ for (let bi = 0; bi < boxes.length; bi++) {
679
+ const b = boxes[bi];
680
+ const drawW = Math.floor(b.bw * progress);
681
+ if (drawW > 2) {
682
+ ctx.strokeStyle = markerColors[bi % 3];
683
+ ctx.lineWidth = 1;
684
+ ctx.strokeRect(b.bx, b.by, drawW, b.bh);
685
+ if (progress > 0.5) {
686
+ ctx.fillStyle = markerColors[bi % 3];
687
+ ctx.font = 'bold 5px monospace';
688
+ ctx.textAlign = 'center';
689
+ ctx.fillText(b.label, b.bx + b.bw / 2, b.by + 7);
690
+ ctx.textAlign = 'start';
691
+ }
692
+ }
693
+ }
694
+ // Vertical arrows
695
+ if (cycleT > 1.5) {
696
+ ctx.fillStyle = activeMarker;
697
+ ctx.fillRect(boardX + 44, boardY + 12, 2, 6);
698
+ ctx.fillRect(boardX + 42, boardY + 16, 6, 2);
699
+ ctx.fillRect(boardX + 44, boardY + 28, 2, 6);
700
+ ctx.fillRect(boardX + 42, boardY + 32, 6, 2);
701
+ }
702
+ },
703
+ // Layout 6-7: Diamond decision + branches
704
+ () => {
705
+ const progress = Math.min(cycleT / 2, 1);
706
+ // Diamond (decision)
707
+ const cx = boardX + boardW / 2;
708
+ const cy = boardY + 12;
709
+ const ds = Math.floor(8 * progress);
710
+ if (ds > 2) {
711
+ ctx.strokeStyle = '#d04040';
712
+ ctx.lineWidth = 1;
713
+ ctx.beginPath();
714
+ ctx.moveTo(cx, cy - ds);
715
+ ctx.lineTo(cx + ds, cy);
716
+ ctx.lineTo(cx, cy + ds);
717
+ ctx.lineTo(cx - ds, cy);
718
+ ctx.closePath();
719
+ ctx.stroke();
720
+ if (progress > 0.5) {
721
+ ctx.fillStyle = '#d04040';
722
+ ctx.font = 'bold 5px monospace';
723
+ ctx.textAlign = 'center';
724
+ ctx.fillText('?', cx, cy + 2);
725
+ ctx.textAlign = 'start';
726
+ }
727
+ }
728
+ // Branch boxes
729
+ if (cycleT > 1.5) {
730
+ const brProg = Math.min((cycleT - 1.5) / 1.5, 1);
731
+ const brW = Math.floor(20 * brProg);
732
+ ctx.strokeStyle = '#4060b0';
733
+ ctx.lineWidth = 1;
734
+ if (brW > 4) {
735
+ ctx.strokeRect(boardX + 8, boardY + 28, brW, 8);
736
+ ctx.strokeRect(boardX + boardW - 8 - brW, boardY + 28, brW, 8);
737
+ ctx.fillStyle = '#4060b0';
738
+ ctx.font = 'bold 5px monospace';
739
+ ctx.textAlign = 'center';
740
+ ctx.fillText('Y', boardX + 18, boardY + 34);
741
+ ctx.fillText('N', boardX + boardW - 18, boardY + 34);
742
+ ctx.textAlign = 'start';
743
+ }
744
+ // Branch lines
745
+ ctx.fillStyle = '#40a040';
746
+ ctx.fillRect(cx - 12, cy + 8, 2, Math.floor(12 * brProg));
747
+ ctx.fillRect(cx + 10, cy + 8, 2, Math.floor(12 * brProg));
748
+ }
749
+ },
750
+ ];
751
+
752
+ // Select layout based on phase (cycle through 3 layouts in 8-second windows)
753
+ const layoutIdx = Math.floor((t / 8) % layouts.length);
754
+ layouts[layoutIdx]();
755
+
756
+ // "Drawing hand" cursor moving across the board
757
+ const handX = boardX + 4 + ((t * 18) % (boardW - 8));
758
+ const handY = boardY + 4 + Math.sin(t * 5) * 6 + 12;
759
+ ctx.fillStyle = activeMarker;
760
+ ctx.fillRect(handX, handY, 3, 3);
761
+ ctx.fillRect(handX + 1, handY + 3, 2, 2);
762
+
763
+ // Glowing border around whiteboard
764
+ ctx.save();
765
+ ctx.globalAlpha = 0.25 + Math.sin(t * 2) * 0.15;
766
+ ctx.fillStyle = '#60c060';
767
+ ctx.fillRect(x + 3, y + 1, w - 6, 2);
768
+ ctx.fillRect(x + 3, y + h - 12, w - 6, 2);
769
+ ctx.restore();
770
+ }
771
+ }
772
+
773
+ // ── Break Room ──────────────────────────────────────────────
774
+
775
+ _drawBreakRoom(ctx, x, y, w, h) {
776
+ // Room label
777
+ renderPixelText(ctx, 'BREAK ROOM', x + w / 2, y + 2, '#a08060', 1);
778
+
779
+ // Table
780
+ ctx.fillStyle = PAL.breakTable;
781
+ ctx.fillRect(x + w / 2 - 16, y + 16, 32, 20);
782
+ ctx.fillStyle = '#5a4020';
783
+ ctx.strokeStyle = '#4a3018';
784
+ ctx.lineWidth = 1;
785
+ ctx.strokeRect(x + w / 2 - 16, y + 16, 32, 20);
786
+
787
+ // Plates
788
+ ctx.fillStyle = '#e0e0d8';
789
+ ctx.beginPath();
790
+ ctx.arc(x + w / 2 - 6, y + 26, 4, 0, Math.PI * 2);
791
+ ctx.fill();
792
+ ctx.beginPath();
793
+ ctx.arc(x + w / 2 + 6, y + 26, 4, 0, Math.PI * 2);
794
+ ctx.fill();
795
+
796
+ // Food items
797
+ ctx.fillStyle = '#d04040'; // apple
798
+ ctx.fillRect(x + w / 2 - 8, y + 24, 4, 4);
799
+ ctx.fillStyle = '#e8d040'; // sandwich
800
+ ctx.fillRect(x + w / 2 + 4, y + 24, 6, 3);
801
+
802
+ // Chairs around table
803
+ this._drawChair(ctx, x + w / 2 - 20, y + 24);
804
+ this._drawChair(ctx, x + w / 2 + 20, y + 24);
805
+ this._drawChair(ctx, x + w / 2, y + 42);
806
+
807
+ // Vending machine
808
+ ctx.fillStyle = '#505060';
809
+ ctx.fillRect(x + 4, y + 10, 16, 24);
810
+ ctx.fillStyle = '#404050';
811
+ ctx.fillRect(x + 6, y + 12, 12, 18);
812
+ // Drinks
813
+ ctx.fillStyle = '#d04040';
814
+ ctx.fillRect(x + 8, y + 14, 3, 5);
815
+ ctx.fillStyle = '#4080d0';
816
+ ctx.fillRect(x + 12, y + 14, 3, 5);
817
+ ctx.fillStyle = '#40b060';
818
+ ctx.fillRect(x + 8, y + 21, 3, 5);
819
+ ctx.fillStyle = '#e8d040';
820
+ ctx.fillRect(x + 12, y + 21, 3, 5);
821
+
822
+ // Microwave
823
+ ctx.fillStyle = '#606068';
824
+ ctx.fillRect(x + w - 20, y + 10, 16, 12);
825
+ ctx.fillStyle = '#1a1a2a';
826
+ ctx.fillRect(x + w - 18, y + 12, 10, 8);
827
+ // Button
828
+ ctx.fillStyle = '#40d060';
829
+ ctx.fillRect(x + w - 6, y + 14, 2, 2);
830
+ }
831
+
832
+ // ── Gym ─────────────────────────────────────────────────────
833
+
834
+ _drawGym(ctx, x, y, w, h) {
835
+ // Room label
836
+ renderPixelText(ctx, 'GYM', x + w / 2, y + 2, '#6080a0', 1);
837
+
838
+ // Yoga mat
839
+ ctx.fillStyle = PAL.gymMat;
840
+ ctx.fillRect(x + 8, y + 14, 24, 12);
841
+ ctx.fillStyle = '#3858b0';
842
+ ctx.fillRect(x + 10, y + 16, 20, 8);
843
+
844
+ // Treadmill
845
+ ctx.fillStyle = '#505058';
846
+ ctx.fillRect(x + 40, y + 12, 20, 26);
847
+ ctx.fillStyle = '#404048';
848
+ ctx.fillRect(x + 42, y + 14, 16, 20);
849
+ // Running belt
850
+ ctx.fillStyle = '#353540';
851
+ ctx.fillRect(x + 44, y + 24, 12, 8);
852
+ // Handles
853
+ ctx.fillStyle = '#707078';
854
+ ctx.fillRect(x + 44, y + 14, 2, 10);
855
+ ctx.fillRect(x + 56, y + 14, 2, 10);
856
+ // Display
857
+ ctx.fillStyle = '#40d060';
858
+ ctx.fillRect(x + 48, y + 16, 6, 4);
859
+
860
+ // Dumbbells
861
+ ctx.fillStyle = '#505058';
862
+ ctx.fillRect(x + 70, y + 30, 16, 4);
863
+ ctx.fillStyle = '#404048';
864
+ ctx.fillRect(x + 68, y + 28, 4, 8);
865
+ ctx.fillRect(x + 84, y + 28, 4, 8);
866
+
867
+ // Weight rack
868
+ ctx.fillStyle = '#404048';
869
+ ctx.fillRect(x + 68, y + 12, 22, 14);
870
+ ctx.fillStyle = '#505058';
871
+ for (let i = 0; i < 3; i++) {
872
+ ctx.fillRect(x + 70 + i * 7, y + 14, 5, 10);
873
+ }
874
+
875
+ // Water cooler
876
+ ctx.fillStyle = '#a0d0e0';
877
+ ctx.fillRect(x + w - 14, y + 14, 8, 14);
878
+ ctx.fillStyle = '#80b0c8';
879
+ ctx.fillRect(x + w - 12, y + 16, 4, 8);
880
+ }
881
+
882
+ // ── Conveyor Belt (animated) ────────────────────────────────
883
+
884
+ _drawConveyorBelt(ctx, x, y, w, h, isDeploying, conveyorOffset, conveyorTime) {
885
+ // Belt base
886
+ ctx.fillStyle = PAL.convBelt;
887
+ ctx.fillRect(x, y + 4, w, h - 8);
888
+
889
+ // Rollers (animated per-project)
890
+ ctx.fillStyle = PAL.convRoller;
891
+ const offset = Math.floor(conveyorOffset);
892
+ for (let rx = offset; rx < w; rx += 8) {
893
+ ctx.fillRect(x + rx, y + 6, 4, h - 12);
894
+ }
895
+
896
+ // Belt edges
897
+ ctx.fillStyle = '#808088';
898
+ ctx.fillRect(x, y + 4, w, 2);
899
+ ctx.fillRect(x, y + h - 6, w, 2);
900
+
901
+ // Moving package on belt when deploying
902
+ if (isDeploying) {
903
+ const pkgX = x + (conveyorTime * 40 % w);
904
+ this._drawMiniPackage(ctx, pkgX, y + h / 2 - 4);
905
+ }
906
+
907
+ // Direction arrows
908
+ ctx.fillStyle = '#ffffff30';
909
+ for (let ax = 0; ax < w - 10; ax += 20) {
910
+ const axx = x + ax + (conveyorOffset * 2 % 20);
911
+ ctx.fillRect(axx + 4, y + h / 2 - 1, 6, 2);
912
+ ctx.fillRect(axx + 8, y + h / 2 - 3, 2, 6);
913
+ }
914
+ }
915
+
916
+ // ── Warehouse ───────────────────────────────────────────────
917
+
918
+ _drawWarehouse(ctx, x, y, w, h, packages) {
919
+ // Warehouse floor
920
+ ctx.fillStyle = PAL.whGrayDk;
921
+ ctx.fillRect(x, y, w, h);
922
+
923
+ // Floor markings (grid)
924
+ ctx.strokeStyle = '#5a5a5a';
925
+ ctx.lineWidth = 0.5;
926
+ for (let gx = 0; gx < w; gx += TILE) {
927
+ ctx.beginPath();
928
+ ctx.moveTo(x + gx, y);
929
+ ctx.lineTo(x + gx, y + h);
930
+ ctx.stroke();
931
+ }
932
+ for (let gy = 0; gy < h; gy += TILE) {
933
+ ctx.beginPath();
934
+ ctx.moveTo(x, y + gy);
935
+ ctx.lineTo(x + w, y + gy);
936
+ ctx.stroke();
937
+ }
938
+
939
+ // Warehouse walls (back and sides)
940
+ ctx.fillStyle = PAL.whGray;
941
+ ctx.fillRect(x, y, w, 6);
942
+ ctx.fillRect(x, y, 4, h);
943
+ ctx.fillRect(x + w - 4, y, 4, h);
944
+
945
+ // Sign
946
+ ctx.fillStyle = '#1a2a3a';
947
+ ctx.fillRect(x + w / 2 - 30, y - 4, 60, 12);
948
+ renderPixelText(ctx, 'WAREHOUSE', x + w / 2, y - 2, '#e8d040', 1);
949
+
950
+ // Shelving units (3 rows)
951
+ for (let row = 0; row < 3; row++) {
952
+ const sy = y + 14 + row * (TILE + 14);
953
+ // Shelf
954
+ ctx.fillStyle = '#6a6a72';
955
+ ctx.fillRect(x + 8, sy, w - 16, 4);
956
+ ctx.fillStyle = '#5a5a62';
957
+ ctx.fillRect(x + 8, sy + 4, w - 16, 2);
958
+ // Shelf supports
959
+ ctx.fillStyle = '#505058';
960
+ ctx.fillRect(x + 10, sy, 3, TILE + 8);
961
+ ctx.fillRect(x + w - 14, sy, 3, TILE + 8);
962
+ ctx.fillRect(x + w / 2, sy, 3, TILE + 8);
963
+ }
964
+
965
+ // Draw shipped packages on shelves
966
+ if (packages && packages.length > 0) {
967
+ for (let i = 0; i < packages.length; i++) {
968
+ const pkg = packages[i];
969
+ const row = Math.floor(i / 4);
970
+ const col = i % 4;
971
+ if (row >= 3) break;
972
+
973
+ const px = x + 18 + col * ((w - 36) / 4);
974
+ const py = y + 14 + row * (TILE + 14) - 14;
975
+ this._drawShippedPackage(ctx, px, py, pkg.name);
976
+ }
977
+ }
978
+
979
+ // Forklift (static decoration)
980
+ ctx.fillStyle = '#e8a020';
981
+ ctx.fillRect(x + w - 40, y + h - 24, 16, 10);
982
+ ctx.fillStyle = '#c88010';
983
+ ctx.fillRect(x + w - 38, y + h - 22, 12, 6);
984
+ // Forks
985
+ ctx.fillStyle = '#808088';
986
+ ctx.fillRect(x + w - 44, y + h - 18, 6, 2);
987
+ ctx.fillRect(x + w - 44, y + h - 14, 6, 2);
988
+ // Wheels
989
+ ctx.fillStyle = '#333';
990
+ ctx.fillRect(x + w - 40, y + h - 14, 5, 4);
991
+ ctx.fillRect(x + w - 28, y + h - 14, 5, 4);
992
+ }
993
+
994
+ _drawShippedPackage(ctx, x, y, name) {
995
+ // Package box
996
+ ctx.fillStyle = PAL.pkgTan;
997
+ ctx.fillRect(x, y, 20, 14);
998
+ // Outline
999
+ ctx.fillStyle = PAL.pkgTan2;
1000
+ ctx.fillRect(x, y, 20, 2);
1001
+ ctx.fillRect(x, y, 2, 14);
1002
+ ctx.fillRect(x + 18, y, 2, 14);
1003
+ ctx.fillRect(x, y + 12, 20, 2);
1004
+ // Cross tape
1005
+ ctx.fillStyle = PAL.pkgTan2;
1006
+ ctx.fillRect(x + 9, y, 2, 14);
1007
+ ctx.fillRect(x, y + 6, 20, 2);
1008
+ // Checkmark (shipped!)
1009
+ ctx.fillStyle = '#40d060';
1010
+ ctx.fillRect(x + 14, y + 2, 2, 2);
1011
+ ctx.fillRect(x + 16, y + 1, 2, 2);
1012
+ ctx.fillRect(x + 12, y + 3, 2, 2);
1013
+ }
1014
+
1015
+ _drawMiniPackage(ctx, x, y) {
1016
+ ctx.fillStyle = PAL.pkgTan;
1017
+ ctx.fillRect(x, y, 12, 8);
1018
+ ctx.fillStyle = PAL.pkgTan2;
1019
+ ctx.fillRect(x + 5, y, 2, 8);
1020
+ ctx.fillRect(x, y + 3, 12, 2);
1021
+ }
1022
+
1023
+ // ── Lounge ──────────────────────────────────────────────────
1024
+
1025
+ _drawLounge(ctx, x, y) {
1026
+ // Bench
1027
+ ctx.fillStyle = '#5a4a3a';
1028
+ ctx.fillRect(x + 4, y + 10, 28, 10);
1029
+ ctx.fillStyle = '#4a3a2a';
1030
+ ctx.fillRect(x + 6, y + 8, 24, 4);
1031
+ // Cushions
1032
+ ctx.fillStyle = '#6a4060';
1033
+ ctx.fillRect(x + 8, y + 12, 10, 6);
1034
+ ctx.fillStyle = '#4060a0';
1035
+ ctx.fillRect(x + 20, y + 12, 10, 6);
1036
+
1037
+ // Plant
1038
+ ctx.fillStyle = '#6a4030';
1039
+ ctx.fillRect(x + 38, y + 20, 8, 12);
1040
+ ctx.fillStyle = '#306028';
1041
+ ctx.beginPath();
1042
+ ctx.arc(x + 42, y + 16, 10, 0, Math.PI * 2);
1043
+ ctx.fill();
1044
+ ctx.fillStyle = '#408030';
1045
+ ctx.beginPath();
1046
+ ctx.arc(x + 40, y + 14, 6, 0, Math.PI * 2);
1047
+ ctx.fill();
1048
+ }
1049
+
1050
+ // ── Existing drawing methods (updated) ──────────────────────
1051
+
1052
+ _drawCarpet(ctx, x, y, w, h) {
1053
+ ctx.fillStyle = PAL.carpet;
1054
+ ctx.fillRect(x, y, w, h);
1055
+ ctx.fillStyle = '#3a2a52';
1056
+ for (let py = 0; py < h; py += 8) {
1057
+ for (let px = 0; px < w; px += 8) {
1058
+ if ((px / 8 + py / 8) % 2 === 0) {
1059
+ ctx.fillRect(x + px, y + py, 4, 4);
1060
+ }
1061
+ }
1062
+ }
1063
+ }
1064
+
1065
+ _drawPixelDesk(ctx, x, y, w, h) {
1066
+ ctx.fillStyle = PAL.shadow;
1067
+ ctx.fillRect(x + 4, y + h - 2, w - 4, 4);
1068
+ ctx.fillStyle = PAL.deskLt;
1069
+ ctx.fillRect(x + 2, y + 2, w - 4, h - 8);
1070
+ ctx.fillStyle = PAL.deskDk;
1071
+ ctx.fillRect(x + 2, y + 2, w - 4, 2);
1072
+ ctx.fillRect(x + 2, y + h - 8, w - 4, 2);
1073
+ ctx.fillRect(x + 2, y + 2, 2, h - 8);
1074
+ ctx.fillRect(x + w - 4, y + 2, 2, h - 8);
1075
+ ctx.fillStyle = PAL.desk;
1076
+ ctx.fillRect(x + 2, y + h - 6, w - 4, 6);
1077
+ ctx.fillStyle = PAL.deskDk;
1078
+ ctx.fillRect(x + 4, y + h, 3, 4);
1079
+ ctx.fillRect(x + w - 7, y + h, 3, 4);
1080
+ }
1081
+
1082
+ _drawPapers(ctx, x, y) {
1083
+ ctx.fillStyle = '#f0f0e8';
1084
+ ctx.fillRect(x, y, 14, 18);
1085
+ ctx.fillStyle = '#e0e0d8';
1086
+ ctx.fillRect(x + 2, y + 2, 14, 18);
1087
+ ctx.fillStyle = '#bbb';
1088
+ for (let i = 0; i < 4; i++) {
1089
+ ctx.fillRect(x + 4, y + 6 + i * 4, 10, 1);
1090
+ }
1091
+ ctx.fillStyle = '#3050a0';
1092
+ ctx.fillRect(x + 16, y + 4, 2, 12);
1093
+ ctx.fillStyle = '#2040a0';
1094
+ ctx.fillRect(x + 16, y + 14, 2, 4);
1095
+ }
1096
+
1097
+ _drawMagnifier(ctx, x, y) {
1098
+ ctx.strokeStyle = '#888';
1099
+ ctx.lineWidth = 2;
1100
+ ctx.beginPath();
1101
+ ctx.arc(x + 8, y + 8, 6, 0, Math.PI * 2);
1102
+ ctx.stroke();
1103
+ ctx.fillStyle = '#c0e0ff80';
1104
+ ctx.beginPath();
1105
+ ctx.arc(x + 8, y + 8, 5, 0, Math.PI * 2);
1106
+ ctx.fill();
1107
+ ctx.fillStyle = '#6a5040';
1108
+ ctx.fillRect(x + 12, y + 12, 3, 8);
1109
+ }
1110
+
1111
+ _drawCodingStation(ctx, x, y, w, h, stage = '', t = 0) {
1112
+ this._drawPixelDesk(ctx, x, y, w, h);
1113
+ const monW = 30, monH = 22, gap = 8;
1114
+ const totalMonW = monW * 3 + gap * 2;
1115
+ const startX = x + (w - totalMonW) / 2;
1116
+
1117
+ // Determine monitor mode from stage
1118
+ const isCoding = ['implementation', 'implementation_complete'].includes(stage);
1119
+ const isReview = ['review', 'review_complete'].includes(stage);
1120
+ const isRejected = stage === 'rejected';
1121
+ const isIdle = !stage || stage === '' || stage === 'inbox';
1122
+
1123
+ // Phase 6: Pseudo-code lines for realistic code scrolling
1124
+ // Each line: [indentLevel, type] where type determines color
1125
+ // Types: 'kw' = keyword, 'str' = string, 'cmt' = comment, 'num' = number, 'code' = normal
1126
+ const CODE_LINES = [
1127
+ [0, 'kw', 10], [0, 'code', 14], [1, 'str', 12], [1, 'code', 8],
1128
+ [2, 'kw', 6], [2, 'num', 10], [2, 'code', 14], [1, 'code', 9],
1129
+ [0, 'cmt', 16], [0, 'kw', 8], [1, 'code', 12], [1, 'str', 10],
1130
+ [2, 'kw', 7], [2, 'num', 5], [3, 'code', 11], [3, 'str', 13],
1131
+ [2, 'code', 9], [1, 'kw', 8], [0, 'code', 15], [0, 'cmt', 18],
1132
+ [0, 'kw', 11], [1, 'code', 7], [1, 'num', 6], [2, 'str', 14],
1133
+ ];
1134
+ const SYNTAX_COLORS = {
1135
+ kw: '#4080d0', // keywords in blue
1136
+ str: '#40a050', // strings in green
1137
+ cmt: '#606060', // comments in gray
1138
+ num: '#d08030', // numbers in orange
1139
+ code: '#c0c0d0', // normal code
1140
+ };
1141
+
1142
+ // Terminal output lines (green on black)
1143
+ const TERM_LINES = [
1144
+ [0, 8], [0, 14], [0, 10], [0, 6], [0, 16], [0, 12],
1145
+ [0, 9], [0, 11], [0, 7], [0, 15], [0, 13], [0, 5],
1146
+ ];
1147
+
1148
+ // Compile error flash: brief red flash every ~20s for 0.3s
1149
+ const errorCycleT = t % 20;
1150
+ const isErrorFlash = isCoding && errorCycleT > 19.7;
1151
+
1152
+ for (let i = 0; i < 3; i++) {
1153
+ const mx = startX + i * (monW + gap);
1154
+ const my = y - 4;
1155
+ const screenX = mx + 2;
1156
+ const screenY = my + 2;
1157
+ const screenW = monW - 4;
1158
+ const screenH = monH - 6;
1159
+
1160
+ // Monitor bezel
1161
+ ctx.fillStyle = PAL.monitor;
1162
+ ctx.fillRect(mx, my, monW, monH);
1163
+
1164
+ // Screen background
1165
+ if (isIdle) {
1166
+ ctx.fillStyle = '#050d15';
1167
+ } else if (isRejected || isErrorFlash) {
1168
+ ctx.fillStyle = '#1a0808';
1169
+ } else if (isReview) {
1170
+ ctx.fillStyle = '#081418';
1171
+ } else if (i === 2 && isCoding) {
1172
+ ctx.fillStyle = '#0a0a0a'; // terminal: pure black
1173
+ } else {
1174
+ ctx.fillStyle = '#0a1a2a';
1175
+ }
1176
+ ctx.fillRect(screenX, screenY, screenW, screenH);
1177
+
1178
+ if (!isIdle) {
1179
+ ctx.save();
1180
+ ctx.beginPath();
1181
+ ctx.rect(screenX, screenY, screenW, screenH);
1182
+ ctx.clip();
1183
+
1184
+ if (isCoding) {
1185
+ // ── Phase 6: Realistic code / terminal scrolling ──
1186
+ const isTerminal = (i === 2); // third monitor is terminal
1187
+ const lineH = 3; // pixel height per code line
1188
+ const maxVisibleLines = Math.floor(screenH / lineH);
1189
+ const scrollOffset = (t * 6 + i * 7) % (CODE_LINES.length * lineH);
1190
+ const lines = isTerminal ? TERM_LINES : CODE_LINES;
1191
+
1192
+ for (let ln = 0; ln < maxVisibleLines + 2; ln++) {
1193
+ const lineIdx = (ln + Math.floor(scrollOffset / lineH)) % lines.length;
1194
+ const lineData = lines[lineIdx];
1195
+ const indentPx = isTerminal ? 1 : lineData[0] * 2; // indent in pixels
1196
+ const lineY = screenY + ln * lineH - (scrollOffset % lineH);
1197
+
1198
+ if (lineY < screenY - lineH || lineY > screenY + screenH) continue;
1199
+
1200
+ if (isTerminal) {
1201
+ // Terminal: green on black
1202
+ ctx.fillStyle = '#30d050';
1203
+ ctx.fillRect(screenX + 1, lineY, lineData[1], 2);
1204
+ } else {
1205
+ // Code: syntax colored
1206
+ const type = lineData[1];
1207
+ const lineW = lineData[2];
1208
+ ctx.fillStyle = (isErrorFlash) ? '#ff4040' : SYNTAX_COLORS[type];
1209
+ ctx.fillRect(screenX + 2 + indentPx, lineY, Math.min(lineW, screenW - 4 - indentPx), 2);
1210
+ }
1211
+ }
1212
+
1213
+ // Blinking cursor at end of current line
1214
+ if (Math.sin(t * 4 + i) > 0) {
1215
+ const cursorLine = Math.floor(maxVisibleLines * 0.7);
1216
+ const cursorLineIdx = (cursorLine + Math.floor(scrollOffset / lineH)) % lines.length;
1217
+ const cLineData = lines[cursorLineIdx];
1218
+ const cIndent = isTerminal ? 1 : cLineData[0] * 2;
1219
+ const cLineW = isTerminal ? cLineData[1] : cLineData[2];
1220
+ ctx.fillStyle = isTerminal ? '#30ff50' : '#40ff80';
1221
+ ctx.fillRect(screenX + 2 + cIndent + cLineW + 1, screenY + cursorLine * lineH, 1, 2);
1222
+ }
1223
+
1224
+ // Scrollbar on right side
1225
+ ctx.fillStyle = '#303040';
1226
+ ctx.fillRect(screenX + screenW - 2, screenY, 2, screenH);
1227
+ const scrollFrac = (scrollOffset / (lines.length * lineH));
1228
+ const thumbH = Math.max(3, Math.floor(screenH * 0.3));
1229
+ ctx.fillStyle = '#606080';
1230
+ ctx.fillRect(screenX + screenW - 2, screenY + scrollFrac * (screenH - thumbH), 2, thumbH);
1231
+
1232
+ } else {
1233
+ // Non-coding states (review, rejected) — keep original simple rendering
1234
+ const lineColors = isRejected
1235
+ ? ['#ff4040', '#ff8040', '#ff4060', '#e04040']
1236
+ : ['#40e0d0', '#40b0ff', '#80e0e0', '#40a8f0'];
1237
+ const LINE_WIDTHS = [14, 8, 12, 6, 10, 16, 7, 13, 9, 15, 11, 6];
1238
+
1239
+ for (let ln = 0; ln < 4; ln++) {
1240
+ ctx.fillStyle = lineColors[(i * 4 + ln) % lineColors.length];
1241
+ const lineW = LINE_WIDTHS[(i * 4 + ln) % LINE_WIDTHS.length];
1242
+ ctx.fillRect(mx + 4, my + 4 + ln * 4, lineW, 2);
1243
+ }
1244
+ }
1245
+
1246
+ ctx.restore();
1247
+
1248
+ // Glow effect on active monitors
1249
+ ctx.save();
1250
+ if (isErrorFlash) {
1251
+ ctx.globalAlpha = 0.3;
1252
+ ctx.fillStyle = '#ff4040';
1253
+ } else {
1254
+ ctx.globalAlpha = 0.15 + Math.sin(t * 2 + i) * 0.08;
1255
+ ctx.fillStyle = isRejected ? '#ff4040' : isReview ? '#40d0d0' : '#40a8f0';
1256
+ }
1257
+ ctx.fillRect(mx - 2, my - 2, monW + 4, monH + 4);
1258
+ ctx.restore();
1259
+ }
1260
+
1261
+ // Monitor stand
1262
+ ctx.fillStyle = '#444';
1263
+ ctx.fillRect(mx + monW / 2 - 3, my + monH, 6, 3);
1264
+ ctx.fillRect(mx + monW / 2 - 6, my + monH + 3, 12, 2);
1265
+ }
1266
+
1267
+ // Keyboard lightning spark when coding
1268
+ if (isCoding && Math.sin(t * 6) > 0.7) {
1269
+ ctx.fillStyle = '#60d0ff';
1270
+ ctx.fillRect(x + w / 2 - 2 + Math.floor(t * 5) % 6, y + h - 8, 2, 4);
1271
+ }
1272
+
1273
+ ctx.fillStyle = '#505058';
1274
+ ctx.fillRect(x + w / 2 - 20, y + h - 6, 40, 8);
1275
+ ctx.fillStyle = '#606068';
1276
+ for (let kx = 0; kx < 9; kx++) {
1277
+ for (let ky = 0; ky < 2; ky++) {
1278
+ ctx.fillRect(x + w / 2 - 18 + kx * 4, y + h - 5 + ky * 4, 3, 3);
1279
+ }
1280
+ }
1281
+ }
1282
+
1283
+ _drawReviewTableAnim(ctx, x, y, w, h, stage = '', t = 0) {
1284
+ if (stage !== 'review') return;
1285
+ // Pulsing glow around review table
1286
+ ctx.save();
1287
+ ctx.globalAlpha = 0.18 + Math.sin(t * 3) * 0.1;
1288
+ ctx.fillStyle = '#e0d040';
1289
+ ctx.fillRect(x - 2, y - 2, w + 4, h + 4);
1290
+ ctx.restore();
1291
+ // Animated magnifier movement
1292
+ const mx = x + w / 2 + Math.sin(t * 2) * 8;
1293
+ const my = y + 4 + Math.cos(t * 1.5) * 3;
1294
+ ctx.save();
1295
+ ctx.globalAlpha = 0.7;
1296
+ ctx.strokeStyle = '#e0d040';
1297
+ ctx.lineWidth = 1;
1298
+ ctx.beginPath();
1299
+ ctx.arc(mx, my, 5, 0, Math.PI * 2);
1300
+ ctx.stroke();
1301
+ ctx.restore();
1302
+ }
1303
+
1304
+ _drawDeployDock(ctx, x, y, w, h, stage = '', t = 0, truckAnimActive = false) {
1305
+ ctx.fillStyle = PAL.dockBg;
1306
+ ctx.fillRect(x + 2, y + 2, w - 4, h + 4);
1307
+ ctx.fillStyle = PAL.dockYel;
1308
+ for (let i = 0; i < w - 4; i += 8) {
1309
+ ctx.fillRect(x + 2 + i, y + 2, 4, 2);
1310
+ ctx.fillRect(x + 2 + i + 4, y + h + 2, 4, 2);
1311
+ }
1312
+ for (let i = 0; i < h + 2; i += 8) {
1313
+ ctx.fillRect(x + 2, y + 2 + i, 2, 4);
1314
+ ctx.fillRect(x + w - 4, y + 2 + i + 4, 2, 4);
1315
+ }
1316
+
1317
+ const isDeploying = stage === 'deploying';
1318
+ const isDeployed = stage === 'deployed';
1319
+
1320
+ // Hide static truck when animated truck is active
1321
+ if (!truckAnimActive) {
1322
+ // Truck drifts slightly when deploying
1323
+ const truckOffX = isDeploying ? Math.sin(t * 3) * 1.5 : 0;
1324
+ this._drawPixelTruck(ctx, x + w / 2 - 16 + truckOffX, y + 8, isDeploying || isDeployed, t);
1325
+ }
1326
+
1327
+ // Dock glow / warning when deploying
1328
+ if (isDeploying) {
1329
+ ctx.save();
1330
+ ctx.globalAlpha = 0.18 + Math.abs(Math.sin(t * 4)) * 0.15;
1331
+ ctx.fillStyle = '#ffcc00';
1332
+ ctx.fillRect(x, y, w, h + 6);
1333
+ ctx.restore();
1334
+ // VROOM label (only show when truck is not animating — it has its own label)
1335
+ if (!truckAnimActive) {
1336
+ ctx.save();
1337
+ ctx.globalAlpha = 0.7 + Math.sin(t * 5) * 0.3;
1338
+ renderPixelText(ctx, '🚀 DEPLOY!', x + w / 2, y + h - 4, '#ffcc00', 1);
1339
+ ctx.restore();
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ _drawPixelTruck(ctx, x, y, active = false, t = 0) {
1345
+ ctx.fillStyle = '#c8a050';
1346
+ ctx.fillRect(x, y, 20, 16);
1347
+ ctx.fillStyle = '#a88030';
1348
+ ctx.fillRect(x, y, 20, 2);
1349
+ ctx.fillRect(x, y, 2, 16);
1350
+ ctx.fillRect(x + 18, y, 2, 16);
1351
+ ctx.fillStyle = '#a88030';
1352
+ ctx.fillRect(x + 9, y, 2, 16);
1353
+ ctx.fillRect(x, y + 7, 20, 2);
1354
+ ctx.fillStyle = '#d04040';
1355
+ ctx.fillRect(x + 20, y + 4, 12, 12);
1356
+ ctx.fillStyle = '#80c0e0';
1357
+ ctx.fillRect(x + 22, y + 6, 8, 6);
1358
+ ctx.fillStyle = '#222';
1359
+ ctx.fillRect(x + 2, y + 16, 6, 4);
1360
+ ctx.fillRect(x + 24, y + 16, 6, 4);
1361
+ ctx.fillStyle = '#666';
1362
+ ctx.fillRect(x + 4, y + 17, 2, 2);
1363
+ ctx.fillRect(x + 26, y + 17, 2, 2);
1364
+ // Headlights flash when active
1365
+ if (active) {
1366
+ const on = Math.sin(t * 6) > 0;
1367
+ ctx.fillStyle = on ? '#ffffc0' : '#806030';
1368
+ ctx.fillRect(x + 2, y + 4, 4, 3);
1369
+ ctx.fillRect(x + 2, y + 9, 4, 3);
1370
+ if (on) {
1371
+ ctx.save();
1372
+ ctx.globalAlpha = 0.4;
1373
+ ctx.fillStyle = '#ffffa0';
1374
+ ctx.fillRect(x - 6, y + 3, 8, 10);
1375
+ ctx.restore();
1376
+ }
1377
+ }
1378
+ }
1379
+
1380
+ _drawCoffeeMachine(ctx, x, y) {
1381
+ // Coffee table
1382
+ ctx.fillStyle = PAL.desk;
1383
+ ctx.fillRect(x + 2, y + 20, 28, 16);
1384
+ ctx.fillStyle = PAL.deskDk;
1385
+ ctx.fillRect(x + 2, y + 34, 28, 3);
1386
+ // Table legs
1387
+ ctx.fillStyle = PAL.deskDk;
1388
+ ctx.fillRect(x + 4, y + 36, 3, 8);
1389
+ ctx.fillRect(x + 25, y + 36, 3, 8);
1390
+
1391
+ // Coffee machine on table
1392
+ ctx.fillStyle = '#606068';
1393
+ ctx.fillRect(x + 6, y + 6, 20, 16);
1394
+ ctx.fillStyle = '#505058';
1395
+ ctx.fillRect(x + 8, y + 8, 16, 12);
1396
+ // Screen
1397
+ ctx.fillStyle = '#40a040';
1398
+ ctx.fillRect(x + 10, y + 9, 12, 5);
1399
+ // Label uses native text for clean rendering
1400
+ ctx.fillStyle = '#1a3a1a';
1401
+ ctx.font = 'bold 5px monospace';
1402
+ ctx.textAlign = 'center';
1403
+ ctx.fillText('COFFEE', x + 16, y + 13);
1404
+ ctx.textAlign = 'start';
1405
+ // Dispenser
1406
+ ctx.fillStyle = '#404048';
1407
+ ctx.fillRect(x + 10, y + 16, 12, 6);
1408
+ // Cup
1409
+ ctx.fillStyle = '#f0f0e8';
1410
+ ctx.fillRect(x + 13, y + 18, 6, 5);
1411
+ ctx.fillStyle = '#6a3a1a';
1412
+ ctx.fillRect(x + 14, y + 19, 4, 3);
1413
+ // Steam
1414
+ ctx.fillStyle = '#ffffff40';
1415
+ ctx.fillRect(x + 14, y + 15, 1, 3);
1416
+ ctx.fillRect(x + 17, y + 14, 1, 3);
1417
+ }
1418
+
1419
+ // ── Phase 4: PM Desk Lamp ──────────────────────────────────
1420
+
1421
+ _drawDeskLamp(ctx, x, y, stage = '', t = 0) {
1422
+ const isActive = stage === 'spec' || stage === 'review';
1423
+
1424
+ // Lamp base (4x2 px dark gray)
1425
+ ctx.fillStyle = '#505058';
1426
+ ctx.fillRect(x, y + 18, 8, 4);
1427
+
1428
+ // Arm (angled, ~10px tall)
1429
+ ctx.fillStyle = '#606068';
1430
+ ctx.save();
1431
+ ctx.translate(x + 4, y + 18);
1432
+ ctx.rotate(-0.35); // slight angle
1433
+ ctx.fillRect(-1, -12, 3, 12);
1434
+ ctx.restore();
1435
+
1436
+ // Shade (trapezoid-ish rectangle at top of arm)
1437
+ const shadeX = x + 1;
1438
+ const shadeY = y + 2;
1439
+ ctx.fillStyle = '#707858';
1440
+ ctx.fillRect(shadeX - 2, shadeY, 14, 5);
1441
+ ctx.fillStyle = '#606850';
1442
+ ctx.fillRect(shadeX - 1, shadeY + 5, 12, 2);
1443
+
1444
+ if (isActive) {
1445
+ // Pulsing yellow glow under shade
1446
+ const pulse = 0.5 + Math.sin(t * 3) * 0.3;
1447
+ ctx.save();
1448
+ ctx.globalAlpha = pulse;
1449
+ // Bulb glow
1450
+ ctx.fillStyle = '#ffe860';
1451
+ ctx.fillRect(shadeX + 2, shadeY + 5, 6, 3);
1452
+ // Light cone (semi-transparent triangle pointing down)
1453
+ ctx.globalAlpha = pulse * 0.35;
1454
+ ctx.fillStyle = '#ffee80';
1455
+ ctx.beginPath();
1456
+ ctx.moveTo(shadeX + 1, shadeY + 7);
1457
+ ctx.lineTo(shadeX + 9, shadeY + 7);
1458
+ ctx.lineTo(shadeX + 12, y + 20);
1459
+ ctx.lineTo(shadeX - 2, y + 20);
1460
+ ctx.closePath();
1461
+ ctx.fill();
1462
+ // Soft glow circle
1463
+ ctx.globalAlpha = pulse * 0.15;
1464
+ ctx.fillStyle = '#ffee60';
1465
+ ctx.beginPath();
1466
+ ctx.arc(shadeX + 5, y + 14, 10, 0, Math.PI * 2);
1467
+ ctx.fill();
1468
+ ctx.restore();
1469
+ } else {
1470
+ // Inactive: dim bulb
1471
+ ctx.fillStyle = '#404040';
1472
+ ctx.fillRect(shadeX + 3, shadeY + 5, 4, 2);
1473
+ }
1474
+ }
1475
+
1476
+ _drawChair(ctx, x, y) {
1477
+ ctx.fillStyle = '#505068';
1478
+ ctx.fillRect(x - 5, y - 3, 10, 8);
1479
+ ctx.fillStyle = '#404058';
1480
+ ctx.fillRect(x - 4, y - 6, 8, 4);
1481
+ ctx.fillStyle = '#606078';
1482
+ ctx.fillRect(x - 4, y - 2, 8, 6);
1483
+ }
1484
+
1485
+ hitTest(wx, wy) {
1486
+ for (const [name, origin] of this._teamOrigins) {
1487
+ const ox = origin.x;
1488
+ const oy = origin.y;
1489
+ if (wx >= ox && wx <= ox + TEAM_WIDTH * TILE && wy >= oy && wy <= oy + TEAM_HEIGHT * TILE) {
1490
+ return { project: name, config: origin.config };
1491
+ }
1492
+ }
1493
+ return null;
1494
+ }
1495
+
1496
+ /** Hit-test warehouse packages; returns { project, pkg } or null */
1497
+ hitTestPackage(wx, wy) {
1498
+ for (const [name, origin] of this._teamOrigins) {
1499
+ const data = this._projectData.get(name);
1500
+ if (!data || !data.packages.length) continue;
1501
+ const wh = STATIONS.warehouse;
1502
+ const ox = origin.x + wh.x * TILE;
1503
+ const oy = origin.y + wh.y * TILE;
1504
+ const w = wh.w * TILE;
1505
+ for (let i = 0; i < data.packages.length; i++) {
1506
+ const row = Math.floor(i / 4);
1507
+ const col = i % 4;
1508
+ if (row >= 3) break;
1509
+ const px = ox + 18 + col * ((w - 36) / 4);
1510
+ const py = oy + 14 + row * (TILE + 14) - 14;
1511
+ if (wx >= px && wx <= px + 22 && wy >= py && wy <= py + 16) {
1512
+ return { project: name, pkg: data.packages[i], index: i, total: data.packages.length };
1513
+ }
1514
+ }
1515
+ }
1516
+ return null;
1517
+ }
1518
+
1519
+ getTeamOrigin(projectName) {
1520
+ return this._teamOrigins.get(projectName);
1521
+ }
1522
+ }
1523
+
1524
+ export { STATIONS, TILE, TEAM_WIDTH, TEAM_HEIGHT };