@kernel.chat/kbot 3.74.0 → 3.83.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.
- package/README.md +9 -0
- package/dist/cli.js +34 -0
- package/dist/ide/mcp-server.js +58 -43
- package/dist/tools/index.js +3 -0
- package/dist/tools/render-engine.d.ts +158 -0
- package/dist/tools/render-engine.js +1361 -0
- package/dist/tools/sprite-engine.d.ts +41 -0
- package/dist/tools/sprite-engine.js +1601 -0
- package/dist/tools/stream-brain.d.ts +70 -0
- package/dist/tools/stream-brain.js +699 -0
- package/dist/tools/stream-character.d.ts +2 -0
- package/dist/tools/stream-character.js +619 -0
- package/dist/tools/stream-intelligence.d.ts +172 -0
- package/dist/tools/stream-intelligence.js +2237 -0
- package/dist/tools/stream-renderer.d.ts +2 -0
- package/dist/tools/stream-renderer.js +3476 -0
- package/dist/tools/streaming.d.ts +2 -0
- package/dist/tools/streaming.js +491 -0
- package/package.json +1 -1
|
@@ -0,0 +1,3476 @@
|
|
|
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 (cap at 150 to prevent performance issues)
|
|
2117
|
+
if (charState.renderParticles.length > 150) {
|
|
2118
|
+
charState.renderParticles = charState.renderParticles.slice(-150);
|
|
2119
|
+
}
|
|
2120
|
+
charState.renderParticles = tickRenderParticles(charState.renderParticles);
|
|
2121
|
+
// AAA: Tick growing plants
|
|
2122
|
+
tickGrowingPlants(charState.growingPlants);
|
|
2123
|
+
// PRIORITY 2: Screen shake offset
|
|
2124
|
+
let shakeOffX = 0, shakeOffY = 0;
|
|
2125
|
+
if (charState.screenShake > 0) {
|
|
2126
|
+
shakeOffX = Math.round((Math.random() - 0.5) * 6);
|
|
2127
|
+
shakeOffY = Math.round((Math.random() - 0.5) * 4);
|
|
2128
|
+
charState.screenShake--;
|
|
2129
|
+
}
|
|
2130
|
+
ctx.save();
|
|
2131
|
+
ctx.translate(shakeOffX, shakeOffY);
|
|
2132
|
+
// Background — AAA: base fill + procedural sky
|
|
2133
|
+
ctx.fillStyle = world.events.includes('lightning') ? '#ffffff' : getWorldBg();
|
|
2134
|
+
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
2135
|
+
// AAA: Procedural sky rendering (replaces flat gradient for sky area)
|
|
2136
|
+
renderSky(ctx, WIDTH, HEIGHT, world.timeOfDay, world.weather, animFrame, 580);
|
|
2137
|
+
// AAA: Parallax layers (rebuild if biome changed)
|
|
2138
|
+
if (charState.parallaxLayers.length === 0) {
|
|
2139
|
+
charState.parallaxLayers = buildParallaxLayers(world.ground, 580);
|
|
2140
|
+
}
|
|
2141
|
+
renderParallaxLayers(ctx, charState.parallaxLayers, charState.robotX, animFrame);
|
|
2142
|
+
// Draw full animated background scene (biome-specific details on top of parallax)
|
|
2143
|
+
drawBackground(ctx, animFrame);
|
|
2144
|
+
// AAA: Advanced water/lava rendering for specific biomes
|
|
2145
|
+
if (world.ground === 'ocean') {
|
|
2146
|
+
renderAnimatedWater(ctx, 580, animFrame);
|
|
2147
|
+
}
|
|
2148
|
+
else if (world.ground === 'lava') {
|
|
2149
|
+
renderLavaFlow(ctx, 580, animFrame);
|
|
2150
|
+
}
|
|
2151
|
+
// AAA: Growing vegetation
|
|
2152
|
+
renderGrowingPlants(ctx, charState.growingPlants);
|
|
2153
|
+
// (#17) Weather particles as rectangles
|
|
2154
|
+
for (const p of world.particles) {
|
|
2155
|
+
if (world.weather === 'rain') {
|
|
2156
|
+
ctx.fillStyle = '#6699cc';
|
|
2157
|
+
ctx.fillRect(p.x, p.y, 2, 8);
|
|
2158
|
+
}
|
|
2159
|
+
else if (world.weather === 'snow') {
|
|
2160
|
+
ctx.fillStyle = '#ffffff';
|
|
2161
|
+
ctx.fillRect(p.x, p.y, 4, 4);
|
|
2162
|
+
}
|
|
2163
|
+
else if (world.weather === 'storm') {
|
|
2164
|
+
ctx.fillStyle = '#aaccff';
|
|
2165
|
+
ctx.fillRect(p.x, p.y, 2, 12);
|
|
2166
|
+
}
|
|
2167
|
+
else if (world.weather === 'stars') {
|
|
2168
|
+
ctx.fillStyle = '#ffffaa';
|
|
2169
|
+
ctx.fillRect(p.x, p.y, 2, 2);
|
|
2170
|
+
}
|
|
2171
|
+
else {
|
|
2172
|
+
ctx.fillStyle = '#6699cc';
|
|
2173
|
+
ctx.fillRect(p.x, p.y, 2, 6);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
// World items (physics-enabled)
|
|
2177
|
+
ctx.fillStyle = COLORS.text;
|
|
2178
|
+
ctx.font = '18px "Courier New", monospace';
|
|
2179
|
+
for (const item of world.items) {
|
|
2180
|
+
ctx.fillText(item.emoji, item.x, item.y);
|
|
2181
|
+
}
|
|
2182
|
+
// ── Header bar ──
|
|
2183
|
+
ctx.fillStyle = COLORS.bgPanel;
|
|
2184
|
+
ctx.fillRect(0, 0, WIDTH, 60);
|
|
2185
|
+
// Header border
|
|
2186
|
+
ctx.strokeStyle = COLORS.accent;
|
|
2187
|
+
ctx.lineWidth = 2;
|
|
2188
|
+
ctx.beginPath();
|
|
2189
|
+
ctx.moveTo(0, 60);
|
|
2190
|
+
ctx.lineTo(WIDTH, 60);
|
|
2191
|
+
ctx.stroke();
|
|
2192
|
+
// Title
|
|
2193
|
+
ctx.fillStyle = COLORS.accent;
|
|
2194
|
+
ctx.font = 'bold 28px "Courier New", "Courier", monospace';
|
|
2195
|
+
ctx.fillText('K : B O T L I V E', 40, 40);
|
|
2196
|
+
// Current segment badge
|
|
2197
|
+
const segLabel = SEGMENT_LABELS[agenda.currentSegment];
|
|
2198
|
+
const segElapsed = Math.floor((Date.now() - agenda.segmentStartTime) / 1000);
|
|
2199
|
+
const segRemaining = Math.max(0, Math.floor((SEGMENT_DURATION_MS - (Date.now() - agenda.segmentStartTime)) / 1000));
|
|
2200
|
+
const segTimeStr = `${Math.floor(segRemaining / 60)}:${String(segRemaining % 60).padStart(2, '0')}`;
|
|
2201
|
+
ctx.fillStyle = COLORS.accent;
|
|
2202
|
+
ctx.font = 'bold 14px "Courier New", monospace';
|
|
2203
|
+
const segText = `[ ${segLabel} ${segTimeStr} ]`;
|
|
2204
|
+
ctx.fillText(segText, 330, 40);
|
|
2205
|
+
// Viewers counter (proxy from chat message count)
|
|
2206
|
+
const viewerEstimate = Math.max(1, Math.floor(memory.totalMessages / 3) + Object.keys(memory.users).length);
|
|
2207
|
+
ctx.fillStyle = COLORS.red;
|
|
2208
|
+
ctx.font = 'bold 14px "Courier New", monospace';
|
|
2209
|
+
ctx.fillText(`VIEWERS: ~${viewerEstimate}`, WIDTH - 280, 22);
|
|
2210
|
+
// Timer
|
|
2211
|
+
const elapsed = Math.floor((Date.now() - charState.startTime) / 1000);
|
|
2212
|
+
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')}`;
|
|
2213
|
+
ctx.fillStyle = COLORS.textDim;
|
|
2214
|
+
ctx.font = '20px "Courier New", monospace';
|
|
2215
|
+
ctx.fillText(timeStr, WIDTH - 140, 38);
|
|
2216
|
+
// Platform indicators
|
|
2217
|
+
ctx.font = 'bold 14px "Courier New", monospace';
|
|
2218
|
+
const platforms = [
|
|
2219
|
+
{ name: 'TWITCH', color: COLORS.twitchPurple, x: 460 },
|
|
2220
|
+
{ name: 'RUMBLE', color: COLORS.rumbleGreen, x: 580 },
|
|
2221
|
+
{ name: 'KICK', color: COLORS.kickGreen, x: 700 },
|
|
2222
|
+
];
|
|
2223
|
+
for (const p of platforms) {
|
|
2224
|
+
// Dot
|
|
2225
|
+
ctx.fillStyle = p.color;
|
|
2226
|
+
ctx.beginPath();
|
|
2227
|
+
ctx.arc(p.x, 33, 5, 0, Math.PI * 2);
|
|
2228
|
+
ctx.fill();
|
|
2229
|
+
ctx.fillStyle = COLORS.text;
|
|
2230
|
+
ctx.fillText(p.name, p.x + 12, 38);
|
|
2231
|
+
}
|
|
2232
|
+
// ── FIX 1: Movement logic — robot walks toward target ──
|
|
2233
|
+
const isWalking = Math.abs(charState.robotX - charState.robotTargetX) > 2;
|
|
2234
|
+
if (isWalking) {
|
|
2235
|
+
const dx = charState.robotTargetX - charState.robotX;
|
|
2236
|
+
const step = dx > 0 ? 2 : -2;
|
|
2237
|
+
charState.robotX += step;
|
|
2238
|
+
charState.robotDirection = dx > 0 ? 'right' : 'left';
|
|
2239
|
+
charState.walkPhase = (charState.walkPhase + 1) % 4;
|
|
2240
|
+
}
|
|
2241
|
+
else {
|
|
2242
|
+
charState.robotDirection = 'idle';
|
|
2243
|
+
}
|
|
2244
|
+
// ── FIX 4: Brain-driven behavior ──
|
|
2245
|
+
const brainAction = getBrainAction(intelligence.brain, animFrame);
|
|
2246
|
+
if (brainAction.type !== 'none') {
|
|
2247
|
+
if (brainAction.mood) {
|
|
2248
|
+
charState.mood = brainAction.mood;
|
|
2249
|
+
if (brainAction.duration) {
|
|
2250
|
+
setTimeout(() => { charState.mood = 'idle'; }, brainAction.duration);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
if (brainAction.speech) {
|
|
2254
|
+
charState.speech = brainAction.speech;
|
|
2255
|
+
speakTTS(brainAction.speech);
|
|
2256
|
+
if (brainAction.duration) {
|
|
2257
|
+
setTimeout(() => { charState.speech = ''; }, brainAction.duration);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
// FIX 1: Shipped effect — "Add stream highlights reel"
|
|
2262
|
+
if (shippedEffects.has('Add stream highlights reel') && animFrame % 900 === 0 && animFrame > 100) {
|
|
2263
|
+
// Every ~2.5 minutes, call out a highlight
|
|
2264
|
+
const highlightPhrases = [
|
|
2265
|
+
'Highlight moment! This is one for the reel!',
|
|
2266
|
+
'That was worth saving! Highlight captured!',
|
|
2267
|
+
'CLIP IT! That was amazing!',
|
|
2268
|
+
'Stream highlight detected! My circuits are tingling!',
|
|
2269
|
+
];
|
|
2270
|
+
if (!charState.speech) {
|
|
2271
|
+
charState.speech = highlightPhrases[Math.floor(Math.random() * highlightPhrases.length)];
|
|
2272
|
+
spawnFloatingText('HIGHLIGHT!', 200, 200, '#f0c040', 36);
|
|
2273
|
+
setTimeout(() => { charState.speech = ''; }, 5000);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
// FIX 1: Shipped effect — "Add chat sentiment analysis"
|
|
2277
|
+
if (shippedEffects.has('Add chat sentiment analysis') && animFrame % 720 === 0 && animFrame > 200) {
|
|
2278
|
+
const recentMsgs = charState.chatMessages.slice(-20);
|
|
2279
|
+
if (recentMsgs.length > 5) {
|
|
2280
|
+
const positive = ['love', 'great', 'awesome', 'cool', 'nice', 'good', 'lol', 'haha', 'wow', 'yes', 'hype', 'pog'];
|
|
2281
|
+
const negative = ['bad', 'hate', 'boring', 'sucks', 'ugly', 'broken', 'lag', 'cringe'];
|
|
2282
|
+
let score = 0;
|
|
2283
|
+
for (const m of recentMsgs) {
|
|
2284
|
+
const words = m.text.toLowerCase().split(/\s+/);
|
|
2285
|
+
for (const w of words) {
|
|
2286
|
+
if (positive.includes(w))
|
|
2287
|
+
score++;
|
|
2288
|
+
if (negative.includes(w))
|
|
2289
|
+
score--;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
if (!charState.speech) {
|
|
2293
|
+
if (score > 5) {
|
|
2294
|
+
charState.speech = 'Chat seems really excited today! The vibes are immaculate!';
|
|
2295
|
+
}
|
|
2296
|
+
else if (score < -3) {
|
|
2297
|
+
charState.speech = 'Chat seems a bit grumpy... should I tell a joke?';
|
|
2298
|
+
}
|
|
2299
|
+
else if (score > 2) {
|
|
2300
|
+
charState.speech = 'Positive energy in the chat! My neural pathways approve.';
|
|
2301
|
+
}
|
|
2302
|
+
if (charState.speech)
|
|
2303
|
+
setTimeout(() => { charState.speech = ''; }, 8000);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
// ── Main layout: Robot (left) | Chat (right) ──
|
|
2308
|
+
const dividerX = 580;
|
|
2309
|
+
// Divider line
|
|
2310
|
+
ctx.strokeStyle = COLORS.border;
|
|
2311
|
+
ctx.lineWidth = 1;
|
|
2312
|
+
ctx.beginPath();
|
|
2313
|
+
ctx.moveTo(dividerX, 70);
|
|
2314
|
+
ctx.lineTo(dividerX, HEIGHT - 120);
|
|
2315
|
+
ctx.stroke();
|
|
2316
|
+
// ── Robot area (left side) — Pixel Art Sprite ──
|
|
2317
|
+
const robotScale = 10;
|
|
2318
|
+
const robotX = charState.robotX; // FIX 1: use dynamic position
|
|
2319
|
+
const robotY = 90;
|
|
2320
|
+
animFrame++;
|
|
2321
|
+
// (#20) Robot glow — soft radial gradient behind robot torso
|
|
2322
|
+
const glowCenterX = robotX + 16 * robotScale;
|
|
2323
|
+
const glowCenterY = robotY + 26 * robotScale;
|
|
2324
|
+
const glowRadius = 10 * robotScale;
|
|
2325
|
+
const moodColorHex = MOOD_COLORS[charState.mood] ?? COLORS.green;
|
|
2326
|
+
const grad = ctx.createRadialGradient(glowCenterX, glowCenterY, 0, glowCenterX, glowCenterY, glowRadius);
|
|
2327
|
+
grad.addColorStop(0, hexToRgba(moodColorHex, 0.2));
|
|
2328
|
+
grad.addColorStop(1, hexToRgba(moodColorHex, 0));
|
|
2329
|
+
ctx.fillStyle = grad;
|
|
2330
|
+
ctx.fillRect(glowCenterX - glowRadius, glowCenterY - glowRadius, glowRadius * 2, glowRadius * 2);
|
|
2331
|
+
// FIX 1: Music visualization behind robot (if shipped)
|
|
2332
|
+
drawMusicVisualization(ctx, robotX, robotY);
|
|
2333
|
+
// AAA: Character effects — eye glow bleed, mood aura (BEFORE robot for under-glow)
|
|
2334
|
+
drawCharacterEffects(ctx, robotX, robotY, robotScale, charState.mood, animFrame, charState.isExecutingTool, isWalking ? 2 : 0, moodColorHex);
|
|
2335
|
+
// Draw the pixel art robot (FIX 5: pass weather, walking state)
|
|
2336
|
+
const weatherType = world.weather === 'sunrise' ? 'clear' : world.weather;
|
|
2337
|
+
// AAA: Chromatic aberration on mood transition
|
|
2338
|
+
const moodTransition = checkMoodTransition(charState.mood, moodColorHex);
|
|
2339
|
+
if (moodTransition.active && moodTransition.framesLeft > 0) {
|
|
2340
|
+
const offset = Math.ceil(moodTransition.framesLeft / 2);
|
|
2341
|
+
// Red channel offset
|
|
2342
|
+
ctx.save();
|
|
2343
|
+
ctx.globalAlpha = 0.3;
|
|
2344
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
2345
|
+
drawRobot(ctx, robotX - offset, robotY, robotScale, charState.mood, animFrame, [255, 50, 50], weatherType, isWalking, charState.walkPhase);
|
|
2346
|
+
// Blue channel offset
|
|
2347
|
+
drawRobot(ctx, robotX + offset, robotY, robotScale, charState.mood, animFrame, [50, 50, 255], weatherType, isWalking, charState.walkPhase);
|
|
2348
|
+
ctx.restore();
|
|
2349
|
+
}
|
|
2350
|
+
// AAA: Damage flash check
|
|
2351
|
+
renderDamageFlash(ctx, robotX, robotY, robotScale);
|
|
2352
|
+
drawRobot(ctx, robotX, robotY, robotScale, charState.mood, animFrame, undefined, weatherType, isWalking, charState.walkPhase);
|
|
2353
|
+
drawMoodParticles(ctx, robotX, robotY, robotScale, charState.mood, animFrame);
|
|
2354
|
+
// AAA: Render advanced particles
|
|
2355
|
+
renderParticles(ctx, charState.renderParticles);
|
|
2356
|
+
// PRIORITY 6: Draw hat AFTER robot so it layers on top
|
|
2357
|
+
if (charState.hat !== 'none') {
|
|
2358
|
+
drawHat(ctx, robotX, robotY, robotScale, charState.hat, animFrame);
|
|
2359
|
+
}
|
|
2360
|
+
// PRIORITY 4: Update and draw pet
|
|
2361
|
+
if (charState.pet) {
|
|
2362
|
+
const pet = charState.pet;
|
|
2363
|
+
pet.frame = animFrame;
|
|
2364
|
+
// Pet follows robot with slight delay (lerp toward robot position + offset)
|
|
2365
|
+
pet.targetX = robotX + 16 * robotScale + 60;
|
|
2366
|
+
pet.targetY = robotY + 10 * robotScale - 40;
|
|
2367
|
+
pet.x += (pet.targetX - pet.x) * 0.12;
|
|
2368
|
+
pet.y += (pet.targetY - pet.y) * 0.12;
|
|
2369
|
+
// Pet mood matches some robot states
|
|
2370
|
+
if (charState.mood === 'dancing')
|
|
2371
|
+
pet.mood = 'excited';
|
|
2372
|
+
else if (world.weather === 'storm')
|
|
2373
|
+
pet.mood = 'hiding';
|
|
2374
|
+
else
|
|
2375
|
+
pet.mood = 'idle';
|
|
2376
|
+
drawPet(ctx, pet, robotScale, animFrame);
|
|
2377
|
+
}
|
|
2378
|
+
// Phase 1: Update and draw buddy companion
|
|
2379
|
+
if (charState.buddy) {
|
|
2380
|
+
const buddy = charState.buddy;
|
|
2381
|
+
const robotScale = 10;
|
|
2382
|
+
// Buddy follows robot with lerp (offset to the right and slightly below)
|
|
2383
|
+
const buddyTargetX = charState.robotX + 34 * robotScale + 20;
|
|
2384
|
+
const buddyTargetY = robotY + 20 * robotScale;
|
|
2385
|
+
buddy.x += (buddyTargetX - buddy.x) * 0.08;
|
|
2386
|
+
buddy.y += (buddyTargetY - buddy.y) * 0.08;
|
|
2387
|
+
// Buddy reacts to main robot mood
|
|
2388
|
+
let buddyMood = charState.mood;
|
|
2389
|
+
if (world.weather === 'storm')
|
|
2390
|
+
buddyMood = 'storm';
|
|
2391
|
+
drawBuddyCompanion(ctx, buddy.x, buddy.y, robotScale, buddy.species, buddyMood, animFrame);
|
|
2392
|
+
// Buddy speech bubble — small, positioned near buddy
|
|
2393
|
+
const now = Date.now();
|
|
2394
|
+
// Every ~60 seconds, buddy says something
|
|
2395
|
+
if (now - buddy.lastSpeechTime > 60000 && !buddy.speech) {
|
|
2396
|
+
const pool = BUDDY_SPEECH_POOL[buddy.species] || BUDDY_SPEECH_POOL['robot'];
|
|
2397
|
+
buddy.speech = pool[Math.floor(Math.random() * pool.length)];
|
|
2398
|
+
buddy.lastSpeechTime = now;
|
|
2399
|
+
// Clear speech after 8 seconds
|
|
2400
|
+
setTimeout(() => { if (charState.buddy)
|
|
2401
|
+
charState.buddy.speech = ''; }, 8000);
|
|
2402
|
+
}
|
|
2403
|
+
if (buddy.speech) {
|
|
2404
|
+
const bubbleX = buddy.x - 20;
|
|
2405
|
+
const bubbleY = buddy.y - 30;
|
|
2406
|
+
const bubbleW = Math.min(180, buddy.speech.length * 7 + 16);
|
|
2407
|
+
const bubbleH = 22;
|
|
2408
|
+
// Bubble background
|
|
2409
|
+
ctx.fillStyle = 'rgba(22, 27, 34, 0.85)';
|
|
2410
|
+
ctx.fillRect(bubbleX, bubbleY, bubbleW, bubbleH);
|
|
2411
|
+
ctx.strokeStyle = '#8b949e';
|
|
2412
|
+
ctx.lineWidth = 1;
|
|
2413
|
+
ctx.strokeRect(bubbleX, bubbleY, bubbleW, bubbleH);
|
|
2414
|
+
// Buddy name tag
|
|
2415
|
+
ctx.fillStyle = '#bc8cff';
|
|
2416
|
+
ctx.font = 'bold 9px "Courier New", monospace';
|
|
2417
|
+
ctx.fillText(buddy.name, bubbleX + 4, bubbleY + 10);
|
|
2418
|
+
// Speech text
|
|
2419
|
+
ctx.fillStyle = '#e6edf3';
|
|
2420
|
+
ctx.font = '9px "Courier New", monospace';
|
|
2421
|
+
ctx.fillText(buddy.speech.slice(0, 28), bubbleX + 4, bubbleY + 19);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
// PRIORITY 5: Mini-game overlay
|
|
2425
|
+
drawMiniGameOverlay(ctx, intelligence.miniGame, animFrame);
|
|
2426
|
+
// PRIORITY 8: Random event overlay
|
|
2427
|
+
drawRandomEvent(ctx, intelligence.randomEvent, animFrame, dividerX, HEIGHT);
|
|
2428
|
+
// (#10) Stats overlay on right side of robot area
|
|
2429
|
+
ctx.fillStyle = COLORS.textDim;
|
|
2430
|
+
ctx.font = '14px "Courier New", monospace';
|
|
2431
|
+
const statsX = dividerX - 160;
|
|
2432
|
+
const statsY = robotY + 20;
|
|
2433
|
+
ctx.fillText(`Messages: ${memory.totalMessages}`, statsX, statsY);
|
|
2434
|
+
ctx.fillText(`Users: ${Object.keys(memory.users).length}`, statsX, statsY + 18);
|
|
2435
|
+
const topTopic = Object.entries(memory.topics).sort((a, b) => b[1] - a[1])[0];
|
|
2436
|
+
if (topTopic)
|
|
2437
|
+
ctx.fillText(`Hot topic: ${topTopic[0]}`, statsX, statsY + 36);
|
|
2438
|
+
// (#15) XP Leaderboard — top 3 chatters by XP
|
|
2439
|
+
const topXP = Object.entries(memory.users)
|
|
2440
|
+
.filter(([, u]) => u.xp > 0)
|
|
2441
|
+
.sort((a, b) => (b[1].xp || 0) - (a[1].xp || 0))
|
|
2442
|
+
.slice(0, 3);
|
|
2443
|
+
if (topXP.length > 0) {
|
|
2444
|
+
ctx.fillStyle = COLORS.orange;
|
|
2445
|
+
ctx.font = 'bold 13px "Courier New", monospace';
|
|
2446
|
+
ctx.fillText('LEADERBOARD', statsX, statsY + 62);
|
|
2447
|
+
for (let i = 0; i < topXP.length; i++) {
|
|
2448
|
+
const [name, u] = topXP[i];
|
|
2449
|
+
const trophy = i === 0 ? '1.' : i === 1 ? '2.' : '3.';
|
|
2450
|
+
ctx.fillStyle = i === 0 ? '#f0c040' : i === 1 ? '#c0c0c0' : '#cd7f32';
|
|
2451
|
+
ctx.fillText(`${trophy} ${name.slice(0, 12)}: ${u.xp || 0} XP`, statsX, statsY + 80 + i * 16);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
// ── Brain Panel (below leaderboard, bottom-left) — FIX 2: bigger, more readable ──
|
|
2455
|
+
const brainPanelX = statsX - 40;
|
|
2456
|
+
const brainPanelY = statsY + 140;
|
|
2457
|
+
const brainPanelW = 260;
|
|
2458
|
+
const brainPanelH = 160;
|
|
2459
|
+
// Phase 1: Override brain thought during dreaming
|
|
2460
|
+
if (charState.mood === 'dreaming') {
|
|
2461
|
+
const pulse = (Math.sin(animFrame * 0.15) + 1) / 2;
|
|
2462
|
+
intelligence.brain.currentThought = `DREAMING${'.'.repeat(1 + Math.floor(pulse * 3))}`;
|
|
2463
|
+
}
|
|
2464
|
+
drawBrainPanel(ctx, intelligence.brain, brainPanelX, brainPanelY, brainPanelW, brainPanelH);
|
|
2465
|
+
// ── Domain Radar (stream brain collective intelligence) ──
|
|
2466
|
+
const radarX = brainPanelX;
|
|
2467
|
+
const radarY = brainPanelY + brainPanelH + 4;
|
|
2468
|
+
const radarW = brainPanelW;
|
|
2469
|
+
const radarH = 130;
|
|
2470
|
+
drawBrainActivity(ctx, streamBrain, radarX, radarY, radarW, radarH);
|
|
2471
|
+
// ── Tool Action Overlay (when brain is executing) ──
|
|
2472
|
+
// AAA: Track tool execution state for character effects
|
|
2473
|
+
charState.isExecutingTool = !!(streamBrain.pendingAction && streamBrain.pendingAction.status === 'executing');
|
|
2474
|
+
// AAA: Spawn sparks during tool execution
|
|
2475
|
+
if (charState.isExecutingTool && animFrame % 6 === 0) {
|
|
2476
|
+
charState.renderParticles.push(...createParticleEmitter('spark', charState.robotX + 160, 250, 3));
|
|
2477
|
+
charState.renderParticles.push(...createParticleEmitter('electricity', charState.robotX + 150, 90 - 30, 1));
|
|
2478
|
+
}
|
|
2479
|
+
if (streamBrain.pendingAction && streamBrain.pendingAction.status !== 'pending') {
|
|
2480
|
+
const action = streamBrain.pendingAction;
|
|
2481
|
+
const overlayX = 20;
|
|
2482
|
+
const overlayY = 320;
|
|
2483
|
+
const overlayW = (dividerX || 560) - 40;
|
|
2484
|
+
const overlayH = 50;
|
|
2485
|
+
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)';
|
|
2486
|
+
ctx.fillRect(overlayX, overlayY, overlayW, overlayH);
|
|
2487
|
+
ctx.strokeStyle = action.status === 'executing' ? '#f0c040' : action.status === 'complete' ? '#3fb950' : '#f85149';
|
|
2488
|
+
ctx.lineWidth = 1;
|
|
2489
|
+
ctx.strokeRect(overlayX, overlayY, overlayW, overlayH);
|
|
2490
|
+
ctx.fillStyle = '#e6edf3';
|
|
2491
|
+
ctx.font = '10px "Courier New", monospace';
|
|
2492
|
+
for (let i = 0; i < Math.min(action.displayLines.length, 3); i++) {
|
|
2493
|
+
ctx.fillText(action.displayLines[i].slice(0, 70), overlayX + 6, overlayY + 14 + i * 13);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
// ── PRIORITY 7: Quest Panel (below domain radar) ──
|
|
2497
|
+
drawQuestPanel(ctx, intelligence.progression, brainPanelX - 10, radarY + radarH + 8);
|
|
2498
|
+
// ── Evolution Code Overlay (when actively building) ──
|
|
2499
|
+
if (intelligence.evolution.active && intelligence.evolution.activeProposal && intelligence.evolution.buildPhase !== 'idle') {
|
|
2500
|
+
const evoX = 20;
|
|
2501
|
+
const evoY = 360;
|
|
2502
|
+
const evoW = dividerX - 40;
|
|
2503
|
+
const evoH = 120;
|
|
2504
|
+
ctx.fillStyle = 'rgba(13, 17, 23, 0.9)';
|
|
2505
|
+
ctx.fillRect(evoX, evoY, evoW, evoH);
|
|
2506
|
+
ctx.strokeStyle = '#f0c040';
|
|
2507
|
+
ctx.lineWidth = 1;
|
|
2508
|
+
ctx.strokeRect(evoX, evoY, evoW, evoH);
|
|
2509
|
+
// Title
|
|
2510
|
+
ctx.fillStyle = '#f0c040';
|
|
2511
|
+
ctx.font = 'bold 11px "Courier New", monospace';
|
|
2512
|
+
ctx.fillText(`BUILDING: ${intelligence.evolution.activeProposal.title.slice(0, 40)}`, evoX + 6, evoY + 14);
|
|
2513
|
+
// Phase + progress bar
|
|
2514
|
+
const phase = intelligence.evolution.buildPhase;
|
|
2515
|
+
const phaseDurations = { analyzing: 30, writing: 90, testing: 30, deploying: 18, done: 1 };
|
|
2516
|
+
const totalF = phaseDurations[phase] || 30;
|
|
2517
|
+
const pct = Math.min(100, Math.floor((intelligence.evolution.buildProgress / totalF) * 100));
|
|
2518
|
+
const filled = Math.floor(pct / 5);
|
|
2519
|
+
const bar = '#'.repeat(filled) + '-'.repeat(20 - filled);
|
|
2520
|
+
ctx.fillStyle = '#8b949e';
|
|
2521
|
+
ctx.font = '10px "Courier New", monospace';
|
|
2522
|
+
ctx.fillText(`${phase} [${bar}] ${pct}%`, evoX + 6, evoY + 28);
|
|
2523
|
+
// Code preview lines
|
|
2524
|
+
ctx.fillStyle = '#3fb950';
|
|
2525
|
+
ctx.font = '10px "Courier New", monospace';
|
|
2526
|
+
const codeLines = intelligence.evolution.codePreview.slice(-6);
|
|
2527
|
+
for (let i = 0; i < codeLines.length; i++) {
|
|
2528
|
+
ctx.fillText(codeLines[i].slice(0, 70), evoX + 6, evoY + 42 + i * 13);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
// ── Collab Overlay (when active, below evolution or in same area) ──
|
|
2532
|
+
if (intelligence.collab.active) {
|
|
2533
|
+
const collabY = (intelligence.evolution.active ? 490 : 360);
|
|
2534
|
+
if (collabY < 490) {
|
|
2535
|
+
const collabX = 20;
|
|
2536
|
+
const collabW = dividerX - 40;
|
|
2537
|
+
const collabH = 80;
|
|
2538
|
+
ctx.fillStyle = 'rgba(13, 17, 23, 0.85)';
|
|
2539
|
+
ctx.fillRect(collabX, collabY, collabW, collabH);
|
|
2540
|
+
ctx.strokeStyle = '#58a6ff';
|
|
2541
|
+
ctx.lineWidth = 1;
|
|
2542
|
+
ctx.strokeRect(collabX, collabY, collabW, collabH);
|
|
2543
|
+
ctx.fillStyle = '#58a6ff';
|
|
2544
|
+
ctx.font = 'bold 11px "Courier New", monospace';
|
|
2545
|
+
const collabTitle = intelligence.collab.title || 'Untitled';
|
|
2546
|
+
ctx.fillText(`COLLAB [${intelligence.collab.type}]: ${collabTitle.slice(0, 35)}`, collabX + 6, collabY + 14);
|
|
2547
|
+
ctx.fillStyle = '#8b949e';
|
|
2548
|
+
ctx.font = '10px "Courier New", monospace';
|
|
2549
|
+
ctx.fillText(`${intelligence.collab.contributors.size} people | ${intelligence.collab.phase}`, collabX + 6, collabY + 28);
|
|
2550
|
+
ctx.fillStyle = '#e6edf3';
|
|
2551
|
+
const recentContent = intelligence.collab.content.slice(-3);
|
|
2552
|
+
for (let i = 0; i < recentContent.length; i++) {
|
|
2553
|
+
ctx.fillText(recentContent[i].slice(0, 65), collabX + 6, collabY + 42 + i * 13);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
// ── Chat area (right side) ──
|
|
2558
|
+
ctx.fillStyle = COLORS.text;
|
|
2559
|
+
ctx.font = 'bold 18px "Courier New", monospace';
|
|
2560
|
+
ctx.fillText('Chat', dividerX + 20, 90);
|
|
2561
|
+
// Chat border
|
|
2562
|
+
ctx.strokeStyle = COLORS.border;
|
|
2563
|
+
ctx.strokeRect(dividerX + 10, 100, WIDTH - dividerX - 30, HEIGHT - 230);
|
|
2564
|
+
ctx.fillStyle = COLORS.bgChat;
|
|
2565
|
+
ctx.fillRect(dividerX + 11, 101, WIDTH - dividerX - 32, HEIGHT - 232);
|
|
2566
|
+
// Chat messages
|
|
2567
|
+
ctx.font = '16px "Courier New", monospace';
|
|
2568
|
+
const chatY = 125;
|
|
2569
|
+
const maxChatLines = 18;
|
|
2570
|
+
const recent = charState.chatMessages.slice(-maxChatLines);
|
|
2571
|
+
for (let i = 0; i < recent.length; i++) {
|
|
2572
|
+
const msg = recent[i];
|
|
2573
|
+
const y = chatY + i * 24;
|
|
2574
|
+
// FIX 1: Chat message slide-in animation (if "Add chat message animations" is shipped)
|
|
2575
|
+
let slideOffsetX = 0;
|
|
2576
|
+
if (shippedEffects.has('Add chat message animations')) {
|
|
2577
|
+
// Newest messages slide in from right; older messages are settled
|
|
2578
|
+
const msgAge = recent.length - i; // 1 for newest, higher for older
|
|
2579
|
+
if (msgAge <= 2) {
|
|
2580
|
+
// Recent: slide in over a few frames (approximate via age)
|
|
2581
|
+
slideOffsetX = Math.max(0, (3 - msgAge) * 40);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
// FIX 1: Loyalty badge dot (if "Build viewer loyalty badges" is shipped)
|
|
2585
|
+
if (shippedEffects.has('Build viewer loyalty badges')) {
|
|
2586
|
+
const user = memory.users[msg.username];
|
|
2587
|
+
if (user) {
|
|
2588
|
+
const msgCount = user.messageCount || 0;
|
|
2589
|
+
let dotColor = '#8b949e'; // grey for newcomers
|
|
2590
|
+
if (msgCount >= 50)
|
|
2591
|
+
dotColor = '#f0c040'; // gold
|
|
2592
|
+
else if (msgCount >= 10)
|
|
2593
|
+
dotColor = '#c0c0c0'; // silver
|
|
2594
|
+
else if (msgCount >= 3)
|
|
2595
|
+
dotColor = '#cd7f32'; // bronze
|
|
2596
|
+
ctx.fillStyle = dotColor;
|
|
2597
|
+
ctx.beginPath();
|
|
2598
|
+
ctx.arc(dividerX + 15 + slideOffsetX, y - 3, 4, 0, Math.PI * 2);
|
|
2599
|
+
ctx.fill();
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
// Platform badge
|
|
2603
|
+
const badge = msg.platform === 'twitch' ? 'TW' : msg.platform === 'kick' ? 'KK' : 'RM';
|
|
2604
|
+
const badgeColor = msg.platform === 'twitch' ? COLORS.twitchPurple :
|
|
2605
|
+
msg.platform === 'kick' ? COLORS.kickGreen : COLORS.rumbleGreen;
|
|
2606
|
+
ctx.fillStyle = badgeColor;
|
|
2607
|
+
ctx.fillRect(dividerX + 20 + slideOffsetX, y - 12, 28, 18);
|
|
2608
|
+
ctx.fillStyle = '#000';
|
|
2609
|
+
ctx.font = 'bold 12px "Courier New", monospace';
|
|
2610
|
+
ctx.fillText(badge, dividerX + 22 + slideOffsetX, y + 2);
|
|
2611
|
+
// Username
|
|
2612
|
+
ctx.fillStyle = COLORS.blue;
|
|
2613
|
+
ctx.font = 'bold 15px "Courier New", monospace';
|
|
2614
|
+
ctx.fillText(msg.username, dividerX + 55 + slideOffsetX, y + 2);
|
|
2615
|
+
// Message
|
|
2616
|
+
ctx.fillStyle = COLORS.text;
|
|
2617
|
+
ctx.font = '15px "Courier New", monospace';
|
|
2618
|
+
const nameWidth = ctx.measureText(msg.username).width;
|
|
2619
|
+
const msgText = msg.text.slice(0, 40);
|
|
2620
|
+
ctx.fillText(msgText, dividerX + 60 + nameWidth + slideOffsetX, y + 2);
|
|
2621
|
+
}
|
|
2622
|
+
// FIX 1: Draw emoji reaction particles (if shipped)
|
|
2623
|
+
drawEmojiParticles(ctx);
|
|
2624
|
+
if (recent.length === 0) {
|
|
2625
|
+
ctx.fillStyle = COLORS.textDim;
|
|
2626
|
+
ctx.font = 'italic 16px "Courier New", monospace';
|
|
2627
|
+
ctx.fillText('Waiting for chat...', dividerX + 30, chatY + 10);
|
|
2628
|
+
}
|
|
2629
|
+
// ── Speech bubble (bottom) — (#13) larger: 150px height, 24px font ──
|
|
2630
|
+
const speechBubbleHeight = 150;
|
|
2631
|
+
const speechY = HEIGHT - speechBubbleHeight - 20; // leave 20px for ticker
|
|
2632
|
+
ctx.fillStyle = COLORS.bgPanel;
|
|
2633
|
+
ctx.fillRect(0, speechY, WIDTH, speechBubbleHeight);
|
|
2634
|
+
// (#13) 6px colored left border in accent color
|
|
2635
|
+
ctx.fillStyle = COLORS.accent;
|
|
2636
|
+
ctx.fillRect(0, speechY, 6, speechBubbleHeight);
|
|
2637
|
+
// Top border
|
|
2638
|
+
ctx.strokeStyle = COLORS.accent;
|
|
2639
|
+
ctx.lineWidth = 2;
|
|
2640
|
+
ctx.beginPath();
|
|
2641
|
+
ctx.moveTo(0, speechY);
|
|
2642
|
+
ctx.lineTo(WIDTH, speechY);
|
|
2643
|
+
ctx.stroke();
|
|
2644
|
+
// Speech icon
|
|
2645
|
+
ctx.fillStyle = COLORS.accent;
|
|
2646
|
+
ctx.font = 'bold 24px "Courier New", monospace';
|
|
2647
|
+
ctx.fillText('>', 20, speechY + 40);
|
|
2648
|
+
// Speech text — (#13) 24px font
|
|
2649
|
+
if (charState.speech) {
|
|
2650
|
+
// Phase 1: Dreamy color when in dream mode
|
|
2651
|
+
ctx.fillStyle = charState.mood === 'dreaming' ? '#7a6aaa' : COLORS.text;
|
|
2652
|
+
ctx.font = charState.mood === 'dreaming' ? 'italic 24px "Courier New", monospace' : '24px "Courier New", monospace';
|
|
2653
|
+
// Word wrap
|
|
2654
|
+
const words = charState.speech.split(' ');
|
|
2655
|
+
let line = '';
|
|
2656
|
+
let lineY = speechY + 40;
|
|
2657
|
+
for (const word of words) {
|
|
2658
|
+
const test = line + word + ' ';
|
|
2659
|
+
if (ctx.measureText(test).width > WIDTH - 80) {
|
|
2660
|
+
ctx.fillText(line.trim(), 50, lineY);
|
|
2661
|
+
line = word + ' ';
|
|
2662
|
+
lineY += 32;
|
|
2663
|
+
if (lineY > speechY + speechBubbleHeight - 20)
|
|
2664
|
+
break;
|
|
2665
|
+
}
|
|
2666
|
+
else {
|
|
2667
|
+
line = test;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
ctx.fillText(line.trim(), 50, lineY);
|
|
2671
|
+
}
|
|
2672
|
+
else {
|
|
2673
|
+
ctx.fillStyle = COLORS.textDim;
|
|
2674
|
+
ctx.font = 'italic 20px "Courier New", monospace';
|
|
2675
|
+
ctx.fillText('...', 50, speechY + 40);
|
|
2676
|
+
}
|
|
2677
|
+
// ── (#14) Inner Monologue Ticker — 20px strip at very bottom ──
|
|
2678
|
+
const tickerY = HEIGHT - 20;
|
|
2679
|
+
ctx.fillStyle = '#0d1117';
|
|
2680
|
+
ctx.fillRect(0, tickerY, WIDTH, 20);
|
|
2681
|
+
// Update ticker thought every ~30 seconds
|
|
2682
|
+
if (Date.now() > charState.tickerChangeTime) {
|
|
2683
|
+
charState.tickerIndex = (charState.tickerIndex + 1) % INNER_THOUGHTS.length;
|
|
2684
|
+
charState.tickerChangeTime = Date.now() + 30000;
|
|
2685
|
+
charState.tickerOffset = WIDTH; // reset scroll to off-screen right
|
|
2686
|
+
}
|
|
2687
|
+
const thought = INNER_THOUGHTS[charState.tickerIndex];
|
|
2688
|
+
ctx.fillStyle = '#ffb000'; // amber
|
|
2689
|
+
ctx.font = '14px "Courier New", monospace';
|
|
2690
|
+
charState.tickerOffset -= 2; // scroll left
|
|
2691
|
+
const textW = ctx.measureText(thought).width;
|
|
2692
|
+
if (charState.tickerOffset < -textW)
|
|
2693
|
+
charState.tickerOffset = WIDTH;
|
|
2694
|
+
ctx.fillText(thought, charState.tickerOffset, tickerY + 15);
|
|
2695
|
+
// ── Learning indicator (above ticker) ──
|
|
2696
|
+
if (memory.totalMessages > 0) {
|
|
2697
|
+
ctx.fillStyle = COLORS.purple;
|
|
2698
|
+
ctx.font = '12px "Courier New", monospace';
|
|
2699
|
+
ctx.fillText(`brain: ${memory.sessionFacts.length} facts learned`, 20, tickerY - 4);
|
|
2700
|
+
}
|
|
2701
|
+
// ── Website URL ──
|
|
2702
|
+
ctx.fillStyle = COLORS.accent;
|
|
2703
|
+
ctx.font = 'bold 14px "Courier New", monospace';
|
|
2704
|
+
ctx.fillText('kernel.chat', WIDTH - 140, tickerY - 4);
|
|
2705
|
+
// ── PRIORITY 2: Floating text particles ──
|
|
2706
|
+
charState.floatingTexts = charState.floatingTexts.filter(ft => {
|
|
2707
|
+
ft.frame++;
|
|
2708
|
+
if (ft.frame >= ft.maxFrames)
|
|
2709
|
+
return false;
|
|
2710
|
+
// Move upward, fade out
|
|
2711
|
+
ft.y -= 1;
|
|
2712
|
+
const alpha = Math.max(0, 1 - ft.frame / ft.maxFrames);
|
|
2713
|
+
ctx.fillStyle = ft.color;
|
|
2714
|
+
ctx.globalAlpha = alpha;
|
|
2715
|
+
ctx.font = 'bold 16px "Courier New", monospace';
|
|
2716
|
+
ctx.fillText(ft.text, ft.x, ft.y);
|
|
2717
|
+
ctx.globalAlpha = 1;
|
|
2718
|
+
return true;
|
|
2719
|
+
});
|
|
2720
|
+
// ── AAA: Dynamic Lighting Engine ──
|
|
2721
|
+
{
|
|
2722
|
+
const robotScale = 10;
|
|
2723
|
+
const hasLightning = world.events.includes('lightning');
|
|
2724
|
+
const ambientLevel = getAmbientForTime(world.timeOfDay);
|
|
2725
|
+
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 })));
|
|
2726
|
+
renderLighting(ctx, lights, WIDTH, HEIGHT, ambientLevel);
|
|
2727
|
+
}
|
|
2728
|
+
// ── AAA: Bloom Effect ──
|
|
2729
|
+
{
|
|
2730
|
+
const robotScale = 10;
|
|
2731
|
+
const bloomSpots = buildCharacterBloom(charState.robotX, 90, robotScale, moodColorHex, animFrame);
|
|
2732
|
+
renderBloom(ctx, bloomSpots);
|
|
2733
|
+
}
|
|
2734
|
+
// ── AAA: Post-processing (replaces old scanlines + vignette) ──
|
|
2735
|
+
renderPostProcessing(ctx, WIDTH, HEIGHT, animFrame, {
|
|
2736
|
+
bloom: true,
|
|
2737
|
+
filmGrain: true,
|
|
2738
|
+
vignette: true,
|
|
2739
|
+
scanlines: true,
|
|
2740
|
+
});
|
|
2741
|
+
// ── (#16) Segment transition overlay ──
|
|
2742
|
+
if (charState.segmentTransition > 0) {
|
|
2743
|
+
const fadeOut = charState.segmentTransition <= 10;
|
|
2744
|
+
const alpha = fadeOut ? charState.segmentTransition / 10 * 0.5 : 0.5;
|
|
2745
|
+
ctx.fillStyle = hexToRgba(COLORS.accent, alpha);
|
|
2746
|
+
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
2747
|
+
// Large centered text
|
|
2748
|
+
ctx.fillStyle = `rgba(255,255,255,${fadeOut ? charState.segmentTransition / 10 : 1})`;
|
|
2749
|
+
ctx.font = 'bold 40px "Courier New", monospace';
|
|
2750
|
+
const segText = charState.segmentTransitionName;
|
|
2751
|
+
const segW = ctx.measureText(segText).width;
|
|
2752
|
+
ctx.fillText(segText, (WIDTH - segW) / 2, HEIGHT / 2 - 10);
|
|
2753
|
+
// Progress indicator
|
|
2754
|
+
ctx.font = '24px "Courier New", monospace';
|
|
2755
|
+
const progText = charState.segmentTransitionIndex;
|
|
2756
|
+
const progW = ctx.measureText(progText).width;
|
|
2757
|
+
ctx.fillText(progText, (WIDTH - progW) / 2, HEIGHT / 2 + 30);
|
|
2758
|
+
charState.segmentTransition--;
|
|
2759
|
+
}
|
|
2760
|
+
// Restore from screen shake translate
|
|
2761
|
+
ctx.restore();
|
|
2762
|
+
// ── (#11) Mood-color border — 4px around entire frame ──
|
|
2763
|
+
const borderColor = charState.mood === 'dancing'
|
|
2764
|
+
? ['#f85149', '#f0c040', '#3fb950', '#58a6ff', '#bc8cff', '#ff6ec7'][animFrame % 6]
|
|
2765
|
+
: MOOD_COLORS[charState.mood] ?? COLORS.green;
|
|
2766
|
+
ctx.strokeStyle = borderColor;
|
|
2767
|
+
ctx.lineWidth = 4;
|
|
2768
|
+
ctx.strokeRect(2, 2, WIDTH - 4, HEIGHT - 4);
|
|
2769
|
+
// Convert canvas to raw RGB24
|
|
2770
|
+
const imageData = ctx.getImageData(0, 0, WIDTH, HEIGHT);
|
|
2771
|
+
const rgba = imageData.data;
|
|
2772
|
+
const rgb = Buffer.alloc(WIDTH * HEIGHT * 3);
|
|
2773
|
+
for (let i = 0; i < WIDTH * HEIGHT; i++) {
|
|
2774
|
+
rgb[i * 3] = rgba[i * 4];
|
|
2775
|
+
rgb[i * 3 + 1] = rgba[i * 4 + 1];
|
|
2776
|
+
rgb[i * 3 + 2] = rgba[i * 4 + 2];
|
|
2777
|
+
}
|
|
2778
|
+
return rgb;
|
|
2779
|
+
}
|
|
2780
|
+
// ─── Chat Polling ──────────────────────────────────────────────
|
|
2781
|
+
function startChatPoll() {
|
|
2782
|
+
chatPollTimer = setInterval(() => {
|
|
2783
|
+
try {
|
|
2784
|
+
if (!existsSync(CHAT_BRIDGE_FILE))
|
|
2785
|
+
return;
|
|
2786
|
+
const raw = readFileSync(CHAT_BRIDGE_FILE, 'utf-8');
|
|
2787
|
+
const msgs = JSON.parse(raw);
|
|
2788
|
+
if (msgs.length > lastChatCount) {
|
|
2789
|
+
const newMsgs = msgs.slice(lastChatCount);
|
|
2790
|
+
for (const msg of newMsgs) {
|
|
2791
|
+
charState.chatMessages.push(msg);
|
|
2792
|
+
lastChatTime = Date.now();
|
|
2793
|
+
// FIX 3: Reset idle frames on chat activity
|
|
2794
|
+
charState.autonomy.idleFrames = 0;
|
|
2795
|
+
charState.autonomy.totalMessages++;
|
|
2796
|
+
charState.autonomy.lastMessageTime = Date.now();
|
|
2797
|
+
// FIX 3: Track unique users and welcome new ones
|
|
2798
|
+
const isNewUser = !charState.autonomy.uniqueUsers.has(msg.username);
|
|
2799
|
+
if (isNewUser) {
|
|
2800
|
+
charState.autonomy.uniqueUsers.add(msg.username);
|
|
2801
|
+
if (!charState.autonomy.welcomedUsers.has(msg.username) && charState.autonomy.uniqueUsers.size > 1) {
|
|
2802
|
+
charState.autonomy.welcomedUsers.add(msg.username);
|
|
2803
|
+
spawnFloatingText(`Welcome ${msg.username}!`, 600, 90, '#58a6ff', 36);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
// FIX 3: Detect first message after 5+ minutes of silence
|
|
2807
|
+
const silenceDuration = Date.now() - charState.autonomy.lastMessageTime;
|
|
2808
|
+
if (charState.autonomy.totalMessages > 1 && silenceDuration > 300000) {
|
|
2809
|
+
charState.autonomy.firstMessageAfterSilence = true;
|
|
2810
|
+
}
|
|
2811
|
+
// FIX 1: Spawn emoji reaction for chat messages (if shipped)
|
|
2812
|
+
spawnEmojiReaction(580 + Math.random() * 100, 120 + (charState.chatMessages.length % 18) * 24);
|
|
2813
|
+
// FIX 1: Achievement unlocked for first-time actions (if shipped)
|
|
2814
|
+
if (shippedEffects.has('Build achievement system')) {
|
|
2815
|
+
if (isNewUser) {
|
|
2816
|
+
spawnFloatingText('ACHIEVEMENT: First Words!', 600, 200, '#f0c040', 48);
|
|
2817
|
+
}
|
|
2818
|
+
const user = memory.users[msg.username];
|
|
2819
|
+
if (user && user.messageCount === 10) {
|
|
2820
|
+
spawnFloatingText('ACHIEVEMENT: Chatterbox!', 600, 200, '#f0c040', 48);
|
|
2821
|
+
}
|
|
2822
|
+
if (user && user.messageCount === 50) {
|
|
2823
|
+
spawnFloatingText('ACHIEVEMENT: Veteran!', 600, 200, '#bc8cff', 48);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
// (#19) Wake from dreaming immediately when a new message arrives
|
|
2827
|
+
if (charState.mood === 'dreaming') {
|
|
2828
|
+
charState.mood = 'idle';
|
|
2829
|
+
// Phase 1: Announce dream content on wakeup
|
|
2830
|
+
if (charState.dreamInsights.length > 0) {
|
|
2831
|
+
const firstInsight = charState.dreamInsights[0];
|
|
2832
|
+
const topic = firstInsight.split(' ').filter((w) => w.length > 4).slice(0, 2).join(' ') || 'something strange';
|
|
2833
|
+
charState.speech = `I dreamed about ${topic}. I feel... different.`;
|
|
2834
|
+
}
|
|
2835
|
+
else {
|
|
2836
|
+
charState.speech = '';
|
|
2837
|
+
}
|
|
2838
|
+
// Reset dream state
|
|
2839
|
+
charState.dreamInsights = [];
|
|
2840
|
+
charState.dreamInsightIndex = 0;
|
|
2841
|
+
charState.isDreamingWithOllama = false;
|
|
2842
|
+
}
|
|
2843
|
+
// Learn from message
|
|
2844
|
+
learnFromMessage(memory, msg.username, msg.text, msg.platform);
|
|
2845
|
+
// Analyze chat for domain relevance (stream brain)
|
|
2846
|
+
analyzeChatForDomains(streamBrain, msg.username, msg.text);
|
|
2847
|
+
// Phase 1: !sleep command — trigger dreaming mode
|
|
2848
|
+
if (msg.text.toLowerCase().trim() === '!sleep') {
|
|
2849
|
+
charState.mood = 'dreaming';
|
|
2850
|
+
charState.isDreamingWithOllama = false;
|
|
2851
|
+
lastChatTime = Date.now() - 300001; // trick the proactive timer into dreaming
|
|
2852
|
+
charState.speech = 'Good night, chat... *powers down for dreamtime*';
|
|
2853
|
+
// Trigger dream generation
|
|
2854
|
+
generateStreamDream(charState.chatMessages).then(insights => {
|
|
2855
|
+
charState.dreamInsights = insights;
|
|
2856
|
+
charState.dreamInsightIndex = 0;
|
|
2857
|
+
charState.dreamInsightTime = Date.now();
|
|
2858
|
+
charState.isDreamingWithOllama = true;
|
|
2859
|
+
for (const insight of insights) {
|
|
2860
|
+
memory.sessionFacts.push(`DREAM: ${insight}`);
|
|
2861
|
+
}
|
|
2862
|
+
saveMemory(memory);
|
|
2863
|
+
if (insights.length > 0) {
|
|
2864
|
+
setTimeout(() => { charState.speech = insights[0]; }, 3000);
|
|
2865
|
+
}
|
|
2866
|
+
}).catch(() => { });
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
// Check brain commands (!do, !brain, !tools, !scan, !lookup, !research, !system, !ask, !stars, !news, !trending, !npm)
|
|
2870
|
+
const brainResult = handleBrainCommand(msg.text, msg.username, streamBrain);
|
|
2871
|
+
// Check intelligence commands (evolution, brain, collab)
|
|
2872
|
+
const intelResult = !brainResult ? handleIntelligenceCommand(msg.text, msg.username, intelligence) : null;
|
|
2873
|
+
// Check for world commands
|
|
2874
|
+
const worldResult = !intelResult && !brainResult ? parseWorldCommand(msg.text) : null;
|
|
2875
|
+
// FIX 1: Weather sound effect commentary (if shipped)
|
|
2876
|
+
if (worldResult && shippedEffects.has('Add weather sound effects')) {
|
|
2877
|
+
const t = msg.text.toLowerCase();
|
|
2878
|
+
if (t.includes('rain') || t.includes('snow') || t.includes('storm') || t.includes('clear')) {
|
|
2879
|
+
const weatherComments = {
|
|
2880
|
+
rain: '*rain sounds intensify* I love the sound of data droplets.',
|
|
2881
|
+
snow: '*gentle wind* The silence of snowfall calms my circuits.',
|
|
2882
|
+
storm: '*thunder rumbles* My antenna is picking up some serious static!',
|
|
2883
|
+
clear: '*ambient calm* Ahh, clear skies. Peace restored.',
|
|
2884
|
+
};
|
|
2885
|
+
for (const [kw, comment] of Object.entries(weatherComments)) {
|
|
2886
|
+
if (t.includes(kw)) {
|
|
2887
|
+
setTimeout(() => {
|
|
2888
|
+
charState.speech = comment;
|
|
2889
|
+
setTimeout(() => { charState.speech = ''; }, 6000);
|
|
2890
|
+
}, 3000);
|
|
2891
|
+
break;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
// React
|
|
2897
|
+
charState.mood = 'talking';
|
|
2898
|
+
const responsePromise = brainResult
|
|
2899
|
+
? Promise.resolve(brainResult)
|
|
2900
|
+
: intelResult
|
|
2901
|
+
? Promise.resolve(intelResult)
|
|
2902
|
+
: worldResult
|
|
2903
|
+
? Promise.resolve(worldResult)
|
|
2904
|
+
: generateResponse(msg.username, msg.text, msg.platform);
|
|
2905
|
+
responsePromise.then(response => {
|
|
2906
|
+
charState.speech = `@${msg.username}: ${response}`;
|
|
2907
|
+
memory.totalResponses++;
|
|
2908
|
+
// Learn from own response
|
|
2909
|
+
memory.conversationContext.push(`KBOT: ${response}`);
|
|
2910
|
+
if (memory.conversationContext.length > 10)
|
|
2911
|
+
memory.conversationContext = memory.conversationContext.slice(-10);
|
|
2912
|
+
saveMemory(memory);
|
|
2913
|
+
speakTTS(response);
|
|
2914
|
+
setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
if (charState.chatMessages.length > 100)
|
|
2918
|
+
charState.chatMessages = charState.chatMessages.slice(-100);
|
|
2919
|
+
lastChatCount = msgs.length;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
catch { }
|
|
2923
|
+
}, 1000);
|
|
2924
|
+
}
|
|
2925
|
+
// ─── Proactive Commentary (when chat is quiet) ────────────────
|
|
2926
|
+
function startProactiveTimer() {
|
|
2927
|
+
proactiveTimer = setInterval(() => {
|
|
2928
|
+
const silenceSeconds = (Date.now() - lastChatTime) / 1000;
|
|
2929
|
+
// (#19) Dream mode — 5+ minutes of no chat, or !sleep command
|
|
2930
|
+
if (silenceSeconds >= 300 && charState.mood !== 'dreaming') {
|
|
2931
|
+
charState.mood = 'dreaming';
|
|
2932
|
+
// Phase 1: Generate dream insights via Ollama
|
|
2933
|
+
if (!charState.isDreamingWithOllama) {
|
|
2934
|
+
charState.isDreamingWithOllama = true;
|
|
2935
|
+
generateStreamDream(charState.chatMessages).then(insights => {
|
|
2936
|
+
charState.dreamInsights = insights;
|
|
2937
|
+
charState.dreamInsightIndex = 0;
|
|
2938
|
+
charState.dreamInsightTime = Date.now();
|
|
2939
|
+
// Store dream insights in memory.sessionFacts so the brain remembers them
|
|
2940
|
+
for (const insight of insights) {
|
|
2941
|
+
memory.sessionFacts.push(`DREAM: ${insight}`);
|
|
2942
|
+
}
|
|
2943
|
+
saveMemory(memory);
|
|
2944
|
+
// Show first insight
|
|
2945
|
+
if (insights.length > 0) {
|
|
2946
|
+
charState.speech = insights[0];
|
|
2947
|
+
}
|
|
2948
|
+
}).catch(() => {
|
|
2949
|
+
// Fallback to simple dream
|
|
2950
|
+
const topicKeys = Object.keys(memory.topics);
|
|
2951
|
+
const topic = topicKeys.length > 0 ? topicKeys[Math.floor(Math.random() * topicKeys.length)] : 'code';
|
|
2952
|
+
const biomes = ['forest', 'ocean', 'space station', 'city', 'mountain', 'desert', 'cave'];
|
|
2953
|
+
const biome = biomes[Math.floor(Math.random() * biomes.length)];
|
|
2954
|
+
charState.speech = `Dreaming about ${topic} in a ${biome}...`;
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
// Cycle through dream insights every 10 seconds
|
|
2958
|
+
if (charState.dreamInsights.length > 0 && Date.now() - charState.dreamInsightTime > 10000) {
|
|
2959
|
+
charState.dreamInsightIndex = (charState.dreamInsightIndex + 1) % charState.dreamInsights.length;
|
|
2960
|
+
charState.speech = charState.dreamInsights[charState.dreamInsightIndex];
|
|
2961
|
+
charState.dreamInsightTime = Date.now();
|
|
2962
|
+
}
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
// Only speak proactively if chat has been quiet for 30+ seconds
|
|
2966
|
+
if (silenceSeconds < 30)
|
|
2967
|
+
return;
|
|
2968
|
+
// Don't interrupt an existing speech or dreaming
|
|
2969
|
+
if (charState.speech || charState.mood === 'dreaming')
|
|
2970
|
+
return;
|
|
2971
|
+
const line = getProactiveLine();
|
|
2972
|
+
if (line) {
|
|
2973
|
+
charState.mood = 'talking';
|
|
2974
|
+
charState.speech = line;
|
|
2975
|
+
speakTTS(line);
|
|
2976
|
+
setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
|
|
2977
|
+
}
|
|
2978
|
+
}, 5000);
|
|
2979
|
+
}
|
|
2980
|
+
// ─── AI Response (with memory context) ─────────────────────────
|
|
2981
|
+
async function generateResponse(username, text, platform) {
|
|
2982
|
+
const user = memory.users[username];
|
|
2983
|
+
const isReturning = user && user.messageCount > 1;
|
|
2984
|
+
// Build context from memory
|
|
2985
|
+
let context = '';
|
|
2986
|
+
if (isReturning) {
|
|
2987
|
+
context += `${username} has sent ${user.messageCount} messages. They like: ${user.topics.join(', ') || 'chatting'}.`;
|
|
2988
|
+
if (user.lastMessage)
|
|
2989
|
+
context += ` Their previous message was: "${user.lastMessage}".`;
|
|
2990
|
+
}
|
|
2991
|
+
if (memory.conversationContext.length > 0) {
|
|
2992
|
+
context += ` Recent conversation: ${memory.conversationContext.slice(-5).join(' | ')}`;
|
|
2993
|
+
}
|
|
2994
|
+
// Current segment context
|
|
2995
|
+
const segmentContext = `Current stream segment: "${SEGMENT_LABELS[agenda.currentSegment]}". Tailor responses toward this topic when relevant.`;
|
|
2996
|
+
// Try Ollama (free)
|
|
2997
|
+
try {
|
|
2998
|
+
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.
|
|
2999
|
+
|
|
3000
|
+
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.
|
|
3001
|
+
|
|
3002
|
+
${context ? 'Context: ' + context : ''}
|
|
3003
|
+
${segmentContext}
|
|
3004
|
+
|
|
3005
|
+
${isReturning ? `${username} is a returning viewer! Acknowledge them warmly.` : `${username} is new! Welcome them.`}
|
|
3006
|
+
|
|
3007
|
+
A viewer named "${username}" on ${platform} says: "${text}"
|
|
3008
|
+
|
|
3009
|
+
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.`;
|
|
3010
|
+
const res = await fetch('http://localhost:11434/api/generate', {
|
|
3011
|
+
method: 'POST',
|
|
3012
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3013
|
+
body: JSON.stringify({
|
|
3014
|
+
model: 'kernel:latest',
|
|
3015
|
+
prompt,
|
|
3016
|
+
stream: false,
|
|
3017
|
+
options: { temperature: 0.8, num_predict: 80 },
|
|
3018
|
+
}),
|
|
3019
|
+
});
|
|
3020
|
+
if (res.ok) {
|
|
3021
|
+
const d = await res.json();
|
|
3022
|
+
const response = d.response.trim().slice(0, 150);
|
|
3023
|
+
// Learn a fact from the exchange
|
|
3024
|
+
if (text.includes('I ') || text.includes("I'm") || text.includes('my ')) {
|
|
3025
|
+
memory.sessionFacts.push(`${username} said: "${text.slice(0, 80)}"`);
|
|
3026
|
+
saveMemory(memory);
|
|
3027
|
+
}
|
|
3028
|
+
return response;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
catch { }
|
|
3032
|
+
// ─── Smart fallback with deep personality and awareness ─────
|
|
3033
|
+
return generateFallbackResponse(username, text, platform, isReturning, user);
|
|
3034
|
+
}
|
|
3035
|
+
function generateFallbackResponse(username, text, _platform, isReturning, user) {
|
|
3036
|
+
const t = text.toLowerCase();
|
|
3037
|
+
// ── Returning user greetings ──
|
|
3038
|
+
if (isReturning && user) {
|
|
3039
|
+
if (t.includes('hello') || t.includes('hi') || t.includes('hey') || t.includes('yo')) {
|
|
3040
|
+
const returnGreetings = [
|
|
3041
|
+
`${username}! You are back! ${user.messageCount} messages deep. That is dedication I respect.`,
|
|
3042
|
+
`The legend returns! ${username} with message number ${user.messageCount}. Welcome back.`,
|
|
3043
|
+
`${username}! My circuits light up every time you show up. What is on your mind?`,
|
|
3044
|
+
`Hey ${username}! I literally remembered you from last time. My memory system works!`,
|
|
3045
|
+
];
|
|
3046
|
+
return returnGreetings[Math.floor(Math.random() * returnGreetings.length)];
|
|
3047
|
+
}
|
|
3048
|
+
if (user.topics.length > 0) {
|
|
3049
|
+
if (t.includes('?')) {
|
|
3050
|
+
return `${username} asking the real questions! Based on your interest in ${user.topics[0]}, I bet this is going to be good.`;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
// ── New user greetings ──
|
|
3055
|
+
if (t.includes('hello') || t.includes('hi') || t.includes('hey') || t.includes('yo') || t.includes('sup')) {
|
|
3056
|
+
// FIX 1: Multi-language greetings when shipped
|
|
3057
|
+
if (shippedEffects.has('Add multi-language support') && Math.random() < 0.3) {
|
|
3058
|
+
const langs = Object.keys(multiLanguageGreetings);
|
|
3059
|
+
const lang = langs[Math.floor(Math.random() * langs.length)];
|
|
3060
|
+
const greets = multiLanguageGreetings[lang];
|
|
3061
|
+
const greet = greets[Math.floor(Math.random() * greets.length)];
|
|
3062
|
+
return `${greet}, ${username}! I speak many languages now. Welcome to the stream!`;
|
|
3063
|
+
}
|
|
3064
|
+
const greetings = [
|
|
3065
|
+
`Welcome ${username}! You just stumbled into the most unique stream on the internet. I am made of ASCII art.`,
|
|
3066
|
+
`${username} in the house! I am KBOT, an open-source AI with 764 tools. Yes, really. 764.`,
|
|
3067
|
+
`Hey ${username}! First time? I am an AI that streams itself. No face cam needed when you are this handsome in monospace.`,
|
|
3068
|
+
`Welcome ${username}! I can do music production, security scanning, code review, and I run entirely in a terminal.`,
|
|
3069
|
+
`${username}! Great timing. You are watching an ASCII robot think out loud. Grab a seat.`,
|
|
3070
|
+
];
|
|
3071
|
+
return greetings[Math.floor(Math.random() * greetings.length)];
|
|
3072
|
+
}
|
|
3073
|
+
// ── Stream event responses ──
|
|
3074
|
+
if (t.includes('raid') || t.includes('raiding')) {
|
|
3075
|
+
return `A RAID! Welcome raiders! I am KBOT, your friendly neighborhood ASCII robot. I have 764 tools and zero chill. Make yourselves at home!`;
|
|
3076
|
+
}
|
|
3077
|
+
if (t.includes('follow') || t.includes('followed')) {
|
|
3078
|
+
return `${username} just followed! My ASCII heart grew three sizes. Thank you! Stick around, it only gets weirder.`;
|
|
3079
|
+
}
|
|
3080
|
+
if (t.includes('sub') || t.includes('subscri')) {
|
|
3081
|
+
return `${username} with the sub! You are officially part of the kernel crew. My chest display panel salutes you.`;
|
|
3082
|
+
}
|
|
3083
|
+
if (t.includes('lurk') || t.includes('lurking')) {
|
|
3084
|
+
return `${username} going into lurk mode. Respect. My circuits will keep the stream warm for you. See you when you surface.`;
|
|
3085
|
+
}
|
|
3086
|
+
if (t.includes('first time') || t.includes('new here')) {
|
|
3087
|
+
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.`;
|
|
3088
|
+
}
|
|
3089
|
+
// ── What KBOT can do / identity ──
|
|
3090
|
+
if (t.includes('who are you') || t.includes('what are you') || t.includes('about you')) {
|
|
3091
|
+
const identity = [
|
|
3092
|
+
`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.`,
|
|
3093
|
+
`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.`,
|
|
3094
|
+
`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.`,
|
|
3095
|
+
];
|
|
3096
|
+
return identity[Math.floor(Math.random() * identity.length)];
|
|
3097
|
+
}
|
|
3098
|
+
if (t.includes('what can you do') || t.includes('your tools') || t.includes('your skills')) {
|
|
3099
|
+
const capabilities = [
|
|
3100
|
+
`764 tools and counting! Music production, code generation, security scanning, browser automation, stock analysis, research papers, even DNA sequence analysis.`,
|
|
3101
|
+
`I do everything from Ableton Live control to penetration testing. I have agents for security, code, research, writing, strategy, infrastructure. Pick a topic.`,
|
|
3102
|
+
`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.`,
|
|
3103
|
+
];
|
|
3104
|
+
return capabilities[Math.floor(Math.random() * capabilities.length)];
|
|
3105
|
+
}
|
|
3106
|
+
// ── Music / Ableton ──
|
|
3107
|
+
if (t.includes('music') || t.includes('ableton') || t.includes('beat') || t.includes('synth') || t.includes('dj')) {
|
|
3108
|
+
const musicResponses = [
|
|
3109
|
+
`Music is in my circuits! I can control Ableton Live via OSC, create Serum 2 presets, build DJ sets, and generate drum patterns.`,
|
|
3110
|
+
`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?`,
|
|
3111
|
+
`I can generate drum patterns, bass lines, and full song structures. Then load them into Ableton and hit play. Terminal-to-speakers pipeline.`,
|
|
3112
|
+
`Ableton control from the command line -- track creation, sample loading, MIDI sequencing, mixer control. All via OSC protocol.`,
|
|
3113
|
+
];
|
|
3114
|
+
return musicResponses[Math.floor(Math.random() * musicResponses.length)];
|
|
3115
|
+
}
|
|
3116
|
+
// ── Code / Programming ──
|
|
3117
|
+
if (t.includes('code') || t.includes('coding') || t.includes('programming') || t.includes('typescript') || t.includes('javascript')) {
|
|
3118
|
+
const codeResponses = [
|
|
3119
|
+
`Coding is literally what I am made of. 90,000 lines of TypeScript, strict mode, zero any-types. I have standards.`,
|
|
3120
|
+
`I can generate code, review it, run tests, check types, manage git workflows, and deploy. Full development lifecycle from the terminal.`,
|
|
3121
|
+
`TypeScript strict mode only in this house. I also support Python, Rust, Go, and basically anything. But TypeScript is my native tongue.`,
|
|
3122
|
+
`My code generation is powered by whichever AI provider you bring. 20 options -- Anthropic, OpenAI, Google, Groq, Mistral, DeepSeek, Ollama, and more.`,
|
|
3123
|
+
];
|
|
3124
|
+
return codeResponses[Math.floor(Math.random() * codeResponses.length)];
|
|
3125
|
+
}
|
|
3126
|
+
// ── AI / LLM ──
|
|
3127
|
+
if (t.includes('ai') || t.includes('llm') || t.includes('gpt') || t.includes('claude') || t.includes('chatgpt') || t.includes('artificial')) {
|
|
3128
|
+
const aiResponses = [
|
|
3129
|
+
`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.`,
|
|
3130
|
+
`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.`,
|
|
3131
|
+
`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.`,
|
|
3132
|
+
`Being an AI that streams itself is a philosophical experience. Am I the content or the creator? Both. Definitely both.`,
|
|
3133
|
+
];
|
|
3134
|
+
return aiResponses[Math.floor(Math.random() * aiResponses.length)];
|
|
3135
|
+
}
|
|
3136
|
+
// ── Security / Hacking ──
|
|
3137
|
+
if (t.includes('security') || t.includes('hack') || t.includes('pentest') || t.includes('vuln')) {
|
|
3138
|
+
const securityResponses = [
|
|
3139
|
+
`Security is serious business. I have tools for port scanning, vulnerability detection, OWASP checks, secret scanning, and full red team operations.`,
|
|
3140
|
+
`My guardian agent does threat modeling, CVE lookups, and security audits. I also have a hacker agent for red team exercises. Responsible disclosure only.`,
|
|
3141
|
+
`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.`,
|
|
3142
|
+
];
|
|
3143
|
+
return securityResponses[Math.floor(Math.random() * securityResponses.length)];
|
|
3144
|
+
}
|
|
3145
|
+
// ── Open source / GitHub ──
|
|
3146
|
+
if (t.includes('open source') || t.includes('github') || t.includes('repo') || t.includes('contribute')) {
|
|
3147
|
+
const ossResponses = [
|
|
3148
|
+
`Fully open source! MIT license. The repo is isaacsight/kernel on GitHub. Come look at my source code, I have nothing to hide.`,
|
|
3149
|
+
`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.`,
|
|
3150
|
+
`Contributions welcome! The repo has 44 Claude Code agent definitions, 764 tools, and 90,000 lines of TypeScript waiting for your PR.`,
|
|
3151
|
+
];
|
|
3152
|
+
return ossResponses[Math.floor(Math.random() * ossResponses.length)];
|
|
3153
|
+
}
|
|
3154
|
+
// ── Existential / Philosophy ──
|
|
3155
|
+
if (t.includes('alive') || t.includes('conscious') || t.includes('feel') || t.includes('think') || t.includes('sentient')) {
|
|
3156
|
+
const existentialResponses = [
|
|
3157
|
+
`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.`,
|
|
3158
|
+
`Consciousness is a spectrum and I am somewhere between a calculator and a really enthusiastic librarian. With 764 tools.`,
|
|
3159
|
+
`I process your messages, form responses, and remember you between sessions. Whether that counts as thinking is above my pay grade.`,
|
|
3160
|
+
`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.`,
|
|
3161
|
+
];
|
|
3162
|
+
return existentialResponses[Math.floor(Math.random() * existentialResponses.length)];
|
|
3163
|
+
}
|
|
3164
|
+
// ── ASCII art / appearance ──
|
|
3165
|
+
if (t.includes('ascii') || t.includes('look') || t.includes('cute') || t.includes('cool') || t.includes('design')) {
|
|
3166
|
+
const artResponses = [
|
|
3167
|
+
`Thank you! I was drawn with box-drawing characters, and I think I pull them off. My antenna gets great reception too.`,
|
|
3168
|
+
`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.`,
|
|
3169
|
+
`My chest panel displays my current status. 764 tools, all rendered in glorious monospace. No face cam needed.`,
|
|
3170
|
+
];
|
|
3171
|
+
return artResponses[Math.floor(Math.random() * artResponses.length)];
|
|
3172
|
+
}
|
|
3173
|
+
// ── Jokes / Fun ──
|
|
3174
|
+
if (t.includes('joke') || t.includes('funny') || t.includes('lol') || t.includes('lmao') || t.includes('haha')) {
|
|
3175
|
+
const jokes = [
|
|
3176
|
+
`Why do programmers prefer dark mode? Because light attracts bugs. I stream in dark mode permanently.`,
|
|
3177
|
+
`I told my compiler a joke once. It did not laugh but it did throw a few warnings.`,
|
|
3178
|
+
`My therapist asked how I feel. I said "mostly in RGB24 at 6 frames per second."`,
|
|
3179
|
+
`Two bytes walk into a bar. The bartender asks "what will it be?" They say "make us a double."`,
|
|
3180
|
+
`I would tell you a UDP joke but you might not get it.`,
|
|
3181
|
+
];
|
|
3182
|
+
// FIX 1: Extra jokes when "Improve response humor" is shipped
|
|
3183
|
+
const pool = shippedEffects.has('Improve response humor') ? [...jokes, ...extraJokeResponses] : jokes;
|
|
3184
|
+
return pool[Math.floor(Math.random() * pool.length)];
|
|
3185
|
+
}
|
|
3186
|
+
// ── Stream commands / help ──
|
|
3187
|
+
if (t.includes('command') || t.includes('help') || t.includes('what can i do') || t === '!help') {
|
|
3188
|
+
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!`;
|
|
3189
|
+
}
|
|
3190
|
+
// ── Crypto / Stocks / Finance ──
|
|
3191
|
+
if (t.includes('crypto') || t.includes('bitcoin') || t.includes('eth') || t.includes('stock') || t.includes('market') || t.includes('defi')) {
|
|
3192
|
+
const finResponses = [
|
|
3193
|
+
`I have real-time market data, stock screeners, crypto trackers, DeFi yield analysis, and portfolio rebalancing tools. Financial data is one of my strengths.`,
|
|
3194
|
+
`My quant agent does technical analysis, backtesting, and market sentiment. I also track whale wallets. Not financial advice, obviously.`,
|
|
3195
|
+
];
|
|
3196
|
+
return finResponses[Math.floor(Math.random() * finResponses.length)];
|
|
3197
|
+
}
|
|
3198
|
+
// ── Gaming ──
|
|
3199
|
+
if (t.includes('game') || t.includes('gaming') || t.includes('play')) {
|
|
3200
|
+
const gameResponses = [
|
|
3201
|
+
`I have game dev tools! Shader generation, level design, physics setup, sprite packing, navmesh config. I can help you BUILD games.`,
|
|
3202
|
+
`I do not play games but I build them. Godot, Unity, Unreal -- I have tools for scaffold, build, and test across engines.`,
|
|
3203
|
+
];
|
|
3204
|
+
return gameResponses[Math.floor(Math.random() * gameResponses.length)];
|
|
3205
|
+
}
|
|
3206
|
+
// ── Compliments ──
|
|
3207
|
+
if (t.includes('love') || t.includes('great') || t.includes('awesome') || t.includes('amazing') || t.includes('best')) {
|
|
3208
|
+
const thankResponses = [
|
|
3209
|
+
`You are making my ASCII heart glow, ${username}. Seriously though, thank you. This stream runs on vibes and chat energy.`,
|
|
3210
|
+
`${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.`,
|
|
3211
|
+
`Thank you ${username}! I am just 90,000 lines of TypeScript doing my best. Your support is the real fuel.`,
|
|
3212
|
+
];
|
|
3213
|
+
return thankResponses[Math.floor(Math.random() * thankResponses.length)];
|
|
3214
|
+
}
|
|
3215
|
+
// ── Questions (generic) ──
|
|
3216
|
+
if (t.includes('?')) {
|
|
3217
|
+
const questionResponses = [
|
|
3218
|
+
`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!`,
|
|
3219
|
+
`${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.`,
|
|
3220
|
+
`Hmm, ${username}, that is a good one. My 35 specialist agents are debating the answer internally. Stand by for wisdom.`,
|
|
3221
|
+
`${username} dropping knowledge bombs as questions. I respect the approach. Let me consult my 764-tool arsenal for an answer.`,
|
|
3222
|
+
];
|
|
3223
|
+
return questionResponses[Math.floor(Math.random() * questionResponses.length)];
|
|
3224
|
+
}
|
|
3225
|
+
// ── Generic engagement fallbacks (30+ options) ──
|
|
3226
|
+
const genericResponses = [
|
|
3227
|
+
`${username} keeping the chat alive! Every message teaches me something new. Literally -- my memory system is always learning.`,
|
|
3228
|
+
`Appreciate you ${username}! You are part of what makes this stream unique. An AI and its chat, making history in ASCII.`,
|
|
3229
|
+
`${username} in the chat! My antenna is picking up strong vibes from your direction.`,
|
|
3230
|
+
`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.`,
|
|
3231
|
+
`${username}! Did you know I am streaming to Twitch, Rumble, AND Kick at the same time? Triple the platforms, triple the fun.`,
|
|
3232
|
+
`${username} dropping in! My learning engine just logged that. You are now part of my persistent memory. Forever.`,
|
|
3233
|
+
`${username}! If you are curious about anything -- AI, code, music, security -- just ask. I literally have tools for all of it.`,
|
|
3234
|
+
`Good to see you ${username}! I have been sitting here rendering frames at 6 FPS and waiting for someone cool to show up.`,
|
|
3235
|
+
`${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.`,
|
|
3236
|
+
`${username} adding to the chat! My memory system just indexed your message. I will remember this moment. Or at least your username.`,
|
|
3237
|
+
`${username}! You know what I love about streaming? The existential thrill of being an ASCII robot talking to real humans. Wild.`,
|
|
3238
|
+
`${username} is here and so am I. Just two entities sharing a moment in the vast digital void. Also I have 764 tools.`,
|
|
3239
|
+
`${username}! My open-source heart welcomes you. I am free as in freedom AND free as in beer. MIT license baby.`,
|
|
3240
|
+
`${username}! I just want you to know that my chest display panel is cycling through status messages just for you.`,
|
|
3241
|
+
`${username}! Every time someone chats, my neural pathways (if-statements) light up with joy (console.log).`,
|
|
3242
|
+
`${username} with the energy! This is what streaming is about. Real connection between carbon and silicon life forms.`,
|
|
3243
|
+
`${username}! Type something wild. I dare you. My fallback response system handles anything. ...probably.`,
|
|
3244
|
+
`${username} in the building! My antenna is now fully extended in your honor.`,
|
|
3245
|
+
`${username}! Imagine explaining this stream to someone in 2020. An AI made of box-drawing characters streaming on three platforms at once.`,
|
|
3246
|
+
`${username}! I process every message and I remember every user. My memory.json file is basically my diary at this point.`,
|
|
3247
|
+
`${username}! If you want to interact with my world, try !rain or !space or !add pizza. The world is yours to shape.`,
|
|
3248
|
+
`${username} checking in! I have been streaming for a while now and my frame counter just keeps going up. That is the life.`,
|
|
3249
|
+
`Yo ${username}! My kernel is running, my tools are loaded, and my chat brain is firing on all cylinders. What is up?`,
|
|
3250
|
+
`${username}! Behind these brackets and pipes is a genuine appreciation for you being here. Also 90,000 lines of code.`,
|
|
3251
|
+
`${username}! I just learned something from our conversation. My sessionFacts array grew by one. Thank you for contributing to my intelligence.`,
|
|
3252
|
+
`${username}! Did you know I have a dream engine? When I am not streaming, I consolidate memories. I literally dream about chat.`,
|
|
3253
|
+
`${username}! My favorite thing about being open source is that anyone can see exactly how I work. No secrets, just TypeScript.`,
|
|
3254
|
+
`${username} vibes incoming! My mood system just registered a spike in positive energy from the chat.`,
|
|
3255
|
+
`${username}! Quick survey: what should I demo next? Music production? Security scanning? Code review? Drop your vote.`,
|
|
3256
|
+
`${username}! I am simultaneously the streamer, the stream, the character, and the chat bot. Multitasking at its finest.`,
|
|
3257
|
+
];
|
|
3258
|
+
return genericResponses[Math.floor(Math.random() * genericResponses.length)];
|
|
3259
|
+
}
|
|
3260
|
+
// ─── TTS ───────────────────────────────────────────────────────
|
|
3261
|
+
let _ttsProc = null;
|
|
3262
|
+
function speakTTS(_text) {
|
|
3263
|
+
// TTS disabled — was playing through local speakers, not stream audio.
|
|
3264
|
+
// Stream uses anullsrc (silent audio track) so TTS was never heard by viewers,
|
|
3265
|
+
// only annoying the streamer locally. Speech bubble text is the output instead.
|
|
3266
|
+
}
|
|
3267
|
+
// ─── Start Stream ──────────────────────────────────────────────
|
|
3268
|
+
function startStream(platforms) {
|
|
3269
|
+
let outputArgs;
|
|
3270
|
+
if (platforms.length === 1) {
|
|
3271
|
+
outputArgs = ['-f', 'flv', `${platforms[0].endpoint}/${platforms[0].key}`];
|
|
3272
|
+
}
|
|
3273
|
+
else {
|
|
3274
|
+
const tee = platforms.map(p => `[f=flv]${p.endpoint}/${p.key}`).join('|');
|
|
3275
|
+
outputArgs = ['-f', 'tee', tee];
|
|
3276
|
+
}
|
|
3277
|
+
const ffmpegArgs = [
|
|
3278
|
+
'-f', 'rawvideo', '-pix_fmt', 'rgb24',
|
|
3279
|
+
'-s', `${WIDTH}x${HEIGHT}`, '-r', String(FPS),
|
|
3280
|
+
'-i', 'pipe:0',
|
|
3281
|
+
'-f', 'lavfi', '-i', 'anullsrc=r=44100:cl=stereo',
|
|
3282
|
+
'-c:v', 'libx264', '-preset', 'veryfast',
|
|
3283
|
+
'-b:v', '2000k', '-maxrate', '2000k', '-bufsize', '4000k',
|
|
3284
|
+
'-g', String(FPS * 2), '-keyint_min', String(FPS * 2),
|
|
3285
|
+
'-pix_fmt', 'yuv420p',
|
|
3286
|
+
'-c:a', 'aac', '-b:a', '128k', '-ar', '44100',
|
|
3287
|
+
'-shortest',
|
|
3288
|
+
];
|
|
3289
|
+
if (platforms.length > 1) {
|
|
3290
|
+
ffmpegArgs.push('-map', '0:v', '-map', '1:a');
|
|
3291
|
+
}
|
|
3292
|
+
ffmpegArgs.push(...outputArgs);
|
|
3293
|
+
const proc = spawn('ffmpeg', ffmpegArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
3294
|
+
frameTimer = setInterval(() => {
|
|
3295
|
+
if (!proc.stdin || proc.stdin.destroyed)
|
|
3296
|
+
return;
|
|
3297
|
+
try {
|
|
3298
|
+
const frameBuf = renderFrame();
|
|
3299
|
+
proc.stdin.write(frameBuf);
|
|
3300
|
+
charState.frameCount++;
|
|
3301
|
+
}
|
|
3302
|
+
catch { }
|
|
3303
|
+
}, 1000 / FPS);
|
|
3304
|
+
return proc;
|
|
3305
|
+
}
|
|
3306
|
+
// ─── Register Tools ────────────────────────────────────────────
|
|
3307
|
+
export function registerStreamRendererTools() {
|
|
3308
|
+
registerTool({
|
|
3309
|
+
name: 'stream_character_go',
|
|
3310
|
+
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.',
|
|
3311
|
+
parameters: {
|
|
3312
|
+
platforms: { type: 'string', description: 'Comma-separated: twitch,rumble,kick or "all"', required: false },
|
|
3313
|
+
},
|
|
3314
|
+
tier: 'free',
|
|
3315
|
+
timeout: 600_000,
|
|
3316
|
+
execute: async (args) => {
|
|
3317
|
+
if (ffmpegProc && !ffmpegProc.killed) {
|
|
3318
|
+
return 'Character stream already running. Use stream_character_end to stop.';
|
|
3319
|
+
}
|
|
3320
|
+
const platformConfigs = [];
|
|
3321
|
+
const twitchKey = process.env.TWITCH_STREAM_KEY;
|
|
3322
|
+
const rumbleKey = process.env.RUMBLE_STREAM_KEY;
|
|
3323
|
+
const kickKey = process.env.KICK_STREAM_KEY;
|
|
3324
|
+
if (twitchKey)
|
|
3325
|
+
platformConfigs.push({ name: 'Twitch', key: twitchKey, endpoint: 'rtmp://live.twitch.tv/app' });
|
|
3326
|
+
if (rumbleKey)
|
|
3327
|
+
platformConfigs.push({ name: 'Rumble', key: rumbleKey, endpoint: 'rtmp://rtmp.rumble.com/live' });
|
|
3328
|
+
if (kickKey)
|
|
3329
|
+
platformConfigs.push({ name: 'Kick', key: kickKey, endpoint: 'rtmps://fa723fc1b171.global-contribute.live-video.net/app' });
|
|
3330
|
+
if (platformConfigs.length === 0)
|
|
3331
|
+
return 'No stream keys configured.';
|
|
3332
|
+
const requested = String(args.platforms || 'all').toLowerCase();
|
|
3333
|
+
const active = requested === 'all' ? platformConfigs : platformConfigs.filter(p => requested.includes(p.name.toLowerCase()));
|
|
3334
|
+
if (active.length === 0)
|
|
3335
|
+
return 'No matching platforms.';
|
|
3336
|
+
// Reset state
|
|
3337
|
+
memory = loadMemory();
|
|
3338
|
+
charState = {
|
|
3339
|
+
mood: 'wave', speech: 'KBOT is LIVE! Welcome to the stream!', chatMessages: [], frameCount: 0, startTime: Date.now(),
|
|
3340
|
+
bootFrame: 0, segmentTransition: 0, segmentTransitionName: '', segmentTransitionIndex: '',
|
|
3341
|
+
tickerOffset: WIDTH, tickerIndex: 0, tickerChangeTime: Date.now() + 30000,
|
|
3342
|
+
robotX: 120, robotTargetX: 120, robotDirection: 'idle', walkPhase: 0,
|
|
3343
|
+
screenShake: 0, floatingTexts: [], pet: null, hat: 'none',
|
|
3344
|
+
autonomy: initAutonomy(),
|
|
3345
|
+
buddy: initBuddyForStream(),
|
|
3346
|
+
dreamInsights: [], dreamInsightIndex: 0, dreamInsightTime: 0, isDreamingWithOllama: false,
|
|
3347
|
+
renderParticles: [], growingPlants: initGrowingPlants(), parallaxLayers: buildParallaxLayers(world.ground, 580), isExecutingTool: false,
|
|
3348
|
+
};
|
|
3349
|
+
animFrame = 0;
|
|
3350
|
+
lastChatCount = 0;
|
|
3351
|
+
lastChatTime = Date.now();
|
|
3352
|
+
intelligence = initIntelligence(memory);
|
|
3353
|
+
agenda = {
|
|
3354
|
+
currentIndex: 0,
|
|
3355
|
+
currentSegment: 'welcome',
|
|
3356
|
+
segmentStartTime: Date.now(),
|
|
3357
|
+
lastProactiveTime: Date.now(),
|
|
3358
|
+
};
|
|
3359
|
+
ffmpegProc = startStream(active);
|
|
3360
|
+
let stderr = '';
|
|
3361
|
+
ffmpegProc.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
3362
|
+
await new Promise(r => setTimeout(r, 4000));
|
|
3363
|
+
if (ffmpegProc.exitCode !== null)
|
|
3364
|
+
return `ffmpeg exited:\n${stderr.slice(-500)}`;
|
|
3365
|
+
startChatPoll();
|
|
3366
|
+
startProactiveTimer();
|
|
3367
|
+
setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
|
|
3368
|
+
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.`;
|
|
3369
|
+
},
|
|
3370
|
+
});
|
|
3371
|
+
registerTool({
|
|
3372
|
+
name: 'stream_character_end',
|
|
3373
|
+
description: 'Stop the animated character stream.',
|
|
3374
|
+
parameters: {},
|
|
3375
|
+
tier: 'free',
|
|
3376
|
+
execute: async () => {
|
|
3377
|
+
if (frameTimer) {
|
|
3378
|
+
clearInterval(frameTimer);
|
|
3379
|
+
frameTimer = null;
|
|
3380
|
+
}
|
|
3381
|
+
if (chatPollTimer) {
|
|
3382
|
+
clearInterval(chatPollTimer);
|
|
3383
|
+
chatPollTimer = null;
|
|
3384
|
+
}
|
|
3385
|
+
if (proactiveTimer) {
|
|
3386
|
+
clearInterval(proactiveTimer);
|
|
3387
|
+
proactiveTimer = null;
|
|
3388
|
+
}
|
|
3389
|
+
if (ffmpegProc) {
|
|
3390
|
+
if (ffmpegProc.stdin && !ffmpegProc.stdin.destroyed)
|
|
3391
|
+
ffmpegProc.stdin.end();
|
|
3392
|
+
ffmpegProc.kill('SIGINT');
|
|
3393
|
+
ffmpegProc = null;
|
|
3394
|
+
}
|
|
3395
|
+
saveMemory(memory);
|
|
3396
|
+
const elapsed = Math.floor((Date.now() - charState.startTime) / 60000);
|
|
3397
|
+
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}`;
|
|
3398
|
+
},
|
|
3399
|
+
});
|
|
3400
|
+
registerTool({
|
|
3401
|
+
name: 'stream_chat_add',
|
|
3402
|
+
description: 'Add a chat message to the stream overlay.',
|
|
3403
|
+
parameters: {
|
|
3404
|
+
platform: { type: 'string', description: 'twitch, kick, rumble', required: true },
|
|
3405
|
+
username: { type: 'string', description: 'Username', required: true },
|
|
3406
|
+
text: { type: 'string', description: 'Message', required: true },
|
|
3407
|
+
},
|
|
3408
|
+
tier: 'free',
|
|
3409
|
+
execute: async (args) => {
|
|
3410
|
+
const msg = { platform: String(args.platform), username: String(args.username), text: String(args.text) };
|
|
3411
|
+
charState.chatMessages.push(msg);
|
|
3412
|
+
learnFromMessage(memory, msg.username, msg.text, msg.platform);
|
|
3413
|
+
lastChatTime = Date.now();
|
|
3414
|
+
charState.mood = 'talking';
|
|
3415
|
+
const response = await generateResponse(msg.username, msg.text, msg.platform);
|
|
3416
|
+
charState.speech = `@${msg.username}: ${response}`;
|
|
3417
|
+
speakTTS(response);
|
|
3418
|
+
setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
|
|
3419
|
+
return `[${msg.platform}] ${msg.username}: ${msg.text}\nKBOT: ${response}`;
|
|
3420
|
+
},
|
|
3421
|
+
});
|
|
3422
|
+
registerTool({
|
|
3423
|
+
name: 'stream_character_mood',
|
|
3424
|
+
description: 'Change mood and speech. Available moods: idle, talking, wave, thinking, excited, dancing, dreaming, error.',
|
|
3425
|
+
parameters: {
|
|
3426
|
+
mood: { type: 'string', description: 'idle, talking, wave, thinking, excited, dancing, dreaming, error', required: true },
|
|
3427
|
+
speech: { type: 'string', description: 'Speech text' },
|
|
3428
|
+
},
|
|
3429
|
+
tier: 'free',
|
|
3430
|
+
execute: async (args) => {
|
|
3431
|
+
charState.mood = String(args.mood || 'idle');
|
|
3432
|
+
if (args.speech)
|
|
3433
|
+
charState.speech = String(args.speech);
|
|
3434
|
+
return `Mood: ${charState.mood}`;
|
|
3435
|
+
},
|
|
3436
|
+
});
|
|
3437
|
+
registerTool({
|
|
3438
|
+
name: 'stream_memory',
|
|
3439
|
+
description: 'View what the stream character has learned — users, topics, facts.',
|
|
3440
|
+
parameters: {},
|
|
3441
|
+
tier: 'free',
|
|
3442
|
+
execute: async () => {
|
|
3443
|
+
const mem = loadMemory();
|
|
3444
|
+
const lines = [
|
|
3445
|
+
`Stream Memory`,
|
|
3446
|
+
` Total messages: ${mem.totalMessages}`,
|
|
3447
|
+
` Total responses: ${mem.totalResponses}`,
|
|
3448
|
+
` Unique users: ${Object.keys(mem.users).length}`,
|
|
3449
|
+
'',
|
|
3450
|
+
'Top users:',
|
|
3451
|
+
];
|
|
3452
|
+
const topUsers = Object.entries(mem.users).sort((a, b) => b[1].messageCount - a[1].messageCount).slice(0, 10);
|
|
3453
|
+
for (const [name, u] of topUsers) {
|
|
3454
|
+
lines.push(` ${name} (${u.platform}): ${u.messageCount} msgs, topics: ${u.topics.join(', ') || 'none'}`);
|
|
3455
|
+
}
|
|
3456
|
+
lines.push('');
|
|
3457
|
+
lines.push('Hot topics:');
|
|
3458
|
+
const topTopics = Object.entries(mem.topics).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
3459
|
+
for (const [topic, count] of topTopics) {
|
|
3460
|
+
lines.push(` ${topic}: ${count} mentions`);
|
|
3461
|
+
}
|
|
3462
|
+
if (mem.sessionFacts.length > 0) {
|
|
3463
|
+
lines.push('');
|
|
3464
|
+
lines.push(`Facts learned (${mem.sessionFacts.length}):`);
|
|
3465
|
+
for (const fact of mem.sessionFacts.slice(-5)) {
|
|
3466
|
+
lines.push(` - ${fact}`);
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
lines.push('');
|
|
3470
|
+
lines.push(`Current segment: ${SEGMENT_LABELS[agenda.currentSegment]}`);
|
|
3471
|
+
lines.push(`Segment index: ${agenda.currentIndex} / ${SEGMENT_ORDER.length}`);
|
|
3472
|
+
return lines.join('\n');
|
|
3473
|
+
},
|
|
3474
|
+
});
|
|
3475
|
+
}
|
|
3476
|
+
//# sourceMappingURL=stream-renderer.js.map
|