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,632 @@
1
+ /**
2
+ * Effects System — particles, bubbles, overlays for character states
3
+ * All rendered in pixel art style to match the retro aesthetic
4
+ */
5
+
6
+ import { renderSprite, renderPixelText, PIXEL_SIZE } from './sprite-renderer.js';
7
+ import { BUG_SPRITE, MAGNIFIER_SPRITE, DOCUMENT_SPRITE, COFFEE_SPRITE } from './pixel-sprites.js';
8
+
9
+ // ============================================================
10
+ // Particle base class
11
+ // ============================================================
12
+ class Particle {
13
+ constructor(x, y, life = 1) {
14
+ this.x = x;
15
+ this.y = y;
16
+ this.life = life;
17
+ this.maxLife = life;
18
+ this.dead = false;
19
+ }
20
+ update(dt) {
21
+ this.life -= dt;
22
+ if (this.life <= 0) this.dead = true;
23
+ }
24
+ render(ctx) {}
25
+ }
26
+
27
+ // ============================================================
28
+ // Z Particle — floating Z for sleeping characters
29
+ // ============================================================
30
+ class ZParticle extends Particle {
31
+ constructor(x, y) {
32
+ super(x, y, 2.0);
33
+ this.startX = x;
34
+ this.vx = (Math.random() - 0.5) * 8;
35
+ this.vy = -15;
36
+ this.size = 1 + Math.random() * 1.5;
37
+ }
38
+ update(dt) {
39
+ super.update(dt);
40
+ this.x += this.vx * dt;
41
+ this.y += this.vy * dt;
42
+ this.vy *= 0.98;
43
+ }
44
+ render(ctx) {
45
+ const alpha = Math.min(1, this.life / 0.5);
46
+ ctx.save();
47
+ ctx.globalAlpha = alpha;
48
+ ctx.fillStyle = '#a0c0ff';
49
+ const s = this.size * PIXEL_SIZE;
50
+ // Draw a pixelated Z
51
+ ctx.fillRect(this.x, this.y, s * 3, s);
52
+ ctx.fillRect(this.x + s * 2, this.y + s, s, s);
53
+ ctx.fillRect(this.x + s, this.y + s * 2, s, s);
54
+ ctx.fillRect(this.x, this.y + s * 3, s * 3, s);
55
+ ctx.restore();
56
+ }
57
+ }
58
+
59
+ // ============================================================
60
+ // Confetti Particle — celebrating
61
+ // ============================================================
62
+ const CONFETTI_COLORS = ['#ff4060', '#40c0ff', '#ffdd20', '#40ff80', '#ff80c0', '#ffa020'];
63
+
64
+ class ConfettiParticle extends Particle {
65
+ constructor(x, y) {
66
+ super(x, y, 1.5 + Math.random());
67
+ this.vx = (Math.random() - 0.5) * 60;
68
+ this.vy = -40 - Math.random() * 30;
69
+ this.gravity = 80;
70
+ this.color = CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)];
71
+ this.size = PIXEL_SIZE;
72
+ this.rotation = Math.random() * Math.PI * 2;
73
+ this.rotSpeed = (Math.random() - 0.5) * 8;
74
+ }
75
+ update(dt) {
76
+ super.update(dt);
77
+ this.x += this.vx * dt;
78
+ this.vy += this.gravity * dt;
79
+ this.y += this.vy * dt;
80
+ this.rotation += this.rotSpeed * dt;
81
+ }
82
+ render(ctx) {
83
+ const alpha = Math.min(1, this.life / 0.3);
84
+ ctx.save();
85
+ ctx.globalAlpha = alpha;
86
+ ctx.fillStyle = this.color;
87
+ ctx.translate(this.x, this.y);
88
+ ctx.rotate(this.rotation);
89
+ ctx.fillRect(-this.size, -this.size / 2, this.size * 2, this.size);
90
+ ctx.restore();
91
+ }
92
+ }
93
+
94
+ // ============================================================
95
+ // Steam Particle — frustrated / rate limited
96
+ // ============================================================
97
+ class SteamParticle extends Particle {
98
+ constructor(x, y) {
99
+ super(x, y, 1.2);
100
+ this.vx = (Math.random() - 0.5) * 10;
101
+ this.vy = -20 - Math.random() * 10;
102
+ this.size = PIXEL_SIZE;
103
+ }
104
+ update(dt) {
105
+ super.update(dt);
106
+ this.x += this.vx * dt;
107
+ this.y += this.vy * dt;
108
+ this.size += dt * 2;
109
+ }
110
+ render(ctx) {
111
+ const alpha = (this.life / this.maxLife) * 0.6;
112
+ ctx.save();
113
+ ctx.globalAlpha = alpha;
114
+ ctx.fillStyle = '#e04040';
115
+ ctx.fillRect(this.x - this.size, this.y - this.size, this.size * 2, this.size * 2);
116
+ ctx.restore();
117
+ }
118
+ }
119
+
120
+ // ============================================================
121
+ // Spark Particle — working/typing sparks
122
+ // ============================================================
123
+ class SparkParticle extends Particle {
124
+ constructor(x, y) {
125
+ super(x, y, 0.4 + Math.random() * 0.3);
126
+ this.vx = (Math.random() - 0.5) * 30;
127
+ this.vy = -10 - Math.random() * 20;
128
+ this.color = Math.random() > 0.5 ? '#80ff80' : '#60d060';
129
+ }
130
+ update(dt) {
131
+ super.update(dt);
132
+ this.x += this.vx * dt;
133
+ this.y += this.vy * dt;
134
+ }
135
+ render(ctx) {
136
+ ctx.fillStyle = this.color;
137
+ ctx.fillRect(this.x, this.y, PIXEL_SIZE, PIXEL_SIZE);
138
+ }
139
+ }
140
+
141
+ // ============================================================
142
+ // Firework Burst Particle — deploy celebration
143
+ // ============================================================
144
+ const FW_COLORS = ['#ff4060','#ff8020','#ffdd20','#40ff80','#40c0ff','#c060ff','#ff60c0','#ffffff'];
145
+
146
+ class FireworkParticle extends Particle {
147
+ constructor(x, y) {
148
+ super(x, y, 0.8 + Math.random() * 0.8);
149
+ const angle = Math.random() * Math.PI * 2;
150
+ const speed = 40 + Math.random() * 80;
151
+ this.vx = Math.cos(angle) * speed;
152
+ this.vy = Math.sin(angle) * speed - 20;
153
+ this.gravity = 60;
154
+ this.color = FW_COLORS[Math.floor(Math.random() * FW_COLORS.length)];
155
+ this.trail = [];
156
+ }
157
+ update(dt) {
158
+ this.trail.push({ x: this.x, y: this.y, a: this.life / this.maxLife });
159
+ if (this.trail.length > 5) this.trail.shift();
160
+ super.update(dt);
161
+ this.x += this.vx * dt;
162
+ this.vy += this.gravity * dt;
163
+ this.y += this.vy * dt;
164
+ }
165
+ render(ctx) {
166
+ const alpha = Math.max(0, this.life / this.maxLife);
167
+ for (let i = 0; i < this.trail.length; i++) {
168
+ const t = this.trail[i];
169
+ ctx.save();
170
+ ctx.globalAlpha = t.a * 0.4;
171
+ ctx.fillStyle = this.color;
172
+ ctx.fillRect(t.x - 1, t.y - 1, 2, 2);
173
+ ctx.restore();
174
+ }
175
+ ctx.save();
176
+ ctx.globalAlpha = alpha;
177
+ ctx.fillStyle = this.color;
178
+ ctx.fillRect(this.x - 2, this.y - 2, 4, 4);
179
+ ctx.restore();
180
+ }
181
+ }
182
+
183
+ // ============================================================
184
+ // Effects Manager
185
+ // ============================================================
186
+ export class EffectsManager {
187
+ constructor() {
188
+ this.particles = [];
189
+ this.timers = {};
190
+ // Deploy banner
191
+ this.bannerTimer = 0;
192
+ this.bannerText = '';
193
+ this.bannerSub = '';
194
+ }
195
+
196
+ update(dt) {
197
+ // Update all particles
198
+ for (let i = this.particles.length - 1; i >= 0; i--) {
199
+ this.particles[i].update(dt);
200
+ if (this.particles[i].dead) {
201
+ this.particles.splice(i, 1);
202
+ }
203
+ }
204
+ // Update timers
205
+ for (const key in this.timers) {
206
+ this.timers[key] -= dt;
207
+ }
208
+ // Banner countdown
209
+ if (this.bannerTimer > 0) this.bannerTimer -= dt;
210
+ }
211
+
212
+ render(ctx) {
213
+ for (const p of this.particles) {
214
+ p.render(ctx);
215
+ }
216
+ // Draw deploy banner overlay
217
+ if (this.bannerTimer > 0) {
218
+ const alpha = Math.min(1, this.bannerTimer / 0.5);
219
+ const cw = ctx.canvas.width;
220
+ ctx.save();
221
+ ctx.globalAlpha = alpha * 0.92;
222
+ ctx.fillStyle = '#1a2a1a';
223
+ ctx.fillRect(cw / 2 - 160, 28, 320, 44);
224
+ ctx.strokeStyle = '#40d060';
225
+ ctx.lineWidth = 2;
226
+ ctx.strokeRect(cw / 2 - 160, 28, 320, 44);
227
+ ctx.globalAlpha = alpha;
228
+ ctx.fillStyle = '#40ff70';
229
+ ctx.font = 'bold 13px "Press Start 2P", monospace';
230
+ ctx.textAlign = 'center';
231
+ ctx.fillText('🎉 SHIPPED!', cw / 2, 50);
232
+ ctx.fillStyle = '#a0e0b0';
233
+ ctx.font = '8px "Press Start 2P", monospace';
234
+ ctx.fillText(this.bannerSub, cw / 2, 65);
235
+ ctx.restore();
236
+ }
237
+ }
238
+
239
+ /** Launch firework bursts at canvas position (cx, cy) */
240
+ launchFireworks(cx, cy, count = 20) {
241
+ for (let wave = 0; wave < 3; wave++) {
242
+ const delay = wave * 0.35;
243
+ // Use timers trick — spawn burst after delay
244
+ const key = `fw_wave_${wave}_${Date.now()}`;
245
+ this.timers[key] = -delay; // negative = fire immediately when <= 0 next frame
246
+ }
247
+ // Spawn immediately for wave 0, then schedule waves
248
+ this._spawnBurst(cx, cy, count);
249
+ this._spawnBurst(cx + (Math.random() - 0.5) * 80, cy - 20, count);
250
+ setTimeout(() => this._spawnBurst(cx + (Math.random() - 0.5) * 60, cy + 10, count), 350);
251
+ setTimeout(() => this._spawnBurst(cx + (Math.random() - 0.5) * 100, cy - 40, count), 700);
252
+ }
253
+
254
+ _spawnBurst(cx, cy, count) {
255
+ for (let i = 0; i < count; i++) {
256
+ this.particles.push(new FireworkParticle(cx, cy));
257
+ }
258
+ for (let i = 0; i < 6; i++) {
259
+ this.particles.push(new ConfettiParticle(cx + (Math.random() - 0.5) * 40, cy));
260
+ }
261
+ }
262
+
263
+ /** Show a deploy success banner for duration seconds */
264
+ showBanner(text, sub = '', duration = 4) {
265
+ this.bannerText = text;
266
+ this.bannerSub = sub;
267
+ this.bannerTimer = duration;
268
+ }
269
+
270
+ // --- Spawn methods ---
271
+
272
+ spawnSleepZ(x, y) {
273
+ const key = `z_${x}_${y}`;
274
+ if (this.timers[key] > 0) return;
275
+ this.timers[key] = 0.8;
276
+ this.particles.push(new ZParticle(x, y));
277
+ }
278
+
279
+ spawnConfetti(x, y, count = 12) {
280
+ const key = `conf_${x}_${y}`;
281
+ if (this.timers[key] > 0) return;
282
+ this.timers[key] = 0.3;
283
+ for (let i = 0; i < count; i++) {
284
+ this.particles.push(new ConfettiParticle(x + (Math.random() - 0.5) * 10, y));
285
+ }
286
+ }
287
+
288
+ spawnSteam(x, y) {
289
+ const key = `steam_${x}_${y}`;
290
+ if (this.timers[key] > 0) return;
291
+ this.timers[key] = 0.4;
292
+ this.particles.push(new SteamParticle(x + (Math.random() - 0.5) * 6, y));
293
+ this.particles.push(new SteamParticle(x + (Math.random() - 0.5) * 6, y));
294
+ }
295
+
296
+ spawnTypingSparks(x, y) {
297
+ const key = `spark_${x}_${y}`;
298
+ if (this.timers[key] > 0) return;
299
+ this.timers[key] = 0.15;
300
+ this.particles.push(new SparkParticle(x + (Math.random() - 0.5) * 16, y));
301
+ }
302
+
303
+ /**
304
+ * Render state-specific overlays on a character
305
+ * Called per-character after sprite rendering
306
+ */
307
+ renderStateOverlay(ctx, state, x, y, stateTime, isAtDesk = true) {
308
+ const pw = PIXEL_SIZE;
309
+
310
+ switch (state) {
311
+ case 'sleeping':
312
+ this.spawnSleepZ(x + 8, y - 36);
313
+ break;
314
+
315
+ case 'celebrating':
316
+ this.spawnConfetti(x, y - 40);
317
+ break;
318
+
319
+ case 'frustrated':
320
+ case 'rate_limited':
321
+ this.spawnSteam(x, y - 38);
322
+ // Exclamation mark
323
+ ctx.fillStyle = '#ff4040';
324
+ ctx.fillRect(x - pw, y - 42, pw * 2, pw * 6);
325
+ ctx.fillRect(x - pw, y - 34, pw * 2, pw * 2);
326
+ break;
327
+
328
+ case 'working':
329
+ case 'designing':
330
+ // Only show work effects when seated at desk, not while walking
331
+ if (isAtDesk) {
332
+ this.spawnTypingSparks(x, y - 8);
333
+ this.renderLightbulb(ctx, x, y - 56, stateTime, 1.0);
334
+ }
335
+ break;
336
+
337
+ case 'thinking':
338
+ // Thought bubble with "..."
339
+ this._drawThoughtBubble(ctx, x, y - 42);
340
+ break;
341
+
342
+ case 'waiting_input':
343
+ // Question mark bubble
344
+ this._drawQuestionBubble(ctx, x, y - 44);
345
+ break;
346
+
347
+ case 'reviewing':
348
+ if (isAtDesk) {
349
+ // Magnifying glass floating
350
+ renderSprite(ctx, MAGNIFIER_SPRITE, x + 16, y - 12, { scale: 0.8 });
351
+ this.renderLightbulb(ctx, x, y - 56, stateTime, 1.0);
352
+ }
353
+ break;
354
+
355
+ case 'fighting_bug': {
356
+ if (isAtDesk) {
357
+ this._renderBugBattle(ctx, x, y, stateTime);
358
+ }
359
+ break;
360
+ }
361
+
362
+ case 'handing_off': {
363
+ // Document floating to the right
364
+ const docX = x + 14 + Math.sin(stateTime * 2) * 4;
365
+ renderSprite(ctx, DOCUMENT_SPRITE, docX, y - 14);
366
+ break;
367
+ }
368
+
369
+ case 'coffee_break':
370
+ renderSprite(ctx, COFFEE_SPRITE, x + 14, y - 10);
371
+ break;
372
+
373
+ case 'deploying':
374
+ // Package above head
375
+ ctx.fillStyle = '#c8a050';
376
+ ctx.fillRect(x - 6, y - 42, 12, 10);
377
+ ctx.strokeStyle = '#a08030';
378
+ ctx.strokeRect(x - 6, y - 42, 12, 10);
379
+ // Cross tape
380
+ ctx.fillStyle = '#a08030';
381
+ ctx.fillRect(x - 1, y - 42, 2, 10);
382
+ ctx.fillRect(x - 6, y - 38, 12, 2);
383
+ break;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Render bug battle animation — coder holds keyboard to fight green virus bugs
389
+ */
390
+ _renderBugBattle(ctx, x, y, stateTime) {
391
+ const pw = PIXEL_SIZE;
392
+
393
+ // ── Green virus bugs (2-3 floating around) ──
394
+ for (let i = 0; i < 3; i++) {
395
+ const phase = i * (Math.PI * 2 / 3);
396
+ const bx = x + 20 + Math.sin(stateTime * 3 + phase) * 12;
397
+ const by = y - 8 + Math.cos(stateTime * 2.5 + phase) * 8;
398
+ // Hit flash — virus blinks white when keyboard swings
399
+ const isHit = Math.sin(stateTime * 6 + i) > 0.8;
400
+ this._drawVirusBug(ctx, bx, by, stateTime, isHit);
401
+ }
402
+
403
+ // ── Keyboard weapon (held by coder, swings at bugs) ──
404
+ const swingAngle = Math.sin(stateTime * 6) * 0.5;
405
+ ctx.save();
406
+ ctx.translate(x + 6, y - 6);
407
+ ctx.rotate(swingAngle);
408
+ this._drawKeyboard(ctx, 0, 0);
409
+ ctx.restore();
410
+
411
+ // ── Impact sparks on hit frames ──
412
+ if (Math.sin(stateTime * 6) > 0.7) {
413
+ const sparkX = x + 20 + Math.sin(stateTime * 3) * 10;
414
+ const sparkY = y - 8;
415
+ ctx.fillStyle = '#ffff40';
416
+ // Star burst
417
+ for (let i = 0; i < 4; i++) {
418
+ const angle = (stateTime * 10) + i * (Math.PI / 2);
419
+ const len = 3 + Math.random() * 3;
420
+ ctx.fillRect(
421
+ sparkX + Math.cos(angle) * len,
422
+ sparkY + Math.sin(angle) * len,
423
+ pw, pw
424
+ );
425
+ }
426
+ }
427
+
428
+ // ── Frantic lightbulb ──
429
+ this.renderLightbulb(ctx, x, y - 56, stateTime * 2, 1.0);
430
+
431
+ // ── Damage numbers floating up ──
432
+ if (Math.sin(stateTime * 4) > 0.9) {
433
+ const dmg = Math.floor(Math.random() * 50 + 10);
434
+ const numY = y - 20 - (stateTime % 1) * 15;
435
+ ctx.save();
436
+ ctx.fillStyle = '#ff4040';
437
+ ctx.font = 'bold 7px "Press Start 2P", monospace';
438
+ ctx.textAlign = 'center';
439
+ ctx.globalAlpha = Math.max(0, 1 - (stateTime % 1));
440
+ ctx.fillText(`-${dmg}`, x + 24, numY);
441
+ ctx.restore();
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Draw a pixel-art green virus bug
447
+ */
448
+ _drawVirusBug(ctx, x, y, time, isHit) {
449
+ const pw = PIXEL_SIZE;
450
+ ctx.save();
451
+
452
+ // Virus body — green circle
453
+ const bodyColor = isHit ? '#ffffff' : '#40c040';
454
+ const eyeColor = isHit ? '#ff0000' : '#200020';
455
+
456
+ ctx.fillStyle = bodyColor;
457
+ // Body (5x5 rounded)
458
+ ctx.fillRect(x - 2, y - 1, 5, 3);
459
+ ctx.fillRect(x - 1, y - 2, 3, 5);
460
+
461
+ // Eyes — angry
462
+ ctx.fillStyle = eyeColor;
463
+ ctx.fillRect(x - 1, y - 1, pw, pw);
464
+ ctx.fillRect(x + 1, y - 1, pw, pw);
465
+
466
+ // Spikes — rotating around body
467
+ ctx.fillStyle = isHit ? '#ffaaaa' : '#20a020';
468
+ for (let i = 0; i < 6; i++) {
469
+ const angle = time * 3 + i * (Math.PI / 3);
470
+ const sx = x + Math.cos(angle) * 5;
471
+ const sy = y + Math.sin(angle) * 5;
472
+ ctx.fillRect(sx, sy, pw, pw);
473
+ }
474
+
475
+ ctx.restore();
476
+ }
477
+
478
+ /**
479
+ * Draw a pixel-art keyboard (weapon)
480
+ */
481
+ _drawKeyboard(ctx, x, y) {
482
+ const pw = PIXEL_SIZE;
483
+ ctx.save();
484
+
485
+ // Keyboard body
486
+ ctx.fillStyle = '#c0c0c0';
487
+ ctx.fillRect(x, y, 14, 6);
488
+
489
+ // Border
490
+ ctx.strokeStyle = '#808080';
491
+ ctx.lineWidth = 0.5;
492
+ ctx.strokeRect(x, y, 14, 6);
493
+
494
+ // Keys (tiny squares)
495
+ ctx.fillStyle = '#404040';
496
+ for (let r = 0; r < 2; r++) {
497
+ for (let c = 0; c < 5; c++) {
498
+ ctx.fillRect(x + 1 + c * 2.5, y + 1 + r * 2.5, 1.5, 1.5);
499
+ }
500
+ }
501
+
502
+ ctx.restore();
503
+ }
504
+
505
+ /**
506
+ * Render a pixel-art lightbulb above a character's head.
507
+ * Pulses between bright and dim to simulate thinking.
508
+ * @param {CanvasRenderingContext2D} ctx
509
+ * @param {number} x - center x of the bulb
510
+ * @param {number} y - top y of the bulb
511
+ * @param {number} time - stateTime (used for animation)
512
+ * @param {number} intensity - 0..1 overall brightness multiplier
513
+ */
514
+ renderLightbulb(ctx, x, y, time, intensity = 1.0) {
515
+ const pw = PIXEL_SIZE;
516
+ // Pulse factor: oscillates 0..1 at ~2Hz (time * 4 → sin period ~1.57s)
517
+ const pulse = (Math.sin(time * 4) + 1) / 2; // 0..1
518
+ const bright = pulse > 0.5;
519
+
520
+ const bulbColor = bright ? '#ffe040' : '#806020';
521
+ const glowColor = '#ffe040';
522
+
523
+ ctx.save();
524
+
525
+ // --- Glow circle (semi-transparent, only when bright) ---
526
+ if (bright) {
527
+ ctx.globalAlpha = 0.15 * intensity;
528
+ ctx.fillStyle = glowColor;
529
+ ctx.beginPath();
530
+ ctx.arc(x, y + 4, 12, 0, Math.PI * 2);
531
+ ctx.fill();
532
+ }
533
+
534
+ ctx.globalAlpha = intensity;
535
+
536
+ // --- Radiating lines (4 directions, only when bright) ---
537
+ if (bright) {
538
+ ctx.fillStyle = '#ffe080';
539
+ const lineLen = pw * 2;
540
+ // Top
541
+ ctx.fillRect(x - 0.5, y - 5, 1, -lineLen);
542
+ // Bottom-left
543
+ ctx.fillRect(x - 6, y + 4 - 0.5, -lineLen, 1);
544
+ // Bottom-right
545
+ ctx.fillRect(x + 6, y + 4 - 0.5, lineLen, 1);
546
+ // Top-left diagonal
547
+ ctx.fillRect(x - 5, y - 3, -pw, 1);
548
+ ctx.fillRect(x - 5, y - 3, 1, -pw);
549
+ // Top-right diagonal
550
+ ctx.fillRect(x + 5, y - 3, pw, 1);
551
+ ctx.fillRect(x + 5, y - 3, 1, -pw);
552
+ }
553
+
554
+ // --- Bulb body (5x6 px, roughly rounded) ---
555
+ ctx.fillStyle = bulbColor;
556
+ // Row 0 (top) — 3px wide, centered
557
+ ctx.fillRect(x - 1.5, y, 3, 1);
558
+ // Row 1 — 5px wide
559
+ ctx.fillRect(x - 2.5, y + 1, 5, 1);
560
+ // Row 2 — 5px wide
561
+ ctx.fillRect(x - 2.5, y + 2, 5, 1);
562
+ // Row 3 — 5px wide
563
+ ctx.fillRect(x - 2.5, y + 3, 5, 1);
564
+ // Row 4 — 3px wide (taper)
565
+ ctx.fillRect(x - 1.5, y + 4, 3, 1);
566
+ // Row 5 — 3px wide (taper)
567
+ ctx.fillRect(x - 1.5, y + 5, 3, 1);
568
+
569
+ // --- Socket/base (3x2 px, gray) ---
570
+ ctx.fillStyle = '#888888';
571
+ ctx.fillRect(x - 1.5, y + 6, 3, 1);
572
+ ctx.fillStyle = '#666666';
573
+ ctx.fillRect(x - 1.5, y + 7, 3, 1);
574
+
575
+ ctx.restore();
576
+ }
577
+
578
+ _drawThoughtBubble(ctx, x, y) {
579
+ const pw = PIXEL_SIZE;
580
+ // Bubble
581
+ ctx.fillStyle = '#fff';
582
+ ctx.fillRect(x - 10, y, 20, 10);
583
+ ctx.fillRect(x - 12, y + 2, 24, 6);
584
+ // Border
585
+ ctx.fillStyle = '#555';
586
+ ctx.fillRect(x - 10, y - 1, 20, 1);
587
+ ctx.fillRect(x - 10, y + 10, 20, 1);
588
+ ctx.fillRect(x - 13, y + 2, 1, 6);
589
+ ctx.fillRect(x + 12, y + 2, 1, 6);
590
+ // Dots (...)
591
+ ctx.fillStyle = '#333';
592
+ ctx.fillRect(x - 5, y + 4, pw, pw);
593
+ ctx.fillRect(x, y + 4, pw, pw);
594
+ ctx.fillRect(x + 5, y + 4, pw, pw);
595
+ // Tail dots
596
+ ctx.fillStyle = '#fff';
597
+ ctx.fillRect(x - 2, y + 12, 3, 3);
598
+ ctx.fillRect(x + 2, y + 16, 2, 2);
599
+ }
600
+
601
+ _drawQuestionBubble(ctx, x, y) {
602
+ const pw = PIXEL_SIZE;
603
+ // Bubble background
604
+ ctx.fillStyle = '#fff';
605
+ ctx.fillRect(x - 8, y, 16, 14);
606
+ ctx.fillRect(x - 10, y + 2, 20, 10);
607
+ // Border
608
+ ctx.fillStyle = '#555';
609
+ ctx.fillRect(x - 8, y - 1, 16, 1);
610
+ ctx.fillRect(x - 8, y + 14, 16, 1);
611
+ ctx.fillRect(x - 11, y + 2, 1, 10);
612
+ ctx.fillRect(x + 10, y + 2, 1, 10);
613
+ // Question mark
614
+ ctx.fillStyle = '#4080c0';
615
+ ctx.fillRect(x - 3, y + 2, 6, pw);
616
+ ctx.fillRect(x + 2, y + 3, pw, 3);
617
+ ctx.fillRect(x, y + 6, pw * 2, pw);
618
+ ctx.fillRect(x, y + 9, pw, pw);
619
+ ctx.fillRect(x, y + 11, pw, pw);
620
+ // Tail
621
+ ctx.fillStyle = '#fff';
622
+ ctx.fillRect(x - 1, y + 15, 3, 2);
623
+ ctx.fillRect(x + 1, y + 18, 2, 2);
624
+ }
625
+ }
626
+
627
+ // Singleton
628
+ let _instance = null;
629
+ export function getEffects() {
630
+ if (!_instance) _instance = new EffectsManager();
631
+ return _instance;
632
+ }