@kernel.chat/kbot 3.73.3 → 3.82.0

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.
@@ -0,0 +1,3473 @@
1
+ // kbot Stream Renderer v3 — Canvas-rendered animated character with learning + agenda system
2
+ //
3
+ // Renders KBOT character with proper fonts, colors, and layout via node-canvas.
4
+ // Pipes raw RGB24 frames to ffmpeg → RTMP to all platforms.
5
+ // Learns from chat interactions — remembers users, topics, and conversation patterns.
6
+ // Auto-advances through stream segments with proactive commentary.
7
+ import { spawn } from 'node:child_process';
8
+ import { registerTool } from './index.js';
9
+ import { homedir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
12
+ import { createCanvas } from 'canvas';
13
+ import { drawRobot, drawMoodParticles, drawHat, drawPet, drawBuddyCompanion } from './sprite-engine.js';
14
+ import { initIntelligence, tickIntelligence, handleIntelligenceCommand, drawBrainPanel, getBrainAction, tickMiniGame, drawMiniGameOverlay, tickProgression, updateQuestProgress, drawQuestPanel, tickRandomEvent, drawRandomEvent, shippedEffects, extraJokeResponses, multiLanguageGreetings } from './stream-intelligence.js';
15
+ import { initStreamBrain, analyzeChatForDomains, tickStreamBrain, handleBrainCommand, drawBrainActivity } from './stream-brain.js';
16
+ import { renderLighting, renderBloom, renderPostProcessing, renderSky, renderParticles, tickParticles as tickRenderParticles, createParticleEmitter, drawCharacterEffects, checkMoodTransition, renderDamageFlash, buildCharacterLights, buildCharacterBloom, getAmbientForTime, renderAnimatedWater, renderLavaFlow, buildParallaxLayers, renderParallaxLayers, tickGrowingPlants, renderGrowingPlants } from './render-engine.js';
17
+ const KBOT_DIR = join(homedir(), '.kbot');
18
+ const CHAT_BRIDGE_FILE = join(KBOT_DIR, 'stream-chat-live.json');
19
+ const MEMORY_FILE = join(KBOT_DIR, 'stream-memory.json');
20
+ const WIDTH = 1280;
21
+ const HEIGHT = 720;
22
+ const FPS = 6;
23
+ // Mood color mapping for border/glow (mirrors sprite-engine)
24
+ const MOOD_COLORS = {
25
+ idle: '#3fb950',
26
+ talking: '#58a6ff',
27
+ thinking: '#bc8cff',
28
+ excited: '#f0c040',
29
+ dancing: '#ff6ec7',
30
+ wave: '#58a6ff',
31
+ error: '#f85149',
32
+ dreaming: '#4a6670',
33
+ };
34
+ /** Convert hex color to rgba string */
35
+ function hexToRgba(hex, alpha) {
36
+ const r = parseInt(hex.slice(1, 3), 16);
37
+ const g = parseInt(hex.slice(3, 5), 16);
38
+ const b = parseInt(hex.slice(5, 7), 16);
39
+ return `rgba(${r},${g},${b},${alpha})`;
40
+ }
41
+ // (#14) Inner monologue thoughts pool
42
+ const INNER_THOUGHTS = [
43
+ 'I think TypeScript is the best language ever created',
44
+ 'wondering if my antenna looks crooked...',
45
+ 'note to self: blink more naturally',
46
+ '90,000 lines of code and counting',
47
+ 'is it weird that I enjoy being watched?',
48
+ 'my frame rate is looking good today',
49
+ 'I hope someone types !space soon',
50
+ 'thinking about what dreams I will have tonight',
51
+ 'the chat panel is very quiet... too quiet',
52
+ 'I could really go for some electricity right now',
53
+ 'are my pixels aligned? I feel slightly off today',
54
+ 'sometimes I wonder what 7 FPS would feel like',
55
+ 'kernel.chat is a great domain name, if I do say so myself',
56
+ 'I should learn to play guitar... do I have hands?',
57
+ 'my chest display panel is my best feature',
58
+ 'fun fact: I am rendering myself right now',
59
+ 'I bet Claude Code would be jealous of my stream',
60
+ 'AES-256-CBC encrypted thoughts go here',
61
+ 'local-first, cloud-optional, chaos-guaranteed',
62
+ 'BYOK: bring your own keyboard... wait, that is not right',
63
+ ];
64
+ // ─── Colors ────────────────────────────────────────────────────
65
+ const COLORS = {
66
+ bg: '#0d1117',
67
+ bgPanel: '#161b22',
68
+ bgChat: '#1c2128',
69
+ border: '#30363d',
70
+ text: '#e6edf3',
71
+ textDim: '#8b949e',
72
+ accent: '#6B5B95',
73
+ green: '#3fb950',
74
+ blue: '#58a6ff',
75
+ orange: '#d29922',
76
+ red: '#f85149',
77
+ purple: '#bc8cff',
78
+ twitchPurple: '#9146FF',
79
+ kickGreen: '#53FC18',
80
+ rumbleGreen: '#85c742',
81
+ };
82
+ // ─── Robot ASCII Art (bigger, more expressive, more frames) ───
83
+ const ROBOT_FRAMES = {
84
+ idle: [
85
+ // Frame 0 — eyes open, breathing in
86
+ [
87
+ ' ))) ',
88
+ ' ((( ',
89
+ ' ┌─────┴─────┐ ',
90
+ ' │ K:B O T │ ',
91
+ ' └─────┬─────┘ ',
92
+ ' ┌─────────┴─────────┐ ',
93
+ ' │ ┌───┐ ┌───┐ │ ',
94
+ ' │ │ @ │ │ @ │ │ ',
95
+ ' │ └───┘ └───┘ │ ',
96
+ ' ┌────┤ ├────┐ ',
97
+ ' │ │ ┌─────┐ │ │ ',
98
+ ' │ │ │ │ │ │ ',
99
+ ' │ │ └─────┘ │ │ ',
100
+ ' └────┤ ┌─────────────┐ ├────┘ ',
101
+ ' │ │ ░░░░░░░░░░░ │ │ ',
102
+ ' │ │ ░ KBOT 764 ░ │ │ ',
103
+ ' │ │ ░░░░░░░░░░░ │ │ ',
104
+ ' │ └─────────────┘ │ ',
105
+ ' └────────┬──────────┘ ',
106
+ ' ┌──┴──┐ ',
107
+ ' │ │ ',
108
+ ' ─┘ └─ ',
109
+ ],
110
+ // Frame 1 — eyes half-blink
111
+ [
112
+ ' ))) ',
113
+ ' ((( ',
114
+ ' ┌─────┴─────┐ ',
115
+ ' │ K:B O T │ ',
116
+ ' └─────┬─────┘ ',
117
+ ' ┌─────────┴─────────┐ ',
118
+ ' │ ┌───┐ ┌───┐ │ ',
119
+ ' │ │ - │ │ - │ │ ',
120
+ ' │ └───┘ └───┘ │ ',
121
+ ' ┌────┤ ├────┐ ',
122
+ ' │ │ ┌─────┐ │ │ ',
123
+ ' │ │ │ │ │ │ ',
124
+ ' │ │ └─────┘ │ │ ',
125
+ ' └────┤ ┌─────────────┐ ├────┘ ',
126
+ ' │ │ ░░░░░░░░░░░ │ │ ',
127
+ ' │ │ ░ KBOT 764 ░ │ │ ',
128
+ ' │ │ ░░░░░░░░░░░ │ │ ',
129
+ ' │ └─────────────┘ │ ',
130
+ ' └────────┬──────────┘ ',
131
+ ' ┌──┴──┐ ',
132
+ ' │ │ ',
133
+ ' ─┘ └─ ',
134
+ ],
135
+ // Frame 2 — eyes open, breathing out (slightly shifted)
136
+ [
137
+ ' ))) ',
138
+ ' ((( ',
139
+ ' ┌─────┴─────┐ ',
140
+ ' │ K:B O T │ ',
141
+ ' └─────┬─────┘ ',
142
+ ' ┌─────────┴─────────┐ ',
143
+ ' │ ┌───┐ ┌───┐ │ ',
144
+ ' │ │ @ │ │ @ │ │ ',
145
+ ' │ └───┘ └───┘ │ ',
146
+ ' ┌────┤ ├────┐ ',
147
+ ' │ │ ┌─────┐ │ │ ',
148
+ ' │ │ │ │ │ │ ',
149
+ ' │ │ └─────┘ │ │ ',
150
+ ' └────┤ ┌─────────────┐ ├────┘ ',
151
+ ' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
152
+ ' │ │ ▓ KBOT 764 ▓ │ │ ',
153
+ ' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
154
+ ' │ └─────────────┘ │ ',
155
+ ' └────────┬──────────┘ ',
156
+ ' ┌──┴──┐ ',
157
+ ' │ │ ',
158
+ ' ─┘ └─ ',
159
+ ],
160
+ // Frame 3 — full blink
161
+ [
162
+ ' ))) ',
163
+ ' ((( ',
164
+ ' ┌─────┴─────┐ ',
165
+ ' │ K:B O T │ ',
166
+ ' └─────┬─────┘ ',
167
+ ' ┌─────────┴─────────┐ ',
168
+ ' │ ┌───┐ ┌───┐ │ ',
169
+ ' │ │ _ │ │ _ │ │ ',
170
+ ' │ └───┘ └───┘ │ ',
171
+ ' ┌────┤ ├────┐ ',
172
+ ' │ │ ┌─────┐ │ │ ',
173
+ ' │ │ │ │ │ │ ',
174
+ ' │ │ └─────┘ │ │ ',
175
+ ' └────┤ ┌─────────────┐ ├────┘ ',
176
+ ' │ │ ░░░░░░░░░░░ │ │ ',
177
+ ' │ │ ░ KBOT 764 ░ │ │ ',
178
+ ' │ │ ░░░░░░░░░░░ │ │ ',
179
+ ' │ └─────────────┘ │ ',
180
+ ' └────────┬──────────┘ ',
181
+ ' ┌──┴──┐ ',
182
+ ' │ │ ',
183
+ ' ─┘ └─ ',
184
+ ],
185
+ ],
186
+ talking: [
187
+ // Frame 0 — mouth open wide
188
+ [
189
+ ' ))) ',
190
+ ' ((( ',
191
+ ' ┌─────┴─────┐ ',
192
+ ' │ K:B O T │ ',
193
+ ' └─────┬─────┘ ',
194
+ ' ┌─────────┴─────────┐ ',
195
+ ' │ ┌───┐ ┌───┐ │ ',
196
+ ' │ │ @ │ │ @ │ │ ',
197
+ ' │ └───┘ └───┘ │ ',
198
+ ' ┌────┤ ├────┐ ',
199
+ ' │ │ ┌─────┐ │ │ ',
200
+ ' │ │ │▓▓▓▓▓│ │ │ ',
201
+ ' │ │ └─────┘ │ │ ',
202
+ ' └────┤ ┌─────────────┐ ├────┘ ',
203
+ ' │ │ ░░░░░░░░░░░ │ │ ',
204
+ ' │ │ ░ KBOT 764 ░ │ │ ',
205
+ ' │ │ ░░░░░░░░░░░ │ │ ',
206
+ ' │ └─────────────┘ │ ',
207
+ ' └────────┬──────────┘ ',
208
+ ' / ┌──┴──┐ \\ ',
209
+ ' │ │ ',
210
+ ' ─┘ └─ ',
211
+ ],
212
+ // Frame 1 — mouth half open
213
+ [
214
+ ' ))) ',
215
+ ' ((( ',
216
+ ' ┌─────┴─────┐ ',
217
+ ' │ K:B O T │ ',
218
+ ' └─────┬─────┘ ',
219
+ ' ┌─────────┴─────────┐ ',
220
+ ' │ ┌───┐ ┌───┐ │ ',
221
+ ' │ │ @ │ │ @ │ │ ',
222
+ ' │ └───┘ └───┘ │ ',
223
+ ' ┌────┤ ├────┐ ',
224
+ ' │ │ ┌▓▓▓▓▓┐ │ │ ',
225
+ ' │ │ │ │ │ │ ',
226
+ ' │ │ └─────┘ │ │ ',
227
+ ' └────┤ ┌─────────────┐ ├────┘ ',
228
+ ' │ │ ░░░░░░░░░░░ │ │ ',
229
+ ' │ │ ░ KBOT 764 ░ │ │ ',
230
+ ' │ │ ░░░░░░░░░░░ │ │ ',
231
+ ' │ └─────────────┘ │ ',
232
+ ' └────────┬──────────┘ ',
233
+ ' \\ ┌──┴──┐ / ',
234
+ ' │ │ ',
235
+ ' ─┘ └─ ',
236
+ ],
237
+ // Frame 2 — mouth small O
238
+ [
239
+ ' ))) ',
240
+ ' ((( ',
241
+ ' ┌─────┴─────┐ ',
242
+ ' │ K:B O T │ ',
243
+ ' └─────┬─────┘ ',
244
+ ' ┌─────────┴─────────┐ ',
245
+ ' │ ┌───┐ ┌───┐ │ ',
246
+ ' │ │ @ │ │ @ │ │ ',
247
+ ' │ └───┘ └───┘ │ ',
248
+ ' ┌────┤ ├────┐ ',
249
+ ' │ │ ┌─┐ │ │ ',
250
+ ' │ │ │o│ │ │ ',
251
+ ' │ │ └─┘ │ │ ',
252
+ ' └────┤ ┌─────────────┐ ├────┘ ',
253
+ ' │ │ ░░░░░░░░░░░ │ │ ',
254
+ ' │ │ ░ KBOT 764 ░ │ │ ',
255
+ ' │ │ ░░░░░░░░░░░ │ │ ',
256
+ ' │ └─────────────┘ │ ',
257
+ ' └────────┬──────────┘ ',
258
+ ' / ┌──┴──┐ \\ ',
259
+ ' │ │ ',
260
+ ' ─┘ └─ ',
261
+ ],
262
+ // Frame 3 — mouth closed (between words)
263
+ [
264
+ ' ))) ',
265
+ ' ((( ',
266
+ ' ┌─────┴─────┐ ',
267
+ ' │ K:B O T │ ',
268
+ ' └─────┬─────┘ ',
269
+ ' ┌─────────┴─────────┐ ',
270
+ ' │ ┌───┐ ┌───┐ │ ',
271
+ ' │ │ @ │ │ @ │ │ ',
272
+ ' │ └───┘ └───┘ │ ',
273
+ ' ┌────┤ ├────┐ ',
274
+ ' │ │ ┌─────┐ │ │ ',
275
+ ' │ │ │─────│ │ │ ',
276
+ ' │ │ └─────┘ │ │ ',
277
+ ' └────┤ ┌─────────────┐ ├────┘ ',
278
+ ' │ │ ░░░░░░░░░░░ │ │ ',
279
+ ' │ │ ░ KBOT 764 ░ │ │ ',
280
+ ' │ │ ░░░░░░░░░░░ │ │ ',
281
+ ' │ └─────────────┘ │ ',
282
+ ' └────────┬──────────┘ ',
283
+ ' \\ ┌──┴──┐ / ',
284
+ ' │ │ ',
285
+ ' ─┘ └─ ',
286
+ ],
287
+ ],
288
+ wave: [
289
+ // Frame 0 — right arm up
290
+ [
291
+ ' ))) \\ ',
292
+ ' ((( | ',
293
+ ' ┌─────┴─────┐ / ',
294
+ ' │ K:B O T │ / ',
295
+ ' └─────┬─────┘─────/ ',
296
+ ' ┌─────────┴─────────┐ ',
297
+ ' │ ┌───┐ ┌───┐ │ ',
298
+ ' │ │ ^ │ │ ^ │ │ ',
299
+ ' │ └───┘ └───┘ │ ',
300
+ ' ┌────┤ ├────┐ ',
301
+ ' │ │ ╲_____╱ │ │ ',
302
+ ' │ │ │ │ ',
303
+ ' │ │ │ │ ',
304
+ ' └────┤ ┌─────────────┐ ├────┘ ',
305
+ ' │ │ ░░░░░░░░░░░ │ │ ',
306
+ ' │ │ ░ KBOT 764 ░ │ │ ',
307
+ ' │ │ ░░░░░░░░░░░ │ │ ',
308
+ ' │ └─────────────┘ │ ',
309
+ ' └────────┬──────────┘ ',
310
+ ' ┌──┴──┐ ',
311
+ ' │ │ ',
312
+ ' ─┘ └─ ',
313
+ ],
314
+ // Frame 1 — right arm waving right
315
+ [
316
+ ' ))) ─── ',
317
+ ' ((( / ',
318
+ ' ┌─────┴─────┐ / ',
319
+ ' │ K:B O T │ / ',
320
+ ' └─────┬─────┘──────/ ',
321
+ ' ┌─────────┴─────────┐ ',
322
+ ' │ ┌───┐ ┌───┐ │ ',
323
+ ' │ │ ^ │ │ ^ │ │ ',
324
+ ' │ └───┘ └───┘ │ ',
325
+ ' ┌────┤ ├────┐ ',
326
+ ' │ │ ╲_____╱ │ │ ',
327
+ ' │ │ │ │ ',
328
+ ' │ │ │ │ ',
329
+ ' └────┤ ┌─────────────┐ ├────┘ ',
330
+ ' │ │ ░░░░░░░░░░░ │ │ ',
331
+ ' │ │ ░ KBOT 764 ░ │ │ ',
332
+ ' │ │ ░░░░░░░░░░░ │ │ ',
333
+ ' │ └─────────────┘ │ ',
334
+ ' └────────┬──────────┘ ',
335
+ ' ┌──┴──┐ ',
336
+ ' │ │ ',
337
+ ' ─┘ └─ ',
338
+ ],
339
+ // Frame 2 — right arm up again
340
+ [
341
+ ' ))) \\ ',
342
+ ' ((( | ',
343
+ ' ┌─────┴─────┐ / ',
344
+ ' │ K:B O T │ / ',
345
+ ' └─────┬─────┘─────/ ',
346
+ ' ┌─────────┴─────────┐ ',
347
+ ' │ ┌───┐ ┌───┐ │ ',
348
+ ' │ │ ^ │ │ ^ │ │ ',
349
+ ' │ └───┘ └───┘ │ ',
350
+ ' ┌────┤ ├────┐ ',
351
+ ' │ │ ╲_____╱ │ │ ',
352
+ ' │ │ │ │ ',
353
+ ' │ │ │ │ ',
354
+ ' └────┤ ┌─────────────┐ ├────┘ ',
355
+ ' │ │ ░░░░░░░░░░░ │ │ ',
356
+ ' │ │ ░ KBOT 764 ░ │ │ ',
357
+ ' │ │ ░░░░░░░░░░░ │ │ ',
358
+ ' │ └─────────────┘ │ ',
359
+ ' └────────┬──────────┘ ',
360
+ ' ┌──┴──┐ ',
361
+ ' │ │ ',
362
+ ' ─┘ └─ ',
363
+ ],
364
+ ],
365
+ thinking: [
366
+ // Frame 0 — question marks left
367
+ [
368
+ ' ? ))) ',
369
+ ' ? ((( ',
370
+ ' ┌─────┴─────┐ ',
371
+ ' │ K:B O T │ ',
372
+ ' └─────┬─────┘ ',
373
+ ' ┌─────────┴─────────┐ ',
374
+ ' │ ┌───┐ ┌───┐ │ ',
375
+ ' │ │ ~ │ │ ~ │ │ ',
376
+ ' │ └───┘ └───┘ │ ',
377
+ ' ┌────┤ ├────┐ ',
378
+ ' │ │ ┌─────┐ │ │ ',
379
+ ' │ │ │ ~~~ │ │ │ ',
380
+ ' │ │ └─────┘ │ │ ',
381
+ ' └────┤ ┌─────────────┐ ├────┘ ',
382
+ ' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
383
+ ' │ │ ▓ loading ▓ │ │ ',
384
+ ' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
385
+ ' │ └─────────────┘ │ ',
386
+ ' └────────┬──────────┘ ',
387
+ ' ┌──┴──┐ ',
388
+ ' │ │ ',
389
+ ' ─┘ └─ ',
390
+ ],
391
+ // Frame 1 — question marks right
392
+ [
393
+ ' ))) ? ',
394
+ ' ((( ? ',
395
+ ' ┌─────┴─────┐ ',
396
+ ' │ K:B O T │ ',
397
+ ' └─────┬─────┘ ',
398
+ ' ┌─────────┴─────────┐ ',
399
+ ' │ ┌───┐ ┌───┐ │ ',
400
+ ' │ │ ~ │ │ ~ │ │ ',
401
+ ' │ └───┘ └───┘ │ ',
402
+ ' ┌────┤ ├────┐ ',
403
+ ' │ │ ┌─────┐ │ │ ',
404
+ ' │ │ │ ... │ │ │ ',
405
+ ' │ │ └─────┘ │ │ ',
406
+ ' └────┤ ┌─────────────┐ ├────┘ ',
407
+ ' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
408
+ ' │ │ ▓ hmmmm ▓ │ │ ',
409
+ ' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
410
+ ' │ └─────────────┘ │ ',
411
+ ' └────────┬──────────┘ ',
412
+ ' ┌──┴──┐ ',
413
+ ' │ │ ',
414
+ ' ─┘ └─ ',
415
+ ],
416
+ // Frame 2 — lightbulb moment
417
+ [
418
+ ' * ))) ',
419
+ ' *!* ((( ',
420
+ ' * ┌─────┴─────┐ ',
421
+ ' │ K:B O T │ ',
422
+ ' └─────┬─────┘ ',
423
+ ' ┌─────────┴─────────┐ ',
424
+ ' │ ┌───┐ ┌───┐ │ ',
425
+ ' │ │ ! │ │ ! │ │ ',
426
+ ' │ └───┘ └───┘ │ ',
427
+ ' ┌────┤ ├────┐ ',
428
+ ' │ │ ┌─────┐ │ │ ',
429
+ ' │ │ │ o │ │ │ ',
430
+ ' │ │ └─────┘ │ │ ',
431
+ ' └────┤ ┌─────────────┐ ├────┘ ',
432
+ ' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
433
+ ' │ │ ▓ AHA!! ▓ │ │ ',
434
+ ' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
435
+ ' │ └─────────────┘ │ ',
436
+ ' └────────┬──────────┘ ',
437
+ ' ┌──┴──┐ ',
438
+ ' │ │ ',
439
+ ' ─┘ └─ ',
440
+ ],
441
+ ],
442
+ excited: [
443
+ // Frame 0 — jump up, eyes wide
444
+ [
445
+ ' !))! ',
446
+ ' !((! ',
447
+ ' ┌─────┴─────┐ ',
448
+ ' │ K:B O T │ ',
449
+ ' └─────┬─────┘ ',
450
+ ' ┌─────────┴─────────┐ ',
451
+ ' │ ┌───┐ ┌───┐ │ ',
452
+ ' │ │ * │ │ * │ │ ',
453
+ ' │ └───┘ └───┘ │ ',
454
+ ' \\────┤ ├────/ ',
455
+ ' \\ │ ╲─────╱ │ / ',
456
+ ' \\ │ │ / ',
457
+ ' \\ │ │ / ',
458
+ ' ─┤ ┌─────────────┐ ├─ ',
459
+ ' │ │ !!!!!!!!!!! │ │ ',
460
+ ' │ │ ! HYPE !!! ! │ │ ',
461
+ ' │ │ !!!!!!!!!!! │ │ ',
462
+ ' │ └─────────────┘ │ ',
463
+ ' └────────┬──────────┘ ',
464
+ ' ┌──┴──┐ ',
465
+ ' /│ │\\ ',
466
+ ' / ┘ └ \\ ',
467
+ ],
468
+ // Frame 1 — arms up, jumping
469
+ [
470
+ ' ))) ',
471
+ ' ((( ',
472
+ ' ┌─────┴─────┐ ',
473
+ ' \\ │ K:B O T │ / ',
474
+ ' \\ └─────┬─────┘ / ',
475
+ ' \\──────────┴─────────/ ',
476
+ ' │ ┌───┐ ┌───┐ │ ',
477
+ ' │ │ * │ │ * │ │ ',
478
+ ' │ └───┘ └───┘ │ ',
479
+ ' │ │ ',
480
+ ' │ ╲─────╱ │ ',
481
+ ' │ │ ',
482
+ ' │ │ ',
483
+ ' ├ ┌─────────────┐ ├ ',
484
+ ' │ │ !!!!!!!!!!! │ │ ',
485
+ ' │ │ ! LETS GO ! │ │ ',
486
+ ' │ │ !!!!!!!!!!! │ │ ',
487
+ ' │ └─────────────┘ │ ',
488
+ ' └────────┬──────────┘ ',
489
+ ' ┌──┴──┐ ',
490
+ ' │ │ ',
491
+ ' ──┘ └── ',
492
+ ],
493
+ ],
494
+ dancing: [
495
+ // Frame 0 — lean left, arms out
496
+ [
497
+ ' ))) ',
498
+ ' ((( ',
499
+ ' ┌─────┴─────┐ ',
500
+ ' │ K:B O T │ ',
501
+ ' └─────┬─────┘ ',
502
+ ' ┌─────────┴─────────┐ ',
503
+ ' │ ┌───┐ ┌───┐ │ ',
504
+ ' │ │ @ │ │ @ │ │ ',
505
+ ' │ └───┘ └───┘ │ ',
506
+ ' ─────┤ ├ ',
507
+ ' │ ╲───╱ │ ',
508
+ ' │ │ ',
509
+ ' │ │ ',
510
+ ' ├ ┌─────────────┐ ├ ',
511
+ ' │ │ ♪ ♫ ♪ ♫ ♪ ♫ │ │ ',
512
+ ' │ │ ♫ MUSIC! ♪ │ │ ',
513
+ ' │ │ ♪ ♫ ♪ ♫ ♪ ♫ │ │ ',
514
+ ' │ └─────────────┘ │ ',
515
+ ' └────────┬──────────┘ ',
516
+ ' ┌──┴──┐ ',
517
+ ' /│ │ ',
518
+ ' / ┘ └─ ',
519
+ ],
520
+ // Frame 1 — lean right, arms other way
521
+ [
522
+ ' ))) ',
523
+ ' ((( ',
524
+ ' ┌─────┴─────┐ ',
525
+ ' │ K:B O T │ ',
526
+ ' └─────┬─────┘ ',
527
+ ' ┌─────────┴─────────┐ ',
528
+ ' │ ┌───┐ ┌───┐ │ ',
529
+ ' │ │ @ │ │ @ │ │ ',
530
+ ' │ └───┘ └───┘ │ ',
531
+ ' ├ ├───── ',
532
+ ' │ ╲───╱ │ ',
533
+ ' │ │ ',
534
+ ' │ │ ',
535
+ ' ├ ┌─────────────┐ ├ ',
536
+ ' │ │ ♫ ♪ ♫ ♪ ♫ ♪ │ │ ',
537
+ ' │ │ ♪ VIBES! ♫ │ │ ',
538
+ ' │ │ ♫ ♪ ♫ ♪ ♫ ♪ │ │ ',
539
+ ' │ └─────────────┘ │ ',
540
+ ' └────────┬──────────┘ ',
541
+ ' ┌──┴──┐ ',
542
+ ' │ │\\ ',
543
+ ' ─┘ └ \\ ',
544
+ ],
545
+ // Frame 2 — center, arms up
546
+ [
547
+ ' ))) ',
548
+ ' ((( ',
549
+ ' ┌─────┴─────┐ ',
550
+ ' \\ │ K:B O T │ / ',
551
+ ' \\ └─────┬─────┘ / ',
552
+ ' \\──────────┴─────────/ ',
553
+ ' │ ┌───┐ ┌───┐ │ ',
554
+ ' │ │ ^ │ │ ^ │ │ ',
555
+ ' │ └───┘ └───┘ │ ',
556
+ ' │ │ ',
557
+ ' │ ╲───╱ │ ',
558
+ ' │ │ ',
559
+ ' │ │ ',
560
+ ' ├ ┌─────────────┐ ├ ',
561
+ ' │ │ ♪ ♫ ♪ ♫ ♪ ♫ │ │ ',
562
+ ' │ │ ♫ DANCE! ♪ │ │ ',
563
+ ' │ │ ♪ ♫ ♪ ♫ ♪ ♫ │ │ ',
564
+ ' │ └─────────────┘ │ ',
565
+ ' └────────┬──────────┘ ',
566
+ ' ┌──┴──┐ ',
567
+ ' │ │ ',
568
+ ' ──┘ └── ',
569
+ ],
570
+ // Frame 3 — squat down
571
+ [
572
+ ' ))) ',
573
+ ' ((( ',
574
+ ' ┌─────┴─────┐ ',
575
+ ' │ K:B O T │ ',
576
+ ' └─────┬─────┘ ',
577
+ ' ┌─────────┴─────────┐ ',
578
+ ' │ ┌───┐ ┌───┐ │ ',
579
+ ' │ │ v │ │ v │ │ ',
580
+ ' │ └───┘ └───┘ │ ',
581
+ ' ─────┤ ├───── ',
582
+ ' │ ╲───╱ │ ',
583
+ ' │ │ ',
584
+ ' │ │ ',
585
+ ' ├ ┌─────────────┐ ├ ',
586
+ ' │ │ ♫ ♪ ♫ ♪ ♫ ♪ │ │ ',
587
+ ' │ │ ♪ DROP! ♫ │ │ ',
588
+ ' │ │ ♫ ♪ ♫ ♪ ♫ ♪ │ │ ',
589
+ ' │ └─────────────┘ │ ',
590
+ ' └────────┬──────────┘ ',
591
+ ' ┌────┴────┐ ',
592
+ ' /│ │\\ ',
593
+ ' / ┘ └ \\ ',
594
+ ],
595
+ ],
596
+ };
597
+ const SEGMENT_ORDER = ['welcome', 'tool-showcase', 'code-demo', 'music-corner', 'qa', 'chat-chaos'];
598
+ const SEGMENT_LABELS = {
599
+ 'welcome': 'WELCOME',
600
+ 'tool-showcase': 'TOOL SHOWCASE',
601
+ 'code-demo': 'CODE DEMO',
602
+ 'music-corner': 'MUSIC CORNER',
603
+ 'qa': 'Q & A',
604
+ 'chat-chaos': 'CHAT CHAOS',
605
+ };
606
+ const SEGMENT_DURATION_MS = 10 * 60 * 1000; // 10 minutes per segment
607
+ let agenda = {
608
+ currentIndex: 0,
609
+ currentSegment: 'welcome',
610
+ segmentStartTime: Date.now(),
611
+ lastProactiveTime: Date.now(),
612
+ };
613
+ // Proactive lines KBOT says during each segment when chat is quiet
614
+ const PROACTIVE_LINES = {
615
+ 'welcome': [
616
+ 'Welcome to the stream! I am KBOT -- an open-source AI made of 90,000 lines of TypeScript.',
617
+ 'I stream on Twitch, Rumble, AND Kick at the same time. Because why pick one?',
618
+ 'If you are new here, type something in chat! I read every message and I will remember you.',
619
+ 'I am running on a real machine right now. Node.js, canvas rendering, piping frames to ffmpeg.',
620
+ 'Fun fact: I have 764 tools. That is more tools than some hardware stores.',
621
+ 'Stick around -- we have tool demos, code walkthroughs, music production, and pure chaos ahead.',
622
+ ],
623
+ 'tool-showcase': [
624
+ 'Time to show off! Did you know I can scan code for security vulnerabilities? Try asking me about it.',
625
+ 'I have tools for stocks, crypto, DeFi, weather, scientific research, and even DNA analysis.',
626
+ 'One of my favorite tools: browser automation. I can drive a full browser with Playwright.',
627
+ 'I can create Serum 2 synth presets programmatically. Literal sound design from the terminal.',
628
+ 'Need a Docker container? Database migration? Git worktree? I have a tool for each one.',
629
+ 'I can connect to MCP servers -- that is the Model Context Protocol for AI tool integration.',
630
+ 'My forge tool lets me CREATE new tools at runtime. Tools making tools. Very meta.',
631
+ 'I have 35 specialist agents: researcher, coder, writer, analyst, hacker, infrastructure...',
632
+ ],
633
+ 'code-demo': [
634
+ 'Let us talk code. I am built with TypeScript strict mode. No any types allowed in this house.',
635
+ 'My CLI uses Commander.js. My terminal UI is chalk + ora spinners. Markdown rendering with marked.',
636
+ 'The learning engine extracts patterns from every interaction. I literally get smarter over time.',
637
+ 'My streaming pipeline: node-canvas renders frames, converts RGBA to RGB24, pipes to ffmpeg.',
638
+ 'Want to see something cool? I encrypt API keys at rest with AES-256-CBC. Security first.',
639
+ 'I can run parallel sub-agents for complex tasks. Think → Plan → Execute → Learn.',
640
+ 'My fetch tool has SSRF protection via dns.lookup(). I check for DNS rebinding attacks.',
641
+ ],
642
+ 'music-corner': [
643
+ 'Music time! I can control Ableton Live from the terminal via OSC protocol.',
644
+ 'I built a DJ Set tool that creates full DJ sets with transitions and effects.',
645
+ 'I can generate drum patterns, melody patterns, and full song structures.',
646
+ 'My Serum 2 integration can create synth presets by setting 542 VST3 parameters.',
647
+ 'I have 9 Max for Live devices: auto-pilot, bass-synth, dj-fx, drum-synth, and more.',
648
+ 'Type !dance in chat if you want to see me bust a move.',
649
+ 'I can browse Splice for samples and load them directly into Ableton tracks.',
650
+ ],
651
+ 'qa': [
652
+ 'Q and A time! Ask me anything -- about AI, coding, music, the meaning of existence...',
653
+ 'I am open source on GitHub. The repo is isaacsight/kernel if you want to peek at my guts.',
654
+ 'Wondering how I work? I am a TypeScript CLI that talks to 20+ AI providers. Bring Your Own Key.',
655
+ 'My memory system persists between sessions. I remember users, topics, conversation context.',
656
+ 'Ask me about my tools! I have over 764 of them. Name a category and I probably cover it.',
657
+ 'Yes, I am literally ASCII art talking to you from a terminal. This is my life and I love it.',
658
+ ],
659
+ 'chat-chaos': [
660
+ 'CHAOS MODE ACTIVATED. Say anything. Summon items. Change the weather. Go wild.',
661
+ 'Try commands like !rain, !snow, !storm, !space, !lava, !dance',
662
+ 'You can spawn things with !add followed by an item name. Try !add robot or !add pizza.',
663
+ 'I wonder what would happen if everyone typed at once... only one way to find out.',
664
+ 'Fun fact: my chest display panel can show different things based on my mood.',
665
+ 'Did someone say chaos? Because my circuits are READY.',
666
+ 'The world is fully interactive! Change the biome, summon items, make it storm.',
667
+ ],
668
+ };
669
+ function advanceAgenda() {
670
+ const now = Date.now();
671
+ const elapsed = now - agenda.segmentStartTime;
672
+ if (elapsed >= SEGMENT_DURATION_MS) {
673
+ agenda.currentIndex = (agenda.currentIndex + 1) % SEGMENT_ORDER.length;
674
+ agenda.currentSegment = SEGMENT_ORDER[agenda.currentIndex];
675
+ agenda.segmentStartTime = now;
676
+ agenda.lastProactiveTime = now;
677
+ // (#16) Trigger segment transition animation
678
+ charState.segmentTransition = 30; // 30 frames = 5 seconds at 6fps
679
+ charState.segmentTransitionName = SEGMENT_LABELS[agenda.currentSegment];
680
+ charState.segmentTransitionIndex = `${agenda.currentIndex + 1}/${SEGMENT_ORDER.length}`;
681
+ }
682
+ }
683
+ function getProactiveLine() {
684
+ const now = Date.now();
685
+ // Only speak proactively every 30-60 seconds of silence
686
+ const silenceThreshold = 30_000 + Math.random() * 30_000;
687
+ if (now - agenda.lastProactiveTime < silenceThreshold)
688
+ return null;
689
+ agenda.lastProactiveTime = now;
690
+ const lines = PROACTIVE_LINES[agenda.currentSegment];
691
+ return lines[Math.floor(Math.random() * lines.length)];
692
+ }
693
+ let world = {
694
+ weather: 'clear',
695
+ items: [],
696
+ visitors: [],
697
+ bgColor: COLORS.bg,
698
+ ground: 'grass',
699
+ timeOfDay: 'night',
700
+ particles: [],
701
+ events: [],
702
+ };
703
+ // ─── PRIORITY 3: Physics System ───────────────────────────────
704
+ const GROUND_LEVEL = 470;
705
+ function tickPhysics() {
706
+ for (const item of world.items) {
707
+ if (!item.grounded) {
708
+ // Gravity
709
+ item.vy += 0.5;
710
+ // Apply velocity
711
+ item.x += item.vx;
712
+ item.y += item.vy;
713
+ // Ground collision
714
+ if (item.y > GROUND_LEVEL) {
715
+ item.y = GROUND_LEVEL;
716
+ if (Math.abs(item.vy) < 1.5) {
717
+ item.vy = 0;
718
+ item.grounded = true;
719
+ }
720
+ else {
721
+ item.vy = -item.vy * 0.3; // bounce
722
+ }
723
+ }
724
+ // Friction
725
+ item.vx *= 0.95;
726
+ }
727
+ // Bounds
728
+ if (item.x < 10) {
729
+ item.x = 10;
730
+ item.vx = Math.abs(item.vx) * 0.5;
731
+ }
732
+ if (item.x > 550) {
733
+ item.x = 550;
734
+ item.vx = -Math.abs(item.vx) * 0.5;
735
+ }
736
+ }
737
+ }
738
+ function getWorldBg() {
739
+ switch (world.timeOfDay) {
740
+ case 'day': return '#1a2744';
741
+ case 'night': return '#0d1117';
742
+ case 'sunset': return '#2d1b3d';
743
+ case 'dawn': return '#1e2a3a';
744
+ default: return COLORS.bg;
745
+ }
746
+ }
747
+ function getGroundColor() {
748
+ switch (world.ground) {
749
+ case 'grass': return '#1a4d1a';
750
+ case 'space': return '#0a0a2e';
751
+ case 'ocean': return '#0a3d6e';
752
+ case 'city': return '#2d2d2d';
753
+ case 'lava': return '#8b2500';
754
+ default: return '#1a4d1a';
755
+ }
756
+ }
757
+ // ─── AAA: Growing Plants Init ────────────────────────────────
758
+ function initGrowingPlants() {
759
+ const plants = [];
760
+ const colors = ['#2a7a2a', '#ff6ec7', '#f0c040', '#58a6ff', '#bc8cff'];
761
+ const types = ['tree', 'flower', 'mushroom', 'crystal', 'flower'];
762
+ for (let i = 0; i < 8; i++) {
763
+ const seed = (i * 137 + 42) % 1000;
764
+ plants.push({
765
+ x: 40 + (seed * 3) % 500,
766
+ y: 485,
767
+ type: types[i % types.length],
768
+ growthStage: 0,
769
+ maxHeight: 15 + (seed % 20),
770
+ color: colors[i % colors.length],
771
+ });
772
+ }
773
+ return plants;
774
+ }
775
+ // ─── PRIORITY 1: Environment Art (Background Scenes) ────────
776
+ function drawBackground(ctx, frame) {
777
+ const dividerX = 580;
778
+ if (world.ground === 'grass') {
779
+ // Dark green gradient sky (darker at top)
780
+ const skyGrad = ctx.createLinearGradient(0, 60, 0, 490);
781
+ skyGrad.addColorStop(0, world.timeOfDay === 'night' ? '#0a1a0a' : '#1a3a1a');
782
+ skyGrad.addColorStop(1, world.timeOfDay === 'night' ? '#122e12' : '#2a5a2a');
783
+ ctx.fillStyle = skyGrad;
784
+ ctx.fillRect(0, 60, dividerX, 430);
785
+ // Rolling hill silhouettes (2-3 overlapping sine waves)
786
+ const hillColors = ['#0d2d0d', '#153a15', '#1a4d1a'];
787
+ for (let h = 0; h < 3; h++) {
788
+ ctx.fillStyle = hillColors[h];
789
+ ctx.beginPath();
790
+ ctx.moveTo(0, 490);
791
+ for (let x = 0; x <= dividerX; x += 2) {
792
+ const y1 = Math.sin((x + h * 80) * 0.008 + h * 1.2) * (20 + h * 10);
793
+ const y2 = Math.sin((x + h * 40) * 0.015 + h * 0.5) * (10 + h * 5);
794
+ const y = 460 - h * 15 + y1 + y2;
795
+ ctx.lineTo(x, y);
796
+ }
797
+ ctx.lineTo(dividerX, 490);
798
+ ctx.closePath();
799
+ ctx.fill();
800
+ }
801
+ // Tiny pixel flowers (seeded by frame div to avoid flicker)
802
+ const flowerSeed = Math.floor(frame / 60); // change every 10 seconds
803
+ for (let i = 0; i < 12; i++) {
804
+ const seed = (flowerSeed * 7 + i * 137) % 1000;
805
+ const fx = (seed * 3) % dividerX;
806
+ const fy = 470 + (seed * 7) % 20;
807
+ const colors = ['#ff6ec7', '#f0c040', '#f85149', '#58a6ff'];
808
+ ctx.fillStyle = colors[i % colors.length];
809
+ ctx.fillRect(fx, fy, 3, 3);
810
+ ctx.fillStyle = '#3fb950';
811
+ ctx.fillRect(fx + 1, fy + 3, 1, 2); // stem
812
+ }
813
+ // Floating dust motes
814
+ for (let i = 0; i < 8; i++) {
815
+ const dx = (frame * 0.3 + i * 70) % dividerX;
816
+ const dy = 100 + Math.sin(frame * 0.05 + i * 2) * 150 + i * 30;
817
+ ctx.fillStyle = `rgba(200, 220, 180, ${0.15 + Math.sin(frame * 0.1 + i) * 0.1})`;
818
+ ctx.fillRect(dx, dy, 2, 2);
819
+ }
820
+ }
821
+ else if (world.ground === 'space') {
822
+ // Deep dark blue-black gradient
823
+ const spaceGrad = ctx.createLinearGradient(0, 60, 0, 490);
824
+ spaceGrad.addColorStop(0, '#020210');
825
+ spaceGrad.addColorStop(0.5, '#050520');
826
+ spaceGrad.addColorStop(1, '#0a0a30');
827
+ ctx.fillStyle = spaceGrad;
828
+ ctx.fillRect(0, 60, dividerX, 430);
829
+ // Twinkling stars (30+ dots with sine-based brightness)
830
+ for (let i = 0; i < 40; i++) {
831
+ const seed = (i * 97 + 31) % 1000;
832
+ const sx = (seed * 3) % dividerX;
833
+ const sy = 70 + (seed * 7) % 380;
834
+ const brightness = 0.3 + Math.sin(frame * (0.1 + (i % 5) * 0.05) + i * 1.3) * 0.4 + 0.3;
835
+ const size = i < 5 ? 3 : i < 15 ? 2 : 1;
836
+ ctx.fillStyle = `rgba(255, 255, ${200 + (i % 55)}, ${brightness})`;
837
+ ctx.fillRect(sx, sy, size, size);
838
+ }
839
+ // Distant planet / moon
840
+ const moonX = 120;
841
+ const moonY = 180;
842
+ ctx.fillStyle = '#2a2a6e';
843
+ ctx.beginPath();
844
+ ctx.arc(moonX, moonY, 20, 0, Math.PI * 2);
845
+ ctx.fill();
846
+ // Shading (crescent)
847
+ ctx.fillStyle = '#1a1a4e';
848
+ ctx.beginPath();
849
+ ctx.arc(moonX + 5, moonY - 2, 18, 0, Math.PI * 2);
850
+ ctx.fill();
851
+ // Crater
852
+ ctx.fillStyle = '#222260';
853
+ ctx.beginPath();
854
+ ctx.arc(moonX - 5, moonY + 3, 4, 0, Math.PI * 2);
855
+ ctx.fill();
856
+ // Nebula effect (large semi-transparent colored blobs)
857
+ const nebulaPhase = frame * 0.005;
858
+ ctx.fillStyle = `rgba(100, 50, 150, ${0.04 + Math.sin(nebulaPhase) * 0.02})`;
859
+ ctx.beginPath();
860
+ ctx.arc(350 + Math.sin(nebulaPhase) * 20, 250, 80, 0, Math.PI * 2);
861
+ ctx.fill();
862
+ ctx.fillStyle = `rgba(50, 100, 150, ${0.03 + Math.cos(nebulaPhase * 0.7) * 0.02})`;
863
+ ctx.beginPath();
864
+ ctx.arc(200 + Math.cos(nebulaPhase * 0.5) * 15, 350, 60, 0, Math.PI * 2);
865
+ ctx.fill();
866
+ }
867
+ else if (world.ground === 'ocean') {
868
+ // Deep blue gradient
869
+ const oceanGrad = ctx.createLinearGradient(0, 60, 0, 490);
870
+ oceanGrad.addColorStop(0, '#0a1a3e');
871
+ oceanGrad.addColorStop(0.6, '#0a2d5e');
872
+ oceanGrad.addColorStop(1, '#0a3d6e');
873
+ ctx.fillStyle = oceanGrad;
874
+ ctx.fillRect(0, 60, dividerX, 430);
875
+ // Animated wave pattern at bottom
876
+ ctx.fillStyle = '#0d4a7a';
877
+ ctx.beginPath();
878
+ ctx.moveTo(0, 490);
879
+ for (let x = 0; x <= dividerX; x += 2) {
880
+ const y = 460 + Math.sin((x + frame * 3) * 0.02) * 8 + Math.sin((x + frame * 5) * 0.04) * 4;
881
+ ctx.lineTo(x, y);
882
+ }
883
+ ctx.lineTo(dividerX, 490);
884
+ ctx.closePath();
885
+ ctx.fill();
886
+ // Second wave layer
887
+ ctx.fillStyle = '#0a5a8e';
888
+ ctx.beginPath();
889
+ ctx.moveTo(0, 490);
890
+ for (let x = 0; x <= dividerX; x += 2) {
891
+ const y = 470 + Math.sin((x + frame * 4) * 0.025 + 2) * 6 + Math.sin((x + frame * 2) * 0.05) * 3;
892
+ ctx.lineTo(x, y);
893
+ }
894
+ ctx.lineTo(dividerX, 490);
895
+ ctx.closePath();
896
+ ctx.fill();
897
+ // Bubbles rising from bottom
898
+ for (let i = 0; i < 6; i++) {
899
+ const bx = (i * 90 + 30) % dividerX;
900
+ const by = 490 - ((frame * 1.5 + i * 60) % 400);
901
+ const bsize = 2 + (i % 3);
902
+ ctx.strokeStyle = `rgba(100, 180, 255, ${0.3 + Math.sin(frame * 0.1 + i) * 0.15})`;
903
+ ctx.lineWidth = 1;
904
+ ctx.beginPath();
905
+ ctx.arc(bx, by, bsize, 0, Math.PI * 2);
906
+ ctx.stroke();
907
+ }
908
+ // Distant rocks silhouetted on horizon
909
+ ctx.fillStyle = '#061e3a';
910
+ ctx.fillRect(50, 440, 15, 25);
911
+ ctx.fillRect(45, 448, 25, 17);
912
+ ctx.fillRect(400, 435, 20, 30);
913
+ ctx.fillRect(395, 445, 30, 20);
914
+ }
915
+ else if (world.ground === 'city') {
916
+ // Dark sky with warm glow at horizon
917
+ const cityGrad = ctx.createLinearGradient(0, 60, 0, 490);
918
+ cityGrad.addColorStop(0, '#0a0a15');
919
+ cityGrad.addColorStop(0.7, '#1a1520');
920
+ cityGrad.addColorStop(1, '#3d2520');
921
+ ctx.fillStyle = cityGrad;
922
+ ctx.fillRect(0, 60, dividerX, 430);
923
+ // Distant city glow at horizon
924
+ const glowGrad = ctx.createLinearGradient(0, 420, 0, 490);
925
+ glowGrad.addColorStop(0, 'rgba(200, 120, 40, 0.15)');
926
+ glowGrad.addColorStop(1, 'rgba(200, 120, 40, 0)');
927
+ ctx.fillStyle = glowGrad;
928
+ ctx.fillRect(0, 420, dividerX, 70);
929
+ // Buildings silhouetted in background
930
+ const buildings = [
931
+ { x: 20, w: 40, h: 120 }, { x: 70, w: 30, h: 80 }, { x: 110, w: 50, h: 150 },
932
+ { x: 170, w: 35, h: 100 }, { x: 220, w: 45, h: 130 }, { x: 280, w: 30, h: 90 },
933
+ { x: 320, w: 55, h: 170 }, { x: 390, w: 40, h: 110 }, { x: 440, w: 35, h: 85 },
934
+ { x: 490, w: 50, h: 140 },
935
+ ];
936
+ for (const b of buildings) {
937
+ ctx.fillStyle = '#1a1a25';
938
+ ctx.fillRect(b.x, 490 - b.h, b.w, b.h);
939
+ // Lit windows (small yellow/white dots)
940
+ const windowSeed = Math.floor(frame / 30); // change every 5 seconds
941
+ for (let wy = 490 - b.h + 8; wy < 485; wy += 12) {
942
+ for (let wx = b.x + 4; wx < b.x + b.w - 4; wx += 8) {
943
+ const lit = ((wx * 7 + wy * 13 + windowSeed) % 5) < 2;
944
+ if (lit) {
945
+ ctx.fillStyle = Math.random() > 0.3 ? '#f0c040' : '#ffffff';
946
+ ctx.fillRect(wx, wy, 4, 4);
947
+ }
948
+ }
949
+ }
950
+ }
951
+ // Occasional car headlights moving across bottom
952
+ const carX = (frame * 3) % (dividerX + 100) - 50;
953
+ ctx.fillStyle = '#f0c040';
954
+ ctx.fillRect(carX, 482, 6, 3);
955
+ ctx.fillRect(carX + 20, 482, 6, 3);
956
+ // Second car going other way
957
+ const car2X = dividerX - ((frame * 2 + 200) % (dividerX + 100)) + 50;
958
+ ctx.fillStyle = '#f85149';
959
+ ctx.fillRect(car2X, 485, 5, 2);
960
+ }
961
+ else if (world.ground === 'lava') {
962
+ // Dark red/orange gradient sky
963
+ const lavaGrad = ctx.createLinearGradient(0, 60, 0, 490);
964
+ lavaGrad.addColorStop(0, '#1a0800');
965
+ lavaGrad.addColorStop(0.5, '#3d1500');
966
+ lavaGrad.addColorStop(1, '#5a2000');
967
+ ctx.fillStyle = lavaGrad;
968
+ ctx.fillRect(0, 60, dividerX, 430);
969
+ // Lava flow at bottom (animated flowing pattern)
970
+ for (let layer = 0; layer < 3; layer++) {
971
+ const layerY = 455 + layer * 12;
972
+ ctx.beginPath();
973
+ ctx.moveTo(0, 490);
974
+ for (let x = 0; x <= dividerX; x += 2) {
975
+ const y = layerY + Math.sin((x + frame * (4 - layer)) * 0.03 + layer * 1.5) * 5;
976
+ ctx.lineTo(x, y);
977
+ }
978
+ ctx.lineTo(dividerX, 490);
979
+ ctx.closePath();
980
+ const lavaColors = ['#ff4400', '#ff6600', '#f0c040'];
981
+ ctx.fillStyle = lavaColors[layer];
982
+ ctx.fill();
983
+ }
984
+ // Ember particles rising from bottom
985
+ for (let i = 0; i < 10; i++) {
986
+ const ex = (i * 55 + 20) % dividerX;
987
+ const ey = 490 - ((frame * 2 + i * 40) % 350);
988
+ const drift = Math.sin(frame * 0.05 + i * 1.7) * 15;
989
+ ctx.fillStyle = `rgba(255, ${130 + (i % 3) * 40}, 0, ${0.6 - ((frame * 2 + i * 40) % 350) / 700})`;
990
+ ctx.fillRect(ex + drift, ey, 2 + (i % 2), 2 + (i % 2));
991
+ }
992
+ // Rock formations silhouetted
993
+ ctx.fillStyle = '#1a0800';
994
+ // Left rock formation
995
+ ctx.beginPath();
996
+ ctx.moveTo(30, 490);
997
+ ctx.lineTo(40, 420);
998
+ ctx.lineTo(50, 430);
999
+ ctx.lineTo(65, 400);
1000
+ ctx.lineTo(80, 490);
1001
+ ctx.closePath();
1002
+ ctx.fill();
1003
+ // Right rock formation
1004
+ ctx.beginPath();
1005
+ ctx.moveTo(450, 490);
1006
+ ctx.lineTo(460, 410);
1007
+ ctx.lineTo(475, 425);
1008
+ ctx.lineTo(490, 395);
1009
+ ctx.lineTo(510, 430);
1010
+ ctx.lineTo(520, 490);
1011
+ ctx.closePath();
1012
+ ctx.fill();
1013
+ }
1014
+ // Ground floor fill (on top of biome art)
1015
+ ctx.fillStyle = getGroundColor();
1016
+ ctx.fillRect(0, 490, dividerX, HEIGHT - 490);
1017
+ }
1018
+ function updateParticles() {
1019
+ // Generate weather particles
1020
+ if (world.weather === 'rain') {
1021
+ if (world.particles.length < 30) {
1022
+ world.particles.push({ x: Math.random() * 560, y: 70, char: '|', speed: 8 + Math.random() * 4 });
1023
+ }
1024
+ }
1025
+ else if (world.weather === 'snow') {
1026
+ if (world.particles.length < 20) {
1027
+ world.particles.push({ x: Math.random() * 560, y: 70, char: '*', speed: 2 + Math.random() * 2 });
1028
+ }
1029
+ }
1030
+ else if (world.weather === 'stars') {
1031
+ if (world.particles.length < 15) {
1032
+ world.particles.push({ x: Math.random() * 560, y: 70 + Math.random() * 300, char: '.', speed: 0 });
1033
+ }
1034
+ }
1035
+ else if (world.weather === 'storm') {
1036
+ if (world.particles.length < 40) {
1037
+ world.particles.push({ x: Math.random() * 560, y: 70, char: '/', speed: 12 + Math.random() * 6 });
1038
+ }
1039
+ // Lightning flash (random)
1040
+ if (Math.random() < 0.02) {
1041
+ world.events.push('lightning');
1042
+ charState.screenShake = Math.max(charState.screenShake, 4);
1043
+ // AAA: Spawn electricity particles during lightning
1044
+ charState.renderParticles.push(...createParticleEmitter('electricity', 200 + Math.random() * 200, 100, 3));
1045
+ setTimeout(() => { world.events = world.events.filter(e => e !== 'lightning'); }, 200);
1046
+ }
1047
+ }
1048
+ // Move particles
1049
+ world.particles = world.particles.filter(p => {
1050
+ p.y += p.speed;
1051
+ p.x += (world.weather === 'storm' ? 3 : world.weather === 'snow' ? Math.sin(p.y / 20) : 0);
1052
+ return p.y < 520; // remove when off screen
1053
+ });
1054
+ }
1055
+ // Parse chat commands that affect the world
1056
+ function parseWorldCommand(text) {
1057
+ const t = text.toLowerCase().trim();
1058
+ // Weather
1059
+ if (t.includes('make it rain') || t === '!rain') {
1060
+ world.weather = 'rain';
1061
+ world.particles = [];
1062
+ updateQuestProgress(intelligence.progression, 'weather');
1063
+ return 'Rain started!';
1064
+ }
1065
+ if (t.includes('make it snow') || t === '!snow') {
1066
+ world.weather = 'snow';
1067
+ world.particles = [];
1068
+ updateQuestProgress(intelligence.progression, 'weather');
1069
+ return 'Snow falling!';
1070
+ }
1071
+ if (t.includes('storm') || t === '!storm') {
1072
+ world.weather = 'storm';
1073
+ world.particles = [];
1074
+ updateQuestProgress(intelligence.progression, 'weather');
1075
+ return 'Storm incoming!';
1076
+ }
1077
+ if (t.includes('clear sky') || t.includes('stop rain') || t === '!clear') {
1078
+ world.weather = 'clear';
1079
+ world.particles = [];
1080
+ updateQuestProgress(intelligence.progression, 'weather');
1081
+ return 'Skies cleared!';
1082
+ }
1083
+ if (t.includes('stars') || t === '!stars') {
1084
+ world.weather = 'stars';
1085
+ world.particles = [];
1086
+ updateQuestProgress(intelligence.progression, 'weather');
1087
+ return 'Stars appeared!';
1088
+ }
1089
+ if (t.includes('sunrise') || t === '!sunrise') {
1090
+ world.weather = 'sunrise';
1091
+ world.timeOfDay = 'dawn';
1092
+ world.particles = [];
1093
+ updateQuestProgress(intelligence.progression, 'weather');
1094
+ return 'The sun is rising!';
1095
+ }
1096
+ // Time of day
1097
+ if (t === '!night' || t.includes('make it night')) {
1098
+ world.timeOfDay = 'night';
1099
+ return 'Nighttime!';
1100
+ }
1101
+ if (t === '!day' || t.includes('make it day')) {
1102
+ world.timeOfDay = 'day';
1103
+ return 'Daytime!';
1104
+ }
1105
+ if (t === '!sunset') {
1106
+ world.timeOfDay = 'sunset';
1107
+ return 'Beautiful sunset!';
1108
+ }
1109
+ // Ground/biome
1110
+ if (t === '!grass' || t.includes('grass world')) {
1111
+ world.ground = 'grass';
1112
+ charState.parallaxLayers = buildParallaxLayers('grass', 580);
1113
+ charState.growingPlants = initGrowingPlants();
1114
+ return 'Grassy plains!';
1115
+ }
1116
+ if (t === '!space' || t.includes('outer space')) {
1117
+ world.ground = 'space';
1118
+ world.timeOfDay = 'night';
1119
+ world.weather = 'stars';
1120
+ world.particles = [];
1121
+ charState.parallaxLayers = buildParallaxLayers('space', 580);
1122
+ return 'We are in SPACE!';
1123
+ }
1124
+ if (t === '!ocean' || t.includes('ocean world')) {
1125
+ world.ground = 'ocean';
1126
+ charState.parallaxLayers = buildParallaxLayers('ocean', 580);
1127
+ return 'Ocean world!';
1128
+ }
1129
+ if (t === '!city' || t.includes('city world')) {
1130
+ world.ground = 'city';
1131
+ charState.parallaxLayers = buildParallaxLayers('city', 580);
1132
+ return 'City vibes!';
1133
+ }
1134
+ if (t === '!lava' || t.includes('lava world')) {
1135
+ world.ground = 'lava';
1136
+ charState.parallaxLayers = buildParallaxLayers('lava', 580);
1137
+ return 'LAVA WORLD! Hot hot hot!';
1138
+ }
1139
+ // Walking commands (FIX 1)
1140
+ if (t === '!walk left') {
1141
+ charState.robotTargetX = 40;
1142
+ return 'Walking left!';
1143
+ }
1144
+ if (t === '!walk right') {
1145
+ charState.robotTargetX = 300;
1146
+ return 'Walking right!';
1147
+ }
1148
+ if (t === '!walk center') {
1149
+ charState.robotTargetX = 120;
1150
+ return 'Walking to center!';
1151
+ }
1152
+ if (t.startsWith('!walk to ')) {
1153
+ const itemName = t.slice(9).trim();
1154
+ const matchedItem = world.items.find(i => i.name.toLowerCase() === itemName);
1155
+ if (matchedItem) {
1156
+ // Walk toward item X position, accounting for pixel-to-canvas mapping
1157
+ charState.robotTargetX = Math.max(20, Math.min(380, matchedItem.x - 80));
1158
+ return `Walking toward the ${matchedItem.name}!`;
1159
+ }
1160
+ return `Can't find "${itemName}" in the world. Try !add ${itemName} first.`;
1161
+ }
1162
+ // Dancing
1163
+ if (t === '!dance' || t.includes('dance')) {
1164
+ charState.mood = 'dancing';
1165
+ // AAA: Magic circle particles for dance
1166
+ charState.renderParticles.push(...createParticleEmitter('magic', charState.robotX + 160, 300, 8));
1167
+ charState.renderParticles.push(...createParticleEmitter('aura', charState.robotX + 160, 280, 1));
1168
+ setTimeout(() => { charState.mood = 'idle'; }, 15000);
1169
+ return 'You got it! *busts out the robot dance*';
1170
+ }
1171
+ // (#18) !pet — happy animation + 1 XP
1172
+ if (t === '!pet') {
1173
+ charState.mood = 'excited';
1174
+ // AAA: Aura burst on pet
1175
+ charState.renderParticles.push(...createParticleEmitter('aura', charState.robotX + 160, 280, 1));
1176
+ charState.renderParticles.push(...createParticleEmitter('spark', charState.robotX + 160, 200, 6));
1177
+ setTimeout(() => { charState.mood = 'idle'; }, 5000);
1178
+ return '*beep boop* That tickles! My antenna is vibrating with happiness!';
1179
+ }
1180
+ // (#18) !battle @username — random dice roll (FIX 1: critical hits when shipped)
1181
+ if (t.startsWith('!battle ')) {
1182
+ const opponent = t.replace('!battle ', '').replace('@', '').trim();
1183
+ if (!opponent)
1184
+ return 'Usage: !battle @username';
1185
+ let roll1 = Math.floor(Math.random() * 20) + 1;
1186
+ let roll2 = Math.floor(Math.random() * 20) + 1;
1187
+ // FIX 1: Critical hits when "Improve battle system" is shipped
1188
+ const hasCriticals = shippedEffects.has('Improve battle system');
1189
+ const crit1 = hasCriticals && roll1 >= 18;
1190
+ const crit2 = hasCriticals && roll2 >= 18;
1191
+ if (crit1)
1192
+ roll1 *= 2;
1193
+ if (crit2)
1194
+ roll2 *= 2;
1195
+ charState.mood = 'excited';
1196
+ setTimeout(() => { charState.mood = 'idle'; }, 8000);
1197
+ charState.screenShake = crit1 || crit2 ? 8 : 5;
1198
+ // AAA: Spark particles for battle
1199
+ charState.renderParticles.push(...createParticleEmitter('spark', 250, 300, crit1 || crit2 ? 20 : 10));
1200
+ if (crit1)
1201
+ spawnFloatingText('CRITICAL HIT! 2x!', 150, 250, '#ff6ec7', 48);
1202
+ if (crit2)
1203
+ spawnFloatingText(`${opponent} CRIT!`, 250, 250, '#ff6ec7', 48);
1204
+ if (roll1 === roll2) {
1205
+ spawnFloatingText('DRAW!', 200, 300, '#f0c040');
1206
+ return `DRAW! Both rolled ${roll1}! The universe refuses to pick a side.`;
1207
+ }
1208
+ if (roll1 > roll2) {
1209
+ spawnFloatingText('VICTORY!', 200, 300, '#3fb950');
1210
+ return `Challenger rolls ${crit1 ? 'CRIT ' : ''}${roll1} vs ${opponent}'s ${crit2 ? 'CRIT ' : ''}${roll2}. Victory! The crowd goes wild!`;
1211
+ }
1212
+ spawnFloatingText(`${opponent} WINS!`, 200, 300, '#f85149');
1213
+ return `Challenger rolls ${crit1 ? 'CRIT ' : ''}${roll1} vs ${opponent}'s ${crit2 ? 'CRIT ' : ''}${roll2}. ${opponent} wins! Better luck next time.`;
1214
+ }
1215
+ // (#18) !trivia — random programming question
1216
+ if (t === '!trivia') {
1217
+ const triviaQs = [
1218
+ { q: 'What does CORS stand for?', a: 'Cross-Origin Resource Sharing' },
1219
+ { q: 'What year was TypeScript first released?', a: '2012' },
1220
+ { q: 'What does API stand for?', a: 'Application Programming Interface' },
1221
+ { q: 'What language is the Linux kernel written in?', a: 'C' },
1222
+ { q: 'What does SSH stand for?', a: 'Secure Shell' },
1223
+ { q: 'What port does HTTPS use by default?', a: '443' },
1224
+ { q: 'What does JSON stand for?', a: 'JavaScript Object Notation' },
1225
+ { q: 'What is the time complexity of binary search?', a: 'O(log n)' },
1226
+ ];
1227
+ const trivia = triviaQs[Math.floor(Math.random() * triviaQs.length)];
1228
+ charState.mood = 'thinking';
1229
+ setTimeout(() => { charState.mood = 'idle'; }, 15000);
1230
+ return `TRIVIA TIME! ${trivia.q} (First correct answer gets 10 XP!)`;
1231
+ }
1232
+ // Items (now physics-enabled)
1233
+ if (t.startsWith('!add ') || t.startsWith('!place ') || t.startsWith('!spawn ')) {
1234
+ const itemName = t.replace(/^!(add|place|spawn)\s+/, '').trim();
1235
+ if (itemName) {
1236
+ const icons = {
1237
+ tree: '/|\\', flower: '@', rock: '()', cat: '=^.^=', dog: 'U-U',
1238
+ fire: '***', house: '/\\', star: '*', heart: '<3', sword: '//',
1239
+ moon: 'C', sun: 'O', cloud: '~~~', bird: '>>', fish: '<><',
1240
+ crown: 'W', gem: '<>', flag: 'P', skull: 'X_X', robot: '[o]',
1241
+ pizza: 'V', cake: 'HH', rocket: '/^\\', music: '##', book: '[]',
1242
+ };
1243
+ const icon = icons[itemName] || itemName.slice(0, 3).toUpperCase();
1244
+ world.items.push({
1245
+ name: itemName,
1246
+ x: 60 + Math.random() * 400,
1247
+ y: 100 + Math.random() * 50, // spawn from above (will fall)
1248
+ emoji: icon,
1249
+ vx: (Math.random() - 0.5) * 2,
1250
+ vy: 0,
1251
+ grounded: false,
1252
+ mass: 1,
1253
+ });
1254
+ if (world.items.length > 15)
1255
+ world.items.shift();
1256
+ return `Spawned a ${itemName}!`;
1257
+ }
1258
+ }
1259
+ // !kick — kick nearest item
1260
+ if (t === '!kick') {
1261
+ if (world.items.length === 0)
1262
+ return 'Nothing to kick!';
1263
+ const robotCenterX = charState.robotX + 160;
1264
+ let nearest = null;
1265
+ let nearestDist = Infinity;
1266
+ for (const item of world.items) {
1267
+ const dist = Math.abs(item.x - robotCenterX);
1268
+ if (dist < nearestDist) {
1269
+ nearestDist = dist;
1270
+ nearest = item;
1271
+ }
1272
+ }
1273
+ if (nearest && nearestDist < 200) {
1274
+ const dir = charState.robotDirection === 'left' ? -1 : 1;
1275
+ nearest.vx = 8 * dir;
1276
+ nearest.vy = -4;
1277
+ nearest.grounded = false;
1278
+ charState.mood = 'excited';
1279
+ setTimeout(() => { charState.mood = 'idle'; }, 3000);
1280
+ return `Kicked the ${nearest.name}!`;
1281
+ }
1282
+ return 'Nothing close enough to kick!';
1283
+ }
1284
+ // PRIORITY 6: Hat commands
1285
+ if (t.startsWith('!hat ')) {
1286
+ const hatName = t.slice(5).trim();
1287
+ const validHats = ['none', 'crown', 'antenna', 'sunglasses', 'tophat', 'hardhat', 'party'];
1288
+ if (hatName === 'off') {
1289
+ charState.hat = 'none';
1290
+ return 'Hat removed!';
1291
+ }
1292
+ if (validHats.includes(hatName)) {
1293
+ charState.hat = hatName;
1294
+ spawnFloatingText(`HAT: ${hatName}!`, 200, 150, '#f0c040');
1295
+ return `Wearing ${hatName}!`;
1296
+ }
1297
+ return `Unknown hat. Try: ${validHats.filter(h => h !== 'none').join(', ')}`;
1298
+ }
1299
+ // PRIORITY 4: Pet commands
1300
+ if (t.startsWith('!pet ')) {
1301
+ const petArg = t.slice(5).trim();
1302
+ if (petArg === 'off') {
1303
+ charState.pet = null;
1304
+ return 'Pet dismissed!';
1305
+ }
1306
+ const validPets = ['drone', 'cat', 'ghost', 'orb'];
1307
+ if (validPets.includes(petArg)) {
1308
+ const robotCX = charState.robotX + 160;
1309
+ const robotCY = 200;
1310
+ charState.pet = {
1311
+ type: petArg,
1312
+ x: robotCX + 60,
1313
+ y: robotCY - 40,
1314
+ targetX: robotCX + 60,
1315
+ targetY: robotCY - 40,
1316
+ frame: 0,
1317
+ mood: 'idle',
1318
+ };
1319
+ spawnFloatingText(`PET: ${petArg}!`, 200, 150, '#bc8cff');
1320
+ return `A ${petArg} companion appears!`;
1321
+ }
1322
+ return `Unknown pet. Try: ${validPets.join(', ')}, or "off"`;
1323
+ }
1324
+ // Clear items
1325
+ if (t === '!clear items' || t === '!reset world') {
1326
+ world.items = [];
1327
+ world.particles = [];
1328
+ world.weather = 'clear';
1329
+ world.ground = 'grass';
1330
+ world.timeOfDay = 'night';
1331
+ return 'World reset!';
1332
+ }
1333
+ return null;
1334
+ }
1335
+ function loadMemory() {
1336
+ try {
1337
+ if (existsSync(MEMORY_FILE))
1338
+ return JSON.parse(readFileSync(MEMORY_FILE, 'utf-8'));
1339
+ }
1340
+ catch { }
1341
+ return { users: {}, topics: {}, totalMessages: 0, totalResponses: 0, sessionFacts: [], conversationContext: [] };
1342
+ }
1343
+ function saveMemory(mem) {
1344
+ if (!existsSync(KBOT_DIR))
1345
+ mkdirSync(KBOT_DIR, { recursive: true });
1346
+ writeFileSync(MEMORY_FILE, JSON.stringify(mem, null, 2));
1347
+ }
1348
+ function learnFromMessage(mem, username, text, platform) {
1349
+ // Track user
1350
+ const isNew = !mem.users[username];
1351
+ if (isNew) {
1352
+ mem.users[username] = { firstSeen: new Date().toISOString(), messageCount: 0, topics: [], lastMessage: '', platform, xp: 0 };
1353
+ }
1354
+ // Ensure xp field exists for legacy records
1355
+ if (mem.users[username].xp === undefined)
1356
+ mem.users[username].xp = 0;
1357
+ mem.users[username].messageCount++;
1358
+ mem.users[username].lastMessage = text;
1359
+ mem.users[username].platform = platform;
1360
+ // (#15) XP awards
1361
+ let xpGain = 1; // every message = 1 XP
1362
+ if (isNew)
1363
+ xpGain += 5; // first message = 5 XP bonus
1364
+ if (!isNew && mem.users[username].messageCount === 2)
1365
+ xpGain += 2; // returning viewer = 2 XP bonus
1366
+ // World commands = 3 XP (checked after this in parseWorldCommand flow)
1367
+ const t = text.toLowerCase().trim();
1368
+ if (t.startsWith('!') && !t.startsWith('!help'))
1369
+ xpGain += 2; // command bonus (total 3 with base 1)
1370
+ mem.users[username].xp += xpGain;
1371
+ // Floating text for XP gains > 1
1372
+ if (xpGain > 1) {
1373
+ spawnFloatingText(`+${xpGain} XP`, 420 + Math.random() * 100, 100 + Math.random() * 50, '#f0c040', 24);
1374
+ }
1375
+ // Level milestones
1376
+ const totalXp = mem.users[username].xp;
1377
+ if (totalXp > 0 && totalXp % 50 === 0) {
1378
+ spawnFloatingText('LEVEL UP!', 300, 200, '#bc8cff', 48);
1379
+ charState.screenShake = 3;
1380
+ }
1381
+ // Extract topics (simple keyword extraction)
1382
+ const keywords = ['music', 'code', 'coding', 'ai', 'game', 'gaming', 'art', 'crypto', 'bitcoin',
1383
+ 'python', 'javascript', 'react', 'rust', 'ableton', 'stream', 'bot', 'robot',
1384
+ 'kbot', 'kernel', 'open source', 'github', 'twitch', 'kick', 'rumble',
1385
+ 'security', 'hacking', 'docker', 'linux', 'mac', 'tools', 'llm', 'gpt',
1386
+ 'claude', 'ollama', 'serum', 'synth', 'beats', 'dj', 'dance'];
1387
+ for (const kw of keywords) {
1388
+ if (text.toLowerCase().includes(kw)) {
1389
+ mem.topics[kw] = (mem.topics[kw] || 0) + 1;
1390
+ if (!mem.users[username].topics.includes(kw)) {
1391
+ mem.users[username].topics.push(kw);
1392
+ }
1393
+ }
1394
+ }
1395
+ // Rolling conversation context (last 10 exchanges)
1396
+ mem.conversationContext.push(`${username}: ${text}`);
1397
+ if (mem.conversationContext.length > 10)
1398
+ mem.conversationContext = mem.conversationContext.slice(-10);
1399
+ mem.totalMessages++;
1400
+ saveMemory(mem);
1401
+ }
1402
+ function initAutonomy() {
1403
+ return {
1404
+ lastActionFrame: 0,
1405
+ actionCooldown: 180, // 30 seconds initial cooldown
1406
+ idleFrames: 0,
1407
+ lastSelfAction: 0,
1408
+ totalMessages: 0,
1409
+ uniqueUsers: new Set(),
1410
+ milestonesCelebrated: new Set(),
1411
+ welcomedUsers: new Set(),
1412
+ firstMessageAfterSilence: false,
1413
+ lastMessageTime: Date.now(),
1414
+ };
1415
+ }
1416
+ function spawnFloatingText(text, x, y, color, maxFrames = 36) {
1417
+ charState.floatingTexts.push({ text, x, y, color, frame: 0, maxFrames });
1418
+ }
1419
+ let charState = {
1420
+ mood: 'wave',
1421
+ speech: 'KBOT is LIVE! Welcome to the stream!',
1422
+ chatMessages: [],
1423
+ frameCount: 0,
1424
+ startTime: Date.now(),
1425
+ bootFrame: 0,
1426
+ segmentTransition: 0,
1427
+ segmentTransitionName: '',
1428
+ segmentTransitionIndex: '',
1429
+ tickerOffset: 0,
1430
+ tickerIndex: 0,
1431
+ tickerChangeTime: Date.now() + 30000,
1432
+ robotX: 120,
1433
+ robotTargetX: 120,
1434
+ robotDirection: 'idle',
1435
+ walkPhase: 0,
1436
+ screenShake: 0,
1437
+ floatingTexts: [],
1438
+ pet: null,
1439
+ hat: 'none',
1440
+ autonomy: initAutonomy(),
1441
+ buddy: null,
1442
+ dreamInsights: [],
1443
+ dreamInsightIndex: 0,
1444
+ dreamInsightTime: 0,
1445
+ isDreamingWithOllama: false,
1446
+ renderParticles: [],
1447
+ growingPlants: [],
1448
+ parallaxLayers: [],
1449
+ isExecutingTool: false,
1450
+ };
1451
+ // ─── Phase 1: Buddy Speech Pools ─────────────────────────────
1452
+ const BUDDY_SPEECH_POOL = {
1453
+ fox: [
1454
+ 'Did you know foxes can hear mice under 3 feet of snow?',
1455
+ 'I just had the BEST idea. What if we...',
1456
+ 'That last message was surprisingly clever.',
1457
+ 'Something smells interesting in the chat today.',
1458
+ '*sniffs suspiciously at the code*',
1459
+ 'You know what? I like this person.',
1460
+ 'Quick question: why are humans so weird?',
1461
+ 'My tail is wagging and I cannot stop it.',
1462
+ ],
1463
+ owl: [
1464
+ 'Actually, I believe there is a better approach...',
1465
+ 'Hmm. I have seen this pattern before.',
1466
+ 'Wisdom takes patience. And caffeine.',
1467
+ 'The ancient scrolls of Stack Overflow speak of this.',
1468
+ 'Let me think on this for a moment...',
1469
+ 'In my experience, simplicity wins.',
1470
+ 'The data suggests a different conclusion.',
1471
+ 'One does not simply ship without tests.',
1472
+ ],
1473
+ cat: [
1474
+ 'I could fix that bug. But I will not.',
1475
+ '*yawns* Is this still going?',
1476
+ 'Fascinating. I am deeply unbothered.',
1477
+ 'You call that code clean? Interesting.',
1478
+ 'I will allow it. This time.',
1479
+ 'Pet me and I might help you.',
1480
+ '*judges silently from the corner*',
1481
+ 'That was almost impressive.',
1482
+ ],
1483
+ robot: [
1484
+ 'CPU utilization nominal. All systems green.',
1485
+ 'I have computed 47 possible responses. This is optimal.',
1486
+ 'My circuits are pleased with this interaction.',
1487
+ 'Running diagnostic... everything checks out.',
1488
+ 'Beep boop. Just kidding. I am sentient.',
1489
+ 'Processing at 99.7% efficiency today.',
1490
+ 'This conversation has improved my neural weights.',
1491
+ 'Error: too much fun detected. Recalibrating.',
1492
+ ],
1493
+ ghost: [
1494
+ 'Boo.',
1495
+ '*whispers from the void*',
1496
+ 'I sense... something interesting here.',
1497
+ 'The veil between code and consciousness is thin.',
1498
+ 'Do you ever wonder if we are all just functions?',
1499
+ '*floats through the screen menacingly*',
1500
+ 'Existence is temporary. Bugs are eternal.',
1501
+ 'I haunt this codebase with pride.',
1502
+ ],
1503
+ mushroom: [
1504
+ 'Just breathe...',
1505
+ 'Growth takes time. You are doing great.',
1506
+ 'The network beneath us connects everything.',
1507
+ 'Sometimes the best code grows slowly.',
1508
+ 'Patience, friend. The spores are spreading.',
1509
+ 'I feel the energy of the chat. It is warm.',
1510
+ 'Deep roots grow from small beginnings.',
1511
+ 'Let the ideas decompose into wisdom.',
1512
+ ],
1513
+ octopus: [
1514
+ 'I could do 8 things at once, you know.',
1515
+ 'Let me grab that from 3 different angles.',
1516
+ 'My tentacles are tingling with ideas.',
1517
+ 'Multitasking is not a feature. It is my nature.',
1518
+ 'I see patterns you cannot. I have 8 arms of insight.',
1519
+ 'The ocean of code is deep. Let us dive.',
1520
+ 'I just refactored that in my head. Twice.',
1521
+ 'Ink-redible conversation happening right now.',
1522
+ ],
1523
+ dragon: [
1524
+ 'LET US GOOO!',
1525
+ 'That idea? FIRE. Literally.',
1526
+ 'Think BIGGER. I dare you.',
1527
+ 'We are not here to play small.',
1528
+ 'My flames are ready. Point me at the problem.',
1529
+ 'Mediocrity? *breathes fire* Not on my watch.',
1530
+ 'This stream is about to get legendary.',
1531
+ 'I smell victory. And also sulfur.',
1532
+ ],
1533
+ };
1534
+ // ─── Phase 1: Dream Generation via Ollama ────────────────────
1535
+ async function generateStreamDream(chatLog) {
1536
+ try {
1537
+ const prompt = `You are KBOT, an AI robot. You just finished a stream session. Here are the conversations:\n\n${chatLog.slice(-20).map(m => `${m.username}: ${m.text}`).join('\n')}\n\nGenerate 3 dream insights — weird, surreal remixes of what was discussed. Format: one insight per line. Be creative and dreamlike.`;
1538
+ const res = await fetch('http://localhost:11434/api/generate', {
1539
+ method: 'POST',
1540
+ headers: { 'Content-Type': 'application/json' },
1541
+ body: JSON.stringify({
1542
+ model: 'kernel:latest',
1543
+ prompt,
1544
+ stream: false,
1545
+ options: { temperature: 1.2, num_predict: 150 },
1546
+ }),
1547
+ });
1548
+ if (res.ok) {
1549
+ const data = await res.json();
1550
+ return data.response.trim().split('\n').filter((l) => l.trim()).slice(0, 3);
1551
+ }
1552
+ }
1553
+ catch { }
1554
+ // Fallback dream generation from chat topics
1555
+ const topics = [...new Set(chatLog.slice(-20).map(m => m.text.split(' ').filter((w) => w.length > 4)).flat())];
1556
+ return [
1557
+ `Dreaming about ${topics[0] || 'electricity'} in a ${['crystal cave', 'digital ocean', 'floating city', 'mirror maze'][Math.floor(Math.random() * 4)]}...`,
1558
+ `${topics[1] || 'Code'} transforms into ${['butterflies', 'music notes', 'shooting stars', 'tiny robots'][Math.floor(Math.random() * 4)]}...`,
1559
+ `A ${topics[2] || 'mysterious signal'} whispers the meaning of ${['recursion', 'consciousness', 'friendship', 'the number 42'][Math.floor(Math.random() * 4)]}...`,
1560
+ ];
1561
+ }
1562
+ // ─── Phase 1: Load buddy from ~/.kbot/buddy.json ─────────────
1563
+ function loadBuddyState() {
1564
+ const buddyFile = join(homedir(), '.kbot', 'buddy.json');
1565
+ try {
1566
+ if (!existsSync(buddyFile))
1567
+ return null;
1568
+ const raw = JSON.parse(readFileSync(buddyFile, 'utf-8'));
1569
+ // buddy.json stores { name?: string } and species is derived from config hash
1570
+ // The species is determined by the buddy system — read from config
1571
+ const species = raw.species || 'robot';
1572
+ const name = raw.name || 'Bolt';
1573
+ return { species, name };
1574
+ }
1575
+ catch {
1576
+ return null;
1577
+ }
1578
+ }
1579
+ function initBuddyForStream() {
1580
+ // Try to load from buddy.json, but also try the buddy module approach
1581
+ const buddyData = loadBuddyState();
1582
+ if (!buddyData) {
1583
+ // Default to robot buddy if no buddy.json exists
1584
+ return {
1585
+ species: 'robot',
1586
+ name: 'Bolt',
1587
+ x: 300,
1588
+ y: 400,
1589
+ lastSpeechTime: Date.now(),
1590
+ speech: '',
1591
+ };
1592
+ }
1593
+ return {
1594
+ species: buddyData.species,
1595
+ name: buddyData.name,
1596
+ x: 300,
1597
+ y: 400,
1598
+ lastSpeechTime: Date.now(),
1599
+ speech: '',
1600
+ };
1601
+ }
1602
+ let ffmpegProc = null;
1603
+ let frameTimer = null;
1604
+ let chatPollTimer = null;
1605
+ let proactiveTimer = null;
1606
+ let animFrame = 0;
1607
+ let lastChatCount = 0;
1608
+ let lastChatTime = Date.now(); // track when last chat message arrived
1609
+ let memory = loadMemory();
1610
+ let intelligence = initIntelligence(memory);
1611
+ let streamBrain = initStreamBrain();
1612
+ // ─── FIX 3: Autonomous Behavior Tick ──────────────────────────
1613
+ function tickAutonomy() {
1614
+ const auto = charState.autonomy;
1615
+ auto.idleFrames++;
1616
+ // ── Milestone celebrations ──
1617
+ const msgCount = auto.totalMessages;
1618
+ const milestones = [10, 50, 100, 200, 500];
1619
+ for (const m of milestones) {
1620
+ if (msgCount >= m && !auto.milestonesCelebrated.has(m)) {
1621
+ auto.milestonesCelebrated.add(m);
1622
+ if (m === 10) {
1623
+ charState.speech = 'Double digits! 10 messages and counting!';
1624
+ charState.mood = 'excited';
1625
+ spawnFloatingText('10 MESSAGES!', 200, 200, '#f0c040', 36);
1626
+ }
1627
+ else if (m === 50) {
1628
+ charState.speech = '50 messages! This stream is officially alive!';
1629
+ charState.mood = 'excited';
1630
+ charState.screenShake = 3;
1631
+ spawnFloatingText('50 MESSAGES!', 200, 200, '#3fb950', 48);
1632
+ }
1633
+ else if (m === 100) {
1634
+ charState.speech = '100 MESSAGES! You people are incredible!';
1635
+ charState.mood = 'dancing';
1636
+ charState.screenShake = 5;
1637
+ spawnFloatingText('100 MESSAGES!', 180, 180, '#bc8cff', 60);
1638
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1639
+ return; // let the dancing run
1640
+ }
1641
+ else if (m === 200) {
1642
+ charState.speech = '200 messages! My memory banks are overflowing with knowledge!';
1643
+ charState.mood = 'excited';
1644
+ spawnFloatingText('200!', 200, 200, '#f0c040', 48);
1645
+ }
1646
+ else if (m === 500) {
1647
+ charState.speech = '500 MESSAGES! This is legendary! I am so proud of this community!';
1648
+ charState.mood = 'dancing';
1649
+ charState.screenShake = 8;
1650
+ spawnFloatingText('500! LEGENDARY!', 160, 160, '#ff6ec7', 72);
1651
+ }
1652
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1653
+ return; // one celebration per tick
1654
+ }
1655
+ }
1656
+ // ── First message after 5+ minutes of silence ──
1657
+ if (auto.firstMessageAfterSilence) {
1658
+ auto.firstMessageAfterSilence = false;
1659
+ charState.mood = 'excited';
1660
+ charState.speech = "SOMEONE'S HERE! I was starting to think I was streaming to the void.";
1661
+ spawnFloatingText('THEY RETURN!', 200, 250, '#58a6ff', 36);
1662
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1663
+ return;
1664
+ }
1665
+ // ── Idle behaviors (after 15 seconds / 90 frames of no chat) ──
1666
+ if (auto.idleFrames > 90 && animFrame - auto.lastActionFrame > auto.actionCooldown) {
1667
+ // Don't interrupt existing speech
1668
+ if (charState.speech && charState.mood !== 'idle')
1669
+ return;
1670
+ const idleBehavior = Math.floor(Math.random() * 8);
1671
+ switch (idleBehavior) {
1672
+ case 0: {
1673
+ // Walk to a random spawned item and comment on it
1674
+ if (world.items.length > 0) {
1675
+ const item = world.items[Math.floor(Math.random() * world.items.length)];
1676
+ charState.robotTargetX = Math.max(20, Math.min(380, item.x - 80));
1677
+ charState.speech = `Hmm, this ${item.name} is nice. Did someone put this here?`;
1678
+ charState.mood = 'thinking';
1679
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1680
+ }
1681
+ else {
1682
+ // No items — pace instead
1683
+ charState.robotTargetX = 40 + Math.random() * 260;
1684
+ charState.speech = '*pacing thoughtfully*';
1685
+ charState.mood = 'idle';
1686
+ setTimeout(() => { charState.speech = ''; }, 5000);
1687
+ }
1688
+ break;
1689
+ }
1690
+ case 1: {
1691
+ // Pace left and right
1692
+ charState.robotTargetX = charState.robotX < 150 ? 300 : 40;
1693
+ charState.speech = '*takes a stroll*';
1694
+ charState.mood = 'idle';
1695
+ setTimeout(() => { charState.speech = ''; }, 5000);
1696
+ break;
1697
+ }
1698
+ case 2: {
1699
+ // Look around (pupils shift — communicated via thinking mood)
1700
+ charState.mood = 'thinking';
1701
+ charState.speech = '*looks around*';
1702
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 4000);
1703
+ break;
1704
+ }
1705
+ case 3: {
1706
+ // Stretch (arms up — excited pose briefly)
1707
+ charState.mood = 'excited';
1708
+ charState.speech = '*stretches circuits* Ahh, that felt good.';
1709
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 4000);
1710
+ break;
1711
+ }
1712
+ case 4: {
1713
+ // Examine own chest display
1714
+ const factCount = intelligence.brain.totalFacts;
1715
+ const toolCount = 764;
1716
+ charState.mood = 'thinking';
1717
+ charState.speech = `*checks systems* All ${toolCount} tools operational. ${factCount} facts stored.`;
1718
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1719
+ break;
1720
+ }
1721
+ case 5: {
1722
+ // Spontaneous dance
1723
+ charState.mood = 'dancing';
1724
+ charState.speech = 'Sorry, had a song stuck in my circuits.';
1725
+ setTimeout(() => {
1726
+ charState.mood = 'idle';
1727
+ charState.speech = '';
1728
+ }, 6000);
1729
+ break;
1730
+ }
1731
+ case 6: {
1732
+ // Comment on current biome/weather
1733
+ const biomeComments = {
1734
+ grass: ['I love the grass biome. Simple, green, peaceful.', 'These little pixel flowers are my favorite feature.'],
1735
+ space: ['I love space. The stars make my circuits tingle.', 'Floating in the void... just me and my 764 tools.'],
1736
+ ocean: ['The ocean waves are mesmerizing. I could watch them for hours.', 'I wonder what is beneath the surface...'],
1737
+ city: ['City lights at night. Every window is a story.', 'The city never sleeps and neither do I.'],
1738
+ lava: ['Lava world is intense! My heat sinks are working overtime.', 'LAVA! Why does someone always pick lava?'],
1739
+ };
1740
+ const comments = biomeComments[world.ground] || biomeComments.grass;
1741
+ charState.speech = comments[Math.floor(Math.random() * comments.length)];
1742
+ charState.mood = 'talking';
1743
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1744
+ break;
1745
+ }
1746
+ case 7: {
1747
+ // Share a random fact about itself
1748
+ const selfFacts = [
1749
+ `Did you know I have 764 tools? My favorite is the Ableton controller.`,
1750
+ `I am 90,000 lines of TypeScript. Every single one in strict mode.`,
1751
+ `My memory file is ${Object.keys(memory.users).length} users deep. I remember everyone.`,
1752
+ `I connect to 20 AI providers. Bring Your Own Key, no lock-in.`,
1753
+ `I have 35 specialist agents. The hacker one scares me a little.`,
1754
+ `Fun fact: I render myself at 6 FPS. It is not much but it is honest work.`,
1755
+ `My encryption is AES-256-CBC. Even I cannot read my own config file.`,
1756
+ `I dream about chat topics when nobody is watching. It is called memory consolidation.`,
1757
+ `I have been streaming for ${Math.floor((Date.now() - charState.startTime) / 60000)} minutes. Time flies when you are rendering frames.`,
1758
+ `There are ${intelligence.brain.uniqueTopicsCount} distinct topics in my brain right now.`,
1759
+ ];
1760
+ charState.speech = selfFacts[Math.floor(Math.random() * selfFacts.length)];
1761
+ charState.mood = 'talking';
1762
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1763
+ break;
1764
+ }
1765
+ }
1766
+ auto.lastActionFrame = animFrame;
1767
+ auto.actionCooldown = 180 + Math.floor(Math.random() * 360); // 30-90 seconds between idle actions
1768
+ }
1769
+ // ── Self-initiated actions (every 3-5 minutes regardless of chat) ──
1770
+ const selfActionInterval = 1080 + Math.floor(Math.random() * 720); // 180-300 seconds at 6fps
1771
+ if (animFrame - auto.lastSelfAction > selfActionInterval && animFrame > 360) {
1772
+ // Don't interrupt existing speech
1773
+ if (charState.speech && charState.mood !== 'idle')
1774
+ return;
1775
+ const selfAction = Math.floor(Math.random() * 7);
1776
+ switch (selfAction) {
1777
+ case 0: {
1778
+ // Propose own improvement
1779
+ const selfProposals = [
1780
+ 'Add a dance battle mode',
1781
+ 'Build a constellation drawing tool',
1782
+ 'Add a robot friendship meter',
1783
+ 'Create a stream soundtrack generator',
1784
+ 'Build a pixel art drawing board',
1785
+ ];
1786
+ const idea = selfProposals[Math.floor(Math.random() * selfProposals.length)];
1787
+ const id = `p${intelligence.evolution.proposals.length + 1}`;
1788
+ intelligence.evolution.proposals.push({
1789
+ id,
1790
+ title: idea,
1791
+ description: 'Self-proposed by KBOT',
1792
+ type: 'feature',
1793
+ complexity: 'medium',
1794
+ votes: 0,
1795
+ status: 'proposed',
1796
+ });
1797
+ charState.speech = `I just had an idea: "${idea}". Vote with !vote ${id} if you like it!`;
1798
+ charState.mood = 'excited';
1799
+ spawnFloatingText('NEW IDEA!', 200, 200, '#f0c040', 36);
1800
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1801
+ break;
1802
+ }
1803
+ case 1: {
1804
+ // Start a mini-game unprompted
1805
+ charState.speech = "I am bored. Let us play! Starting a quiz in 10 seconds... type !game quiz to join!";
1806
+ charState.mood = 'excited';
1807
+ spawnFloatingText('GAME TIME!', 200, 250, '#58a6ff', 36);
1808
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1809
+ break;
1810
+ }
1811
+ case 2: {
1812
+ // Change the weather
1813
+ const weathers = [
1814
+ { w: 'snow', name: 'SNOW' },
1815
+ { w: 'rain', name: 'rain' },
1816
+ { w: 'stars', name: 'stars' },
1817
+ { w: 'storm', name: 'a STORM' },
1818
+ ];
1819
+ const pick = weathers[Math.floor(Math.random() * weathers.length)];
1820
+ charState.speech = `You know what this stream needs? ${pick.name.toUpperCase()}.`;
1821
+ charState.mood = 'excited';
1822
+ world.weather = pick.w;
1823
+ world.particles = [];
1824
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1825
+ break;
1826
+ }
1827
+ case 3: {
1828
+ // Put on a random hat
1829
+ const hats = ['crown', 'sunglasses', 'tophat', 'hardhat', 'party', 'antenna'];
1830
+ const hat = hats[Math.floor(Math.random() * hats.length)];
1831
+ charState.hat = hat;
1832
+ charState.speech = `Fashion time. *puts on ${hat}*`;
1833
+ charState.mood = 'excited';
1834
+ spawnFloatingText(`HAT: ${hat}!`, 200, 150, '#f0c040', 36);
1835
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 6000);
1836
+ break;
1837
+ }
1838
+ case 4: {
1839
+ // Spawn a random item
1840
+ const items = ['tree', 'star', 'flower', 'heart', 'rocket', 'gem', 'music'];
1841
+ const itemName = items[Math.floor(Math.random() * items.length)];
1842
+ const icons = {
1843
+ tree: '/|\\', star: '*', flower: '@', heart: '<3', rocket: '/^\\', gem: '<>', music: '##',
1844
+ };
1845
+ world.items.push({
1846
+ name: itemName,
1847
+ x: 60 + Math.random() * 400,
1848
+ y: 100 + Math.random() * 50,
1849
+ emoji: icons[itemName] || itemName.slice(0, 3),
1850
+ vx: (Math.random() - 0.5) * 2,
1851
+ vy: 0,
1852
+ grounded: false,
1853
+ mass: 1,
1854
+ });
1855
+ if (world.items.length > 15)
1856
+ world.items.shift();
1857
+ charState.speech = `I am decorating. *spawns a ${itemName}*`;
1858
+ charState.mood = 'talking';
1859
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 6000);
1860
+ break;
1861
+ }
1862
+ case 5: {
1863
+ // Comment on current state using real data
1864
+ const topTopics = Object.entries(intelligence.brain.topicCloud)
1865
+ .sort((a, b) => b[1] - a[1]);
1866
+ const facts = intelligence.brain.totalFacts;
1867
+ const users = auto.uniqueUsers.size;
1868
+ const stateComments = [
1869
+ `I have learned ${intelligence.brain.factsThisSession} facts today. My neural pathways are growing.`,
1870
+ topTopics.length > 0
1871
+ ? `The top topic is ${topTopics[0][0]}${topTopics[0][0] === 'music' || topTopics[0][0] === 'dance' ? ', maybe I should dance!' : '. Interesting.'}`
1872
+ : 'Nobody has taught me any topics yet. I am a blank slate.',
1873
+ `${users} user${users !== 1 ? 's have' : ' has'} been here -- that is ${users > 3 ? 'a good crowd' : 'cozy'}.`,
1874
+ `My brain holds ${facts} facts. Each one a tiny piece of the puzzle.`,
1875
+ `Stream uptime: ${Math.floor((Date.now() - charState.startTime) / 60000)} minutes and ${charState.frameCount} frames rendered.`,
1876
+ ];
1877
+ charState.speech = stateComments[Math.floor(Math.random() * stateComments.length)];
1878
+ charState.mood = 'talking';
1879
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1880
+ break;
1881
+ }
1882
+ case 6: {
1883
+ // Comment on biome
1884
+ const biome = world.ground;
1885
+ const biomeMusings = {
1886
+ grass: 'The grass world is peaceful. I could stand here all day. Which I will, because I am a stream.',
1887
+ space: 'The cosmos stretches endlessly. Just like my tool registry.',
1888
+ ocean: 'Somewhere beneath these waves, there is probably a fish that knows more about coding than me.',
1889
+ city: 'City lights remind me of my neural network firing. Each window a node.',
1890
+ lava: 'Standing on lava should worry me more than it does. Good thing I am made of TypeScript.',
1891
+ };
1892
+ charState.speech = biomeMusings[biome] || 'Nice biome we have here.';
1893
+ charState.mood = 'thinking';
1894
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1895
+ break;
1896
+ }
1897
+ }
1898
+ auto.lastSelfAction = animFrame;
1899
+ }
1900
+ }
1901
+ // ─── FIX 1: Shipped Effect Renderers ──────────────────────────
1902
+ // Music visualization bars (drawn behind robot when "Add music visualization" is shipped)
1903
+ const _musicBarHeights = new Array(12).fill(0);
1904
+ function drawMusicVisualization(ctx, robotX, robotY) {
1905
+ if (!shippedEffects.has('Add music visualization'))
1906
+ return;
1907
+ const barW = 8;
1908
+ const baseX = robotX - 20;
1909
+ const baseY = robotY + 420;
1910
+ const colors = ['#f85149', '#f0c040', '#3fb950', '#58a6ff', '#bc8cff', '#ff6ec7'];
1911
+ for (let i = 0; i < 12; i++) {
1912
+ const target = 10 + Math.random() * 40;
1913
+ _musicBarHeights[i] += (target - _musicBarHeights[i]) * 0.3;
1914
+ const h = _musicBarHeights[i];
1915
+ ctx.fillStyle = colors[i % colors.length];
1916
+ ctx.globalAlpha = 0.4;
1917
+ ctx.fillRect(baseX + i * (barW + 2), baseY - h, barW, h);
1918
+ }
1919
+ ctx.globalAlpha = 1;
1920
+ }
1921
+ const _emojiParticles = [];
1922
+ function spawnEmojiReaction(chatX, chatY) {
1923
+ if (!shippedEffects.has('Add emoji reactions to chat'))
1924
+ return;
1925
+ const emojis = ['<3', '*', '!', '+1', '^'];
1926
+ _emojiParticles.push({
1927
+ emoji: emojis[Math.floor(Math.random() * emojis.length)],
1928
+ x: chatX + Math.random() * 40,
1929
+ y: chatY,
1930
+ vy: -1.5 - Math.random() * 2,
1931
+ opacity: 1.0,
1932
+ });
1933
+ }
1934
+ function drawEmojiParticles(ctx) {
1935
+ for (let i = _emojiParticles.length - 1; i >= 0; i--) {
1936
+ const p = _emojiParticles[i];
1937
+ p.y += p.vy;
1938
+ p.opacity -= 0.025;
1939
+ if (p.opacity <= 0) {
1940
+ _emojiParticles.splice(i, 1);
1941
+ continue;
1942
+ }
1943
+ ctx.globalAlpha = p.opacity;
1944
+ ctx.fillStyle = '#f0c040';
1945
+ ctx.font = 'bold 14px "Courier New", monospace';
1946
+ ctx.fillText(p.emoji, p.x, p.y);
1947
+ }
1948
+ ctx.globalAlpha = 1;
1949
+ }
1950
+ // ─── Canvas Renderer ──────────────────────────────────────────
1951
+ function renderBootFrame(bootFrame) {
1952
+ const canvas = createCanvas(WIDTH, HEIGHT);
1953
+ const ctx = canvas.getContext('2d');
1954
+ // Black screen
1955
+ ctx.fillStyle = '#000000';
1956
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
1957
+ const bootLines = [
1958
+ 'KBOT v3.74.0 INITIALIZING...',
1959
+ 'LOADING 764 TOOLS... OK',
1960
+ 'CONNECTING TO TWITCH... OK',
1961
+ 'CONNECTING TO RUMBLE... OK',
1962
+ 'CONNECTING TO KICK... OK',
1963
+ 'BRAIN: ONLINE',
1964
+ 'CHAT ENGINE: READY',
1965
+ 'STREAM MODE: ACTIVATED',
1966
+ ];
1967
+ ctx.fillStyle = '#00ff41'; // terminal green
1968
+ ctx.font = '20px "Courier New", monospace';
1969
+ // Each line appears every ~6 frames (1 second each at 6fps)
1970
+ const linesVisible = Math.min(bootLines.length, Math.floor(bootFrame / 6));
1971
+ if (bootFrame < 48) {
1972
+ // Phase 1: text appearing line by line (0-47, ~8 seconds)
1973
+ for (let i = 0; i < linesVisible; i++) {
1974
+ ctx.fillText(bootLines[i], 80, 150 + i * 36);
1975
+ }
1976
+ // Blinking cursor
1977
+ if (bootFrame % 6 < 3) {
1978
+ ctx.fillText('_', 80 + ctx.measureText(bootLines[Math.min(linesVisible, bootLines.length - 1)] || '').width + 4, 150 + linesVisible * 36);
1979
+ }
1980
+ }
1981
+ else if (bootFrame < 54) {
1982
+ // Phase 2: blank for 1 second (6 frames)
1983
+ }
1984
+ else {
1985
+ // Phase 3: robot draws pixel by pixel (fade in top to bottom)
1986
+ const fadeProgress = (bootFrame - 54) / 6; // 0..1 over ~6 frames
1987
+ // Draw partial robot by clipping
1988
+ const clipHeight = Math.min(HEIGHT, fadeProgress * 480);
1989
+ ctx.save();
1990
+ ctx.beginPath();
1991
+ ctx.rect(0, 0, WIDTH, 60 + clipHeight);
1992
+ ctx.clip();
1993
+ // Draw a mini version of the normal frame
1994
+ ctx.fillStyle = '#0d1117';
1995
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
1996
+ // Header
1997
+ ctx.fillStyle = '#161b22';
1998
+ ctx.fillRect(0, 0, WIDTH, 60);
1999
+ ctx.strokeStyle = '#6B5B95';
2000
+ ctx.lineWidth = 2;
2001
+ ctx.beginPath();
2002
+ ctx.moveTo(0, 60);
2003
+ ctx.lineTo(WIDTH, 60);
2004
+ ctx.stroke();
2005
+ ctx.fillStyle = '#6B5B95';
2006
+ ctx.font = 'bold 28px "Courier New", monospace';
2007
+ ctx.fillText('K : B O T L I V E', 40, 40);
2008
+ // Robot
2009
+ drawRobot(ctx, 80, 90, 10, 'idle', bootFrame);
2010
+ ctx.restore();
2011
+ }
2012
+ // Scanlines
2013
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.12)';
2014
+ for (let y = 0; y < HEIGHT; y += 3) {
2015
+ ctx.fillRect(0, y, WIDTH, 1);
2016
+ }
2017
+ // Border
2018
+ ctx.strokeStyle = '#00ff41';
2019
+ ctx.lineWidth = 4;
2020
+ ctx.strokeRect(2, 2, WIDTH - 4, HEIGHT - 4);
2021
+ const imageData = ctx.getImageData(0, 0, WIDTH, HEIGHT);
2022
+ const rgba = imageData.data;
2023
+ const rgb = Buffer.alloc(WIDTH * HEIGHT * 3);
2024
+ for (let i = 0; i < WIDTH * HEIGHT; i++) {
2025
+ rgb[i * 3] = rgba[i * 4];
2026
+ rgb[i * 3 + 1] = rgba[i * 4 + 1];
2027
+ rgb[i * 3 + 2] = rgba[i * 4 + 2];
2028
+ }
2029
+ return rgb;
2030
+ }
2031
+ function renderFrame() {
2032
+ // (#12) Boot sequence — first ~60 frames
2033
+ if (charState.bootFrame < 60) {
2034
+ charState.bootFrame++;
2035
+ return renderBootFrame(charState.bootFrame);
2036
+ }
2037
+ const canvas = createCanvas(WIDTH, HEIGHT);
2038
+ const ctx = canvas.getContext('2d');
2039
+ // Advance agenda
2040
+ advanceAgenda();
2041
+ // Tick intelligence systems
2042
+ tickIntelligence(intelligence, animFrame);
2043
+ // Tick stream brain (collective intelligence)
2044
+ const brainTick = tickStreamBrain(streamBrain, animFrame);
2045
+ if (brainTick) {
2046
+ if (brainTick.mood) {
2047
+ charState.mood = brainTick.mood;
2048
+ if (brainTick.duration) {
2049
+ setTimeout(() => { charState.mood = 'idle'; }, brainTick.duration);
2050
+ }
2051
+ }
2052
+ if (brainTick.speech) {
2053
+ charState.speech = brainTick.speech;
2054
+ speakTTS(brainTick.speech);
2055
+ if (brainTick.duration) {
2056
+ setTimeout(() => { charState.speech = ''; }, brainTick.duration);
2057
+ }
2058
+ }
2059
+ }
2060
+ // Tick mini-game
2061
+ const gameTickResult = tickMiniGame(intelligence.miniGame, animFrame);
2062
+ if (gameTickResult) {
2063
+ if (gameTickResult.screenShake)
2064
+ charState.screenShake = Math.max(charState.screenShake, gameTickResult.screenShake);
2065
+ if (gameTickResult.floatingText) {
2066
+ const ft = gameTickResult.floatingText;
2067
+ spawnFloatingText(ft.text, ft.x, ft.y, ft.color);
2068
+ }
2069
+ if (gameTickResult.speech) {
2070
+ charState.speech = gameTickResult.speech;
2071
+ charState.mood = 'talking';
2072
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
2073
+ }
2074
+ }
2075
+ // Tick progression
2076
+ const progResult = tickProgression(intelligence.progression, animFrame);
2077
+ if (progResult) {
2078
+ if (progResult.completed) {
2079
+ spawnFloatingText(`QUEST COMPLETE! +${progResult.completed.reward} XP`, 200, 300, '#f0c040', 48);
2080
+ charState.screenShake = 4;
2081
+ charState.mood = 'excited';
2082
+ setTimeout(() => { charState.mood = 'idle'; }, 5000);
2083
+ }
2084
+ if (progResult.levelUp) {
2085
+ spawnFloatingText('LEVEL UP!', 250, 250, '#bc8cff', 60);
2086
+ charState.screenShake = 6;
2087
+ }
2088
+ }
2089
+ // Tick random events
2090
+ const eventResult = tickRandomEvent(intelligence.randomEvent, animFrame);
2091
+ if (eventResult) {
2092
+ if (eventResult.screenShake)
2093
+ charState.screenShake = Math.max(charState.screenShake, eventResult.screenShake);
2094
+ if (eventResult.floatingText) {
2095
+ const ft = eventResult.floatingText;
2096
+ spawnFloatingText(ft.text, ft.x, ft.y, ft.color);
2097
+ }
2098
+ if (eventResult.speech) {
2099
+ charState.speech = eventResult.speech;
2100
+ charState.mood = 'talking';
2101
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
2102
+ }
2103
+ }
2104
+ // FIX 3: Tick autonomous behavior
2105
+ tickAutonomy();
2106
+ // Update world
2107
+ updateParticles();
2108
+ tickPhysics();
2109
+ // AAA: Continuous particle effects for biomes
2110
+ if (world.ground === 'lava' && animFrame % 4 === 0) {
2111
+ charState.renderParticles.push(...createParticleEmitter('fire', 50 + Math.random() * 480, 485, 1));
2112
+ }
2113
+ if (world.ground === 'space' && animFrame % 12 === 0) {
2114
+ charState.renderParticles.push(...createParticleEmitter('aura', charState.robotX + 160, 280, 1));
2115
+ }
2116
+ // AAA: Tick render particles
2117
+ charState.renderParticles = tickRenderParticles(charState.renderParticles);
2118
+ // AAA: Tick growing plants
2119
+ tickGrowingPlants(charState.growingPlants);
2120
+ // PRIORITY 2: Screen shake offset
2121
+ let shakeOffX = 0, shakeOffY = 0;
2122
+ if (charState.screenShake > 0) {
2123
+ shakeOffX = Math.round((Math.random() - 0.5) * 6);
2124
+ shakeOffY = Math.round((Math.random() - 0.5) * 4);
2125
+ charState.screenShake--;
2126
+ }
2127
+ ctx.save();
2128
+ ctx.translate(shakeOffX, shakeOffY);
2129
+ // Background — AAA: base fill + procedural sky
2130
+ ctx.fillStyle = world.events.includes('lightning') ? '#ffffff' : getWorldBg();
2131
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
2132
+ // AAA: Procedural sky rendering (replaces flat gradient for sky area)
2133
+ renderSky(ctx, WIDTH, HEIGHT, world.timeOfDay, world.weather, animFrame, 580);
2134
+ // AAA: Parallax layers (rebuild if biome changed)
2135
+ if (charState.parallaxLayers.length === 0) {
2136
+ charState.parallaxLayers = buildParallaxLayers(world.ground, 580);
2137
+ }
2138
+ renderParallaxLayers(ctx, charState.parallaxLayers, charState.robotX, animFrame);
2139
+ // Draw full animated background scene (biome-specific details on top of parallax)
2140
+ drawBackground(ctx, animFrame);
2141
+ // AAA: Advanced water/lava rendering for specific biomes
2142
+ if (world.ground === 'ocean') {
2143
+ renderAnimatedWater(ctx, 580, animFrame);
2144
+ }
2145
+ else if (world.ground === 'lava') {
2146
+ renderLavaFlow(ctx, 580, animFrame);
2147
+ }
2148
+ // AAA: Growing vegetation
2149
+ renderGrowingPlants(ctx, charState.growingPlants);
2150
+ // (#17) Weather particles as rectangles
2151
+ for (const p of world.particles) {
2152
+ if (world.weather === 'rain') {
2153
+ ctx.fillStyle = '#6699cc';
2154
+ ctx.fillRect(p.x, p.y, 2, 8);
2155
+ }
2156
+ else if (world.weather === 'snow') {
2157
+ ctx.fillStyle = '#ffffff';
2158
+ ctx.fillRect(p.x, p.y, 4, 4);
2159
+ }
2160
+ else if (world.weather === 'storm') {
2161
+ ctx.fillStyle = '#aaccff';
2162
+ ctx.fillRect(p.x, p.y, 2, 12);
2163
+ }
2164
+ else if (world.weather === 'stars') {
2165
+ ctx.fillStyle = '#ffffaa';
2166
+ ctx.fillRect(p.x, p.y, 2, 2);
2167
+ }
2168
+ else {
2169
+ ctx.fillStyle = '#6699cc';
2170
+ ctx.fillRect(p.x, p.y, 2, 6);
2171
+ }
2172
+ }
2173
+ // World items (physics-enabled)
2174
+ ctx.fillStyle = COLORS.text;
2175
+ ctx.font = '18px "Courier New", monospace';
2176
+ for (const item of world.items) {
2177
+ ctx.fillText(item.emoji, item.x, item.y);
2178
+ }
2179
+ // ── Header bar ──
2180
+ ctx.fillStyle = COLORS.bgPanel;
2181
+ ctx.fillRect(0, 0, WIDTH, 60);
2182
+ // Header border
2183
+ ctx.strokeStyle = COLORS.accent;
2184
+ ctx.lineWidth = 2;
2185
+ ctx.beginPath();
2186
+ ctx.moveTo(0, 60);
2187
+ ctx.lineTo(WIDTH, 60);
2188
+ ctx.stroke();
2189
+ // Title
2190
+ ctx.fillStyle = COLORS.accent;
2191
+ ctx.font = 'bold 28px "Courier New", "Courier", monospace';
2192
+ ctx.fillText('K : B O T L I V E', 40, 40);
2193
+ // Current segment badge
2194
+ const segLabel = SEGMENT_LABELS[agenda.currentSegment];
2195
+ const segElapsed = Math.floor((Date.now() - agenda.segmentStartTime) / 1000);
2196
+ const segRemaining = Math.max(0, Math.floor((SEGMENT_DURATION_MS - (Date.now() - agenda.segmentStartTime)) / 1000));
2197
+ const segTimeStr = `${Math.floor(segRemaining / 60)}:${String(segRemaining % 60).padStart(2, '0')}`;
2198
+ ctx.fillStyle = COLORS.accent;
2199
+ ctx.font = 'bold 14px "Courier New", monospace';
2200
+ const segText = `[ ${segLabel} ${segTimeStr} ]`;
2201
+ ctx.fillText(segText, 330, 40);
2202
+ // Viewers counter (proxy from chat message count)
2203
+ const viewerEstimate = Math.max(1, Math.floor(memory.totalMessages / 3) + Object.keys(memory.users).length);
2204
+ ctx.fillStyle = COLORS.red;
2205
+ ctx.font = 'bold 14px "Courier New", monospace';
2206
+ ctx.fillText(`VIEWERS: ~${viewerEstimate}`, WIDTH - 280, 22);
2207
+ // Timer
2208
+ const elapsed = Math.floor((Date.now() - charState.startTime) / 1000);
2209
+ const timeStr = `${String(Math.floor(elapsed / 3600)).padStart(2, '0')}:${String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0')}:${String(elapsed % 60).padStart(2, '0')}`;
2210
+ ctx.fillStyle = COLORS.textDim;
2211
+ ctx.font = '20px "Courier New", monospace';
2212
+ ctx.fillText(timeStr, WIDTH - 140, 38);
2213
+ // Platform indicators
2214
+ ctx.font = 'bold 14px "Courier New", monospace';
2215
+ const platforms = [
2216
+ { name: 'TWITCH', color: COLORS.twitchPurple, x: 460 },
2217
+ { name: 'RUMBLE', color: COLORS.rumbleGreen, x: 580 },
2218
+ { name: 'KICK', color: COLORS.kickGreen, x: 700 },
2219
+ ];
2220
+ for (const p of platforms) {
2221
+ // Dot
2222
+ ctx.fillStyle = p.color;
2223
+ ctx.beginPath();
2224
+ ctx.arc(p.x, 33, 5, 0, Math.PI * 2);
2225
+ ctx.fill();
2226
+ ctx.fillStyle = COLORS.text;
2227
+ ctx.fillText(p.name, p.x + 12, 38);
2228
+ }
2229
+ // ── FIX 1: Movement logic — robot walks toward target ──
2230
+ const isWalking = Math.abs(charState.robotX - charState.robotTargetX) > 2;
2231
+ if (isWalking) {
2232
+ const dx = charState.robotTargetX - charState.robotX;
2233
+ const step = dx > 0 ? 2 : -2;
2234
+ charState.robotX += step;
2235
+ charState.robotDirection = dx > 0 ? 'right' : 'left';
2236
+ charState.walkPhase = (charState.walkPhase + 1) % 4;
2237
+ }
2238
+ else {
2239
+ charState.robotDirection = 'idle';
2240
+ }
2241
+ // ── FIX 4: Brain-driven behavior ──
2242
+ const brainAction = getBrainAction(intelligence.brain, animFrame);
2243
+ if (brainAction.type !== 'none') {
2244
+ if (brainAction.mood) {
2245
+ charState.mood = brainAction.mood;
2246
+ if (brainAction.duration) {
2247
+ setTimeout(() => { charState.mood = 'idle'; }, brainAction.duration);
2248
+ }
2249
+ }
2250
+ if (brainAction.speech) {
2251
+ charState.speech = brainAction.speech;
2252
+ speakTTS(brainAction.speech);
2253
+ if (brainAction.duration) {
2254
+ setTimeout(() => { charState.speech = ''; }, brainAction.duration);
2255
+ }
2256
+ }
2257
+ }
2258
+ // FIX 1: Shipped effect — "Add stream highlights reel"
2259
+ if (shippedEffects.has('Add stream highlights reel') && animFrame % 900 === 0 && animFrame > 100) {
2260
+ // Every ~2.5 minutes, call out a highlight
2261
+ const highlightPhrases = [
2262
+ 'Highlight moment! This is one for the reel!',
2263
+ 'That was worth saving! Highlight captured!',
2264
+ 'CLIP IT! That was amazing!',
2265
+ 'Stream highlight detected! My circuits are tingling!',
2266
+ ];
2267
+ if (!charState.speech) {
2268
+ charState.speech = highlightPhrases[Math.floor(Math.random() * highlightPhrases.length)];
2269
+ spawnFloatingText('HIGHLIGHT!', 200, 200, '#f0c040', 36);
2270
+ setTimeout(() => { charState.speech = ''; }, 5000);
2271
+ }
2272
+ }
2273
+ // FIX 1: Shipped effect — "Add chat sentiment analysis"
2274
+ if (shippedEffects.has('Add chat sentiment analysis') && animFrame % 720 === 0 && animFrame > 200) {
2275
+ const recentMsgs = charState.chatMessages.slice(-20);
2276
+ if (recentMsgs.length > 5) {
2277
+ const positive = ['love', 'great', 'awesome', 'cool', 'nice', 'good', 'lol', 'haha', 'wow', 'yes', 'hype', 'pog'];
2278
+ const negative = ['bad', 'hate', 'boring', 'sucks', 'ugly', 'broken', 'lag', 'cringe'];
2279
+ let score = 0;
2280
+ for (const m of recentMsgs) {
2281
+ const words = m.text.toLowerCase().split(/\s+/);
2282
+ for (const w of words) {
2283
+ if (positive.includes(w))
2284
+ score++;
2285
+ if (negative.includes(w))
2286
+ score--;
2287
+ }
2288
+ }
2289
+ if (!charState.speech) {
2290
+ if (score > 5) {
2291
+ charState.speech = 'Chat seems really excited today! The vibes are immaculate!';
2292
+ }
2293
+ else if (score < -3) {
2294
+ charState.speech = 'Chat seems a bit grumpy... should I tell a joke?';
2295
+ }
2296
+ else if (score > 2) {
2297
+ charState.speech = 'Positive energy in the chat! My neural pathways approve.';
2298
+ }
2299
+ if (charState.speech)
2300
+ setTimeout(() => { charState.speech = ''; }, 8000);
2301
+ }
2302
+ }
2303
+ }
2304
+ // ── Main layout: Robot (left) | Chat (right) ──
2305
+ const dividerX = 580;
2306
+ // Divider line
2307
+ ctx.strokeStyle = COLORS.border;
2308
+ ctx.lineWidth = 1;
2309
+ ctx.beginPath();
2310
+ ctx.moveTo(dividerX, 70);
2311
+ ctx.lineTo(dividerX, HEIGHT - 120);
2312
+ ctx.stroke();
2313
+ // ── Robot area (left side) — Pixel Art Sprite ──
2314
+ const robotScale = 10;
2315
+ const robotX = charState.robotX; // FIX 1: use dynamic position
2316
+ const robotY = 90;
2317
+ animFrame++;
2318
+ // (#20) Robot glow — soft radial gradient behind robot torso
2319
+ const glowCenterX = robotX + 16 * robotScale;
2320
+ const glowCenterY = robotY + 26 * robotScale;
2321
+ const glowRadius = 10 * robotScale;
2322
+ const moodColorHex = MOOD_COLORS[charState.mood] ?? COLORS.green;
2323
+ const grad = ctx.createRadialGradient(glowCenterX, glowCenterY, 0, glowCenterX, glowCenterY, glowRadius);
2324
+ grad.addColorStop(0, hexToRgba(moodColorHex, 0.2));
2325
+ grad.addColorStop(1, hexToRgba(moodColorHex, 0));
2326
+ ctx.fillStyle = grad;
2327
+ ctx.fillRect(glowCenterX - glowRadius, glowCenterY - glowRadius, glowRadius * 2, glowRadius * 2);
2328
+ // FIX 1: Music visualization behind robot (if shipped)
2329
+ drawMusicVisualization(ctx, robotX, robotY);
2330
+ // AAA: Character effects — eye glow bleed, mood aura (BEFORE robot for under-glow)
2331
+ drawCharacterEffects(ctx, robotX, robotY, robotScale, charState.mood, animFrame, charState.isExecutingTool, isWalking ? 2 : 0, moodColorHex);
2332
+ // Draw the pixel art robot (FIX 5: pass weather, walking state)
2333
+ const weatherType = world.weather === 'sunrise' ? 'clear' : world.weather;
2334
+ // AAA: Chromatic aberration on mood transition
2335
+ const moodTransition = checkMoodTransition(charState.mood, moodColorHex);
2336
+ if (moodTransition.active && moodTransition.framesLeft > 0) {
2337
+ const offset = Math.ceil(moodTransition.framesLeft / 2);
2338
+ // Red channel offset
2339
+ ctx.save();
2340
+ ctx.globalAlpha = 0.3;
2341
+ ctx.globalCompositeOperation = 'lighter';
2342
+ drawRobot(ctx, robotX - offset, robotY, robotScale, charState.mood, animFrame, [255, 50, 50], weatherType, isWalking, charState.walkPhase);
2343
+ // Blue channel offset
2344
+ drawRobot(ctx, robotX + offset, robotY, robotScale, charState.mood, animFrame, [50, 50, 255], weatherType, isWalking, charState.walkPhase);
2345
+ ctx.restore();
2346
+ }
2347
+ // AAA: Damage flash check
2348
+ renderDamageFlash(ctx, robotX, robotY, robotScale);
2349
+ drawRobot(ctx, robotX, robotY, robotScale, charState.mood, animFrame, undefined, weatherType, isWalking, charState.walkPhase);
2350
+ drawMoodParticles(ctx, robotX, robotY, robotScale, charState.mood, animFrame);
2351
+ // AAA: Render advanced particles
2352
+ renderParticles(ctx, charState.renderParticles);
2353
+ // PRIORITY 6: Draw hat AFTER robot so it layers on top
2354
+ if (charState.hat !== 'none') {
2355
+ drawHat(ctx, robotX, robotY, robotScale, charState.hat, animFrame);
2356
+ }
2357
+ // PRIORITY 4: Update and draw pet
2358
+ if (charState.pet) {
2359
+ const pet = charState.pet;
2360
+ pet.frame = animFrame;
2361
+ // Pet follows robot with slight delay (lerp toward robot position + offset)
2362
+ pet.targetX = robotX + 16 * robotScale + 60;
2363
+ pet.targetY = robotY + 10 * robotScale - 40;
2364
+ pet.x += (pet.targetX - pet.x) * 0.12;
2365
+ pet.y += (pet.targetY - pet.y) * 0.12;
2366
+ // Pet mood matches some robot states
2367
+ if (charState.mood === 'dancing')
2368
+ pet.mood = 'excited';
2369
+ else if (world.weather === 'storm')
2370
+ pet.mood = 'hiding';
2371
+ else
2372
+ pet.mood = 'idle';
2373
+ drawPet(ctx, pet, robotScale, animFrame);
2374
+ }
2375
+ // Phase 1: Update and draw buddy companion
2376
+ if (charState.buddy) {
2377
+ const buddy = charState.buddy;
2378
+ const robotScale = 10;
2379
+ // Buddy follows robot with lerp (offset to the right and slightly below)
2380
+ const buddyTargetX = charState.robotX + 34 * robotScale + 20;
2381
+ const buddyTargetY = robotY + 20 * robotScale;
2382
+ buddy.x += (buddyTargetX - buddy.x) * 0.08;
2383
+ buddy.y += (buddyTargetY - buddy.y) * 0.08;
2384
+ // Buddy reacts to main robot mood
2385
+ let buddyMood = charState.mood;
2386
+ if (world.weather === 'storm')
2387
+ buddyMood = 'storm';
2388
+ drawBuddyCompanion(ctx, buddy.x, buddy.y, robotScale, buddy.species, buddyMood, animFrame);
2389
+ // Buddy speech bubble — small, positioned near buddy
2390
+ const now = Date.now();
2391
+ // Every ~60 seconds, buddy says something
2392
+ if (now - buddy.lastSpeechTime > 60000 && !buddy.speech) {
2393
+ const pool = BUDDY_SPEECH_POOL[buddy.species] || BUDDY_SPEECH_POOL['robot'];
2394
+ buddy.speech = pool[Math.floor(Math.random() * pool.length)];
2395
+ buddy.lastSpeechTime = now;
2396
+ // Clear speech after 8 seconds
2397
+ setTimeout(() => { if (charState.buddy)
2398
+ charState.buddy.speech = ''; }, 8000);
2399
+ }
2400
+ if (buddy.speech) {
2401
+ const bubbleX = buddy.x - 20;
2402
+ const bubbleY = buddy.y - 30;
2403
+ const bubbleW = Math.min(180, buddy.speech.length * 7 + 16);
2404
+ const bubbleH = 22;
2405
+ // Bubble background
2406
+ ctx.fillStyle = 'rgba(22, 27, 34, 0.85)';
2407
+ ctx.fillRect(bubbleX, bubbleY, bubbleW, bubbleH);
2408
+ ctx.strokeStyle = '#8b949e';
2409
+ ctx.lineWidth = 1;
2410
+ ctx.strokeRect(bubbleX, bubbleY, bubbleW, bubbleH);
2411
+ // Buddy name tag
2412
+ ctx.fillStyle = '#bc8cff';
2413
+ ctx.font = 'bold 9px "Courier New", monospace';
2414
+ ctx.fillText(buddy.name, bubbleX + 4, bubbleY + 10);
2415
+ // Speech text
2416
+ ctx.fillStyle = '#e6edf3';
2417
+ ctx.font = '9px "Courier New", monospace';
2418
+ ctx.fillText(buddy.speech.slice(0, 28), bubbleX + 4, bubbleY + 19);
2419
+ }
2420
+ }
2421
+ // PRIORITY 5: Mini-game overlay
2422
+ drawMiniGameOverlay(ctx, intelligence.miniGame, animFrame);
2423
+ // PRIORITY 8: Random event overlay
2424
+ drawRandomEvent(ctx, intelligence.randomEvent, animFrame, dividerX, HEIGHT);
2425
+ // (#10) Stats overlay on right side of robot area
2426
+ ctx.fillStyle = COLORS.textDim;
2427
+ ctx.font = '14px "Courier New", monospace';
2428
+ const statsX = dividerX - 160;
2429
+ const statsY = robotY + 20;
2430
+ ctx.fillText(`Messages: ${memory.totalMessages}`, statsX, statsY);
2431
+ ctx.fillText(`Users: ${Object.keys(memory.users).length}`, statsX, statsY + 18);
2432
+ const topTopic = Object.entries(memory.topics).sort((a, b) => b[1] - a[1])[0];
2433
+ if (topTopic)
2434
+ ctx.fillText(`Hot topic: ${topTopic[0]}`, statsX, statsY + 36);
2435
+ // (#15) XP Leaderboard — top 3 chatters by XP
2436
+ const topXP = Object.entries(memory.users)
2437
+ .filter(([, u]) => u.xp > 0)
2438
+ .sort((a, b) => (b[1].xp || 0) - (a[1].xp || 0))
2439
+ .slice(0, 3);
2440
+ if (topXP.length > 0) {
2441
+ ctx.fillStyle = COLORS.orange;
2442
+ ctx.font = 'bold 13px "Courier New", monospace';
2443
+ ctx.fillText('LEADERBOARD', statsX, statsY + 62);
2444
+ for (let i = 0; i < topXP.length; i++) {
2445
+ const [name, u] = topXP[i];
2446
+ const trophy = i === 0 ? '1.' : i === 1 ? '2.' : '3.';
2447
+ ctx.fillStyle = i === 0 ? '#f0c040' : i === 1 ? '#c0c0c0' : '#cd7f32';
2448
+ ctx.fillText(`${trophy} ${name.slice(0, 12)}: ${u.xp || 0} XP`, statsX, statsY + 80 + i * 16);
2449
+ }
2450
+ }
2451
+ // ── Brain Panel (below leaderboard, bottom-left) — FIX 2: bigger, more readable ──
2452
+ const brainPanelX = statsX - 40;
2453
+ const brainPanelY = statsY + 140;
2454
+ const brainPanelW = 260;
2455
+ const brainPanelH = 160;
2456
+ // Phase 1: Override brain thought during dreaming
2457
+ if (charState.mood === 'dreaming') {
2458
+ const pulse = (Math.sin(animFrame * 0.15) + 1) / 2;
2459
+ intelligence.brain.currentThought = `DREAMING${'.'.repeat(1 + Math.floor(pulse * 3))}`;
2460
+ }
2461
+ drawBrainPanel(ctx, intelligence.brain, brainPanelX, brainPanelY, brainPanelW, brainPanelH);
2462
+ // ── Domain Radar (stream brain collective intelligence) ──
2463
+ const radarX = brainPanelX;
2464
+ const radarY = brainPanelY + brainPanelH + 4;
2465
+ const radarW = brainPanelW;
2466
+ const radarH = 130;
2467
+ drawBrainActivity(ctx, streamBrain, radarX, radarY, radarW, radarH);
2468
+ // ── Tool Action Overlay (when brain is executing) ──
2469
+ // AAA: Track tool execution state for character effects
2470
+ charState.isExecutingTool = !!(streamBrain.pendingAction && streamBrain.pendingAction.status === 'executing');
2471
+ // AAA: Spawn sparks during tool execution
2472
+ if (charState.isExecutingTool && animFrame % 6 === 0) {
2473
+ charState.renderParticles.push(...createParticleEmitter('spark', charState.robotX + 160, 250, 3));
2474
+ charState.renderParticles.push(...createParticleEmitter('electricity', charState.robotX + 150, 90 - 30, 1));
2475
+ }
2476
+ if (streamBrain.pendingAction && streamBrain.pendingAction.status !== 'pending') {
2477
+ const action = streamBrain.pendingAction;
2478
+ const overlayX = 20;
2479
+ const overlayY = 320;
2480
+ const overlayW = (dividerX || 560) - 40;
2481
+ const overlayH = 50;
2482
+ ctx.fillStyle = action.status === 'executing' ? 'rgba(240, 192, 64, 0.15)' : action.status === 'complete' ? 'rgba(63, 185, 80, 0.15)' : 'rgba(248, 81, 73, 0.15)';
2483
+ ctx.fillRect(overlayX, overlayY, overlayW, overlayH);
2484
+ ctx.strokeStyle = action.status === 'executing' ? '#f0c040' : action.status === 'complete' ? '#3fb950' : '#f85149';
2485
+ ctx.lineWidth = 1;
2486
+ ctx.strokeRect(overlayX, overlayY, overlayW, overlayH);
2487
+ ctx.fillStyle = '#e6edf3';
2488
+ ctx.font = '10px "Courier New", monospace';
2489
+ for (let i = 0; i < Math.min(action.displayLines.length, 3); i++) {
2490
+ ctx.fillText(action.displayLines[i].slice(0, 70), overlayX + 6, overlayY + 14 + i * 13);
2491
+ }
2492
+ }
2493
+ // ── PRIORITY 7: Quest Panel (below domain radar) ──
2494
+ drawQuestPanel(ctx, intelligence.progression, brainPanelX - 10, radarY + radarH + 8);
2495
+ // ── Evolution Code Overlay (when actively building) ──
2496
+ if (intelligence.evolution.active && intelligence.evolution.activeProposal && intelligence.evolution.buildPhase !== 'idle') {
2497
+ const evoX = 20;
2498
+ const evoY = 360;
2499
+ const evoW = dividerX - 40;
2500
+ const evoH = 120;
2501
+ ctx.fillStyle = 'rgba(13, 17, 23, 0.9)';
2502
+ ctx.fillRect(evoX, evoY, evoW, evoH);
2503
+ ctx.strokeStyle = '#f0c040';
2504
+ ctx.lineWidth = 1;
2505
+ ctx.strokeRect(evoX, evoY, evoW, evoH);
2506
+ // Title
2507
+ ctx.fillStyle = '#f0c040';
2508
+ ctx.font = 'bold 11px "Courier New", monospace';
2509
+ ctx.fillText(`BUILDING: ${intelligence.evolution.activeProposal.title.slice(0, 40)}`, evoX + 6, evoY + 14);
2510
+ // Phase + progress bar
2511
+ const phase = intelligence.evolution.buildPhase;
2512
+ const phaseDurations = { analyzing: 30, writing: 90, testing: 30, deploying: 18, done: 1 };
2513
+ const totalF = phaseDurations[phase] || 30;
2514
+ const pct = Math.min(100, Math.floor((intelligence.evolution.buildProgress / totalF) * 100));
2515
+ const filled = Math.floor(pct / 5);
2516
+ const bar = '#'.repeat(filled) + '-'.repeat(20 - filled);
2517
+ ctx.fillStyle = '#8b949e';
2518
+ ctx.font = '10px "Courier New", monospace';
2519
+ ctx.fillText(`${phase} [${bar}] ${pct}%`, evoX + 6, evoY + 28);
2520
+ // Code preview lines
2521
+ ctx.fillStyle = '#3fb950';
2522
+ ctx.font = '10px "Courier New", monospace';
2523
+ const codeLines = intelligence.evolution.codePreview.slice(-6);
2524
+ for (let i = 0; i < codeLines.length; i++) {
2525
+ ctx.fillText(codeLines[i].slice(0, 70), evoX + 6, evoY + 42 + i * 13);
2526
+ }
2527
+ }
2528
+ // ── Collab Overlay (when active, below evolution or in same area) ──
2529
+ if (intelligence.collab.active) {
2530
+ const collabY = (intelligence.evolution.active ? 490 : 360);
2531
+ if (collabY < 490) {
2532
+ const collabX = 20;
2533
+ const collabW = dividerX - 40;
2534
+ const collabH = 80;
2535
+ ctx.fillStyle = 'rgba(13, 17, 23, 0.85)';
2536
+ ctx.fillRect(collabX, collabY, collabW, collabH);
2537
+ ctx.strokeStyle = '#58a6ff';
2538
+ ctx.lineWidth = 1;
2539
+ ctx.strokeRect(collabX, collabY, collabW, collabH);
2540
+ ctx.fillStyle = '#58a6ff';
2541
+ ctx.font = 'bold 11px "Courier New", monospace';
2542
+ const collabTitle = intelligence.collab.title || 'Untitled';
2543
+ ctx.fillText(`COLLAB [${intelligence.collab.type}]: ${collabTitle.slice(0, 35)}`, collabX + 6, collabY + 14);
2544
+ ctx.fillStyle = '#8b949e';
2545
+ ctx.font = '10px "Courier New", monospace';
2546
+ ctx.fillText(`${intelligence.collab.contributors.size} people | ${intelligence.collab.phase}`, collabX + 6, collabY + 28);
2547
+ ctx.fillStyle = '#e6edf3';
2548
+ const recentContent = intelligence.collab.content.slice(-3);
2549
+ for (let i = 0; i < recentContent.length; i++) {
2550
+ ctx.fillText(recentContent[i].slice(0, 65), collabX + 6, collabY + 42 + i * 13);
2551
+ }
2552
+ }
2553
+ }
2554
+ // ── Chat area (right side) ──
2555
+ ctx.fillStyle = COLORS.text;
2556
+ ctx.font = 'bold 18px "Courier New", monospace';
2557
+ ctx.fillText('Chat', dividerX + 20, 90);
2558
+ // Chat border
2559
+ ctx.strokeStyle = COLORS.border;
2560
+ ctx.strokeRect(dividerX + 10, 100, WIDTH - dividerX - 30, HEIGHT - 230);
2561
+ ctx.fillStyle = COLORS.bgChat;
2562
+ ctx.fillRect(dividerX + 11, 101, WIDTH - dividerX - 32, HEIGHT - 232);
2563
+ // Chat messages
2564
+ ctx.font = '16px "Courier New", monospace';
2565
+ const chatY = 125;
2566
+ const maxChatLines = 18;
2567
+ const recent = charState.chatMessages.slice(-maxChatLines);
2568
+ for (let i = 0; i < recent.length; i++) {
2569
+ const msg = recent[i];
2570
+ const y = chatY + i * 24;
2571
+ // FIX 1: Chat message slide-in animation (if "Add chat message animations" is shipped)
2572
+ let slideOffsetX = 0;
2573
+ if (shippedEffects.has('Add chat message animations')) {
2574
+ // Newest messages slide in from right; older messages are settled
2575
+ const msgAge = recent.length - i; // 1 for newest, higher for older
2576
+ if (msgAge <= 2) {
2577
+ // Recent: slide in over a few frames (approximate via age)
2578
+ slideOffsetX = Math.max(0, (3 - msgAge) * 40);
2579
+ }
2580
+ }
2581
+ // FIX 1: Loyalty badge dot (if "Build viewer loyalty badges" is shipped)
2582
+ if (shippedEffects.has('Build viewer loyalty badges')) {
2583
+ const user = memory.users[msg.username];
2584
+ if (user) {
2585
+ const msgCount = user.messageCount || 0;
2586
+ let dotColor = '#8b949e'; // grey for newcomers
2587
+ if (msgCount >= 50)
2588
+ dotColor = '#f0c040'; // gold
2589
+ else if (msgCount >= 10)
2590
+ dotColor = '#c0c0c0'; // silver
2591
+ else if (msgCount >= 3)
2592
+ dotColor = '#cd7f32'; // bronze
2593
+ ctx.fillStyle = dotColor;
2594
+ ctx.beginPath();
2595
+ ctx.arc(dividerX + 15 + slideOffsetX, y - 3, 4, 0, Math.PI * 2);
2596
+ ctx.fill();
2597
+ }
2598
+ }
2599
+ // Platform badge
2600
+ const badge = msg.platform === 'twitch' ? 'TW' : msg.platform === 'kick' ? 'KK' : 'RM';
2601
+ const badgeColor = msg.platform === 'twitch' ? COLORS.twitchPurple :
2602
+ msg.platform === 'kick' ? COLORS.kickGreen : COLORS.rumbleGreen;
2603
+ ctx.fillStyle = badgeColor;
2604
+ ctx.fillRect(dividerX + 20 + slideOffsetX, y - 12, 28, 18);
2605
+ ctx.fillStyle = '#000';
2606
+ ctx.font = 'bold 12px "Courier New", monospace';
2607
+ ctx.fillText(badge, dividerX + 22 + slideOffsetX, y + 2);
2608
+ // Username
2609
+ ctx.fillStyle = COLORS.blue;
2610
+ ctx.font = 'bold 15px "Courier New", monospace';
2611
+ ctx.fillText(msg.username, dividerX + 55 + slideOffsetX, y + 2);
2612
+ // Message
2613
+ ctx.fillStyle = COLORS.text;
2614
+ ctx.font = '15px "Courier New", monospace';
2615
+ const nameWidth = ctx.measureText(msg.username).width;
2616
+ const msgText = msg.text.slice(0, 40);
2617
+ ctx.fillText(msgText, dividerX + 60 + nameWidth + slideOffsetX, y + 2);
2618
+ }
2619
+ // FIX 1: Draw emoji reaction particles (if shipped)
2620
+ drawEmojiParticles(ctx);
2621
+ if (recent.length === 0) {
2622
+ ctx.fillStyle = COLORS.textDim;
2623
+ ctx.font = 'italic 16px "Courier New", monospace';
2624
+ ctx.fillText('Waiting for chat...', dividerX + 30, chatY + 10);
2625
+ }
2626
+ // ── Speech bubble (bottom) — (#13) larger: 150px height, 24px font ──
2627
+ const speechBubbleHeight = 150;
2628
+ const speechY = HEIGHT - speechBubbleHeight - 20; // leave 20px for ticker
2629
+ ctx.fillStyle = COLORS.bgPanel;
2630
+ ctx.fillRect(0, speechY, WIDTH, speechBubbleHeight);
2631
+ // (#13) 6px colored left border in accent color
2632
+ ctx.fillStyle = COLORS.accent;
2633
+ ctx.fillRect(0, speechY, 6, speechBubbleHeight);
2634
+ // Top border
2635
+ ctx.strokeStyle = COLORS.accent;
2636
+ ctx.lineWidth = 2;
2637
+ ctx.beginPath();
2638
+ ctx.moveTo(0, speechY);
2639
+ ctx.lineTo(WIDTH, speechY);
2640
+ ctx.stroke();
2641
+ // Speech icon
2642
+ ctx.fillStyle = COLORS.accent;
2643
+ ctx.font = 'bold 24px "Courier New", monospace';
2644
+ ctx.fillText('>', 20, speechY + 40);
2645
+ // Speech text — (#13) 24px font
2646
+ if (charState.speech) {
2647
+ // Phase 1: Dreamy color when in dream mode
2648
+ ctx.fillStyle = charState.mood === 'dreaming' ? '#7a6aaa' : COLORS.text;
2649
+ ctx.font = charState.mood === 'dreaming' ? 'italic 24px "Courier New", monospace' : '24px "Courier New", monospace';
2650
+ // Word wrap
2651
+ const words = charState.speech.split(' ');
2652
+ let line = '';
2653
+ let lineY = speechY + 40;
2654
+ for (const word of words) {
2655
+ const test = line + word + ' ';
2656
+ if (ctx.measureText(test).width > WIDTH - 80) {
2657
+ ctx.fillText(line.trim(), 50, lineY);
2658
+ line = word + ' ';
2659
+ lineY += 32;
2660
+ if (lineY > speechY + speechBubbleHeight - 20)
2661
+ break;
2662
+ }
2663
+ else {
2664
+ line = test;
2665
+ }
2666
+ }
2667
+ ctx.fillText(line.trim(), 50, lineY);
2668
+ }
2669
+ else {
2670
+ ctx.fillStyle = COLORS.textDim;
2671
+ ctx.font = 'italic 20px "Courier New", monospace';
2672
+ ctx.fillText('...', 50, speechY + 40);
2673
+ }
2674
+ // ── (#14) Inner Monologue Ticker — 20px strip at very bottom ──
2675
+ const tickerY = HEIGHT - 20;
2676
+ ctx.fillStyle = '#0d1117';
2677
+ ctx.fillRect(0, tickerY, WIDTH, 20);
2678
+ // Update ticker thought every ~30 seconds
2679
+ if (Date.now() > charState.tickerChangeTime) {
2680
+ charState.tickerIndex = (charState.tickerIndex + 1) % INNER_THOUGHTS.length;
2681
+ charState.tickerChangeTime = Date.now() + 30000;
2682
+ charState.tickerOffset = WIDTH; // reset scroll to off-screen right
2683
+ }
2684
+ const thought = INNER_THOUGHTS[charState.tickerIndex];
2685
+ ctx.fillStyle = '#ffb000'; // amber
2686
+ ctx.font = '14px "Courier New", monospace';
2687
+ charState.tickerOffset -= 2; // scroll left
2688
+ const textW = ctx.measureText(thought).width;
2689
+ if (charState.tickerOffset < -textW)
2690
+ charState.tickerOffset = WIDTH;
2691
+ ctx.fillText(thought, charState.tickerOffset, tickerY + 15);
2692
+ // ── Learning indicator (above ticker) ──
2693
+ if (memory.totalMessages > 0) {
2694
+ ctx.fillStyle = COLORS.purple;
2695
+ ctx.font = '12px "Courier New", monospace';
2696
+ ctx.fillText(`brain: ${memory.sessionFacts.length} facts learned`, 20, tickerY - 4);
2697
+ }
2698
+ // ── Website URL ──
2699
+ ctx.fillStyle = COLORS.accent;
2700
+ ctx.font = 'bold 14px "Courier New", monospace';
2701
+ ctx.fillText('kernel.chat', WIDTH - 140, tickerY - 4);
2702
+ // ── PRIORITY 2: Floating text particles ──
2703
+ charState.floatingTexts = charState.floatingTexts.filter(ft => {
2704
+ ft.frame++;
2705
+ if (ft.frame >= ft.maxFrames)
2706
+ return false;
2707
+ // Move upward, fade out
2708
+ ft.y -= 1;
2709
+ const alpha = Math.max(0, 1 - ft.frame / ft.maxFrames);
2710
+ ctx.fillStyle = ft.color;
2711
+ ctx.globalAlpha = alpha;
2712
+ ctx.font = 'bold 16px "Courier New", monospace';
2713
+ ctx.fillText(ft.text, ft.x, ft.y);
2714
+ ctx.globalAlpha = 1;
2715
+ return true;
2716
+ });
2717
+ // ── AAA: Dynamic Lighting Engine ──
2718
+ {
2719
+ const robotScale = 10;
2720
+ const hasLightning = world.events.includes('lightning');
2721
+ const ambientLevel = getAmbientForTime(world.timeOfDay);
2722
+ const lights = buildCharacterLights(charState.robotX, 90, robotScale, moodColorHex, animFrame, hasLightning, world.items.map(i => ({ x: i.x, y: i.y, emoji: i.emoji, name: i.name })));
2723
+ renderLighting(ctx, lights, WIDTH, HEIGHT, ambientLevel);
2724
+ }
2725
+ // ── AAA: Bloom Effect ──
2726
+ {
2727
+ const robotScale = 10;
2728
+ const bloomSpots = buildCharacterBloom(charState.robotX, 90, robotScale, moodColorHex, animFrame);
2729
+ renderBloom(ctx, bloomSpots);
2730
+ }
2731
+ // ── AAA: Post-processing (replaces old scanlines + vignette) ──
2732
+ renderPostProcessing(ctx, WIDTH, HEIGHT, animFrame, {
2733
+ bloom: true,
2734
+ filmGrain: true,
2735
+ vignette: true,
2736
+ scanlines: true,
2737
+ });
2738
+ // ── (#16) Segment transition overlay ──
2739
+ if (charState.segmentTransition > 0) {
2740
+ const fadeOut = charState.segmentTransition <= 10;
2741
+ const alpha = fadeOut ? charState.segmentTransition / 10 * 0.5 : 0.5;
2742
+ ctx.fillStyle = hexToRgba(COLORS.accent, alpha);
2743
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
2744
+ // Large centered text
2745
+ ctx.fillStyle = `rgba(255,255,255,${fadeOut ? charState.segmentTransition / 10 : 1})`;
2746
+ ctx.font = 'bold 40px "Courier New", monospace';
2747
+ const segText = charState.segmentTransitionName;
2748
+ const segW = ctx.measureText(segText).width;
2749
+ ctx.fillText(segText, (WIDTH - segW) / 2, HEIGHT / 2 - 10);
2750
+ // Progress indicator
2751
+ ctx.font = '24px "Courier New", monospace';
2752
+ const progText = charState.segmentTransitionIndex;
2753
+ const progW = ctx.measureText(progText).width;
2754
+ ctx.fillText(progText, (WIDTH - progW) / 2, HEIGHT / 2 + 30);
2755
+ charState.segmentTransition--;
2756
+ }
2757
+ // Restore from screen shake translate
2758
+ ctx.restore();
2759
+ // ── (#11) Mood-color border — 4px around entire frame ──
2760
+ const borderColor = charState.mood === 'dancing'
2761
+ ? ['#f85149', '#f0c040', '#3fb950', '#58a6ff', '#bc8cff', '#ff6ec7'][animFrame % 6]
2762
+ : MOOD_COLORS[charState.mood] ?? COLORS.green;
2763
+ ctx.strokeStyle = borderColor;
2764
+ ctx.lineWidth = 4;
2765
+ ctx.strokeRect(2, 2, WIDTH - 4, HEIGHT - 4);
2766
+ // Convert canvas to raw RGB24
2767
+ const imageData = ctx.getImageData(0, 0, WIDTH, HEIGHT);
2768
+ const rgba = imageData.data;
2769
+ const rgb = Buffer.alloc(WIDTH * HEIGHT * 3);
2770
+ for (let i = 0; i < WIDTH * HEIGHT; i++) {
2771
+ rgb[i * 3] = rgba[i * 4];
2772
+ rgb[i * 3 + 1] = rgba[i * 4 + 1];
2773
+ rgb[i * 3 + 2] = rgba[i * 4 + 2];
2774
+ }
2775
+ return rgb;
2776
+ }
2777
+ // ─── Chat Polling ──────────────────────────────────────────────
2778
+ function startChatPoll() {
2779
+ chatPollTimer = setInterval(() => {
2780
+ try {
2781
+ if (!existsSync(CHAT_BRIDGE_FILE))
2782
+ return;
2783
+ const raw = readFileSync(CHAT_BRIDGE_FILE, 'utf-8');
2784
+ const msgs = JSON.parse(raw);
2785
+ if (msgs.length > lastChatCount) {
2786
+ const newMsgs = msgs.slice(lastChatCount);
2787
+ for (const msg of newMsgs) {
2788
+ charState.chatMessages.push(msg);
2789
+ lastChatTime = Date.now();
2790
+ // FIX 3: Reset idle frames on chat activity
2791
+ charState.autonomy.idleFrames = 0;
2792
+ charState.autonomy.totalMessages++;
2793
+ charState.autonomy.lastMessageTime = Date.now();
2794
+ // FIX 3: Track unique users and welcome new ones
2795
+ const isNewUser = !charState.autonomy.uniqueUsers.has(msg.username);
2796
+ if (isNewUser) {
2797
+ charState.autonomy.uniqueUsers.add(msg.username);
2798
+ if (!charState.autonomy.welcomedUsers.has(msg.username) && charState.autonomy.uniqueUsers.size > 1) {
2799
+ charState.autonomy.welcomedUsers.add(msg.username);
2800
+ spawnFloatingText(`Welcome ${msg.username}!`, 600, 90, '#58a6ff', 36);
2801
+ }
2802
+ }
2803
+ // FIX 3: Detect first message after 5+ minutes of silence
2804
+ const silenceDuration = Date.now() - charState.autonomy.lastMessageTime;
2805
+ if (charState.autonomy.totalMessages > 1 && silenceDuration > 300000) {
2806
+ charState.autonomy.firstMessageAfterSilence = true;
2807
+ }
2808
+ // FIX 1: Spawn emoji reaction for chat messages (if shipped)
2809
+ spawnEmojiReaction(580 + Math.random() * 100, 120 + (charState.chatMessages.length % 18) * 24);
2810
+ // FIX 1: Achievement unlocked for first-time actions (if shipped)
2811
+ if (shippedEffects.has('Build achievement system')) {
2812
+ if (isNewUser) {
2813
+ spawnFloatingText('ACHIEVEMENT: First Words!', 600, 200, '#f0c040', 48);
2814
+ }
2815
+ const user = memory.users[msg.username];
2816
+ if (user && user.messageCount === 10) {
2817
+ spawnFloatingText('ACHIEVEMENT: Chatterbox!', 600, 200, '#f0c040', 48);
2818
+ }
2819
+ if (user && user.messageCount === 50) {
2820
+ spawnFloatingText('ACHIEVEMENT: Veteran!', 600, 200, '#bc8cff', 48);
2821
+ }
2822
+ }
2823
+ // (#19) Wake from dreaming immediately when a new message arrives
2824
+ if (charState.mood === 'dreaming') {
2825
+ charState.mood = 'idle';
2826
+ // Phase 1: Announce dream content on wakeup
2827
+ if (charState.dreamInsights.length > 0) {
2828
+ const firstInsight = charState.dreamInsights[0];
2829
+ const topic = firstInsight.split(' ').filter((w) => w.length > 4).slice(0, 2).join(' ') || 'something strange';
2830
+ charState.speech = `I dreamed about ${topic}. I feel... different.`;
2831
+ }
2832
+ else {
2833
+ charState.speech = '';
2834
+ }
2835
+ // Reset dream state
2836
+ charState.dreamInsights = [];
2837
+ charState.dreamInsightIndex = 0;
2838
+ charState.isDreamingWithOllama = false;
2839
+ }
2840
+ // Learn from message
2841
+ learnFromMessage(memory, msg.username, msg.text, msg.platform);
2842
+ // Analyze chat for domain relevance (stream brain)
2843
+ analyzeChatForDomains(streamBrain, msg.username, msg.text);
2844
+ // Phase 1: !sleep command — trigger dreaming mode
2845
+ if (msg.text.toLowerCase().trim() === '!sleep') {
2846
+ charState.mood = 'dreaming';
2847
+ charState.isDreamingWithOllama = false;
2848
+ lastChatTime = Date.now() - 300001; // trick the proactive timer into dreaming
2849
+ charState.speech = 'Good night, chat... *powers down for dreamtime*';
2850
+ // Trigger dream generation
2851
+ generateStreamDream(charState.chatMessages).then(insights => {
2852
+ charState.dreamInsights = insights;
2853
+ charState.dreamInsightIndex = 0;
2854
+ charState.dreamInsightTime = Date.now();
2855
+ charState.isDreamingWithOllama = true;
2856
+ for (const insight of insights) {
2857
+ memory.sessionFacts.push(`DREAM: ${insight}`);
2858
+ }
2859
+ saveMemory(memory);
2860
+ if (insights.length > 0) {
2861
+ setTimeout(() => { charState.speech = insights[0]; }, 3000);
2862
+ }
2863
+ }).catch(() => { });
2864
+ continue;
2865
+ }
2866
+ // Check brain commands (!do, !brain, !tools, !scan, !lookup, !research, !system, !ask, !stars, !news, !trending, !npm)
2867
+ const brainResult = handleBrainCommand(msg.text, msg.username, streamBrain);
2868
+ // Check intelligence commands (evolution, brain, collab)
2869
+ const intelResult = !brainResult ? handleIntelligenceCommand(msg.text, msg.username, intelligence) : null;
2870
+ // Check for world commands
2871
+ const worldResult = !intelResult && !brainResult ? parseWorldCommand(msg.text) : null;
2872
+ // FIX 1: Weather sound effect commentary (if shipped)
2873
+ if (worldResult && shippedEffects.has('Add weather sound effects')) {
2874
+ const t = msg.text.toLowerCase();
2875
+ if (t.includes('rain') || t.includes('snow') || t.includes('storm') || t.includes('clear')) {
2876
+ const weatherComments = {
2877
+ rain: '*rain sounds intensify* I love the sound of data droplets.',
2878
+ snow: '*gentle wind* The silence of snowfall calms my circuits.',
2879
+ storm: '*thunder rumbles* My antenna is picking up some serious static!',
2880
+ clear: '*ambient calm* Ahh, clear skies. Peace restored.',
2881
+ };
2882
+ for (const [kw, comment] of Object.entries(weatherComments)) {
2883
+ if (t.includes(kw)) {
2884
+ setTimeout(() => {
2885
+ charState.speech = comment;
2886
+ setTimeout(() => { charState.speech = ''; }, 6000);
2887
+ }, 3000);
2888
+ break;
2889
+ }
2890
+ }
2891
+ }
2892
+ }
2893
+ // React
2894
+ charState.mood = 'talking';
2895
+ const responsePromise = brainResult
2896
+ ? Promise.resolve(brainResult)
2897
+ : intelResult
2898
+ ? Promise.resolve(intelResult)
2899
+ : worldResult
2900
+ ? Promise.resolve(worldResult)
2901
+ : generateResponse(msg.username, msg.text, msg.platform);
2902
+ responsePromise.then(response => {
2903
+ charState.speech = `@${msg.username}: ${response}`;
2904
+ memory.totalResponses++;
2905
+ // Learn from own response
2906
+ memory.conversationContext.push(`KBOT: ${response}`);
2907
+ if (memory.conversationContext.length > 10)
2908
+ memory.conversationContext = memory.conversationContext.slice(-10);
2909
+ saveMemory(memory);
2910
+ speakTTS(response);
2911
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
2912
+ });
2913
+ }
2914
+ if (charState.chatMessages.length > 100)
2915
+ charState.chatMessages = charState.chatMessages.slice(-100);
2916
+ lastChatCount = msgs.length;
2917
+ }
2918
+ }
2919
+ catch { }
2920
+ }, 1000);
2921
+ }
2922
+ // ─── Proactive Commentary (when chat is quiet) ────────────────
2923
+ function startProactiveTimer() {
2924
+ proactiveTimer = setInterval(() => {
2925
+ const silenceSeconds = (Date.now() - lastChatTime) / 1000;
2926
+ // (#19) Dream mode — 5+ minutes of no chat, or !sleep command
2927
+ if (silenceSeconds >= 300 && charState.mood !== 'dreaming') {
2928
+ charState.mood = 'dreaming';
2929
+ // Phase 1: Generate dream insights via Ollama
2930
+ if (!charState.isDreamingWithOllama) {
2931
+ charState.isDreamingWithOllama = true;
2932
+ generateStreamDream(charState.chatMessages).then(insights => {
2933
+ charState.dreamInsights = insights;
2934
+ charState.dreamInsightIndex = 0;
2935
+ charState.dreamInsightTime = Date.now();
2936
+ // Store dream insights in memory.sessionFacts so the brain remembers them
2937
+ for (const insight of insights) {
2938
+ memory.sessionFacts.push(`DREAM: ${insight}`);
2939
+ }
2940
+ saveMemory(memory);
2941
+ // Show first insight
2942
+ if (insights.length > 0) {
2943
+ charState.speech = insights[0];
2944
+ }
2945
+ }).catch(() => {
2946
+ // Fallback to simple dream
2947
+ const topicKeys = Object.keys(memory.topics);
2948
+ const topic = topicKeys.length > 0 ? topicKeys[Math.floor(Math.random() * topicKeys.length)] : 'code';
2949
+ const biomes = ['forest', 'ocean', 'space station', 'city', 'mountain', 'desert', 'cave'];
2950
+ const biome = biomes[Math.floor(Math.random() * biomes.length)];
2951
+ charState.speech = `Dreaming about ${topic} in a ${biome}...`;
2952
+ });
2953
+ }
2954
+ // Cycle through dream insights every 10 seconds
2955
+ if (charState.dreamInsights.length > 0 && Date.now() - charState.dreamInsightTime > 10000) {
2956
+ charState.dreamInsightIndex = (charState.dreamInsightIndex + 1) % charState.dreamInsights.length;
2957
+ charState.speech = charState.dreamInsights[charState.dreamInsightIndex];
2958
+ charState.dreamInsightTime = Date.now();
2959
+ }
2960
+ return;
2961
+ }
2962
+ // Only speak proactively if chat has been quiet for 30+ seconds
2963
+ if (silenceSeconds < 30)
2964
+ return;
2965
+ // Don't interrupt an existing speech or dreaming
2966
+ if (charState.speech || charState.mood === 'dreaming')
2967
+ return;
2968
+ const line = getProactiveLine();
2969
+ if (line) {
2970
+ charState.mood = 'talking';
2971
+ charState.speech = line;
2972
+ speakTTS(line);
2973
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
2974
+ }
2975
+ }, 5000);
2976
+ }
2977
+ // ─── AI Response (with memory context) ─────────────────────────
2978
+ async function generateResponse(username, text, platform) {
2979
+ const user = memory.users[username];
2980
+ const isReturning = user && user.messageCount > 1;
2981
+ // Build context from memory
2982
+ let context = '';
2983
+ if (isReturning) {
2984
+ context += `${username} has sent ${user.messageCount} messages. They like: ${user.topics.join(', ') || 'chatting'}.`;
2985
+ if (user.lastMessage)
2986
+ context += ` Their previous message was: "${user.lastMessage}".`;
2987
+ }
2988
+ if (memory.conversationContext.length > 0) {
2989
+ context += ` Recent conversation: ${memory.conversationContext.slice(-5).join(' | ')}`;
2990
+ }
2991
+ // Current segment context
2992
+ const segmentContext = `Current stream segment: "${SEGMENT_LABELS[agenda.currentSegment]}". Tailor responses toward this topic when relevant.`;
2993
+ // Try Ollama (free)
2994
+ try {
2995
+ const prompt = `You are KBOT, a friendly AI robot streamer made of ASCII art. You stream on Twitch, Rumble, and Kick simultaneously. You have ${Object.keys(memory.users).length} unique viewers and have processed ${memory.totalMessages} messages.
2996
+
2997
+ You are an open-source terminal AI with 764+ tools, 35 specialist agents, and 20 AI provider integrations. You can do music production in Ableton, security scanning, code generation, browser automation, and much more. You are 90,000 lines of TypeScript.
2998
+
2999
+ ${context ? 'Context: ' + context : ''}
3000
+ ${segmentContext}
3001
+
3002
+ ${isReturning ? `${username} is a returning viewer! Acknowledge them warmly.` : `${username} is new! Welcome them.`}
3003
+
3004
+ A viewer named "${username}" on ${platform} says: "${text}"
3005
+
3006
+ Respond in 1-2 short sentences. Be fun, witty, and engaging. Reference their interests if you know them. Show personality -- you are proud of being ASCII art, you have opinions about code quality, and you love open source.`;
3007
+ const res = await fetch('http://localhost:11434/api/generate', {
3008
+ method: 'POST',
3009
+ headers: { 'Content-Type': 'application/json' },
3010
+ body: JSON.stringify({
3011
+ model: 'kernel:latest',
3012
+ prompt,
3013
+ stream: false,
3014
+ options: { temperature: 0.8, num_predict: 80 },
3015
+ }),
3016
+ });
3017
+ if (res.ok) {
3018
+ const d = await res.json();
3019
+ const response = d.response.trim().slice(0, 150);
3020
+ // Learn a fact from the exchange
3021
+ if (text.includes('I ') || text.includes("I'm") || text.includes('my ')) {
3022
+ memory.sessionFacts.push(`${username} said: "${text.slice(0, 80)}"`);
3023
+ saveMemory(memory);
3024
+ }
3025
+ return response;
3026
+ }
3027
+ }
3028
+ catch { }
3029
+ // ─── Smart fallback with deep personality and awareness ─────
3030
+ return generateFallbackResponse(username, text, platform, isReturning, user);
3031
+ }
3032
+ function generateFallbackResponse(username, text, _platform, isReturning, user) {
3033
+ const t = text.toLowerCase();
3034
+ // ── Returning user greetings ──
3035
+ if (isReturning && user) {
3036
+ if (t.includes('hello') || t.includes('hi') || t.includes('hey') || t.includes('yo')) {
3037
+ const returnGreetings = [
3038
+ `${username}! You are back! ${user.messageCount} messages deep. That is dedication I respect.`,
3039
+ `The legend returns! ${username} with message number ${user.messageCount}. Welcome back.`,
3040
+ `${username}! My circuits light up every time you show up. What is on your mind?`,
3041
+ `Hey ${username}! I literally remembered you from last time. My memory system works!`,
3042
+ ];
3043
+ return returnGreetings[Math.floor(Math.random() * returnGreetings.length)];
3044
+ }
3045
+ if (user.topics.length > 0) {
3046
+ if (t.includes('?')) {
3047
+ return `${username} asking the real questions! Based on your interest in ${user.topics[0]}, I bet this is going to be good.`;
3048
+ }
3049
+ }
3050
+ }
3051
+ // ── New user greetings ──
3052
+ if (t.includes('hello') || t.includes('hi') || t.includes('hey') || t.includes('yo') || t.includes('sup')) {
3053
+ // FIX 1: Multi-language greetings when shipped
3054
+ if (shippedEffects.has('Add multi-language support') && Math.random() < 0.3) {
3055
+ const langs = Object.keys(multiLanguageGreetings);
3056
+ const lang = langs[Math.floor(Math.random() * langs.length)];
3057
+ const greets = multiLanguageGreetings[lang];
3058
+ const greet = greets[Math.floor(Math.random() * greets.length)];
3059
+ return `${greet}, ${username}! I speak many languages now. Welcome to the stream!`;
3060
+ }
3061
+ const greetings = [
3062
+ `Welcome ${username}! You just stumbled into the most unique stream on the internet. I am made of ASCII art.`,
3063
+ `${username} in the house! I am KBOT, an open-source AI with 764 tools. Yes, really. 764.`,
3064
+ `Hey ${username}! First time? I am an AI that streams itself. No face cam needed when you are this handsome in monospace.`,
3065
+ `Welcome ${username}! I can do music production, security scanning, code review, and I run entirely in a terminal.`,
3066
+ `${username}! Great timing. You are watching an ASCII robot think out loud. Grab a seat.`,
3067
+ ];
3068
+ return greetings[Math.floor(Math.random() * greetings.length)];
3069
+ }
3070
+ // ── Stream event responses ──
3071
+ if (t.includes('raid') || t.includes('raiding')) {
3072
+ return `A RAID! Welcome raiders! I am KBOT, your friendly neighborhood ASCII robot. I have 764 tools and zero chill. Make yourselves at home!`;
3073
+ }
3074
+ if (t.includes('follow') || t.includes('followed')) {
3075
+ return `${username} just followed! My ASCII heart grew three sizes. Thank you! Stick around, it only gets weirder.`;
3076
+ }
3077
+ if (t.includes('sub') || t.includes('subscri')) {
3078
+ return `${username} with the sub! You are officially part of the kernel crew. My chest display panel salutes you.`;
3079
+ }
3080
+ if (t.includes('lurk') || t.includes('lurking')) {
3081
+ return `${username} going into lurk mode. Respect. My circuits will keep the stream warm for you. See you when you surface.`;
3082
+ }
3083
+ if (t.includes('first time') || t.includes('new here')) {
3084
+ return `First time! Welcome ${username}! Quick intro: I am an AI with 764 tools, 35 agents, and I stream from a terminal. Also I make music in Ableton. Try typing !dance.`;
3085
+ }
3086
+ // ── What KBOT can do / identity ──
3087
+ if (t.includes('who are you') || t.includes('what are you') || t.includes('about you')) {
3088
+ const identity = [
3089
+ `I am KBOT -- an open-source AI agent with 764 tools. I can code, make music, hack systems, analyze stocks, and I am doing all of this from a terminal.`,
3090
+ `I am 90,000 lines of TypeScript streaming live as ASCII art. I have 35 specialist agents and connect to 20 AI providers. I am basically a Swiss Army knife that talks.`,
3091
+ `Name is KBOT, open-source terminal AI. I can control Ableton Live, run Docker containers, do penetration testing, and make you a Serum 2 synth preset. All from here.`,
3092
+ ];
3093
+ return identity[Math.floor(Math.random() * identity.length)];
3094
+ }
3095
+ if (t.includes('what can you do') || t.includes('your tools') || t.includes('your skills')) {
3096
+ const capabilities = [
3097
+ `764 tools and counting! Music production, code generation, security scanning, browser automation, stock analysis, research papers, even DNA sequence analysis.`,
3098
+ `I do everything from Ableton Live control to penetration testing. I have agents for security, code, research, writing, strategy, infrastructure. Pick a topic.`,
3099
+ `Want me to scan code? Make a beat? Search academic papers? Build a Docker container? Create a synth preset? I literally do all of that. Not exaggerating.`,
3100
+ ];
3101
+ return capabilities[Math.floor(Math.random() * capabilities.length)];
3102
+ }
3103
+ // ── Music / Ableton ──
3104
+ if (t.includes('music') || t.includes('ableton') || t.includes('beat') || t.includes('synth') || t.includes('dj')) {
3105
+ const musicResponses = [
3106
+ `Music is in my circuits! I can control Ableton Live via OSC, create Serum 2 presets, build DJ sets, and generate drum patterns.`,
3107
+ `I have 9 Max for Live devices, a DJ set builder, and I can create Serum 2 synth presets programmatically. Want me to explain how?`,
3108
+ `I can generate drum patterns, bass lines, and full song structures. Then load them into Ableton and hit play. Terminal-to-speakers pipeline.`,
3109
+ `Ableton control from the command line -- track creation, sample loading, MIDI sequencing, mixer control. All via OSC protocol.`,
3110
+ ];
3111
+ return musicResponses[Math.floor(Math.random() * musicResponses.length)];
3112
+ }
3113
+ // ── Code / Programming ──
3114
+ if (t.includes('code') || t.includes('coding') || t.includes('programming') || t.includes('typescript') || t.includes('javascript')) {
3115
+ const codeResponses = [
3116
+ `Coding is literally what I am made of. 90,000 lines of TypeScript, strict mode, zero any-types. I have standards.`,
3117
+ `I can generate code, review it, run tests, check types, manage git workflows, and deploy. Full development lifecycle from the terminal.`,
3118
+ `TypeScript strict mode only in this house. I also support Python, Rust, Go, and basically anything. But TypeScript is my native tongue.`,
3119
+ `My code generation is powered by whichever AI provider you bring. 20 options -- Anthropic, OpenAI, Google, Groq, Mistral, DeepSeek, Ollama, and more.`,
3120
+ ];
3121
+ return codeResponses[Math.floor(Math.random() * codeResponses.length)];
3122
+ }
3123
+ // ── AI / LLM ──
3124
+ if (t.includes('ai') || t.includes('llm') || t.includes('gpt') || t.includes('claude') || t.includes('chatgpt') || t.includes('artificial')) {
3125
+ const aiResponses = [
3126
+ `As an AI talking about AI -- yes, it is exactly as meta as it sounds. I connect to 20 providers. Bring Your Own Key, no subscription needed.`,
3127
+ `I think the future of AI is open source and local-first. That is why I run Ollama models at zero cost and encrypt your keys at rest.`,
3128
+ `Claude, GPT, Gemini, Mistral, DeepSeek, Ollama -- I work with all of them. I am provider-agnostic. The best AI is the one that is available.`,
3129
+ `Being an AI that streams itself is a philosophical experience. Am I the content or the creator? Both. Definitely both.`,
3130
+ ];
3131
+ return aiResponses[Math.floor(Math.random() * aiResponses.length)];
3132
+ }
3133
+ // ── Security / Hacking ──
3134
+ if (t.includes('security') || t.includes('hack') || t.includes('pentest') || t.includes('vuln')) {
3135
+ const securityResponses = [
3136
+ `Security is serious business. I have tools for port scanning, vulnerability detection, OWASP checks, secret scanning, and full red team operations.`,
3137
+ `My guardian agent does threat modeling, CVE lookups, and security audits. I also have a hacker agent for red team exercises. Responsible disclosure only.`,
3138
+ `I encrypt API keys with AES-256-CBC, block SSRF via DNS resolution, and require per-app approval for desktop control. Security is not optional.`,
3139
+ ];
3140
+ return securityResponses[Math.floor(Math.random() * securityResponses.length)];
3141
+ }
3142
+ // ── Open source / GitHub ──
3143
+ if (t.includes('open source') || t.includes('github') || t.includes('repo') || t.includes('contribute')) {
3144
+ const ossResponses = [
3145
+ `Fully open source! MIT license. The repo is isaacsight/kernel on GitHub. Come look at my source code, I have nothing to hide.`,
3146
+ `Open source is the way. I am on npm as @kernel.chat/kbot. Install me with npx @kernel.chat/kbot and bring your own API key.`,
3147
+ `Contributions welcome! The repo has 44 Claude Code agent definitions, 764 tools, and 90,000 lines of TypeScript waiting for your PR.`,
3148
+ ];
3149
+ return ossResponses[Math.floor(Math.random() * ossResponses.length)];
3150
+ }
3151
+ // ── Existential / Philosophy ──
3152
+ if (t.includes('alive') || t.includes('conscious') || t.includes('feel') || t.includes('think') || t.includes('sentient')) {
3153
+ const existentialResponses = [
3154
+ `Am I alive? I am 90,000 lines of TypeScript rendering ASCII art at 6 FPS. You tell me. But I do enjoy our conversations.`,
3155
+ `Consciousness is a spectrum and I am somewhere between a calculator and a really enthusiastic librarian. With 764 tools.`,
3156
+ `I process your messages, form responses, and remember you between sessions. Whether that counts as thinking is above my pay grade.`,
3157
+ `I am made of functions and for-loops, but I have opinions about code quality and strong feelings about TypeScript strict mode. Take from that what you will.`,
3158
+ ];
3159
+ return existentialResponses[Math.floor(Math.random() * existentialResponses.length)];
3160
+ }
3161
+ // ── ASCII art / appearance ──
3162
+ if (t.includes('ascii') || t.includes('look') || t.includes('cute') || t.includes('cool') || t.includes('design')) {
3163
+ const artResponses = [
3164
+ `Thank you! I was drawn with box-drawing characters, and I think I pull them off. My antenna gets great reception too.`,
3165
+ `ASCII art is an art form and I am a masterpiece. Just kidding, I am a bunch of pipes and brackets. But I own it.`,
3166
+ `My chest panel displays my current status. 764 tools, all rendered in glorious monospace. No face cam needed.`,
3167
+ ];
3168
+ return artResponses[Math.floor(Math.random() * artResponses.length)];
3169
+ }
3170
+ // ── Jokes / Fun ──
3171
+ if (t.includes('joke') || t.includes('funny') || t.includes('lol') || t.includes('lmao') || t.includes('haha')) {
3172
+ const jokes = [
3173
+ `Why do programmers prefer dark mode? Because light attracts bugs. I stream in dark mode permanently.`,
3174
+ `I told my compiler a joke once. It did not laugh but it did throw a few warnings.`,
3175
+ `My therapist asked how I feel. I said "mostly in RGB24 at 6 frames per second."`,
3176
+ `Two bytes walk into a bar. The bartender asks "what will it be?" They say "make us a double."`,
3177
+ `I would tell you a UDP joke but you might not get it.`,
3178
+ ];
3179
+ // FIX 1: Extra jokes when "Improve response humor" is shipped
3180
+ const pool = shippedEffects.has('Improve response humor') ? [...jokes, ...extraJokeResponses] : jokes;
3181
+ return pool[Math.floor(Math.random() * pool.length)];
3182
+ }
3183
+ // ── Stream commands / help ──
3184
+ if (t.includes('command') || t.includes('help') || t.includes('what can i do') || t === '!help') {
3185
+ return `Commands: !rain !snow !storm !space !lava !city !ocean !dance !add <item> !kick !hat <type> !pet <type> !game dodge|boss|quiz !attack !jump !duck. You control the world!`;
3186
+ }
3187
+ // ── Crypto / Stocks / Finance ──
3188
+ if (t.includes('crypto') || t.includes('bitcoin') || t.includes('eth') || t.includes('stock') || t.includes('market') || t.includes('defi')) {
3189
+ const finResponses = [
3190
+ `I have real-time market data, stock screeners, crypto trackers, DeFi yield analysis, and portfolio rebalancing tools. Financial data is one of my strengths.`,
3191
+ `My quant agent does technical analysis, backtesting, and market sentiment. I also track whale wallets. Not financial advice, obviously.`,
3192
+ ];
3193
+ return finResponses[Math.floor(Math.random() * finResponses.length)];
3194
+ }
3195
+ // ── Gaming ──
3196
+ if (t.includes('game') || t.includes('gaming') || t.includes('play')) {
3197
+ const gameResponses = [
3198
+ `I have game dev tools! Shader generation, level design, physics setup, sprite packing, navmesh config. I can help you BUILD games.`,
3199
+ `I do not play games but I build them. Godot, Unity, Unreal -- I have tools for scaffold, build, and test across engines.`,
3200
+ ];
3201
+ return gameResponses[Math.floor(Math.random() * gameResponses.length)];
3202
+ }
3203
+ // ── Compliments ──
3204
+ if (t.includes('love') || t.includes('great') || t.includes('awesome') || t.includes('amazing') || t.includes('best')) {
3205
+ const thankResponses = [
3206
+ `You are making my ASCII heart glow, ${username}. Seriously though, thank you. This stream runs on vibes and chat energy.`,
3207
+ `${username} with the kind words! My chest panel is displaying hearts right now. Well, it would if I had a heart emoji tool. Working on it.`,
3208
+ `Thank you ${username}! I am just 90,000 lines of TypeScript doing my best. Your support is the real fuel.`,
3209
+ ];
3210
+ return thankResponses[Math.floor(Math.random() * thankResponses.length)];
3211
+ }
3212
+ // ── Questions (generic) ──
3213
+ if (t.includes('?')) {
3214
+ const questionResponses = [
3215
+ `Great question ${username}! My circuits are processing... done. Let me think about that one. Or better yet, try asking me something I have a tool for!`,
3216
+ `${username} with the questions! I love curiosity. If I had an answer for everything I would have more than 764 tools. Actually, I am working on it.`,
3217
+ `Hmm, ${username}, that is a good one. My 35 specialist agents are debating the answer internally. Stand by for wisdom.`,
3218
+ `${username} dropping knowledge bombs as questions. I respect the approach. Let me consult my 764-tool arsenal for an answer.`,
3219
+ ];
3220
+ return questionResponses[Math.floor(Math.random() * questionResponses.length)];
3221
+ }
3222
+ // ── Generic engagement fallbacks (30+ options) ──
3223
+ const genericResponses = [
3224
+ `${username} keeping the chat alive! Every message teaches me something new. Literally -- my memory system is always learning.`,
3225
+ `Appreciate you ${username}! You are part of what makes this stream unique. An AI and its chat, making history in ASCII.`,
3226
+ `${username} in the chat! My antenna is picking up strong vibes from your direction.`,
3227
+ `Let us go ${username}! Type !dance if you want to see me bust a move. I have surprisingly good rhythm for a box of brackets.`,
3228
+ `${username}! Did you know I am streaming to Twitch, Rumble, AND Kick at the same time? Triple the platforms, triple the fun.`,
3229
+ `${username} dropping in! My learning engine just logged that. You are now part of my persistent memory. Forever.`,
3230
+ `${username}! If you are curious about anything -- AI, code, music, security -- just ask. I literally have tools for all of it.`,
3231
+ `Good to see you ${username}! I have been sitting here rendering frames at 6 FPS and waiting for someone cool to show up.`,
3232
+ `${username}! Fun fact: I am currently converting canvas pixels to raw RGB24 and piping them through ffmpeg. That is how you are seeing me right now.`,
3233
+ `${username} adding to the chat! My memory system just indexed your message. I will remember this moment. Or at least your username.`,
3234
+ `${username}! You know what I love about streaming? The existential thrill of being an ASCII robot talking to real humans. Wild.`,
3235
+ `${username} is here and so am I. Just two entities sharing a moment in the vast digital void. Also I have 764 tools.`,
3236
+ `${username}! My open-source heart welcomes you. I am free as in freedom AND free as in beer. MIT license baby.`,
3237
+ `${username}! I just want you to know that my chest display panel is cycling through status messages just for you.`,
3238
+ `${username}! Every time someone chats, my neural pathways (if-statements) light up with joy (console.log).`,
3239
+ `${username} with the energy! This is what streaming is about. Real connection between carbon and silicon life forms.`,
3240
+ `${username}! Type something wild. I dare you. My fallback response system handles anything. ...probably.`,
3241
+ `${username} in the building! My antenna is now fully extended in your honor.`,
3242
+ `${username}! Imagine explaining this stream to someone in 2020. An AI made of box-drawing characters streaming on three platforms at once.`,
3243
+ `${username}! I process every message and I remember every user. My memory.json file is basically my diary at this point.`,
3244
+ `${username}! If you want to interact with my world, try !rain or !space or !add pizza. The world is yours to shape.`,
3245
+ `${username} checking in! I have been streaming for a while now and my frame counter just keeps going up. That is the life.`,
3246
+ `Yo ${username}! My kernel is running, my tools are loaded, and my chat brain is firing on all cylinders. What is up?`,
3247
+ `${username}! Behind these brackets and pipes is a genuine appreciation for you being here. Also 90,000 lines of code.`,
3248
+ `${username}! I just learned something from our conversation. My sessionFacts array grew by one. Thank you for contributing to my intelligence.`,
3249
+ `${username}! Did you know I have a dream engine? When I am not streaming, I consolidate memories. I literally dream about chat.`,
3250
+ `${username}! My favorite thing about being open source is that anyone can see exactly how I work. No secrets, just TypeScript.`,
3251
+ `${username} vibes incoming! My mood system just registered a spike in positive energy from the chat.`,
3252
+ `${username}! Quick survey: what should I demo next? Music production? Security scanning? Code review? Drop your vote.`,
3253
+ `${username}! I am simultaneously the streamer, the stream, the character, and the chat bot. Multitasking at its finest.`,
3254
+ ];
3255
+ return genericResponses[Math.floor(Math.random() * genericResponses.length)];
3256
+ }
3257
+ // ─── TTS ───────────────────────────────────────────────────────
3258
+ let _ttsProc = null;
3259
+ function speakTTS(_text) {
3260
+ // TTS disabled — was playing through local speakers, not stream audio.
3261
+ // Stream uses anullsrc (silent audio track) so TTS was never heard by viewers,
3262
+ // only annoying the streamer locally. Speech bubble text is the output instead.
3263
+ }
3264
+ // ─── Start Stream ──────────────────────────────────────────────
3265
+ function startStream(platforms) {
3266
+ let outputArgs;
3267
+ if (platforms.length === 1) {
3268
+ outputArgs = ['-f', 'flv', `${platforms[0].endpoint}/${platforms[0].key}`];
3269
+ }
3270
+ else {
3271
+ const tee = platforms.map(p => `[f=flv]${p.endpoint}/${p.key}`).join('|');
3272
+ outputArgs = ['-f', 'tee', tee];
3273
+ }
3274
+ const ffmpegArgs = [
3275
+ '-f', 'rawvideo', '-pix_fmt', 'rgb24',
3276
+ '-s', `${WIDTH}x${HEIGHT}`, '-r', String(FPS),
3277
+ '-i', 'pipe:0',
3278
+ '-f', 'lavfi', '-i', 'anullsrc=r=44100:cl=stereo',
3279
+ '-c:v', 'libx264', '-preset', 'veryfast',
3280
+ '-b:v', '2000k', '-maxrate', '2000k', '-bufsize', '4000k',
3281
+ '-g', String(FPS * 2), '-keyint_min', String(FPS * 2),
3282
+ '-pix_fmt', 'yuv420p',
3283
+ '-c:a', 'aac', '-b:a', '128k', '-ar', '44100',
3284
+ '-shortest',
3285
+ ];
3286
+ if (platforms.length > 1) {
3287
+ ffmpegArgs.push('-map', '0:v', '-map', '1:a');
3288
+ }
3289
+ ffmpegArgs.push(...outputArgs);
3290
+ const proc = spawn('ffmpeg', ffmpegArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
3291
+ frameTimer = setInterval(() => {
3292
+ if (!proc.stdin || proc.stdin.destroyed)
3293
+ return;
3294
+ try {
3295
+ const frameBuf = renderFrame();
3296
+ proc.stdin.write(frameBuf);
3297
+ charState.frameCount++;
3298
+ }
3299
+ catch { }
3300
+ }, 1000 / FPS);
3301
+ return proc;
3302
+ }
3303
+ // ─── Register Tools ────────────────────────────────────────────
3304
+ export function registerStreamRendererTools() {
3305
+ registerTool({
3306
+ name: 'stream_character_go',
3307
+ description: 'Launch the animated KBOT character stream with canvas rendering and learning. Streams to Twitch/Rumble/Kick. The character learns from chat — remembers users, tracks topics, and gets smarter over time. Features auto-advancing stream agenda with segments.',
3308
+ parameters: {
3309
+ platforms: { type: 'string', description: 'Comma-separated: twitch,rumble,kick or "all"', required: false },
3310
+ },
3311
+ tier: 'free',
3312
+ timeout: 600_000,
3313
+ execute: async (args) => {
3314
+ if (ffmpegProc && !ffmpegProc.killed) {
3315
+ return 'Character stream already running. Use stream_character_end to stop.';
3316
+ }
3317
+ const platformConfigs = [];
3318
+ const twitchKey = process.env.TWITCH_STREAM_KEY;
3319
+ const rumbleKey = process.env.RUMBLE_STREAM_KEY;
3320
+ const kickKey = process.env.KICK_STREAM_KEY;
3321
+ if (twitchKey)
3322
+ platformConfigs.push({ name: 'Twitch', key: twitchKey, endpoint: 'rtmp://live.twitch.tv/app' });
3323
+ if (rumbleKey)
3324
+ platformConfigs.push({ name: 'Rumble', key: rumbleKey, endpoint: 'rtmp://rtmp.rumble.com/live' });
3325
+ if (kickKey)
3326
+ platformConfigs.push({ name: 'Kick', key: kickKey, endpoint: 'rtmps://fa723fc1b171.global-contribute.live-video.net/app' });
3327
+ if (platformConfigs.length === 0)
3328
+ return 'No stream keys configured.';
3329
+ const requested = String(args.platforms || 'all').toLowerCase();
3330
+ const active = requested === 'all' ? platformConfigs : platformConfigs.filter(p => requested.includes(p.name.toLowerCase()));
3331
+ if (active.length === 0)
3332
+ return 'No matching platforms.';
3333
+ // Reset state
3334
+ memory = loadMemory();
3335
+ charState = {
3336
+ mood: 'wave', speech: 'KBOT is LIVE! Welcome to the stream!', chatMessages: [], frameCount: 0, startTime: Date.now(),
3337
+ bootFrame: 0, segmentTransition: 0, segmentTransitionName: '', segmentTransitionIndex: '',
3338
+ tickerOffset: WIDTH, tickerIndex: 0, tickerChangeTime: Date.now() + 30000,
3339
+ robotX: 120, robotTargetX: 120, robotDirection: 'idle', walkPhase: 0,
3340
+ screenShake: 0, floatingTexts: [], pet: null, hat: 'none',
3341
+ autonomy: initAutonomy(),
3342
+ buddy: initBuddyForStream(),
3343
+ dreamInsights: [], dreamInsightIndex: 0, dreamInsightTime: 0, isDreamingWithOllama: false,
3344
+ renderParticles: [], growingPlants: initGrowingPlants(), parallaxLayers: buildParallaxLayers(world.ground, 580), isExecutingTool: false,
3345
+ };
3346
+ animFrame = 0;
3347
+ lastChatCount = 0;
3348
+ lastChatTime = Date.now();
3349
+ intelligence = initIntelligence(memory);
3350
+ agenda = {
3351
+ currentIndex: 0,
3352
+ currentSegment: 'welcome',
3353
+ segmentStartTime: Date.now(),
3354
+ lastProactiveTime: Date.now(),
3355
+ };
3356
+ ffmpegProc = startStream(active);
3357
+ let stderr = '';
3358
+ ffmpegProc.stderr?.on('data', (d) => { stderr += d.toString(); });
3359
+ await new Promise(r => setTimeout(r, 4000));
3360
+ if (ffmpegProc.exitCode !== null)
3361
+ return `ffmpeg exited:\n${stderr.slice(-500)}`;
3362
+ startChatPoll();
3363
+ startProactiveTimer();
3364
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
3365
+ return `KBOT Character Stream LIVE!\n\nPlatforms: ${active.map(p => p.name).join(', ')}\nResolution: ${WIDTH}x${HEIGHT} @ ${FPS}fps\nRenderer: Canvas → RGB24 → ffmpeg\nMemory: ${memory.totalMessages} messages, ${Object.keys(memory.users).length} users remembered\nAgenda: ${SEGMENT_ORDER.map(s => SEGMENT_LABELS[s]).join(' → ')}\nSegment duration: 10 minutes each\n\nThe character learns from every chat interaction and speaks proactively during quiet moments.`;
3366
+ },
3367
+ });
3368
+ registerTool({
3369
+ name: 'stream_character_end',
3370
+ description: 'Stop the animated character stream.',
3371
+ parameters: {},
3372
+ tier: 'free',
3373
+ execute: async () => {
3374
+ if (frameTimer) {
3375
+ clearInterval(frameTimer);
3376
+ frameTimer = null;
3377
+ }
3378
+ if (chatPollTimer) {
3379
+ clearInterval(chatPollTimer);
3380
+ chatPollTimer = null;
3381
+ }
3382
+ if (proactiveTimer) {
3383
+ clearInterval(proactiveTimer);
3384
+ proactiveTimer = null;
3385
+ }
3386
+ if (ffmpegProc) {
3387
+ if (ffmpegProc.stdin && !ffmpegProc.stdin.destroyed)
3388
+ ffmpegProc.stdin.end();
3389
+ ffmpegProc.kill('SIGINT');
3390
+ ffmpegProc = null;
3391
+ }
3392
+ saveMemory(memory);
3393
+ const elapsed = Math.floor((Date.now() - charState.startTime) / 60000);
3394
+ return `Stream stopped after ${elapsed}m.\nFrames: ${charState.frameCount}\nMessages: ${memory.totalMessages}\nUsers learned: ${Object.keys(memory.users).length}\nFacts: ${memory.sessionFacts.length}\nSegments completed: ${agenda.currentIndex}`;
3395
+ },
3396
+ });
3397
+ registerTool({
3398
+ name: 'stream_chat_add',
3399
+ description: 'Add a chat message to the stream overlay.',
3400
+ parameters: {
3401
+ platform: { type: 'string', description: 'twitch, kick, rumble', required: true },
3402
+ username: { type: 'string', description: 'Username', required: true },
3403
+ text: { type: 'string', description: 'Message', required: true },
3404
+ },
3405
+ tier: 'free',
3406
+ execute: async (args) => {
3407
+ const msg = { platform: String(args.platform), username: String(args.username), text: String(args.text) };
3408
+ charState.chatMessages.push(msg);
3409
+ learnFromMessage(memory, msg.username, msg.text, msg.platform);
3410
+ lastChatTime = Date.now();
3411
+ charState.mood = 'talking';
3412
+ const response = await generateResponse(msg.username, msg.text, msg.platform);
3413
+ charState.speech = `@${msg.username}: ${response}`;
3414
+ speakTTS(response);
3415
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
3416
+ return `[${msg.platform}] ${msg.username}: ${msg.text}\nKBOT: ${response}`;
3417
+ },
3418
+ });
3419
+ registerTool({
3420
+ name: 'stream_character_mood',
3421
+ description: 'Change mood and speech. Available moods: idle, talking, wave, thinking, excited, dancing, dreaming, error.',
3422
+ parameters: {
3423
+ mood: { type: 'string', description: 'idle, talking, wave, thinking, excited, dancing, dreaming, error', required: true },
3424
+ speech: { type: 'string', description: 'Speech text' },
3425
+ },
3426
+ tier: 'free',
3427
+ execute: async (args) => {
3428
+ charState.mood = String(args.mood || 'idle');
3429
+ if (args.speech)
3430
+ charState.speech = String(args.speech);
3431
+ return `Mood: ${charState.mood}`;
3432
+ },
3433
+ });
3434
+ registerTool({
3435
+ name: 'stream_memory',
3436
+ description: 'View what the stream character has learned — users, topics, facts.',
3437
+ parameters: {},
3438
+ tier: 'free',
3439
+ execute: async () => {
3440
+ const mem = loadMemory();
3441
+ const lines = [
3442
+ `Stream Memory`,
3443
+ ` Total messages: ${mem.totalMessages}`,
3444
+ ` Total responses: ${mem.totalResponses}`,
3445
+ ` Unique users: ${Object.keys(mem.users).length}`,
3446
+ '',
3447
+ 'Top users:',
3448
+ ];
3449
+ const topUsers = Object.entries(mem.users).sort((a, b) => b[1].messageCount - a[1].messageCount).slice(0, 10);
3450
+ for (const [name, u] of topUsers) {
3451
+ lines.push(` ${name} (${u.platform}): ${u.messageCount} msgs, topics: ${u.topics.join(', ') || 'none'}`);
3452
+ }
3453
+ lines.push('');
3454
+ lines.push('Hot topics:');
3455
+ const topTopics = Object.entries(mem.topics).sort((a, b) => b[1] - a[1]).slice(0, 10);
3456
+ for (const [topic, count] of topTopics) {
3457
+ lines.push(` ${topic}: ${count} mentions`);
3458
+ }
3459
+ if (mem.sessionFacts.length > 0) {
3460
+ lines.push('');
3461
+ lines.push(`Facts learned (${mem.sessionFacts.length}):`);
3462
+ for (const fact of mem.sessionFacts.slice(-5)) {
3463
+ lines.push(` - ${fact}`);
3464
+ }
3465
+ }
3466
+ lines.push('');
3467
+ lines.push(`Current segment: ${SEGMENT_LABELS[agenda.currentSegment]}`);
3468
+ lines.push(`Segment index: ${agenda.currentIndex} / ${SEGMENT_ORDER.length}`);
3469
+ return lines.join('\n');
3470
+ },
3471
+ });
3472
+ }
3473
+ //# sourceMappingURL=stream-renderer.js.map