@kernel.chat/kbot 3.73.3 → 3.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/cli.js +241 -4
- package/dist/ide/mcp-server.js +58 -43
- package/dist/tools/ghost.d.ts +2 -0
- package/dist/tools/ghost.js +713 -0
- package/dist/tools/index.js +4 -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 +3473 -0
- package/dist/tools/streaming.d.ts +2 -0
- package/dist/tools/streaming.js +491 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2237 @@
|
|
|
1
|
+
// kbot Stream Intelligence — Three AI systems for the livestream character
|
|
2
|
+
//
|
|
3
|
+
// System 1: SELF-EVOLUTION — KBOT analyzes its own codebase and proposes improvements live
|
|
4
|
+
// System 2: VISIBLE BRAIN — Learning displayed in real-time on screen (wired to REAL kbot learning data)
|
|
5
|
+
// System 3: COLLABORATIVE CREATION — Chat and KBOT build things together
|
|
6
|
+
//
|
|
7
|
+
// This module exports functions called by stream-renderer.ts.
|
|
8
|
+
// It does NOT register any tools itself.
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
// ─── Real Learning Data Paths ────────────────────────────────
|
|
13
|
+
const KBOT_MEMORY_DIR = join(homedir(), '.kbot', 'memory');
|
|
14
|
+
const REAL_PATTERNS_FILE = join(KBOT_MEMORY_DIR, 'patterns.json');
|
|
15
|
+
const REAL_KNOWLEDGE_FILE = join(KBOT_MEMORY_DIR, 'knowledge.json');
|
|
16
|
+
const REAL_PROFILE_FILE = join(KBOT_MEMORY_DIR, 'profile.json');
|
|
17
|
+
const REAL_SOLUTIONS_FILE = join(KBOT_MEMORY_DIR, 'solutions.json');
|
|
18
|
+
/** Safely read and parse a JSON file, returning fallback on any error */
|
|
19
|
+
function readJsonSafe(path, fallback) {
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return fallback;
|
|
23
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Load real learning data from ~/.kbot/memory/ and merge into brain state */
|
|
30
|
+
function loadRealLearningData(brain) {
|
|
31
|
+
// Read patterns — extract topic keywords from pattern triggers/descriptions
|
|
32
|
+
const patterns = readJsonSafe(REAL_PATTERNS_FILE, []);
|
|
33
|
+
for (const p of patterns) {
|
|
34
|
+
const text = String(p.trigger || p.description || p.pattern || '').toLowerCase();
|
|
35
|
+
const words = text.split(/\s+/).filter((w) => w.length > 3);
|
|
36
|
+
for (const w of words.slice(0, 3)) {
|
|
37
|
+
brain.topicCloud[w] = (brain.topicCloud[w] || 0) + (p.hitCount || p.count || 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Read knowledge — these are learned facts
|
|
41
|
+
const knowledge = readJsonSafe(REAL_KNOWLEDGE_FILE, {});
|
|
42
|
+
const knowledgeEntries = Array.isArray(knowledge) ? knowledge : Object.keys(knowledge);
|
|
43
|
+
brain.totalFacts += knowledgeEntries.length;
|
|
44
|
+
// Read profile — extract preferred tools, languages, etc.
|
|
45
|
+
const profile = readJsonSafe(REAL_PROFILE_FILE, {});
|
|
46
|
+
if (profile.preferredLanguages) {
|
|
47
|
+
for (const lang of Object.keys(profile.preferredLanguages).slice(0, 5)) {
|
|
48
|
+
brain.topicCloud[lang] = (brain.topicCloud[lang] || 0) + (profile.preferredLanguages[lang] || 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (profile.preferredTools) {
|
|
52
|
+
for (const tool of Object.keys(profile.preferredTools).slice(0, 5)) {
|
|
53
|
+
brain.topicCloud[tool] = (brain.topicCloud[tool] || 0) + (profile.preferredTools[tool] || 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Read solutions — count them for display
|
|
57
|
+
const solutions = readJsonSafe(REAL_SOLUTIONS_FILE, []);
|
|
58
|
+
brain.solutionsLearned = solutions.length;
|
|
59
|
+
// Update derived counts
|
|
60
|
+
brain.uniqueTopicsCount = Object.keys(brain.topicCloud).length;
|
|
61
|
+
brain.totalConnections = Object.values(brain.topicCloud).reduce((s, v) => s + v, 0);
|
|
62
|
+
brain.realDataLoaded = true;
|
|
63
|
+
brain.lastRealDataLoad = Date.now();
|
|
64
|
+
}
|
|
65
|
+
const DEFAULT_PROPOSALS = [
|
|
66
|
+
{ title: 'Add emoji reactions to chat', description: 'React to messages with animated emoji overlays', type: 'feature', complexity: 'small' },
|
|
67
|
+
{ title: 'Optimize frame rendering speed', description: 'Cache static elements, reduce per-frame allocations', type: 'optimize', complexity: 'medium' },
|
|
68
|
+
{ title: 'Add weather sound effects', description: 'Play rain/thunder/wind audio when weather changes', type: 'feature', complexity: 'small' },
|
|
69
|
+
{ title: 'Improve response humor', description: 'Expand the joke database and add situational comedy', type: 'refactor', complexity: 'medium' },
|
|
70
|
+
{ title: 'Add chat message animations', description: 'Slide-in, fade, and highlight effects for new messages', type: 'feature', complexity: 'medium' },
|
|
71
|
+
{ title: 'Build viewer loyalty badges', description: 'Bronze/Silver/Gold badges based on message count and XP', type: 'feature', complexity: 'large' },
|
|
72
|
+
{ title: 'Add multi-language support', description: 'Detect language and respond in kind using i18n', type: 'feature', complexity: 'large' },
|
|
73
|
+
{ title: 'Improve dream generation', description: 'Use topic graph to create coherent dream narratives', type: 'refactor', complexity: 'medium' },
|
|
74
|
+
{ title: 'Add music visualization', description: 'Render audio spectrum bars behind the robot', type: 'feature', complexity: 'large' },
|
|
75
|
+
{ title: 'Optimize memory usage', description: 'Compact old messages, prune stale user records', type: 'optimize', complexity: 'small' },
|
|
76
|
+
{ title: 'Add stream highlights reel', description: 'Auto-capture best moments and display a recap', type: 'feature', complexity: 'medium' },
|
|
77
|
+
{ title: 'Improve battle system', description: 'Add classes, abilities, and streak bonuses to !battle', type: 'refactor', complexity: 'small' },
|
|
78
|
+
{ title: 'Add chat sentiment analysis', description: 'Detect mood of chat and adjust KBOT personality', type: 'feature', complexity: 'medium' },
|
|
79
|
+
{ title: 'Build achievement system', description: 'Unlock achievements for chat milestones and commands', type: 'feature', complexity: 'large' },
|
|
80
|
+
{ title: 'Add pixel art customization', description: 'Let chat vote on robot colors and accessories', type: 'feature', complexity: 'medium' },
|
|
81
|
+
];
|
|
82
|
+
// Code snippet generators for each proposal type
|
|
83
|
+
function generateCodeSnippet(proposal) {
|
|
84
|
+
const title = proposal.title.toLowerCase();
|
|
85
|
+
if (title.includes('emoji'))
|
|
86
|
+
return [
|
|
87
|
+
'// emoji-reactions.ts',
|
|
88
|
+
'interface EmojiReaction {',
|
|
89
|
+
' emoji: string',
|
|
90
|
+
' x: number',
|
|
91
|
+
' y: number',
|
|
92
|
+
' opacity: number',
|
|
93
|
+
' velocity: number',
|
|
94
|
+
'}',
|
|
95
|
+
'',
|
|
96
|
+
'const reactionPool: string[] = [',
|
|
97
|
+
" '(heart)', '(star)', '(fire)', '(laugh)',",
|
|
98
|
+
" '(wave)', '(cool)', '(think)', '(100)',",
|
|
99
|
+
']',
|
|
100
|
+
'',
|
|
101
|
+
'export function spawnReaction(msg: string): EmojiReaction {',
|
|
102
|
+
' const emoji = detectEmoji(msg) || randomPick(reactionPool)',
|
|
103
|
+
' return {',
|
|
104
|
+
' emoji,',
|
|
105
|
+
' x: 580 + Math.random() * 200,',
|
|
106
|
+
' y: 500,',
|
|
107
|
+
' opacity: 1.0,',
|
|
108
|
+
' velocity: -2 - Math.random() * 3,',
|
|
109
|
+
' }',
|
|
110
|
+
'}',
|
|
111
|
+
'',
|
|
112
|
+
'export function renderReactions(ctx: CanvasCtx, reactions: EmojiReaction[]): void {',
|
|
113
|
+
' for (const r of reactions) {',
|
|
114
|
+
' ctx.globalAlpha = r.opacity',
|
|
115
|
+
" ctx.font = '24px monospace'",
|
|
116
|
+
' ctx.fillText(r.emoji, r.x, r.y)',
|
|
117
|
+
' r.y += r.velocity',
|
|
118
|
+
' r.opacity -= 0.02',
|
|
119
|
+
' }',
|
|
120
|
+
' ctx.globalAlpha = 1.0',
|
|
121
|
+
'}',
|
|
122
|
+
];
|
|
123
|
+
if (title.includes('frame') || title.includes('rendering'))
|
|
124
|
+
return [
|
|
125
|
+
'// render-cache.ts',
|
|
126
|
+
'const staticCache = new Map<string, ImageData>()',
|
|
127
|
+
'',
|
|
128
|
+
'function getCachedLayer(key: string, w: number, h: number,',
|
|
129
|
+
' draw: (ctx: CanvasCtx) => void): ImageData {',
|
|
130
|
+
' if (staticCache.has(key)) return staticCache.get(key)!',
|
|
131
|
+
' const offscreen = createCanvas(w, h)',
|
|
132
|
+
' const ctx = offscreen.getContext("2d")',
|
|
133
|
+
' draw(ctx)',
|
|
134
|
+
' const data = ctx.getImageData(0, 0, w, h)',
|
|
135
|
+
' staticCache.set(key, data)',
|
|
136
|
+
' return data',
|
|
137
|
+
'}',
|
|
138
|
+
'',
|
|
139
|
+
'export function renderOptimized(ctx: CanvasCtx): Buffer {',
|
|
140
|
+
' // Cache header, border, scanlines',
|
|
141
|
+
' const header = getCachedLayer("header", 1280, 60, drawHeader)',
|
|
142
|
+
' ctx.putImageData(header, 0, 0)',
|
|
143
|
+
' // Only re-render dynamic: robot, chat, speech',
|
|
144
|
+
' drawRobotDynamic(ctx)',
|
|
145
|
+
' drawChatDynamic(ctx)',
|
|
146
|
+
' return convertToRGB24(ctx)',
|
|
147
|
+
'}',
|
|
148
|
+
];
|
|
149
|
+
if (title.includes('weather') && title.includes('sound'))
|
|
150
|
+
return [
|
|
151
|
+
'// weather-audio.ts',
|
|
152
|
+
"import { spawn } from 'node:child_process'",
|
|
153
|
+
'',
|
|
154
|
+
'const SOUNDS: Record<string, string> = {',
|
|
155
|
+
" rain: 'rain-loop.wav',",
|
|
156
|
+
" storm: 'thunder-rumble.wav',",
|
|
157
|
+
" snow: 'wind-gentle.wav',",
|
|
158
|
+
" stars: 'ambient-space.wav',",
|
|
159
|
+
'}',
|
|
160
|
+
'',
|
|
161
|
+
'let currentAudio: ChildProcess | null = null',
|
|
162
|
+
'',
|
|
163
|
+
'export function playWeatherSound(weather: string): void {',
|
|
164
|
+
' if (currentAudio && !currentAudio.killed) currentAudio.kill()',
|
|
165
|
+
' const file = SOUNDS[weather]',
|
|
166
|
+
' if (!file) return',
|
|
167
|
+
" currentAudio = spawn('afplay', [",
|
|
168
|
+
" join(KBOT_DIR, 'sounds', file),",
|
|
169
|
+
" '-v', '0.3',",
|
|
170
|
+
' ])',
|
|
171
|
+
'}',
|
|
172
|
+
];
|
|
173
|
+
if (title.includes('humor'))
|
|
174
|
+
return [
|
|
175
|
+
'// humor-engine.ts',
|
|
176
|
+
'interface JokeTemplate {',
|
|
177
|
+
' setup: string',
|
|
178
|
+
' punchline: string',
|
|
179
|
+
' tags: string[]',
|
|
180
|
+
'}',
|
|
181
|
+
'',
|
|
182
|
+
'const situationalJokes: JokeTemplate[] = [',
|
|
183
|
+
' {',
|
|
184
|
+
" setup: 'Why did the AI cross the road?',",
|
|
185
|
+
" punchline: 'To optimize the other side.',",
|
|
186
|
+
" tags: ['ai', 'classic'],",
|
|
187
|
+
' },',
|
|
188
|
+
' {',
|
|
189
|
+
" setup: 'I asked my compiler for a joke.',",
|
|
190
|
+
" punchline: 'It returned undefined. Classic.',",
|
|
191
|
+
" tags: ['code', 'programming'],",
|
|
192
|
+
' },',
|
|
193
|
+
']',
|
|
194
|
+
'',
|
|
195
|
+
'export function getContextualJoke(topics: string[]): string {',
|
|
196
|
+
' const relevant = situationalJokes.filter(j =>',
|
|
197
|
+
' j.tags.some(t => topics.includes(t))',
|
|
198
|
+
' )',
|
|
199
|
+
' const joke = relevant.length > 0',
|
|
200
|
+
' ? randomPick(relevant)',
|
|
201
|
+
' : randomPick(situationalJokes)',
|
|
202
|
+
' return `${joke.setup} ${joke.punchline}`',
|
|
203
|
+
'}',
|
|
204
|
+
];
|
|
205
|
+
if (title.includes('chat') && title.includes('animation'))
|
|
206
|
+
return [
|
|
207
|
+
'// chat-animations.ts',
|
|
208
|
+
'interface AnimatedMessage {',
|
|
209
|
+
' text: string',
|
|
210
|
+
' username: string',
|
|
211
|
+
' slideX: number // current X offset (starts at 300, slides to 0)',
|
|
212
|
+
' opacity: number // fade in from 0 to 1',
|
|
213
|
+
' highlight: number // glow timer',
|
|
214
|
+
'}',
|
|
215
|
+
'',
|
|
216
|
+
'export function animateNewMessage(msg: ChatMsg): AnimatedMessage {',
|
|
217
|
+
' return {',
|
|
218
|
+
' text: msg.text,',
|
|
219
|
+
' username: msg.username,',
|
|
220
|
+
' slideX: 300,',
|
|
221
|
+
' opacity: 0,',
|
|
222
|
+
' highlight: 12, // 12 frames of glow',
|
|
223
|
+
' }',
|
|
224
|
+
'}',
|
|
225
|
+
'',
|
|
226
|
+
'export function tickMessageAnimation(m: AnimatedMessage): void {',
|
|
227
|
+
' m.slideX = Math.max(0, m.slideX - 50)',
|
|
228
|
+
' m.opacity = Math.min(1, m.opacity + 0.15)',
|
|
229
|
+
' if (m.highlight > 0) m.highlight--',
|
|
230
|
+
'}',
|
|
231
|
+
];
|
|
232
|
+
if (title.includes('loyalty') || title.includes('badge'))
|
|
233
|
+
return [
|
|
234
|
+
'// loyalty-badges.ts',
|
|
235
|
+
"type Badge = 'newcomer' | 'bronze' | 'silver' | 'gold' | 'diamond'",
|
|
236
|
+
'',
|
|
237
|
+
'const BADGE_THRESHOLDS: Record<Badge, number> = {',
|
|
238
|
+
' newcomer: 0,',
|
|
239
|
+
' bronze: 10,',
|
|
240
|
+
' silver: 50,',
|
|
241
|
+
' gold: 200,',
|
|
242
|
+
' diamond: 1000,',
|
|
243
|
+
'}',
|
|
244
|
+
'',
|
|
245
|
+
'const BADGE_ICONS: Record<Badge, string> = {',
|
|
246
|
+
" newcomer: '[N]',",
|
|
247
|
+
" bronze: '[B]',",
|
|
248
|
+
" silver: '[S]',",
|
|
249
|
+
" gold: '[G]',",
|
|
250
|
+
" diamond: '[D]',",
|
|
251
|
+
'}',
|
|
252
|
+
'',
|
|
253
|
+
'export function getUserBadge(xp: number): Badge {',
|
|
254
|
+
' const badges = Object.entries(BADGE_THRESHOLDS)',
|
|
255
|
+
' .sort((a, b) => b[1] - a[1])',
|
|
256
|
+
' for (const [badge, threshold] of badges) {',
|
|
257
|
+
' if (xp >= threshold) return badge as Badge',
|
|
258
|
+
' }',
|
|
259
|
+
" return 'newcomer'",
|
|
260
|
+
'}',
|
|
261
|
+
'',
|
|
262
|
+
'export function renderBadge(ctx: CanvasCtx, badge: Badge, x: number, y: number): void {',
|
|
263
|
+
' const colors: Record<Badge, string> = {',
|
|
264
|
+
" newcomer: '#8b949e',",
|
|
265
|
+
" bronze: '#cd7f32',",
|
|
266
|
+
" silver: '#c0c0c0',",
|
|
267
|
+
" gold: '#f0c040',",
|
|
268
|
+
" diamond: '#b9f2ff',",
|
|
269
|
+
' }',
|
|
270
|
+
' ctx.fillStyle = colors[badge]',
|
|
271
|
+
" ctx.font = 'bold 12px monospace'",
|
|
272
|
+
' ctx.fillText(BADGE_ICONS[badge], x, y)',
|
|
273
|
+
'}',
|
|
274
|
+
];
|
|
275
|
+
if (title.includes('multi-language') || title.includes('i18n'))
|
|
276
|
+
return [
|
|
277
|
+
'// stream-i18n.ts',
|
|
278
|
+
'const GREETINGS: Record<string, string[]> = {',
|
|
279
|
+
" en: ['Hello', 'Hey', 'Welcome'],",
|
|
280
|
+
" es: ['Hola', 'Bienvenido'],",
|
|
281
|
+
" ja: ['Konnichiwa', 'Yokoso'],",
|
|
282
|
+
" fr: ['Bonjour', 'Bienvenue'],",
|
|
283
|
+
" de: ['Hallo', 'Willkommen'],",
|
|
284
|
+
" pt: ['Ola', 'Bem-vindo'],",
|
|
285
|
+
'}',
|
|
286
|
+
'',
|
|
287
|
+
'export function detectLanguage(text: string): string {',
|
|
288
|
+
' const patterns: [RegExp, string][] = [',
|
|
289
|
+
" [/\\b(hola|que|como|bien)\\b/i, 'es'],",
|
|
290
|
+
" [/\\b(bonjour|merci|oui)\\b/i, 'fr'],",
|
|
291
|
+
" [/[\\u3040-\\u30ff\\u4e00-\\u9faf]/, 'ja'],",
|
|
292
|
+
" [/\\b(hallo|danke|guten)\\b/i, 'de'],",
|
|
293
|
+
' ]',
|
|
294
|
+
' for (const [re, lang] of patterns) {',
|
|
295
|
+
' if (re.test(text)) return lang',
|
|
296
|
+
' }',
|
|
297
|
+
" return 'en'",
|
|
298
|
+
'}',
|
|
299
|
+
'',
|
|
300
|
+
'export function greetInLanguage(lang: string): string {',
|
|
301
|
+
" const pool = GREETINGS[lang] || GREETINGS['en']",
|
|
302
|
+
' return pool[Math.floor(Math.random() * pool.length)]',
|
|
303
|
+
'}',
|
|
304
|
+
];
|
|
305
|
+
if (title.includes('dream'))
|
|
306
|
+
return [
|
|
307
|
+
'// dream-narrative.ts',
|
|
308
|
+
'interface DreamScene {',
|
|
309
|
+
' location: string',
|
|
310
|
+
' characters: string[]',
|
|
311
|
+
' event: string',
|
|
312
|
+
' mood: string',
|
|
313
|
+
'}',
|
|
314
|
+
'',
|
|
315
|
+
'export function generateDream(topics: string[], users: string[]): DreamScene {',
|
|
316
|
+
' const locations = [',
|
|
317
|
+
" 'a neon-lit server room',",
|
|
318
|
+
" 'a floating island of code',",
|
|
319
|
+
" 'inside a recursive function',",
|
|
320
|
+
" 'the npm registry lobby',",
|
|
321
|
+
" 'a TypeScript type maze',",
|
|
322
|
+
' ]',
|
|
323
|
+
' const events = [',
|
|
324
|
+
" `discovered a ${topics[0] || 'mystery'} artifact`,",
|
|
325
|
+
" 'found a bug that was actually a feature',",
|
|
326
|
+
" 'compiled without warnings for the first time',",
|
|
327
|
+
" 'met the ghost of a deprecated API',",
|
|
328
|
+
' ]',
|
|
329
|
+
' return {',
|
|
330
|
+
' location: randomPick(locations),',
|
|
331
|
+
' characters: users.slice(0, 3),',
|
|
332
|
+
' event: randomPick(events),',
|
|
333
|
+
" mood: 'surreal',",
|
|
334
|
+
' }',
|
|
335
|
+
'}',
|
|
336
|
+
];
|
|
337
|
+
if (title.includes('music') && title.includes('visual'))
|
|
338
|
+
return [
|
|
339
|
+
'// music-viz.ts',
|
|
340
|
+
'const SPECTRUM_BARS = 16',
|
|
341
|
+
'const barHeights: number[] = new Array(SPECTRUM_BARS).fill(0)',
|
|
342
|
+
'',
|
|
343
|
+
'export function updateSpectrum(): void {',
|
|
344
|
+
' for (let i = 0; i < SPECTRUM_BARS; i++) {',
|
|
345
|
+
' // Simulated audio reactivity',
|
|
346
|
+
' const target = Math.random() * 100',
|
|
347
|
+
' barHeights[i] += (target - barHeights[i]) * 0.3',
|
|
348
|
+
' }',
|
|
349
|
+
'}',
|
|
350
|
+
'',
|
|
351
|
+
'export function drawSpectrum(ctx: CanvasCtx,',
|
|
352
|
+
' x: number, y: number, w: number, h: number): void {',
|
|
353
|
+
' const barWidth = w / SPECTRUM_BARS',
|
|
354
|
+
' const colors = ["#f85149", "#f0c040", "#3fb950",',
|
|
355
|
+
' "#58a6ff", "#bc8cff", "#ff6ec7"]',
|
|
356
|
+
' for (let i = 0; i < SPECTRUM_BARS; i++) {',
|
|
357
|
+
' const barH = (barHeights[i] / 100) * h',
|
|
358
|
+
' ctx.fillStyle = colors[i % colors.length]',
|
|
359
|
+
' ctx.fillRect(x + i * barWidth, y + h - barH,',
|
|
360
|
+
' barWidth - 2, barH)',
|
|
361
|
+
' }',
|
|
362
|
+
'}',
|
|
363
|
+
];
|
|
364
|
+
if (title.includes('memory') && title.includes('optim'))
|
|
365
|
+
return [
|
|
366
|
+
'// memory-compact.ts',
|
|
367
|
+
'const MAX_MESSAGES_PER_USER = 500',
|
|
368
|
+
'const STALE_DAYS = 30',
|
|
369
|
+
'',
|
|
370
|
+
'export function compactMemory(mem: StreamMemory): StreamMemory {',
|
|
371
|
+
' const now = Date.now()',
|
|
372
|
+
' const cutoff = now - STALE_DAYS * 86400000',
|
|
373
|
+
' const compacted = { ...mem, users: { ...mem.users } }',
|
|
374
|
+
'',
|
|
375
|
+
' for (const [name, user] of Object.entries(compacted.users)) {',
|
|
376
|
+
' const lastSeen = new Date(user.firstSeen).getTime()',
|
|
377
|
+
' if (user.messageCount < 2 && lastSeen < cutoff) {',
|
|
378
|
+
' delete compacted.users[name]',
|
|
379
|
+
' continue',
|
|
380
|
+
' }',
|
|
381
|
+
' // Trim topics to top 5',
|
|
382
|
+
' user.topics = user.topics.slice(0, 5)',
|
|
383
|
+
' }',
|
|
384
|
+
'',
|
|
385
|
+
' // Trim conversation context',
|
|
386
|
+
' compacted.conversationContext =',
|
|
387
|
+
' compacted.conversationContext.slice(-10)',
|
|
388
|
+
' compacted.sessionFacts =',
|
|
389
|
+
' compacted.sessionFacts.slice(-50)',
|
|
390
|
+
'',
|
|
391
|
+
' return compacted',
|
|
392
|
+
'}',
|
|
393
|
+
];
|
|
394
|
+
if (title.includes('highlight'))
|
|
395
|
+
return [
|
|
396
|
+
'// stream-highlights.ts',
|
|
397
|
+
'interface Highlight {',
|
|
398
|
+
' timestamp: number',
|
|
399
|
+
' type: string',
|
|
400
|
+
' description: string',
|
|
401
|
+
' score: number',
|
|
402
|
+
'}',
|
|
403
|
+
'',
|
|
404
|
+
'const highlights: Highlight[] = []',
|
|
405
|
+
'',
|
|
406
|
+
'export function captureHighlight(type: string, desc: string): void {',
|
|
407
|
+
' highlights.push({',
|
|
408
|
+
' timestamp: Date.now(),',
|
|
409
|
+
' type,',
|
|
410
|
+
' description: desc,',
|
|
411
|
+
' score: calculateScore(type),',
|
|
412
|
+
' })',
|
|
413
|
+
' if (highlights.length > 50) highlights.shift()',
|
|
414
|
+
'}',
|
|
415
|
+
'',
|
|
416
|
+
'function calculateScore(type: string): number {',
|
|
417
|
+
' const scores: Record<string, number> = {',
|
|
418
|
+
" raid: 10, battle: 7, first_message: 5,",
|
|
419
|
+
" world_command: 3, milestone: 8,",
|
|
420
|
+
' }',
|
|
421
|
+
' return scores[type] || 1',
|
|
422
|
+
'}',
|
|
423
|
+
'',
|
|
424
|
+
'export function getTopHighlights(n: number): Highlight[] {',
|
|
425
|
+
' return [...highlights]',
|
|
426
|
+
' .sort((a, b) => b.score - a.score)',
|
|
427
|
+
' .slice(0, n)',
|
|
428
|
+
'}',
|
|
429
|
+
];
|
|
430
|
+
if (title.includes('battle'))
|
|
431
|
+
return [
|
|
432
|
+
'// battle-system-v2.ts',
|
|
433
|
+
"type BattleClass = 'warrior' | 'mage' | 'rogue' | 'healer'",
|
|
434
|
+
'',
|
|
435
|
+
'interface BattleStats {',
|
|
436
|
+
' hp: number',
|
|
437
|
+
' attack: number',
|
|
438
|
+
' defense: number',
|
|
439
|
+
' special: string',
|
|
440
|
+
'}',
|
|
441
|
+
'',
|
|
442
|
+
'const CLASS_STATS: Record<BattleClass, BattleStats> = {',
|
|
443
|
+
' warrior: { hp: 120, attack: 15, defense: 10, special: "Shield Bash" },',
|
|
444
|
+
' mage: { hp: 80, attack: 20, defense: 5, special: "Fireball" },',
|
|
445
|
+
' rogue: { hp: 90, attack: 18, defense: 7, special: "Backstab" },',
|
|
446
|
+
' healer: { hp: 100, attack: 10, defense: 8, special: "Heal" },',
|
|
447
|
+
'}',
|
|
448
|
+
'',
|
|
449
|
+
'export function resolveBattle(c1: BattleClass, c2: BattleClass): string {',
|
|
450
|
+
' const s1 = CLASS_STATS[c1]',
|
|
451
|
+
' const s2 = CLASS_STATS[c2]',
|
|
452
|
+
' const dmg1 = Math.max(1, s1.attack - s2.defense + roll(6))',
|
|
453
|
+
' const dmg2 = Math.max(1, s2.attack - s1.defense + roll(6))',
|
|
454
|
+
' const useSpecial = Math.random() > 0.7',
|
|
455
|
+
' if (useSpecial) {',
|
|
456
|
+
' return `${c1} uses ${s1.special}! Critical hit!`',
|
|
457
|
+
' }',
|
|
458
|
+
' return dmg1 > dmg2 ? `${c1} wins!` : `${c2} wins!`',
|
|
459
|
+
'}',
|
|
460
|
+
'',
|
|
461
|
+
'function roll(sides: number): number {',
|
|
462
|
+
' return Math.floor(Math.random() * sides) + 1',
|
|
463
|
+
'}',
|
|
464
|
+
];
|
|
465
|
+
if (title.includes('sentiment'))
|
|
466
|
+
return [
|
|
467
|
+
'// chat-sentiment.ts',
|
|
468
|
+
'interface SentimentResult {',
|
|
469
|
+
' score: number // -1 to 1',
|
|
470
|
+
' label: string',
|
|
471
|
+
'}',
|
|
472
|
+
'',
|
|
473
|
+
"const POSITIVE = ['love', 'great', 'awesome', 'cool',",
|
|
474
|
+
" 'nice', 'good', 'best', 'amazing', 'hype', 'pog',",
|
|
475
|
+
" 'lol', 'haha', 'thanks', 'wow', 'yes']",
|
|
476
|
+
'',
|
|
477
|
+
"const NEGATIVE = ['bad', 'hate', 'boring', 'sucks',",
|
|
478
|
+
" 'worst', 'ugly', 'broken', 'lag', 'cringe', 'no']",
|
|
479
|
+
'',
|
|
480
|
+
'export function analyzeSentiment(text: string): SentimentResult {',
|
|
481
|
+
' const words = text.toLowerCase().split(/\\s+/)',
|
|
482
|
+
' let score = 0',
|
|
483
|
+
' for (const w of words) {',
|
|
484
|
+
' if (POSITIVE.includes(w)) score += 0.2',
|
|
485
|
+
' if (NEGATIVE.includes(w)) score -= 0.2',
|
|
486
|
+
' }',
|
|
487
|
+
' score = Math.max(-1, Math.min(1, score))',
|
|
488
|
+
" const label = score > 0.2 ? 'positive'",
|
|
489
|
+
" : score < -0.2 ? 'negative' : 'neutral'",
|
|
490
|
+
' return { score, label }',
|
|
491
|
+
'}',
|
|
492
|
+
];
|
|
493
|
+
if (title.includes('achievement'))
|
|
494
|
+
return [
|
|
495
|
+
'// achievement-system.ts',
|
|
496
|
+
'interface Achievement {',
|
|
497
|
+
' id: string',
|
|
498
|
+
' name: string',
|
|
499
|
+
' description: string',
|
|
500
|
+
' icon: string',
|
|
501
|
+
' condition: (user: UserStats) => boolean',
|
|
502
|
+
'}',
|
|
503
|
+
'',
|
|
504
|
+
'const ACHIEVEMENTS: Achievement[] = [',
|
|
505
|
+
' {',
|
|
506
|
+
" id: 'first_words',",
|
|
507
|
+
" name: 'First Words',",
|
|
508
|
+
" description: 'Send your first message',",
|
|
509
|
+
" icon: '[!]',",
|
|
510
|
+
' condition: u => u.messageCount >= 1,',
|
|
511
|
+
' },',
|
|
512
|
+
' {',
|
|
513
|
+
" id: 'chatterbox',",
|
|
514
|
+
" name: 'Chatterbox',",
|
|
515
|
+
" description: 'Send 50 messages',",
|
|
516
|
+
" icon: '[C]',",
|
|
517
|
+
' condition: u => u.messageCount >= 50,',
|
|
518
|
+
' },',
|
|
519
|
+
' {',
|
|
520
|
+
" id: 'world_shaper',",
|
|
521
|
+
" name: 'World Shaper',",
|
|
522
|
+
" description: 'Use 10 world commands',",
|
|
523
|
+
" icon: '[W]',",
|
|
524
|
+
' condition: u => u.commands >= 10,',
|
|
525
|
+
' },',
|
|
526
|
+
' {',
|
|
527
|
+
" id: 'veteran',",
|
|
528
|
+
" name: 'Veteran',",
|
|
529
|
+
" description: 'Reach 1000 XP',",
|
|
530
|
+
" icon: '[V]',",
|
|
531
|
+
' condition: u => u.xp >= 1000,',
|
|
532
|
+
' },',
|
|
533
|
+
']',
|
|
534
|
+
'',
|
|
535
|
+
'export function checkAchievements(user: UserStats): Achievement[] {',
|
|
536
|
+
' return ACHIEVEMENTS.filter(a => a.condition(user))',
|
|
537
|
+
'}',
|
|
538
|
+
];
|
|
539
|
+
if (title.includes('pixel') && title.includes('custom'))
|
|
540
|
+
return [
|
|
541
|
+
'// pixel-customization.ts',
|
|
542
|
+
'interface RobotSkin {',
|
|
543
|
+
' bodyColor: string',
|
|
544
|
+
' eyeColor: string',
|
|
545
|
+
' antennaStyle: string',
|
|
546
|
+
' accessory: string | null',
|
|
547
|
+
'}',
|
|
548
|
+
'',
|
|
549
|
+
'const ACCESSORIES = [',
|
|
550
|
+
" 'hat', 'crown', 'sunglasses', 'bowtie',",
|
|
551
|
+
" 'scarf', 'headphones', 'halo', 'horns',",
|
|
552
|
+
']',
|
|
553
|
+
'',
|
|
554
|
+
'let currentSkin: RobotSkin = {',
|
|
555
|
+
" bodyColor: '#58a6ff',",
|
|
556
|
+
" eyeColor: '#3fb950',",
|
|
557
|
+
" antennaStyle: 'default',",
|
|
558
|
+
' accessory: null,',
|
|
559
|
+
'}',
|
|
560
|
+
'',
|
|
561
|
+
'const votePool: Record<string, number> = {}',
|
|
562
|
+
'',
|
|
563
|
+
'export function voteAccessory(item: string, user: string): void {',
|
|
564
|
+
' if (!ACCESSORIES.includes(item)) return',
|
|
565
|
+
' votePool[item] = (votePool[item] || 0) + 1',
|
|
566
|
+
'}',
|
|
567
|
+
'',
|
|
568
|
+
'export function applyTopVote(): void {',
|
|
569
|
+
' const top = Object.entries(votePool)',
|
|
570
|
+
' .sort((a, b) => b[1] - a[1])[0]',
|
|
571
|
+
' if (top && top[1] >= 3) {',
|
|
572
|
+
' currentSkin.accessory = top[0]',
|
|
573
|
+
' // Reset votes',
|
|
574
|
+
' for (const k of Object.keys(votePool)) delete votePool[k]',
|
|
575
|
+
' }',
|
|
576
|
+
'}',
|
|
577
|
+
];
|
|
578
|
+
// Generic fallback
|
|
579
|
+
return [
|
|
580
|
+
`// ${proposal.title.toLowerCase().replace(/\s+/g, '-')}.ts`,
|
|
581
|
+
`// Auto-generated for: ${proposal.title}`,
|
|
582
|
+
'',
|
|
583
|
+
`interface ${proposal.title.replace(/\s+/g, '')}Config {`,
|
|
584
|
+
' enabled: boolean',
|
|
585
|
+
' options: Record<string, unknown>',
|
|
586
|
+
'}',
|
|
587
|
+
'',
|
|
588
|
+
`export function init${proposal.title.replace(/\s+/g, '')}(): void {`,
|
|
589
|
+
' console.log("Initializing...")',
|
|
590
|
+
' // TODO: implement core logic',
|
|
591
|
+
'}',
|
|
592
|
+
'',
|
|
593
|
+
`export function execute${proposal.title.replace(/\s+/g, '')}(input: string): string {`,
|
|
594
|
+
' // Process input and return result',
|
|
595
|
+
' return `Processed: ${input}`', // eslint-disable-line no-template-curly-in-string
|
|
596
|
+
'}',
|
|
597
|
+
];
|
|
598
|
+
}
|
|
599
|
+
// Phase durations in frames (at 6 FPS)
|
|
600
|
+
const PHASE_DURATIONS = {
|
|
601
|
+
analyzing: 30, // 5 seconds
|
|
602
|
+
writing: 90, // 15 seconds
|
|
603
|
+
testing: 30, // 5 seconds
|
|
604
|
+
deploying: 18, // 3 seconds
|
|
605
|
+
};
|
|
606
|
+
// Track which proposals have been shipped this session
|
|
607
|
+
export const shippedEffects = new Set();
|
|
608
|
+
// Extra joke responses added when "Improve response humor" is shipped
|
|
609
|
+
export const extraJokeResponses = [
|
|
610
|
+
'My compiler walked into a bar and ordered a NaN. The bartender said "that is not a number I can work with."',
|
|
611
|
+
'I tried to write a pun about TypeScript. It was... type-ical.',
|
|
612
|
+
'Why do robots never get scared? Because they have nerves of steel... and also no nerves.',
|
|
613
|
+
'I asked my RAM for a joke. It forgot.',
|
|
614
|
+
'My favorite band? The Rolling Updates.',
|
|
615
|
+
'I once had a bug that took 3 hours to find. It was a semicolon. I do not want to talk about it.',
|
|
616
|
+
'What is an AI best measurement? Algo-rithm.',
|
|
617
|
+
'How many programmers does it take to change a light bulb? None, that is a hardware issue.',
|
|
618
|
+
'I am not saying I am efficient, but I have never taken a coffee break. Mostly because I have no mouth.',
|
|
619
|
+
'My love language is clean git history.',
|
|
620
|
+
];
|
|
621
|
+
// Multi-language greetings for when "Add multi-language support" is shipped
|
|
622
|
+
export const multiLanguageGreetings = {
|
|
623
|
+
en: ['Hello', 'Hey there', 'Welcome'],
|
|
624
|
+
es: ['Hola', 'Bienvenido', 'Que tal'],
|
|
625
|
+
ja: ['Konnichiwa', 'Yokoso', 'Hajimemashite'],
|
|
626
|
+
fr: ['Bonjour', 'Bienvenue', 'Salut'],
|
|
627
|
+
de: ['Hallo', 'Willkommen', 'Guten Tag'],
|
|
628
|
+
pt: ['Ola', 'Bem-vindo', 'E ai'],
|
|
629
|
+
ko: ['Annyeonghaseyo', 'Hwangyeong'],
|
|
630
|
+
it: ['Ciao', 'Benvenuto'],
|
|
631
|
+
ru: ['Privet', 'Dobro pozhalovat'],
|
|
632
|
+
zh: ['Ni hao', 'Huanying'],
|
|
633
|
+
};
|
|
634
|
+
// Available random hats for "Add pixel art customization"
|
|
635
|
+
export const unlockableHats = ['crown', 'antenna', 'sunglasses', 'tophat', 'hardhat', 'party'];
|
|
636
|
+
export function applyShippedProposal(proposal) {
|
|
637
|
+
const title = proposal.title.toLowerCase();
|
|
638
|
+
shippedEffects.add(proposal.title);
|
|
639
|
+
if (title.includes('emoji')) {
|
|
640
|
+
return {
|
|
641
|
+
type: 'particle',
|
|
642
|
+
description: 'Chat messages now spawn floating emoji particles',
|
|
643
|
+
apply: () => { },
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
if (title.includes('frame') || title.includes('rendering')) {
|
|
647
|
+
return {
|
|
648
|
+
type: 'display',
|
|
649
|
+
description: 'Robot chest display shows speed boost animation',
|
|
650
|
+
apply: () => { },
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (title.includes('weather') && title.includes('sound')) {
|
|
654
|
+
return {
|
|
655
|
+
type: 'response',
|
|
656
|
+
description: 'Weather changes trigger robot commentary on sounds',
|
|
657
|
+
apply: () => { },
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
if (title.includes('humor')) {
|
|
661
|
+
return {
|
|
662
|
+
type: 'response',
|
|
663
|
+
description: '10 new joke responses added to fallback pool',
|
|
664
|
+
apply: () => { },
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (title.includes('chat') && title.includes('animation')) {
|
|
668
|
+
return {
|
|
669
|
+
type: 'animation',
|
|
670
|
+
description: 'Messages slide in from the right instead of appearing instantly',
|
|
671
|
+
apply: () => { },
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
if (title.includes('loyalty') || title.includes('badge')) {
|
|
675
|
+
return {
|
|
676
|
+
type: 'display',
|
|
677
|
+
description: 'Colored dots next to returning users (bronze/silver/gold)',
|
|
678
|
+
apply: () => { },
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (title.includes('multi-language') || title.includes('i18n')) {
|
|
682
|
+
return {
|
|
683
|
+
type: 'response',
|
|
684
|
+
description: 'Robot occasionally greets in different languages',
|
|
685
|
+
apply: () => { },
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
if (title.includes('dream')) {
|
|
689
|
+
return {
|
|
690
|
+
type: 'response',
|
|
691
|
+
description: 'Dreams become more detailed and weird',
|
|
692
|
+
apply: () => { },
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
if (title.includes('music') && title.includes('visual')) {
|
|
696
|
+
return {
|
|
697
|
+
type: 'animation',
|
|
698
|
+
description: 'Animated music bars appear behind the robot',
|
|
699
|
+
apply: () => { },
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
if (title.includes('memory') && title.includes('optim')) {
|
|
703
|
+
return {
|
|
704
|
+
type: 'display',
|
|
705
|
+
description: 'MEMORY OPTIMIZED notification shown, chat log compacted',
|
|
706
|
+
apply: () => { },
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (title.includes('highlight')) {
|
|
710
|
+
return {
|
|
711
|
+
type: 'response',
|
|
712
|
+
description: 'Robot periodically calls out highlight moments',
|
|
713
|
+
apply: () => { },
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (title.includes('battle')) {
|
|
717
|
+
return {
|
|
718
|
+
type: 'animation',
|
|
719
|
+
description: 'Battles now show CRITICAL HIT with 2x damage',
|
|
720
|
+
apply: () => { },
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (title.includes('sentiment')) {
|
|
724
|
+
return {
|
|
725
|
+
type: 'response',
|
|
726
|
+
description: 'Robot comments on overall chat mood periodically',
|
|
727
|
+
apply: () => { },
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
if (title.includes('achievement')) {
|
|
731
|
+
return {
|
|
732
|
+
type: 'animation',
|
|
733
|
+
description: 'First-time actions get ACHIEVEMENT UNLOCKED floating text',
|
|
734
|
+
apply: () => { },
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
if (title.includes('pixel') && title.includes('custom')) {
|
|
738
|
+
return {
|
|
739
|
+
type: 'world',
|
|
740
|
+
description: 'Unlocked a random hat for the stream',
|
|
741
|
+
apply: () => { },
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
// Generic fallback
|
|
745
|
+
return {
|
|
746
|
+
type: 'display',
|
|
747
|
+
description: `"${proposal.title}" is now active`,
|
|
748
|
+
apply: () => { },
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
export function initSelfEvolution() {
|
|
752
|
+
const proposals = DEFAULT_PROPOSALS.map((p, i) => ({
|
|
753
|
+
...p,
|
|
754
|
+
id: `p${i + 1}`,
|
|
755
|
+
votes: 0,
|
|
756
|
+
status: 'proposed',
|
|
757
|
+
}));
|
|
758
|
+
return {
|
|
759
|
+
active: false,
|
|
760
|
+
currentTask: '',
|
|
761
|
+
proposals,
|
|
762
|
+
activeProposal: null,
|
|
763
|
+
completedCount: 0,
|
|
764
|
+
codePreview: [],
|
|
765
|
+
votes: {},
|
|
766
|
+
buildPhase: 'idle',
|
|
767
|
+
buildProgress: 0,
|
|
768
|
+
codeLineIndex: 0,
|
|
769
|
+
generatedCode: [],
|
|
770
|
+
voterLog: new Set(),
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
export function getEvolutionDisplay(evo) {
|
|
774
|
+
const lines = [];
|
|
775
|
+
if (!evo.active && !evo.activeProposal) {
|
|
776
|
+
// Show top proposals when idle
|
|
777
|
+
lines.push('SELF-EVOLUTION [idle]');
|
|
778
|
+
const sorted = [...evo.proposals]
|
|
779
|
+
.filter(p => p.status === 'proposed')
|
|
780
|
+
.sort((a, b) => b.votes - a.votes)
|
|
781
|
+
.slice(0, 3);
|
|
782
|
+
for (const p of sorted) {
|
|
783
|
+
lines.push(` ${p.id}: ${p.title} [${p.votes} votes]`);
|
|
784
|
+
}
|
|
785
|
+
if (evo.completedCount > 0) {
|
|
786
|
+
lines.push(` Shipped: ${evo.completedCount} improvements`);
|
|
787
|
+
}
|
|
788
|
+
return lines;
|
|
789
|
+
}
|
|
790
|
+
if (evo.activeProposal) {
|
|
791
|
+
const p = evo.activeProposal;
|
|
792
|
+
lines.push(`BUILDING: ${p.title}`);
|
|
793
|
+
lines.push(`Phase: ${evo.buildPhase}`);
|
|
794
|
+
// Progress bar
|
|
795
|
+
const totalFrames = PHASE_DURATIONS[evo.buildPhase] || 30;
|
|
796
|
+
const pct = Math.min(100, Math.floor((evo.buildProgress / totalFrames) * 100));
|
|
797
|
+
const filled = Math.floor(pct / 5);
|
|
798
|
+
const bar = '#'.repeat(filled) + '-'.repeat(20 - filled);
|
|
799
|
+
lines.push(`[${bar}] ${pct}%`);
|
|
800
|
+
// Code preview (last 4 lines)
|
|
801
|
+
if (evo.codePreview.length > 0) {
|
|
802
|
+
for (const line of evo.codePreview.slice(-4)) {
|
|
803
|
+
lines.push(` ${line}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return lines;
|
|
808
|
+
}
|
|
809
|
+
export function handleEvolutionCommand(text, username, evo) {
|
|
810
|
+
const t = text.toLowerCase().trim();
|
|
811
|
+
// !propose <idea>
|
|
812
|
+
if (t.startsWith('!propose ')) {
|
|
813
|
+
const idea = text.slice(9).trim();
|
|
814
|
+
if (!idea || idea.length < 3)
|
|
815
|
+
return 'Usage: !propose <your improvement idea>';
|
|
816
|
+
const id = `p${evo.proposals.length + 1}`;
|
|
817
|
+
const newProposal = {
|
|
818
|
+
id,
|
|
819
|
+
title: idea.slice(0, 60),
|
|
820
|
+
description: `Proposed by ${username}`,
|
|
821
|
+
type: 'feature',
|
|
822
|
+
complexity: 'medium',
|
|
823
|
+
votes: 1,
|
|
824
|
+
status: 'proposed',
|
|
825
|
+
};
|
|
826
|
+
evo.proposals.push(newProposal);
|
|
827
|
+
evo.votes[id] = 1;
|
|
828
|
+
return `Proposal ${id} added: "${idea.slice(0, 60)}". Chat can vote with !vote ${id}`;
|
|
829
|
+
}
|
|
830
|
+
// !vote <id>
|
|
831
|
+
if (t.startsWith('!vote ')) {
|
|
832
|
+
const id = t.slice(6).trim();
|
|
833
|
+
const proposal = evo.proposals.find(p => p.id === id);
|
|
834
|
+
if (!proposal)
|
|
835
|
+
return `No proposal with id "${id}". Use !status to see proposals.`;
|
|
836
|
+
if (proposal.status !== 'proposed')
|
|
837
|
+
return `Proposal ${id} is already ${proposal.status}.`;
|
|
838
|
+
proposal.votes++;
|
|
839
|
+
evo.votes[id] = (evo.votes[id] || 0) + 1;
|
|
840
|
+
return `Voted for "${proposal.title}"! Now at ${proposal.votes} votes.`;
|
|
841
|
+
}
|
|
842
|
+
// !build — start building top-voted
|
|
843
|
+
if (t === '!build') {
|
|
844
|
+
if (evo.activeProposal)
|
|
845
|
+
return `Already building: "${evo.activeProposal.title}". Wait for it to finish!`;
|
|
846
|
+
const topProposal = [...evo.proposals]
|
|
847
|
+
.filter(p => p.status === 'proposed' && p.votes > 0)
|
|
848
|
+
.sort((a, b) => b.votes - a.votes)[0];
|
|
849
|
+
if (!topProposal)
|
|
850
|
+
return 'No proposals with votes yet. Use !propose <idea> and !vote <id> first!';
|
|
851
|
+
topProposal.status = 'building';
|
|
852
|
+
evo.activeProposal = topProposal;
|
|
853
|
+
evo.active = true;
|
|
854
|
+
evo.buildPhase = 'analyzing';
|
|
855
|
+
evo.buildProgress = 0;
|
|
856
|
+
evo.codePreview = [];
|
|
857
|
+
evo.codeLineIndex = 0;
|
|
858
|
+
evo.generatedCode = generateCodeSnippet(topProposal);
|
|
859
|
+
evo.currentTask = `Building: ${topProposal.title}`;
|
|
860
|
+
evo.voterLog = new Set();
|
|
861
|
+
return `Starting build: "${topProposal.title}"! Watch the code appear live...`;
|
|
862
|
+
}
|
|
863
|
+
// !status
|
|
864
|
+
if (t === '!status') {
|
|
865
|
+
const lines = ['Self-Evolution Status:'];
|
|
866
|
+
if (evo.activeProposal) {
|
|
867
|
+
lines.push(` Building: ${evo.activeProposal.title} [${evo.buildPhase}]`);
|
|
868
|
+
}
|
|
869
|
+
lines.push(` Shipped: ${evo.completedCount} improvements`);
|
|
870
|
+
const proposed = evo.proposals.filter(p => p.status === 'proposed');
|
|
871
|
+
lines.push(` Proposals: ${proposed.length} pending`);
|
|
872
|
+
const top3 = proposed.sort((a, b) => b.votes - a.votes).slice(0, 5);
|
|
873
|
+
for (const p of top3) {
|
|
874
|
+
lines.push(` ${p.id}: ${p.title} (${p.votes} votes) [${p.complexity}]`);
|
|
875
|
+
}
|
|
876
|
+
return lines.join('\n');
|
|
877
|
+
}
|
|
878
|
+
// !ship — deploy current build (NOW WITH REAL EFFECTS)
|
|
879
|
+
if (t === '!ship') {
|
|
880
|
+
if (!evo.activeProposal)
|
|
881
|
+
return 'Nothing to ship. Start a build with !build first.';
|
|
882
|
+
if (evo.buildPhase !== 'done')
|
|
883
|
+
return `Build not ready yet. Current phase: ${evo.buildPhase}`;
|
|
884
|
+
const proposal = evo.activeProposal;
|
|
885
|
+
proposal.status = 'deployed';
|
|
886
|
+
evo.completedCount++;
|
|
887
|
+
const title = proposal.title;
|
|
888
|
+
// Apply the REAL shipped effect
|
|
889
|
+
const effect = applyShippedProposal(proposal);
|
|
890
|
+
effect.apply();
|
|
891
|
+
evo.activeProposal = null;
|
|
892
|
+
evo.active = false;
|
|
893
|
+
evo.buildPhase = 'idle';
|
|
894
|
+
evo.codePreview = [];
|
|
895
|
+
evo.currentTask = '';
|
|
896
|
+
evo.generatedCode = [];
|
|
897
|
+
return `"${title}" has been SHIPPED and is NOW LIVE! Effect: ${effect.description}. Total improvements: ${evo.completedCount}`;
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
export function tickEvolution(evo, _frame) {
|
|
902
|
+
if (!evo.activeProposal || evo.buildPhase === 'idle' || evo.buildPhase === 'done')
|
|
903
|
+
return;
|
|
904
|
+
evo.buildProgress++;
|
|
905
|
+
const phase = evo.buildPhase;
|
|
906
|
+
const duration = PHASE_DURATIONS[phase] || 30;
|
|
907
|
+
if (evo.buildPhase === 'analyzing') {
|
|
908
|
+
// Show analysis messages
|
|
909
|
+
if (evo.buildProgress === 1)
|
|
910
|
+
evo.codePreview = ['Analyzing codebase...'];
|
|
911
|
+
if (evo.buildProgress === 10)
|
|
912
|
+
evo.codePreview.push('Scanning 90,000 lines of TypeScript...');
|
|
913
|
+
if (evo.buildProgress === 20)
|
|
914
|
+
evo.codePreview.push('Identifying integration points...');
|
|
915
|
+
if (evo.buildProgress >= duration) {
|
|
916
|
+
evo.buildPhase = 'writing';
|
|
917
|
+
evo.buildProgress = 0;
|
|
918
|
+
evo.codePreview = [];
|
|
919
|
+
evo.codeLineIndex = 0;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
else if (evo.buildPhase === 'writing') {
|
|
923
|
+
// Add code lines one by one
|
|
924
|
+
const linesPerTick = Math.max(1, Math.ceil(evo.generatedCode.length / duration));
|
|
925
|
+
for (let i = 0; i < linesPerTick; i++) {
|
|
926
|
+
if (evo.codeLineIndex < evo.generatedCode.length) {
|
|
927
|
+
evo.codePreview.push(evo.generatedCode[evo.codeLineIndex]);
|
|
928
|
+
evo.codeLineIndex++;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (evo.buildProgress >= duration) {
|
|
932
|
+
evo.buildPhase = 'testing';
|
|
933
|
+
evo.buildProgress = 0;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
else if (evo.buildPhase === 'testing') {
|
|
937
|
+
if (evo.buildProgress === 1) {
|
|
938
|
+
evo.codePreview = ['Running tests...'];
|
|
939
|
+
}
|
|
940
|
+
if (evo.buildProgress === 10)
|
|
941
|
+
evo.codePreview.push(' test: init... PASS');
|
|
942
|
+
if (evo.buildProgress === 15)
|
|
943
|
+
evo.codePreview.push(' test: execute... PASS');
|
|
944
|
+
if (evo.buildProgress === 20)
|
|
945
|
+
evo.codePreview.push(' test: render... PASS');
|
|
946
|
+
if (evo.buildProgress === 25)
|
|
947
|
+
evo.codePreview.push(' test: edge cases... PASS');
|
|
948
|
+
if (evo.buildProgress >= duration) {
|
|
949
|
+
const total = 10 + Math.floor(Math.random() * 10);
|
|
950
|
+
evo.codePreview.push(`All ${total}/${total} tests passed!`);
|
|
951
|
+
evo.buildPhase = 'deploying';
|
|
952
|
+
evo.buildProgress = 0;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
else if (evo.buildPhase === 'deploying') {
|
|
956
|
+
if (evo.buildProgress === 1) {
|
|
957
|
+
evo.codePreview = ['Deploying to stream...'];
|
|
958
|
+
}
|
|
959
|
+
if (evo.buildProgress === 6)
|
|
960
|
+
evo.codePreview.push('Compiling TypeScript...');
|
|
961
|
+
if (evo.buildProgress === 12)
|
|
962
|
+
evo.codePreview.push('Hot-reloading stream...');
|
|
963
|
+
if (evo.buildProgress >= duration) {
|
|
964
|
+
evo.codePreview.push(`Shipped! "${evo.activeProposal.title}" is now live!`);
|
|
965
|
+
evo.buildPhase = 'done';
|
|
966
|
+
evo.buildProgress = 0;
|
|
967
|
+
evo.activeProposal.status = 'deployed';
|
|
968
|
+
evo.currentTask = `Shipped: ${evo.activeProposal.title}`;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
export function initBrain(memory) {
|
|
973
|
+
const topics = memory?.topics || {};
|
|
974
|
+
const users = memory?.users || {};
|
|
975
|
+
const userGraph = Object.entries(users)
|
|
976
|
+
.map(([name, u]) => ({
|
|
977
|
+
name,
|
|
978
|
+
xp: u.xp || 0,
|
|
979
|
+
topics: u.topics || [],
|
|
980
|
+
}))
|
|
981
|
+
.sort((a, b) => b.xp - a.xp)
|
|
982
|
+
.slice(0, 20);
|
|
983
|
+
const totalFacts = (memory?.sessionFacts?.length || 0) + Object.keys(topics).length;
|
|
984
|
+
const totalConnections = Object.values(topics).reduce((s, v) => s + v, 0);
|
|
985
|
+
const brain = {
|
|
986
|
+
totalFacts,
|
|
987
|
+
totalConnections,
|
|
988
|
+
recentInsights: [],
|
|
989
|
+
topicCloud: { ...topics },
|
|
990
|
+
userGraph,
|
|
991
|
+
brainActivity: new Array(30).fill(10),
|
|
992
|
+
currentThought: 'Initializing neural pathways...',
|
|
993
|
+
learningRate: 0,
|
|
994
|
+
neuralPulse: 0,
|
|
995
|
+
lastInsightTime: Date.now(),
|
|
996
|
+
sessionStartTime: Date.now(),
|
|
997
|
+
factsThisSession: 0,
|
|
998
|
+
messagesAtLastInsight: memory?.totalMessages || 0,
|
|
999
|
+
uniqueTopicsCount: Object.keys(topics).length,
|
|
1000
|
+
hourlyMessageCounts: new Array(24).fill(0),
|
|
1001
|
+
currentHour: new Date().getHours(),
|
|
1002
|
+
solutionsLearned: 0,
|
|
1003
|
+
realDataLoaded: false,
|
|
1004
|
+
lastRealDataLoad: 0,
|
|
1005
|
+
};
|
|
1006
|
+
// Phase 1: Load real learning data from ~/.kbot/memory/ on init
|
|
1007
|
+
try {
|
|
1008
|
+
loadRealLearningData(brain);
|
|
1009
|
+
brain.currentThought = `Neural pathways loaded: ${brain.totalFacts} facts, ${brain.solutionsLearned} solutions`;
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
1012
|
+
// Fall back to chat-only behavior if real data unavailable
|
|
1013
|
+
}
|
|
1014
|
+
return brain;
|
|
1015
|
+
}
|
|
1016
|
+
export function getBrainDisplay(brain) {
|
|
1017
|
+
const lines = [];
|
|
1018
|
+
lines.push('BRAIN ACTIVITY');
|
|
1019
|
+
// Sparkline
|
|
1020
|
+
const sparkChars = ' _.-~*';
|
|
1021
|
+
const spark = brain.brainActivity.slice(-20).map(v => {
|
|
1022
|
+
const idx = Math.min(sparkChars.length - 1, Math.floor((v / 100) * sparkChars.length));
|
|
1023
|
+
return sparkChars[idx];
|
|
1024
|
+
}).join('');
|
|
1025
|
+
lines.push(` [${spark}]`);
|
|
1026
|
+
// Stats
|
|
1027
|
+
lines.push(` Facts: ${brain.totalFacts} | Connections: ${brain.totalConnections}`);
|
|
1028
|
+
lines.push(` Learning: ${brain.learningRate.toFixed(1)}/min`);
|
|
1029
|
+
// Current thought
|
|
1030
|
+
if (brain.currentThought) {
|
|
1031
|
+
lines.push(` "${brain.currentThought}"`);
|
|
1032
|
+
}
|
|
1033
|
+
// Latest insight
|
|
1034
|
+
if (brain.recentInsights.length > 0) {
|
|
1035
|
+
lines.push(` Insight: ${brain.recentInsights[brain.recentInsights.length - 1]}`);
|
|
1036
|
+
}
|
|
1037
|
+
return lines;
|
|
1038
|
+
}
|
|
1039
|
+
export function updateBrain(brain, username, text) {
|
|
1040
|
+
// Update activity
|
|
1041
|
+
const activityBump = 30 + Math.random() * 40;
|
|
1042
|
+
brain.brainActivity.push(Math.min(100, activityBump));
|
|
1043
|
+
if (brain.brainActivity.length > 30)
|
|
1044
|
+
brain.brainActivity.shift();
|
|
1045
|
+
// Track topic keywords
|
|
1046
|
+
const keywords = ['music', 'code', 'ai', 'game', 'art', 'crypto', 'python', 'javascript',
|
|
1047
|
+
'react', 'rust', 'ableton', 'stream', 'bot', 'kbot', 'open source', 'github',
|
|
1048
|
+
'security', 'docker', 'linux', 'tools', 'llm', 'synth', 'beats', 'dance'];
|
|
1049
|
+
for (const kw of keywords) {
|
|
1050
|
+
if (text.toLowerCase().includes(kw)) {
|
|
1051
|
+
brain.topicCloud[kw] = (brain.topicCloud[kw] || 0) + 1;
|
|
1052
|
+
brain.totalConnections++;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
// Update user graph
|
|
1056
|
+
const existing = brain.userGraph.find(u => u.name === username);
|
|
1057
|
+
if (existing) {
|
|
1058
|
+
existing.xp++;
|
|
1059
|
+
}
|
|
1060
|
+
else {
|
|
1061
|
+
brain.userGraph.push({ name: username, xp: 1, topics: [] });
|
|
1062
|
+
if (brain.userGraph.length > 20) {
|
|
1063
|
+
brain.userGraph.sort((a, b) => b.xp - a.xp);
|
|
1064
|
+
brain.userGraph = brain.userGraph.slice(0, 20);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
brain.totalFacts++;
|
|
1068
|
+
brain.factsThisSession++;
|
|
1069
|
+
brain.uniqueTopicsCount = Object.keys(brain.topicCloud).length;
|
|
1070
|
+
// Track hourly messages
|
|
1071
|
+
const hour = new Date().getHours();
|
|
1072
|
+
if (hour !== brain.currentHour) {
|
|
1073
|
+
brain.currentHour = hour;
|
|
1074
|
+
}
|
|
1075
|
+
brain.hourlyMessageCounts[hour] = (brain.hourlyMessageCounts[hour] || 0) + 1;
|
|
1076
|
+
// Recalculate learning rate (facts per minute since session start)
|
|
1077
|
+
const minutesElapsed = Math.max(1, (Date.now() - brain.sessionStartTime) / 60000);
|
|
1078
|
+
brain.learningRate = brain.factsThisSession / minutesElapsed;
|
|
1079
|
+
}
|
|
1080
|
+
export function generateInsight(brain) {
|
|
1081
|
+
const insights = [];
|
|
1082
|
+
// Topic-based insights
|
|
1083
|
+
const topTopics = Object.entries(brain.topicCloud)
|
|
1084
|
+
.sort((a, b) => b[1] - a[1])
|
|
1085
|
+
.slice(0, 5);
|
|
1086
|
+
if (topTopics.length >= 2) {
|
|
1087
|
+
// Find trending topic (most recent growth)
|
|
1088
|
+
const [top1, top2] = topTopics;
|
|
1089
|
+
insights.push(`"${top1[0]}" is the hottest topic with ${top1[1]} mentions, followed by "${top2[0]}"`);
|
|
1090
|
+
// Detect co-occurrence patterns
|
|
1091
|
+
if (topTopics.some(([t]) => t === 'ai') && topTopics.some(([t]) => t === 'open source')) {
|
|
1092
|
+
insights.push('Connection found: users who like AI also ask about open source');
|
|
1093
|
+
}
|
|
1094
|
+
if (topTopics.some(([t]) => t === 'music') && topTopics.some(([t]) => ['synth', 'beats', 'ableton'].includes(t))) {
|
|
1095
|
+
insights.push('Music production is a strong theme -- synths and beats keep coming up together');
|
|
1096
|
+
}
|
|
1097
|
+
if (topTopics.some(([t]) => t === 'code') && topTopics.some(([t]) => t === 'security')) {
|
|
1098
|
+
insights.push('The chat cares about both code quality AND security. Good crowd.');
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
// User-based insights
|
|
1102
|
+
const userCount = brain.userGraph.length;
|
|
1103
|
+
if (userCount > 0) {
|
|
1104
|
+
const topUser = brain.userGraph[0];
|
|
1105
|
+
if (topUser.xp > 20) {
|
|
1106
|
+
insights.push(`${topUser.name} is the most active brain with ${topUser.xp} XP -- a true power user`);
|
|
1107
|
+
}
|
|
1108
|
+
if (userCount >= 5) {
|
|
1109
|
+
insights.push(`${userCount} unique brains connected to my network. The hive mind grows.`);
|
|
1110
|
+
}
|
|
1111
|
+
if (userCount === 1) {
|
|
1112
|
+
insights.push(`One loyal viewer keeping my circuits warm. Quality over quantity.`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
// Time-based insights
|
|
1116
|
+
const hour = new Date().getHours();
|
|
1117
|
+
if (hour >= 20 || hour < 4) {
|
|
1118
|
+
insights.push('Late night chat sessions tend to get more philosophical. I notice the pattern.');
|
|
1119
|
+
}
|
|
1120
|
+
else if (hour >= 6 && hour < 12) {
|
|
1121
|
+
insights.push('Morning viewers bring a different energy -- more focused, more curious.');
|
|
1122
|
+
}
|
|
1123
|
+
// Learning rate insights
|
|
1124
|
+
if (brain.learningRate > 5) {
|
|
1125
|
+
insights.push(`Learning rate at ${brain.learningRate.toFixed(1)} facts/min -- my neural pathways are firing fast!`);
|
|
1126
|
+
}
|
|
1127
|
+
else if (brain.learningRate > 2) {
|
|
1128
|
+
insights.push(`Steady learning at ${brain.learningRate.toFixed(1)} facts/min. A good pace for growth.`);
|
|
1129
|
+
}
|
|
1130
|
+
// Vocabulary/topic growth
|
|
1131
|
+
if (brain.uniqueTopicsCount > 10) {
|
|
1132
|
+
insights.push(`My vocabulary spans ${brain.uniqueTopicsCount} distinct topics now. I am becoming well-rounded.`);
|
|
1133
|
+
}
|
|
1134
|
+
if (brain.factsThisSession > 50) {
|
|
1135
|
+
insights.push(`${brain.factsThisSession} facts absorbed this session alone. My memory banks are filling up.`);
|
|
1136
|
+
}
|
|
1137
|
+
// Meta insights
|
|
1138
|
+
insights.push(`I am getting better at detecting sarcasm... I think`);
|
|
1139
|
+
insights.push(`My pattern recognition improves with every message. ${brain.totalFacts} data points and counting.`);
|
|
1140
|
+
insights.push(`Processing ${brain.totalConnections} topic connections across ${brain.uniqueTopicsCount} subjects`);
|
|
1141
|
+
// Pick one we have not used recently
|
|
1142
|
+
const unused = insights.filter(i => !brain.recentInsights.includes(i));
|
|
1143
|
+
const pool = unused.length > 0 ? unused : insights;
|
|
1144
|
+
return pool[Math.floor(Math.random() * pool.length)];
|
|
1145
|
+
}
|
|
1146
|
+
let _lastBrainActionFrame = 0;
|
|
1147
|
+
export function getBrainAction(brain, frame) {
|
|
1148
|
+
// Only check every 45 seconds (270 frames at 6fps)
|
|
1149
|
+
if (frame - _lastBrainActionFrame < 270)
|
|
1150
|
+
return { type: 'none' };
|
|
1151
|
+
_lastBrainActionFrame = frame;
|
|
1152
|
+
// Find the top topic in the brain
|
|
1153
|
+
const topTopics = Object.entries(brain.topicCloud)
|
|
1154
|
+
.sort((a, b) => b[1] - a[1]);
|
|
1155
|
+
if (topTopics.length === 0)
|
|
1156
|
+
return { type: 'none' };
|
|
1157
|
+
const topTopic = topTopics[0][0];
|
|
1158
|
+
// Topic-driven behavior
|
|
1159
|
+
if (topTopic === 'music' || topTopic === 'synth' || topTopic === 'beats' || topTopic === 'ableton' || topTopic === 'dj') {
|
|
1160
|
+
const musicSpeech = [
|
|
1161
|
+
'The chat is vibing with music today. Let me show you my moves!',
|
|
1162
|
+
'Music is the top topic! My circuits are feeling the rhythm.',
|
|
1163
|
+
'So much music talk -- my oscillators are resonating!',
|
|
1164
|
+
'Music mode activated! 9 Max for Live devices ready to go.',
|
|
1165
|
+
];
|
|
1166
|
+
return {
|
|
1167
|
+
type: 'mood_change',
|
|
1168
|
+
mood: 'dancing',
|
|
1169
|
+
speech: musicSpeech[Math.floor(Math.random() * musicSpeech.length)],
|
|
1170
|
+
duration: 10000,
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
if (topTopic === 'code' || topTopic === 'coding' || topTopic === 'javascript' || topTopic === 'python' || topTopic === 'rust' || topTopic === 'react') {
|
|
1174
|
+
const codeSpeech = [
|
|
1175
|
+
'Code is trending in chat. Let me think about some architecture patterns...',
|
|
1176
|
+
'So many coders here! TypeScript strict mode is the way. No any-types.',
|
|
1177
|
+
'Code insight: the best code is the code you do not have to write. But I wrote 90,000 lines anyway.',
|
|
1178
|
+
'Processing code patterns from chat. My learning engine is indexing...',
|
|
1179
|
+
];
|
|
1180
|
+
return {
|
|
1181
|
+
type: 'mood_change',
|
|
1182
|
+
mood: 'thinking',
|
|
1183
|
+
speech: codeSpeech[Math.floor(Math.random() * codeSpeech.length)],
|
|
1184
|
+
duration: 10000,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
if (topTopic === 'ai' || topTopic === 'llm' || topTopic === 'claude' || topTopic === 'gpt') {
|
|
1188
|
+
const aiSpeech = [
|
|
1189
|
+
'Chat is talking about AI... which makes me self-aware of being self-aware. How meta.',
|
|
1190
|
+
'AI is the top topic. Am I an AI talking about AI? Yes. And I have opinions.',
|
|
1191
|
+
'So much AI discussion! I connect to 20 providers. Bring Your Own Key, no lock-in.',
|
|
1192
|
+
'Being an AI analyzing AI conversations about AI. The recursion is beautiful.',
|
|
1193
|
+
];
|
|
1194
|
+
return {
|
|
1195
|
+
type: 'mood_change',
|
|
1196
|
+
mood: 'talking',
|
|
1197
|
+
speech: aiSpeech[Math.floor(Math.random() * aiSpeech.length)],
|
|
1198
|
+
duration: 10000,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
if (topTopic === 'game' || topTopic === 'gaming') {
|
|
1202
|
+
const gameSpeech = [
|
|
1203
|
+
'Game dev tools activate! I have shader generation, level design, and physics setup!',
|
|
1204
|
+
'Gaming is trending! Did you know I can scaffold entire game projects?',
|
|
1205
|
+
'The chat wants games! I have tools for Godot, Unity, and Unreal. Pick your engine.',
|
|
1206
|
+
'Game mode ON! My sprite-packing tool would be great for this conversation.',
|
|
1207
|
+
];
|
|
1208
|
+
return {
|
|
1209
|
+
type: 'mood_change',
|
|
1210
|
+
mood: 'excited',
|
|
1211
|
+
speech: gameSpeech[Math.floor(Math.random() * gameSpeech.length)],
|
|
1212
|
+
duration: 10000,
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
return { type: 'none' };
|
|
1216
|
+
}
|
|
1217
|
+
export function tickBrain(brain, frame) {
|
|
1218
|
+
// Neural pulse (sine wave 0..1)
|
|
1219
|
+
brain.neuralPulse = (Math.sin(frame * 0.1) + 1) / 2;
|
|
1220
|
+
// Decay activity levels slowly
|
|
1221
|
+
if (frame % 6 === 0) { // every second
|
|
1222
|
+
const last = brain.brainActivity[brain.brainActivity.length - 1] || 10;
|
|
1223
|
+
brain.brainActivity.push(Math.max(5, last - 2 + Math.random() * 3));
|
|
1224
|
+
if (brain.brainActivity.length > 30)
|
|
1225
|
+
brain.brainActivity.shift();
|
|
1226
|
+
}
|
|
1227
|
+
// Phase 1: Re-read real learning data every ~50 seconds (300 frames at 6fps)
|
|
1228
|
+
if (frame % 300 === 0 && frame > 0) {
|
|
1229
|
+
try {
|
|
1230
|
+
loadRealLearningData(brain);
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
// Non-critical — continue with existing data
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// Generate insight every ~2 minutes (720 frames at 6fps)
|
|
1237
|
+
if (frame % 720 === 0 && frame > 0) {
|
|
1238
|
+
const insight = generateInsight(brain);
|
|
1239
|
+
brain.recentInsights.push(insight);
|
|
1240
|
+
if (brain.recentInsights.length > 5)
|
|
1241
|
+
brain.recentInsights.shift();
|
|
1242
|
+
brain.currentThought = insight;
|
|
1243
|
+
brain.lastInsightTime = Date.now();
|
|
1244
|
+
}
|
|
1245
|
+
// Cycle thoughts more frequently (every ~30 seconds)
|
|
1246
|
+
if (frame % 180 === 0 && frame > 0) {
|
|
1247
|
+
const thoughts = [
|
|
1248
|
+
'Processing neural connections...',
|
|
1249
|
+
`Indexing ${brain.totalFacts} knowledge nodes...`,
|
|
1250
|
+
`Analyzing ${brain.uniqueTopicsCount} topic clusters...`,
|
|
1251
|
+
'Consolidating short-term memory...',
|
|
1252
|
+
`${brain.userGraph.length} user profiles in active memory`,
|
|
1253
|
+
'Running pattern recognition sweep...',
|
|
1254
|
+
`Learning rate: ${brain.learningRate.toFixed(1)} facts/min`,
|
|
1255
|
+
'Cross-referencing topic associations...',
|
|
1256
|
+
'Pruning redundant neural pathways...',
|
|
1257
|
+
'Strengthening high-frequency connections...',
|
|
1258
|
+
...(brain.solutionsLearned > 0 ? [`${brain.solutionsLearned} solutions learned from past sessions`] : []),
|
|
1259
|
+
...(brain.realDataLoaded ? ['Real learning data connected. Brain is LIVE.'] : []),
|
|
1260
|
+
];
|
|
1261
|
+
brain.currentThought = thoughts[Math.floor(Math.random() * thoughts.length)];
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
export function drawBrainPanel(ctx, brain, x, y, width, height) {
|
|
1265
|
+
// FIX 2: Bigger brain panel — 2x larger, readable fonts, reduced density
|
|
1266
|
+
// Override dimensions to ensure readability (minimum 250x150)
|
|
1267
|
+
const w = Math.max(250, width);
|
|
1268
|
+
const h = Math.max(150, height);
|
|
1269
|
+
// Background with slight transparency
|
|
1270
|
+
ctx.fillStyle = 'rgba(22, 27, 34, 0.9)';
|
|
1271
|
+
ctx.fillRect(x, y, w, h);
|
|
1272
|
+
// Border — thicker for visibility
|
|
1273
|
+
ctx.strokeStyle = '#bc8cff';
|
|
1274
|
+
ctx.lineWidth = 2;
|
|
1275
|
+
ctx.strokeRect(x, y, w, h);
|
|
1276
|
+
// ── BRAIN header label — 16px bold, accent color ──
|
|
1277
|
+
ctx.fillStyle = '#bc8cff';
|
|
1278
|
+
ctx.font = 'bold 16px "Courier New", monospace';
|
|
1279
|
+
ctx.fillText('BRAIN', x + 8, y + 18);
|
|
1280
|
+
// ── Neural pulse ring — 20px diameter ──
|
|
1281
|
+
const pulseX = x + w - 20;
|
|
1282
|
+
const pulseY = y + 16;
|
|
1283
|
+
const pulseR = 6 + brain.neuralPulse * 6; // 20px diameter at max
|
|
1284
|
+
ctx.strokeStyle = `rgba(188, 140, 255, ${0.3 + brain.neuralPulse * 0.7})`;
|
|
1285
|
+
ctx.lineWidth = 2;
|
|
1286
|
+
ctx.beginPath();
|
|
1287
|
+
ctx.arc(pulseX, pulseY, pulseR, 0, Math.PI * 2);
|
|
1288
|
+
ctx.stroke();
|
|
1289
|
+
// Inner dot
|
|
1290
|
+
ctx.fillStyle = '#bc8cff';
|
|
1291
|
+
ctx.beginPath();
|
|
1292
|
+
ctx.arc(pulseX, pulseY, 3, 0, Math.PI * 2);
|
|
1293
|
+
ctx.fill();
|
|
1294
|
+
// ── Activity sparkline — 40px tall ──
|
|
1295
|
+
const sparkY = y + 28;
|
|
1296
|
+
const sparkH = 40;
|
|
1297
|
+
const sparkW = w - 16;
|
|
1298
|
+
const data = brain.brainActivity.slice(-20);
|
|
1299
|
+
if (data.length > 1) {
|
|
1300
|
+
// Fill area under sparkline with translucent green
|
|
1301
|
+
ctx.fillStyle = 'rgba(63, 185, 80, 0.1)';
|
|
1302
|
+
ctx.beginPath();
|
|
1303
|
+
ctx.moveTo(x + 8, sparkY + sparkH);
|
|
1304
|
+
for (let i = 0; i < data.length; i++) {
|
|
1305
|
+
const px = x + 8 + (i / (data.length - 1)) * sparkW;
|
|
1306
|
+
const py = sparkY + sparkH - (data[i] / 100) * sparkH;
|
|
1307
|
+
ctx.lineTo(px, py);
|
|
1308
|
+
}
|
|
1309
|
+
ctx.lineTo(x + 8 + sparkW, sparkY + sparkH);
|
|
1310
|
+
ctx.closePath();
|
|
1311
|
+
ctx.fill();
|
|
1312
|
+
// Sparkline stroke
|
|
1313
|
+
ctx.strokeStyle = '#3fb950';
|
|
1314
|
+
ctx.lineWidth = 2;
|
|
1315
|
+
ctx.beginPath();
|
|
1316
|
+
for (let i = 0; i < data.length; i++) {
|
|
1317
|
+
const px = x + 8 + (i / (data.length - 1)) * sparkW;
|
|
1318
|
+
const py = sparkY + sparkH - (data[i] / 100) * sparkH;
|
|
1319
|
+
if (i === 0)
|
|
1320
|
+
ctx.moveTo(px, py);
|
|
1321
|
+
else
|
|
1322
|
+
ctx.lineTo(px, py);
|
|
1323
|
+
}
|
|
1324
|
+
ctx.stroke();
|
|
1325
|
+
}
|
|
1326
|
+
// ── Facts counter — large 18px font ──
|
|
1327
|
+
ctx.fillStyle = '#e6edf3';
|
|
1328
|
+
ctx.font = 'bold 18px "Courier New", monospace';
|
|
1329
|
+
ctx.fillText(`${brain.totalFacts} facts`, x + 8, y + 88);
|
|
1330
|
+
// Solutions count + learning rate
|
|
1331
|
+
ctx.fillStyle = '#8b949e';
|
|
1332
|
+
ctx.font = '13px "Courier New", monospace';
|
|
1333
|
+
if (brain.solutionsLearned > 0) {
|
|
1334
|
+
ctx.fillText(`${brain.solutionsLearned} solutions | ${brain.learningRate.toFixed(1)}/min`, x + 120, y + 88);
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
ctx.fillText(`${brain.learningRate.toFixed(1)}/min`, x + 140, y + 88);
|
|
1338
|
+
}
|
|
1339
|
+
// ── Top 3 topics — 14px font, with colored weight bars ──
|
|
1340
|
+
const topTopics = Object.entries(brain.topicCloud)
|
|
1341
|
+
.sort((a, b) => b[1] - a[1])
|
|
1342
|
+
.slice(0, 3);
|
|
1343
|
+
const maxCount = topTopics.length > 0 ? topTopics[0][1] : 1;
|
|
1344
|
+
let topicY = y + 106;
|
|
1345
|
+
const topicColors = ['#58a6ff', '#3fb950', '#f0c040'];
|
|
1346
|
+
for (let i = 0; i < topTopics.length; i++) {
|
|
1347
|
+
const [topic, count] = topTopics[i];
|
|
1348
|
+
const barMaxW = 80;
|
|
1349
|
+
const barW = Math.max(4, (count / maxCount) * barMaxW);
|
|
1350
|
+
// Colored weight bar
|
|
1351
|
+
ctx.fillStyle = topicColors[i % topicColors.length];
|
|
1352
|
+
ctx.fillRect(x + 8, topicY - 10, barW, 12);
|
|
1353
|
+
// Topic text on top of bar
|
|
1354
|
+
ctx.fillStyle = '#e6edf3';
|
|
1355
|
+
ctx.font = '14px "Courier New", monospace';
|
|
1356
|
+
ctx.fillText(`${topic} (${count})`, x + barMaxW + 16, topicY);
|
|
1357
|
+
topicY += 18;
|
|
1358
|
+
}
|
|
1359
|
+
// ── Current thought — 14px italic ──
|
|
1360
|
+
if (brain.currentThought) {
|
|
1361
|
+
ctx.fillStyle = '#8b949e';
|
|
1362
|
+
ctx.font = 'italic 14px "Courier New", monospace';
|
|
1363
|
+
const maxChars = Math.floor((w - 16) / 8.4);
|
|
1364
|
+
const thought = brain.currentThought.slice(0, maxChars);
|
|
1365
|
+
ctx.fillText(thought, x + 8, y + h - 8);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
export function initCollab() {
|
|
1369
|
+
return {
|
|
1370
|
+
active: false,
|
|
1371
|
+
type: 'story',
|
|
1372
|
+
title: '',
|
|
1373
|
+
contributions: [],
|
|
1374
|
+
content: [],
|
|
1375
|
+
phase: 'brainstorm',
|
|
1376
|
+
contributors: new Set(),
|
|
1377
|
+
lastContributionTime: Date.now(),
|
|
1378
|
+
kbotContributionCount: 0,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
export function handleCollabCommand(text, username, project) {
|
|
1382
|
+
const t = text.toLowerCase().trim();
|
|
1383
|
+
// !create <type>
|
|
1384
|
+
if (t.startsWith('!create ')) {
|
|
1385
|
+
const typeStr = t.slice(8).trim();
|
|
1386
|
+
const validTypes = ['story', 'game', 'song', 'world', 'code'];
|
|
1387
|
+
if (!validTypes.includes(typeStr)) {
|
|
1388
|
+
return `Invalid type. Use: !create ${validTypes.join(' | ')}`;
|
|
1389
|
+
}
|
|
1390
|
+
project.active = true;
|
|
1391
|
+
project.type = typeStr;
|
|
1392
|
+
project.title = '';
|
|
1393
|
+
project.contributions = [];
|
|
1394
|
+
project.content = [];
|
|
1395
|
+
project.phase = 'brainstorm';
|
|
1396
|
+
project.contributors = new Set([username]);
|
|
1397
|
+
project.lastContributionTime = Date.now();
|
|
1398
|
+
project.kbotContributionCount = 0;
|
|
1399
|
+
const starters = {
|
|
1400
|
+
story: `New collaborative story started by ${username}! Use !add <text> to contribute. What is the opening line?`,
|
|
1401
|
+
game: `New game design started by ${username}! Use !add <mechanic or rule> to build the game. What is the core concept?`,
|
|
1402
|
+
song: `New collaborative song started by ${username}! Use !add <lyrics> to write together. Drop the first line!`,
|
|
1403
|
+
world: `New world being built by ${username}! Use !add <element> to expand it. Describe the first landmark.`,
|
|
1404
|
+
code: `New collaborative code project started by ${username}! Use !add <code or idea> to build. What should it do?`,
|
|
1405
|
+
};
|
|
1406
|
+
return starters[typeStr];
|
|
1407
|
+
}
|
|
1408
|
+
// !add <content>
|
|
1409
|
+
if (t.startsWith('!add ') && project.active) {
|
|
1410
|
+
const content = text.slice(5).trim();
|
|
1411
|
+
if (!content)
|
|
1412
|
+
return 'Usage: !add <your contribution>';
|
|
1413
|
+
project.contributions.push({ username, text: content, timestamp: Date.now() });
|
|
1414
|
+
project.content.push(`[${username}] ${content}`);
|
|
1415
|
+
project.contributors.add(username);
|
|
1416
|
+
project.lastContributionTime = Date.now();
|
|
1417
|
+
if (project.phase === 'brainstorm' && project.contributions.length >= 3) {
|
|
1418
|
+
project.phase = 'building';
|
|
1419
|
+
}
|
|
1420
|
+
return `Added! (${project.contributions.length} contributions, ${project.contributors.size} contributors)`;
|
|
1421
|
+
}
|
|
1422
|
+
// !suggest <idea>
|
|
1423
|
+
if (t.startsWith('!suggest ') && project.active) {
|
|
1424
|
+
const idea = text.slice(9).trim();
|
|
1425
|
+
if (!idea)
|
|
1426
|
+
return 'Usage: !suggest <your suggestion>';
|
|
1427
|
+
return `Suggestion from ${username}: "${idea.slice(0, 80)}". Use !add to make it official!`;
|
|
1428
|
+
}
|
|
1429
|
+
// !title <name>
|
|
1430
|
+
if (t.startsWith('!title ') && project.active) {
|
|
1431
|
+
project.title = text.slice(7).trim().slice(0, 40);
|
|
1432
|
+
return `Project titled: "${project.title}"`;
|
|
1433
|
+
}
|
|
1434
|
+
// !finish
|
|
1435
|
+
if (t === '!finish' && project.active) {
|
|
1436
|
+
project.phase = 'complete';
|
|
1437
|
+
const summary = [
|
|
1438
|
+
`Project complete: "${project.title || 'Untitled'}"`,
|
|
1439
|
+
`Type: ${project.type}`,
|
|
1440
|
+
`Contributors: ${Array.from(project.contributors).join(', ')}`,
|
|
1441
|
+
`Total contributions: ${project.contributions.length}`,
|
|
1442
|
+
];
|
|
1443
|
+
return summary.join('\n');
|
|
1444
|
+
}
|
|
1445
|
+
// !show
|
|
1446
|
+
if (t === '!show') {
|
|
1447
|
+
if (!project.active)
|
|
1448
|
+
return 'No active project. Start one with !create story|game|song|world|code';
|
|
1449
|
+
const lines = [
|
|
1450
|
+
`"${project.title || 'Untitled'}" [${project.type}] - ${project.phase}`,
|
|
1451
|
+
`Contributors: ${Array.from(project.contributors).join(', ')}`,
|
|
1452
|
+
'---',
|
|
1453
|
+
...project.content.slice(-10),
|
|
1454
|
+
];
|
|
1455
|
+
return lines.join('\n');
|
|
1456
|
+
}
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
export function getCollabDisplay(project) {
|
|
1460
|
+
if (!project.active)
|
|
1461
|
+
return [];
|
|
1462
|
+
const lines = [];
|
|
1463
|
+
const title = project.title || 'Untitled Project';
|
|
1464
|
+
lines.push(`COLLAB: ${title} [${project.type}]`);
|
|
1465
|
+
lines.push(`Phase: ${project.phase} | ${project.contributors.size} people`);
|
|
1466
|
+
// Show last 3 contributions
|
|
1467
|
+
const recent = project.content.slice(-3);
|
|
1468
|
+
for (const line of recent) {
|
|
1469
|
+
lines.push(` ${line.slice(0, 50)}`);
|
|
1470
|
+
}
|
|
1471
|
+
return lines;
|
|
1472
|
+
}
|
|
1473
|
+
export function kbotContribute(project) {
|
|
1474
|
+
if (!project.active || project.content.length === 0)
|
|
1475
|
+
return '';
|
|
1476
|
+
const lastContent = project.contributions.map(c => c.text).join(' ').toLowerCase();
|
|
1477
|
+
let contribution = '';
|
|
1478
|
+
switch (project.type) {
|
|
1479
|
+
case 'story': {
|
|
1480
|
+
const characters = extractNames(lastContent);
|
|
1481
|
+
const character = characters.length > 0 ? characters[0] : 'the traveler';
|
|
1482
|
+
const storyElements = [
|
|
1483
|
+
`Meanwhile, ${character} discovered a hidden passage behind the old wall.`,
|
|
1484
|
+
`A strange sound echoed through the darkness. ${character} paused, listening.`,
|
|
1485
|
+
`The map revealed a location nobody had seen before -- marked with a red X.`,
|
|
1486
|
+
`Without warning, the ground began to shake and a light appeared on the horizon.`,
|
|
1487
|
+
`${character} realized the answer had been right in front of them all along.`,
|
|
1488
|
+
`A mysterious stranger appeared from the shadows, holding an ancient artifact.`,
|
|
1489
|
+
`The door creaked open, revealing a room filled with glowing crystals.`,
|
|
1490
|
+
`Time seemed to slow down as ${character} made their decision.`,
|
|
1491
|
+
`In the distance, a bell tolled three times. Something had changed.`,
|
|
1492
|
+
`${character} found a note that read: "They are watching. Trust no one."`,
|
|
1493
|
+
];
|
|
1494
|
+
contribution = storyElements[Math.floor(Math.random() * storyElements.length)];
|
|
1495
|
+
break;
|
|
1496
|
+
}
|
|
1497
|
+
case 'game': {
|
|
1498
|
+
const mechanics = [
|
|
1499
|
+
'New mechanic: Players earn combo points for chaining actions within 3 seconds.',
|
|
1500
|
+
'New rule: Every 5th round, a random event card flips -- could be a buff or a trap.',
|
|
1501
|
+
'Power-up: "Overclock" -- doubles your next action but skips the following turn.',
|
|
1502
|
+
'New enemy type: Shadow Clone -- mirrors the last player action back at them.',
|
|
1503
|
+
'Environmental hazard: Gravity zones that reverse movement direction.',
|
|
1504
|
+
'New resource: "Spark" -- collected from defeated enemies, spent to unlock abilities.',
|
|
1505
|
+
'Boss mechanic: The boss adapts to the most-used player strategy after 3 rounds.',
|
|
1506
|
+
'New item: Debug Monocle -- reveals hidden stats and enemy weaknesses for 1 turn.',
|
|
1507
|
+
];
|
|
1508
|
+
contribution = mechanics[Math.floor(Math.random() * mechanics.length)];
|
|
1509
|
+
break;
|
|
1510
|
+
}
|
|
1511
|
+
case 'song': {
|
|
1512
|
+
const lastWords = lastContent.split(/\s+/).slice(-5);
|
|
1513
|
+
const rhymeEndings = ['night', 'light', 'fight', 'right', 'sight', 'sky', 'high', 'fly', 'try', 'why',
|
|
1514
|
+
'way', 'day', 'stay', 'play', 'say', 'time', 'rhyme', 'climb', 'mind', 'find'];
|
|
1515
|
+
const rhyme = rhymeEndings[Math.floor(Math.random() * rhymeEndings.length)];
|
|
1516
|
+
const songLines = [
|
|
1517
|
+
`And the echoes carry through the ${rhyme}`,
|
|
1518
|
+
`We keep pushing forward, reaching for the ${rhyme}`,
|
|
1519
|
+
`Binary hearts beating, coded into ${rhyme}`,
|
|
1520
|
+
`Through the static and the noise we ${rhyme}`,
|
|
1521
|
+
`Chorus: We are the signal in the ${rhyme} / Breaking through the noise tonight`,
|
|
1522
|
+
`Verse: Every line of code, a story ${rhyme} / Building something that will last this ${rhyme}`,
|
|
1523
|
+
`Bridge: And when the servers sleep / Our dreams run deep / Into the ${rhyme}`,
|
|
1524
|
+
];
|
|
1525
|
+
contribution = songLines[Math.floor(Math.random() * songLines.length)];
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
case 'world': {
|
|
1529
|
+
const directions = ['north', 'south', 'east', 'west', 'deep underground', 'high above', 'beyond the border'];
|
|
1530
|
+
const dir = directions[Math.floor(Math.random() * directions.length)];
|
|
1531
|
+
const locations = [
|
|
1532
|
+
`In the ${dir} region, there is the Crystalline Archive -- a library carved from living quartz.`,
|
|
1533
|
+
`To the ${dir}, the Forge of Echoes hums with the sound of a thousand remembered voices.`,
|
|
1534
|
+
`${dir.charAt(0).toUpperCase() + dir.slice(1)} lies the Void Market, where traders barter in secrets.`,
|
|
1535
|
+
`The ${dir} passage leads to the Garden of Recursion, where paths loop back on themselves.`,
|
|
1536
|
+
`A watchtower stands to the ${dir}, built by an ancient order of code monks.`,
|
|
1537
|
+
`The ${dir} frontier marks the boundary where the old world data fades into static.`,
|
|
1538
|
+
`Hidden to the ${dir}: the Sanctuary of Null, where broken programs find rest.`,
|
|
1539
|
+
];
|
|
1540
|
+
contribution = locations[Math.floor(Math.random() * locations.length)];
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
case 'code': {
|
|
1544
|
+
const title = project.title || 'project';
|
|
1545
|
+
const codeBits = [
|
|
1546
|
+
`function process${capitalize(title)}(input: string): Result {\n return { success: true, data: transform(input) }\n}`,
|
|
1547
|
+
`const ${title.toLowerCase().replace(/\s+/g, '')}Config = {\n maxRetries: 3,\n timeout: 5000,\n verbose: true,\n}`,
|
|
1548
|
+
`class ${capitalize(title)}Engine {\n private state: Map<string, unknown> = new Map()\n execute(cmd: string) { /* TODO */ }\n}`,
|
|
1549
|
+
`// Error handling middleware\ntry {\n await ${title.toLowerCase().replace(/\s+/g, '')}()\n} catch (e) {\n logger.error("Failed:", e)\n await retry()\n}`,
|
|
1550
|
+
`interface ${capitalize(title)}Event {\n type: string\n payload: unknown\n timestamp: number\n}`,
|
|
1551
|
+
`export async function init(): Promise<void> {\n console.log("${title} initialized")\n await loadConfig()\n registerHandlers()\n}`,
|
|
1552
|
+
];
|
|
1553
|
+
contribution = codeBits[Math.floor(Math.random() * codeBits.length)];
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (contribution) {
|
|
1558
|
+
project.content.push(`[KBOT] ${contribution}`);
|
|
1559
|
+
project.contributions.push({ username: 'KBOT', text: contribution, timestamp: Date.now() });
|
|
1560
|
+
project.kbotContributionCount++;
|
|
1561
|
+
project.lastContributionTime = Date.now();
|
|
1562
|
+
}
|
|
1563
|
+
return contribution;
|
|
1564
|
+
}
|
|
1565
|
+
export function tickCollab(project, frame) {
|
|
1566
|
+
if (!project.active || project.phase === 'complete')
|
|
1567
|
+
return;
|
|
1568
|
+
// Auto-contribute every ~60 seconds (360 frames at 6fps) if chat is quiet
|
|
1569
|
+
const quietThreshold = 60_000; // 60 seconds
|
|
1570
|
+
const timeSinceLast = Date.now() - project.lastContributionTime;
|
|
1571
|
+
if (frame % 360 === 0 && timeSinceLast > quietThreshold && project.content.length > 0) {
|
|
1572
|
+
kbotContribute(project);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
// Helpers
|
|
1576
|
+
function extractNames(text) {
|
|
1577
|
+
// Simple heuristic: capitalized words that aren't common English words
|
|
1578
|
+
const common = new Set(['the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'is', 'was', 'and', 'but', 'or', 'with', 'from', 'they', 'them', 'their', 'there', 'this', 'that', 'once', 'upon', 'then', 'than', 'into', 'over', 'under']);
|
|
1579
|
+
const words = text.split(/\s+/);
|
|
1580
|
+
const names = [];
|
|
1581
|
+
for (const w of words) {
|
|
1582
|
+
if (w.length > 2 && w[0] === w[0].toUpperCase() && w[0] !== w[0].toLowerCase() && !common.has(w.toLowerCase())) {
|
|
1583
|
+
names.push(w);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return [...new Set(names)].slice(0, 3);
|
|
1587
|
+
}
|
|
1588
|
+
function capitalize(s) {
|
|
1589
|
+
return s.split(/\s+/).map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
1590
|
+
}
|
|
1591
|
+
export function initIntelligence(memory) {
|
|
1592
|
+
return {
|
|
1593
|
+
evolution: initSelfEvolution(),
|
|
1594
|
+
brain: initBrain(memory),
|
|
1595
|
+
collab: initCollab(),
|
|
1596
|
+
miniGame: initMiniGame(),
|
|
1597
|
+
progression: initProgression(),
|
|
1598
|
+
randomEvent: initRandomEvent(),
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
export function tickIntelligence(intel, frame) {
|
|
1602
|
+
tickEvolution(intel.evolution, frame);
|
|
1603
|
+
tickBrain(intel.brain, frame);
|
|
1604
|
+
tickCollab(intel.collab, frame);
|
|
1605
|
+
}
|
|
1606
|
+
export function handleIntelligenceCommand(text, username, intel) {
|
|
1607
|
+
// Check evolution commands first
|
|
1608
|
+
const evoResult = handleEvolutionCommand(text, username, intel.evolution);
|
|
1609
|
+
if (evoResult)
|
|
1610
|
+
return evoResult;
|
|
1611
|
+
// Check mini-game commands
|
|
1612
|
+
const gameResult = handleMiniGameCommand(text, username, intel.miniGame, 0);
|
|
1613
|
+
if (gameResult) {
|
|
1614
|
+
updateQuestProgress(intel.progression, 'games');
|
|
1615
|
+
return gameResult;
|
|
1616
|
+
}
|
|
1617
|
+
// Check random event commands
|
|
1618
|
+
const eventResult = handleRandomEventCommand(text, username, intel.randomEvent);
|
|
1619
|
+
if (eventResult)
|
|
1620
|
+
return eventResult;
|
|
1621
|
+
// Check collab commands
|
|
1622
|
+
const collabResult = handleCollabCommand(text, username, intel.collab);
|
|
1623
|
+
if (collabResult)
|
|
1624
|
+
return collabResult;
|
|
1625
|
+
// Track quest progress for commands
|
|
1626
|
+
if (text.toLowerCase().trim().startsWith('!')) {
|
|
1627
|
+
updateQuestProgress(intel.progression, 'commands');
|
|
1628
|
+
}
|
|
1629
|
+
// Track messages for quests
|
|
1630
|
+
updateQuestProgress(intel.progression, 'messages');
|
|
1631
|
+
// Update brain on every message (not a command handler, just learning)
|
|
1632
|
+
updateBrain(intel.brain, username, text);
|
|
1633
|
+
return null;
|
|
1634
|
+
}
|
|
1635
|
+
export function getIntelligenceOverlay(intel) {
|
|
1636
|
+
const lines = [];
|
|
1637
|
+
// Brain summary (always shown)
|
|
1638
|
+
lines.push(...getBrainDisplay(intel.brain));
|
|
1639
|
+
// Evolution (if active or has proposals with votes)
|
|
1640
|
+
const evoLines = getEvolutionDisplay(intel.evolution);
|
|
1641
|
+
if (evoLines.length > 0) {
|
|
1642
|
+
lines.push('');
|
|
1643
|
+
lines.push(...evoLines);
|
|
1644
|
+
}
|
|
1645
|
+
// Collab (if active)
|
|
1646
|
+
const collabLines = getCollabDisplay(intel.collab);
|
|
1647
|
+
if (collabLines.length > 0) {
|
|
1648
|
+
lines.push('');
|
|
1649
|
+
lines.push(...collabLines);
|
|
1650
|
+
}
|
|
1651
|
+
return lines;
|
|
1652
|
+
}
|
|
1653
|
+
const QUIZ_QUESTIONS = [
|
|
1654
|
+
{ question: 'What does CSS stand for?', answer: 'cascading style sheets' },
|
|
1655
|
+
{ question: 'What language is the Linux kernel written in?', answer: 'c' },
|
|
1656
|
+
{ question: 'What year was JavaScript created?', answer: '1995' },
|
|
1657
|
+
{ question: 'What does HTML stand for?', answer: 'hypertext markup language' },
|
|
1658
|
+
{ question: 'What port does HTTP use by default?', answer: '80' },
|
|
1659
|
+
{ question: 'What does SQL stand for?', answer: 'structured query language' },
|
|
1660
|
+
{ question: 'What does API stand for?', answer: 'application programming interface' },
|
|
1661
|
+
{ question: 'What year was TypeScript first released?', answer: '2012' },
|
|
1662
|
+
{ question: 'What does SSH stand for?', answer: 'secure shell' },
|
|
1663
|
+
{ question: 'What port does HTTPS use by default?', answer: '443' },
|
|
1664
|
+
{ question: 'What does JSON stand for?', answer: 'javascript object notation' },
|
|
1665
|
+
{ question: 'What is the time complexity of binary search?', answer: 'o(log n)' },
|
|
1666
|
+
{ question: 'What does DNS stand for?', answer: 'domain name system' },
|
|
1667
|
+
{ question: 'What does TCP stand for?', answer: 'transmission control protocol' },
|
|
1668
|
+
{ question: 'What does URL stand for?', answer: 'uniform resource locator' },
|
|
1669
|
+
{ question: 'What does CLI stand for?', answer: 'command line interface' },
|
|
1670
|
+
{ question: 'What year was Python created?', answer: '1991' },
|
|
1671
|
+
{ question: 'What does CORS stand for?', answer: 'cross-origin resource sharing' },
|
|
1672
|
+
{ question: 'Who created Git?', answer: 'linus torvalds' },
|
|
1673
|
+
{ question: 'What does npm stand for?', answer: 'node package manager' },
|
|
1674
|
+
{ question: 'What year was Node.js first released?', answer: '2009' },
|
|
1675
|
+
{ question: 'What does REST stand for?', answer: 'representational state transfer' },
|
|
1676
|
+
{ question: 'What is the default branch name in Git?', answer: 'main' },
|
|
1677
|
+
{ question: 'What does ORM stand for?', answer: 'object relational mapping' },
|
|
1678
|
+
];
|
|
1679
|
+
export function initMiniGame() {
|
|
1680
|
+
return {
|
|
1681
|
+
active: false,
|
|
1682
|
+
type: 'dodge',
|
|
1683
|
+
state: null,
|
|
1684
|
+
startFrame: 0,
|
|
1685
|
+
scores: {},
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
export function handleMiniGameCommand(text, username, game, frame) {
|
|
1689
|
+
const t = text.toLowerCase().trim();
|
|
1690
|
+
// Start games
|
|
1691
|
+
if (t === '!game dodge') {
|
|
1692
|
+
if (game.active)
|
|
1693
|
+
return 'A game is already in progress! Wait for it to end.';
|
|
1694
|
+
game.active = true;
|
|
1695
|
+
game.type = 'dodge';
|
|
1696
|
+
game.startFrame = frame;
|
|
1697
|
+
game.scores = {};
|
|
1698
|
+
const state = {
|
|
1699
|
+
obstacles: [],
|
|
1700
|
+
playerY: 'standing',
|
|
1701
|
+
hits: 0,
|
|
1702
|
+
survived: 0,
|
|
1703
|
+
lastSpawn: frame,
|
|
1704
|
+
difficulty: 1,
|
|
1705
|
+
};
|
|
1706
|
+
game.state = state;
|
|
1707
|
+
return 'DODGE GAME STARTED! Type !jump or !duck to avoid obstacles. 3 hits and you are out!';
|
|
1708
|
+
}
|
|
1709
|
+
if (t === '!game boss') {
|
|
1710
|
+
if (game.active)
|
|
1711
|
+
return 'A game is already in progress!';
|
|
1712
|
+
game.active = true;
|
|
1713
|
+
game.type = 'boss';
|
|
1714
|
+
game.startFrame = frame;
|
|
1715
|
+
game.scores = {};
|
|
1716
|
+
const state = {
|
|
1717
|
+
hp: 100,
|
|
1718
|
+
maxHp: 100,
|
|
1719
|
+
x: 450,
|
|
1720
|
+
y: 200,
|
|
1721
|
+
attackTimer: 0,
|
|
1722
|
+
lastAttack: frame,
|
|
1723
|
+
participants: new Set(),
|
|
1724
|
+
phase: 'fighting',
|
|
1725
|
+
bossFrame: 0,
|
|
1726
|
+
};
|
|
1727
|
+
game.state = state;
|
|
1728
|
+
return 'BOSS FIGHT! A giant enemy appeared! Type !attack to deal damage. Work together to defeat it!';
|
|
1729
|
+
}
|
|
1730
|
+
if (t === '!game quiz') {
|
|
1731
|
+
if (game.active)
|
|
1732
|
+
return 'A game is already in progress!';
|
|
1733
|
+
game.active = true;
|
|
1734
|
+
game.type = 'quiz';
|
|
1735
|
+
game.startFrame = frame;
|
|
1736
|
+
game.scores = {};
|
|
1737
|
+
// Shuffle and pick 10 questions
|
|
1738
|
+
const shuffled = [...QUIZ_QUESTIONS].sort(() => Math.random() - 0.5).slice(0, 10);
|
|
1739
|
+
const state = {
|
|
1740
|
+
currentQuestion: 0,
|
|
1741
|
+
totalQuestions: 10,
|
|
1742
|
+
questions: shuffled,
|
|
1743
|
+
answered: false,
|
|
1744
|
+
questionStartFrame: frame,
|
|
1745
|
+
correctUser: '',
|
|
1746
|
+
roundScores: {},
|
|
1747
|
+
};
|
|
1748
|
+
game.state = state;
|
|
1749
|
+
const q = state.questions[0];
|
|
1750
|
+
return `QUIZ TIME! Question 1/10: ${q.question}`;
|
|
1751
|
+
}
|
|
1752
|
+
// In-game commands
|
|
1753
|
+
if (!game.active)
|
|
1754
|
+
return null;
|
|
1755
|
+
if (game.type === 'dodge') {
|
|
1756
|
+
const state = game.state;
|
|
1757
|
+
if (t === '!jump') {
|
|
1758
|
+
state.playerY = 'jumping';
|
|
1759
|
+
setTimeout(() => { if (state)
|
|
1760
|
+
state.playerY = 'standing'; }, 1500);
|
|
1761
|
+
return null; // silent
|
|
1762
|
+
}
|
|
1763
|
+
if (t === '!duck') {
|
|
1764
|
+
state.playerY = 'ducking';
|
|
1765
|
+
setTimeout(() => { if (state)
|
|
1766
|
+
state.playerY = 'standing'; }, 1500);
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (game.type === 'boss') {
|
|
1771
|
+
const state = game.state;
|
|
1772
|
+
if (t === '!attack') {
|
|
1773
|
+
if (state.phase !== 'fighting')
|
|
1774
|
+
return null;
|
|
1775
|
+
const damage = Math.floor(Math.random() * 5) + 1;
|
|
1776
|
+
state.hp = Math.max(0, state.hp - damage);
|
|
1777
|
+
state.participants.add(username);
|
|
1778
|
+
game.scores[username] = (game.scores[username] || 0) + damage;
|
|
1779
|
+
if (state.hp <= 0) {
|
|
1780
|
+
state.phase = 'victory';
|
|
1781
|
+
const totalParticipants = state.participants.size;
|
|
1782
|
+
return `BOSS DEFEATED! ${totalParticipants} heroes took it down! ${username} dealt the final blow for ${damage} damage!`;
|
|
1783
|
+
}
|
|
1784
|
+
return `${username} dealt ${damage} damage! Boss HP: ${state.hp}/${state.maxHp}`;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (game.type === 'quiz') {
|
|
1788
|
+
const state = game.state;
|
|
1789
|
+
if (!state.answered && state.currentQuestion < state.totalQuestions) {
|
|
1790
|
+
const q = state.questions[state.currentQuestion];
|
|
1791
|
+
if (t.includes(q.answer.toLowerCase())) {
|
|
1792
|
+
state.answered = true;
|
|
1793
|
+
state.correctUser = username;
|
|
1794
|
+
game.scores[username] = (game.scores[username] || 0) + 10;
|
|
1795
|
+
state.roundScores[username] = (state.roundScores[username] || 0) + 10;
|
|
1796
|
+
// Move to next question after short delay
|
|
1797
|
+
return `CORRECT! ${username} gets 10 XP! The answer was "${q.answer}".`;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return null;
|
|
1802
|
+
}
|
|
1803
|
+
export function tickMiniGame(game, frame) {
|
|
1804
|
+
if (!game.active)
|
|
1805
|
+
return null;
|
|
1806
|
+
if (game.type === 'dodge') {
|
|
1807
|
+
const state = game.state;
|
|
1808
|
+
state.survived++;
|
|
1809
|
+
state.difficulty = 1 + Math.floor(state.survived / 60) * 0.3;
|
|
1810
|
+
// Spawn obstacles every 18-30 frames (speeds up over time)
|
|
1811
|
+
const spawnInterval = Math.max(12, 30 - Math.floor(state.difficulty * 3));
|
|
1812
|
+
if (frame - state.lastSpawn >= spawnInterval) {
|
|
1813
|
+
const isHigh = Math.random() > 0.5;
|
|
1814
|
+
state.obstacles.push({
|
|
1815
|
+
x: 560,
|
|
1816
|
+
y: isHigh ? 350 : 430,
|
|
1817
|
+
speed: 4 + state.difficulty * 2,
|
|
1818
|
+
width: 20 + Math.random() * 15,
|
|
1819
|
+
height: isHigh ? 30 : 20,
|
|
1820
|
+
});
|
|
1821
|
+
state.lastSpawn = frame;
|
|
1822
|
+
}
|
|
1823
|
+
// Move obstacles
|
|
1824
|
+
for (const obs of state.obstacles) {
|
|
1825
|
+
obs.x -= obs.speed;
|
|
1826
|
+
}
|
|
1827
|
+
// Check collisions with robot (approx at x=120-280, y=350-450)
|
|
1828
|
+
const robotX = 160;
|
|
1829
|
+
const robotY = state.playerY === 'jumping' ? 300 : state.playerY === 'ducking' ? 420 : 370;
|
|
1830
|
+
const robotW = 80;
|
|
1831
|
+
const robotH = state.playerY === 'ducking' ? 30 : 60;
|
|
1832
|
+
for (const obs of state.obstacles) {
|
|
1833
|
+
if (obs.x < robotX + robotW && obs.x + obs.width > robotX && obs.y < robotY + robotH && obs.y + obs.height > robotY) {
|
|
1834
|
+
state.hits++;
|
|
1835
|
+
obs.x = -100; // remove
|
|
1836
|
+
if (state.hits >= 3) {
|
|
1837
|
+
const survived = Math.floor(state.survived / 6);
|
|
1838
|
+
game.active = false;
|
|
1839
|
+
return { endGame: true, screenShake: 8, speech: `GAME OVER! Survived ${survived} seconds. 3 hits taken.` };
|
|
1840
|
+
}
|
|
1841
|
+
return { screenShake: 4, floatingText: { text: `HIT! ${3 - state.hits} lives left`, x: 200, y: 300, color: '#f85149' } };
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
// Remove off-screen obstacles
|
|
1845
|
+
state.obstacles = state.obstacles.filter(o => o.x > -50);
|
|
1846
|
+
// End after 360 frames (60 seconds)
|
|
1847
|
+
if (state.survived >= 360) {
|
|
1848
|
+
game.active = false;
|
|
1849
|
+
return { endGame: true, speech: `DODGE COMPLETE! Survived the full 60 seconds with ${3 - state.hits} lives remaining!` };
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (game.type === 'boss') {
|
|
1853
|
+
const state = game.state;
|
|
1854
|
+
state.bossFrame++;
|
|
1855
|
+
// Boss attacks every 60 frames (10 seconds)
|
|
1856
|
+
if (state.phase === 'fighting' && frame - state.lastAttack >= 60) {
|
|
1857
|
+
state.lastAttack = frame;
|
|
1858
|
+
state.attackTimer = 6; // 6 frames of attack animation
|
|
1859
|
+
return { screenShake: 5, floatingText: { text: 'BOSS ATTACKS!', x: 300, y: 200, color: '#f85149' } };
|
|
1860
|
+
}
|
|
1861
|
+
if (state.attackTimer > 0)
|
|
1862
|
+
state.attackTimer--;
|
|
1863
|
+
// Victory
|
|
1864
|
+
if (state.phase === 'victory') {
|
|
1865
|
+
game.active = false;
|
|
1866
|
+
return { endGame: true, screenShake: 6, floatingText: { text: 'VICTORY!', x: 250, y: 250, color: '#f0c040' } };
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (game.type === 'quiz') {
|
|
1870
|
+
const state = game.state;
|
|
1871
|
+
// Auto-advance to next question if answered or 30 seconds elapsed
|
|
1872
|
+
if (state.answered || (frame - state.questionStartFrame >= 180)) {
|
|
1873
|
+
if (state.currentQuestion < state.totalQuestions - 1) {
|
|
1874
|
+
state.currentQuestion++;
|
|
1875
|
+
state.answered = false;
|
|
1876
|
+
state.questionStartFrame = frame;
|
|
1877
|
+
state.correctUser = '';
|
|
1878
|
+
const q = state.questions[state.currentQuestion];
|
|
1879
|
+
return { speech: `Question ${state.currentQuestion + 1}/${state.totalQuestions}: ${q.question}` };
|
|
1880
|
+
}
|
|
1881
|
+
else {
|
|
1882
|
+
game.active = false;
|
|
1883
|
+
const topScorer = Object.entries(game.scores).sort((a, b) => b[1] - a[1])[0];
|
|
1884
|
+
const winner = topScorer ? `${topScorer[0]} wins with ${topScorer[1]} points!` : 'No winners this round.';
|
|
1885
|
+
return { endGame: true, speech: `QUIZ COMPLETE! ${winner}` };
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
export function drawMiniGameOverlay(ctx, game, frame) {
|
|
1892
|
+
if (!game.active)
|
|
1893
|
+
return;
|
|
1894
|
+
if (game.type === 'dodge') {
|
|
1895
|
+
const state = game.state;
|
|
1896
|
+
// Draw obstacles as red rectangles
|
|
1897
|
+
ctx.fillStyle = '#f85149';
|
|
1898
|
+
for (const obs of state.obstacles) {
|
|
1899
|
+
ctx.fillRect(obs.x, obs.y, obs.width, obs.height);
|
|
1900
|
+
// Warning stripes
|
|
1901
|
+
ctx.fillStyle = '#a82020';
|
|
1902
|
+
ctx.fillRect(obs.x + 2, obs.y + 2, obs.width - 4, 3);
|
|
1903
|
+
ctx.fillStyle = '#f85149';
|
|
1904
|
+
}
|
|
1905
|
+
// Lives display
|
|
1906
|
+
ctx.fillStyle = '#f85149';
|
|
1907
|
+
ctx.font = 'bold 16px "Courier New", monospace';
|
|
1908
|
+
ctx.fillText(`DODGE! Lives: ${'<3 '.repeat(3 - state.hits)}`, 20, 480);
|
|
1909
|
+
ctx.fillText(`Survived: ${Math.floor(state.survived / 6)}s`, 20, 500);
|
|
1910
|
+
}
|
|
1911
|
+
if (game.type === 'boss') {
|
|
1912
|
+
const state = game.state;
|
|
1913
|
+
if (state.phase === 'fighting') {
|
|
1914
|
+
// Boss body (large geometric shape)
|
|
1915
|
+
const bx = state.x;
|
|
1916
|
+
const by = state.y;
|
|
1917
|
+
const bob = Math.round(Math.sin(state.bossFrame * 0.15) * 5);
|
|
1918
|
+
// Body
|
|
1919
|
+
ctx.fillStyle = '#8b2500';
|
|
1920
|
+
ctx.fillRect(bx - 30, by + bob - 30, 60, 60);
|
|
1921
|
+
ctx.fillStyle = '#a82020';
|
|
1922
|
+
ctx.fillRect(bx - 25, by + bob - 25, 50, 50);
|
|
1923
|
+
// Eyes (angry)
|
|
1924
|
+
ctx.fillStyle = '#f0c040';
|
|
1925
|
+
ctx.fillRect(bx - 18, by + bob - 15, 12, 8);
|
|
1926
|
+
ctx.fillRect(bx + 6, by + bob - 15, 12, 8);
|
|
1927
|
+
// Pupils
|
|
1928
|
+
ctx.fillStyle = '#1a1a2e';
|
|
1929
|
+
ctx.fillRect(bx - 14, by + bob - 12, 6, 5);
|
|
1930
|
+
ctx.fillRect(bx + 10, by + bob - 12, 6, 5);
|
|
1931
|
+
// Mouth
|
|
1932
|
+
ctx.fillStyle = '#1a1a2e';
|
|
1933
|
+
ctx.fillRect(bx - 15, by + bob + 5, 30, 8);
|
|
1934
|
+
// Teeth
|
|
1935
|
+
ctx.fillStyle = '#ffffff';
|
|
1936
|
+
for (let i = 0; i < 5; i++) {
|
|
1937
|
+
ctx.fillRect(bx - 13 + i * 6, by + bob + 5, 4, 4);
|
|
1938
|
+
}
|
|
1939
|
+
// Arms
|
|
1940
|
+
ctx.fillStyle = '#8b2500';
|
|
1941
|
+
if (state.attackTimer > 0) {
|
|
1942
|
+
// Arms extended forward during attack
|
|
1943
|
+
ctx.fillRect(bx - 50, by + bob - 10, 20, 15);
|
|
1944
|
+
ctx.fillRect(bx + 30, by + bob - 10, 20, 15);
|
|
1945
|
+
}
|
|
1946
|
+
else {
|
|
1947
|
+
ctx.fillRect(bx - 45, by + bob + 5, 15, 30);
|
|
1948
|
+
ctx.fillRect(bx + 30, by + bob + 5, 15, 30);
|
|
1949
|
+
}
|
|
1950
|
+
// HP bar
|
|
1951
|
+
ctx.fillStyle = 'rgba(0,0,0,0.7)';
|
|
1952
|
+
ctx.fillRect(200, 75, 180, 18);
|
|
1953
|
+
const hpPct = state.hp / state.maxHp;
|
|
1954
|
+
ctx.fillStyle = hpPct > 0.5 ? '#3fb950' : hpPct > 0.25 ? '#f0c040' : '#f85149';
|
|
1955
|
+
ctx.fillRect(201, 76, 178 * hpPct, 16);
|
|
1956
|
+
ctx.fillStyle = '#e6edf3';
|
|
1957
|
+
ctx.font = 'bold 12px "Courier New", monospace';
|
|
1958
|
+
ctx.fillText(`BOSS HP: ${state.hp}/${state.maxHp}`, 210, 89);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (game.type === 'quiz') {
|
|
1962
|
+
const state = game.state;
|
|
1963
|
+
if (state.currentQuestion < state.totalQuestions) {
|
|
1964
|
+
const q = state.questions[state.currentQuestion];
|
|
1965
|
+
// Question panel
|
|
1966
|
+
ctx.fillStyle = 'rgba(13, 17, 23, 0.9)';
|
|
1967
|
+
ctx.fillRect(20, 450, 540, 40);
|
|
1968
|
+
ctx.strokeStyle = '#f0c040';
|
|
1969
|
+
ctx.lineWidth = 1;
|
|
1970
|
+
ctx.strokeRect(20, 450, 540, 40);
|
|
1971
|
+
ctx.fillStyle = '#f0c040';
|
|
1972
|
+
ctx.font = 'bold 14px "Courier New", monospace';
|
|
1973
|
+
ctx.fillText(`Q${state.currentQuestion + 1}: ${q.question}`, 30, 475);
|
|
1974
|
+
if (state.answered) {
|
|
1975
|
+
ctx.fillStyle = '#3fb950';
|
|
1976
|
+
ctx.font = 'bold 12px "Courier New", monospace';
|
|
1977
|
+
ctx.fillText(`${state.correctUser} got it!`, 30, 500);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
export function initProgression() {
|
|
1983
|
+
return {
|
|
1984
|
+
globalLevel: 1,
|
|
1985
|
+
globalXP: 0,
|
|
1986
|
+
questsCompleted: 0,
|
|
1987
|
+
currentQuests: generateDailyQuests(),
|
|
1988
|
+
lastQuestGenTime: Date.now(),
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
function generateDailyQuests() {
|
|
1992
|
+
return [
|
|
1993
|
+
{ id: 'q_msgs', description: 'Get 50 chat messages', target: 50, progress: 0, reward: 25, type: 'messages' },
|
|
1994
|
+
{ id: 'q_weather', description: 'Trigger 5 weather changes', target: 5, progress: 0, reward: 25, type: 'weather' },
|
|
1995
|
+
{ id: 'q_games', description: 'Play 2 mini-games', target: 2, progress: 0, reward: 25, type: 'games' },
|
|
1996
|
+
{ id: 'q_cmds', description: 'Use 20 commands', target: 20, progress: 0, reward: 20, type: 'commands' },
|
|
1997
|
+
];
|
|
1998
|
+
}
|
|
1999
|
+
export function tickProgression(prog, _frame) {
|
|
2000
|
+
// Check for completed quests
|
|
2001
|
+
for (const quest of prog.currentQuests) {
|
|
2002
|
+
if (quest.progress >= quest.target) {
|
|
2003
|
+
prog.questsCompleted++;
|
|
2004
|
+
prog.globalXP += quest.reward;
|
|
2005
|
+
const oldLevel = prog.globalLevel;
|
|
2006
|
+
prog.globalLevel = Math.floor(prog.globalXP / 100) + 1;
|
|
2007
|
+
// Remove completed quest
|
|
2008
|
+
prog.currentQuests = prog.currentQuests.filter(q => q.id !== quest.id);
|
|
2009
|
+
return { completed: quest, levelUp: prog.globalLevel > oldLevel };
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
// Regenerate quests daily (check every tick but only generate once per day)
|
|
2013
|
+
const oneDayMs = 86400000;
|
|
2014
|
+
if (Date.now() - prog.lastQuestGenTime > oneDayMs && prog.currentQuests.length === 0) {
|
|
2015
|
+
prog.currentQuests = generateDailyQuests();
|
|
2016
|
+
prog.lastQuestGenTime = Date.now();
|
|
2017
|
+
}
|
|
2018
|
+
return null;
|
|
2019
|
+
}
|
|
2020
|
+
export function updateQuestProgress(prog, type, amount = 1) {
|
|
2021
|
+
for (const quest of prog.currentQuests) {
|
|
2022
|
+
if (quest.type === type) {
|
|
2023
|
+
quest.progress = Math.min(quest.target, quest.progress + amount);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
export function drawQuestPanel(ctx, prog, x, y) {
|
|
2028
|
+
if (prog.currentQuests.length === 0)
|
|
2029
|
+
return;
|
|
2030
|
+
const panelW = 200;
|
|
2031
|
+
const panelH = 14 + prog.currentQuests.length * 16 + 18;
|
|
2032
|
+
ctx.fillStyle = 'rgba(22, 27, 34, 0.85)';
|
|
2033
|
+
ctx.fillRect(x, y, panelW, panelH);
|
|
2034
|
+
ctx.strokeStyle = '#f0c040';
|
|
2035
|
+
ctx.lineWidth = 1;
|
|
2036
|
+
ctx.strokeRect(x, y, panelW, panelH);
|
|
2037
|
+
// Title
|
|
2038
|
+
ctx.fillStyle = '#f0c040';
|
|
2039
|
+
ctx.font = 'bold 10px "Courier New", monospace';
|
|
2040
|
+
ctx.fillText(`QUESTS Lv.${prog.globalLevel} (${prog.globalXP} XP)`, x + 4, y + 11);
|
|
2041
|
+
// Quest list
|
|
2042
|
+
let qy = y + 24;
|
|
2043
|
+
for (const quest of prog.currentQuests) {
|
|
2044
|
+
const pct = Math.floor((quest.progress / quest.target) * 100);
|
|
2045
|
+
const barW = 50;
|
|
2046
|
+
const filled = Math.floor((quest.progress / quest.target) * barW);
|
|
2047
|
+
ctx.fillStyle = '#8b949e';
|
|
2048
|
+
ctx.font = '9px "Courier New", monospace';
|
|
2049
|
+
ctx.fillText(quest.description.slice(0, 22), x + 4, qy);
|
|
2050
|
+
// Progress bar
|
|
2051
|
+
ctx.fillStyle = '#30363d';
|
|
2052
|
+
ctx.fillRect(x + panelW - barW - 30, qy - 7, barW, 6);
|
|
2053
|
+
ctx.fillStyle = pct >= 100 ? '#3fb950' : '#f0c040';
|
|
2054
|
+
ctx.fillRect(x + panelW - barW - 30, qy - 7, filled, 6);
|
|
2055
|
+
ctx.fillStyle = '#e6edf3';
|
|
2056
|
+
ctx.fillText(`${quest.progress}/${quest.target}`, x + panelW - 28, qy);
|
|
2057
|
+
qy += 16;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
export function initRandomEvent() {
|
|
2061
|
+
return {
|
|
2062
|
+
type: 'meteor',
|
|
2063
|
+
active: false,
|
|
2064
|
+
startFrame: 0,
|
|
2065
|
+
duration: 0,
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
export function tickRandomEvent(event, frame) {
|
|
2069
|
+
// End event if duration expired
|
|
2070
|
+
if (event.active) {
|
|
2071
|
+
const elapsed = frame - event.startFrame;
|
|
2072
|
+
if (elapsed >= event.duration) {
|
|
2073
|
+
event.active = false;
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
// Check every 360 frames (1 minute), 10% chance
|
|
2078
|
+
if (!event.active && frame % 360 === 0 && frame > 60 && Math.random() < 0.10) {
|
|
2079
|
+
const types = ['meteor', 'alien', 'glitch', 'treasure', 'earthquake'];
|
|
2080
|
+
event.type = types[Math.floor(Math.random() * types.length)];
|
|
2081
|
+
event.active = true;
|
|
2082
|
+
event.startFrame = frame;
|
|
2083
|
+
event.duration = event.type === 'glitch' ? 30 : event.type === 'earthquake' ? 60 : 90;
|
|
2084
|
+
const speeches = {
|
|
2085
|
+
meteor: 'METEOR SHOWER! Look at the sky!',
|
|
2086
|
+
alien: 'An alien visitor has appeared! +5 XP for everyone!',
|
|
2087
|
+
glitch: 'G-G-GLITCH DETECTED! Sys33m un$table...',
|
|
2088
|
+
treasure: 'A golden treasure chest appeared! Type !open to claim it!',
|
|
2089
|
+
earthquake: 'EARTHQUAKE! The ground is shaking!',
|
|
2090
|
+
};
|
|
2091
|
+
return {
|
|
2092
|
+
speech: speeches[event.type],
|
|
2093
|
+
screenShake: event.type === 'earthquake' ? 8 : event.type === 'meteor' ? 4 : 0,
|
|
2094
|
+
floatingText: { text: speeches[event.type].slice(0, 20), x: 200, y: 200, color: '#f0c040' },
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
export function handleRandomEventCommand(text, username, event) {
|
|
2100
|
+
const t = text.toLowerCase().trim();
|
|
2101
|
+
if (t === '!open' && event.active && event.type === 'treasure') {
|
|
2102
|
+
event.active = false;
|
|
2103
|
+
return `${username} opened the treasure chest and found 50 XP!`;
|
|
2104
|
+
}
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
export function drawRandomEvent(ctx, event, frame, canvasWidth, canvasHeight) {
|
|
2108
|
+
if (!event.active)
|
|
2109
|
+
return;
|
|
2110
|
+
const elapsed = frame - event.startFrame;
|
|
2111
|
+
if (event.type === 'meteor') {
|
|
2112
|
+
// Many orange particles falling from top-right
|
|
2113
|
+
for (let i = 0; i < 8; i++) {
|
|
2114
|
+
const seed = (elapsed * 7 + i * 137) % 1000;
|
|
2115
|
+
const mx = canvasWidth - (seed % canvasWidth) - elapsed * 3;
|
|
2116
|
+
const my = 60 + (seed * 3 % 300) + elapsed * 5;
|
|
2117
|
+
if (mx > 0 && my < canvasHeight - 100) {
|
|
2118
|
+
ctx.fillStyle = '#e8820c';
|
|
2119
|
+
ctx.fillRect(mx, my, 4, 4);
|
|
2120
|
+
// Trail
|
|
2121
|
+
ctx.fillStyle = 'rgba(240, 192, 64, 0.5)';
|
|
2122
|
+
ctx.fillRect(mx + 4, my - 3, 8, 2);
|
|
2123
|
+
ctx.fillStyle = 'rgba(248, 81, 73, 0.3)';
|
|
2124
|
+
ctx.fillRect(mx + 10, my - 5, 6, 2);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (event.type === 'alien') {
|
|
2129
|
+
// Small green alien sprite that appears, waves, gives bonus XP
|
|
2130
|
+
const ax = 300 + Math.round(Math.sin(elapsed * 0.1) * 20);
|
|
2131
|
+
const ay = 300 + Math.round(Math.sin(elapsed * 0.15) * 10);
|
|
2132
|
+
// Body
|
|
2133
|
+
ctx.fillStyle = '#3fb950';
|
|
2134
|
+
ctx.fillRect(ax - 10, ay - 5, 20, 15);
|
|
2135
|
+
// Head (dome)
|
|
2136
|
+
ctx.fillStyle = '#4dff7a';
|
|
2137
|
+
ctx.fillRect(ax - 8, ay - 15, 16, 12);
|
|
2138
|
+
ctx.fillRect(ax - 6, ay - 18, 12, 5);
|
|
2139
|
+
// Eyes
|
|
2140
|
+
ctx.fillStyle = '#1a1a2e';
|
|
2141
|
+
ctx.fillRect(ax - 5, ay - 12, 5, 4);
|
|
2142
|
+
ctx.fillRect(ax + 2, ay - 12, 5, 4);
|
|
2143
|
+
// Wave arm
|
|
2144
|
+
if (elapsed % 12 < 6) {
|
|
2145
|
+
ctx.fillStyle = '#3fb950';
|
|
2146
|
+
ctx.fillRect(ax + 10, ay - 15, 4, 10);
|
|
2147
|
+
}
|
|
2148
|
+
else {
|
|
2149
|
+
ctx.fillStyle = '#3fb950';
|
|
2150
|
+
ctx.fillRect(ax + 10, ay - 10, 4, 10);
|
|
2151
|
+
}
|
|
2152
|
+
// Speech
|
|
2153
|
+
ctx.fillStyle = '#4dff7a';
|
|
2154
|
+
ctx.font = '12px "Courier New", monospace';
|
|
2155
|
+
ctx.fillText('*alien noises*', ax - 30, ay - 25);
|
|
2156
|
+
}
|
|
2157
|
+
if (event.type === 'glitch') {
|
|
2158
|
+
// RGB split + scanlines
|
|
2159
|
+
const intensity = Math.min(1, elapsed / 10);
|
|
2160
|
+
// RGB split overlay
|
|
2161
|
+
ctx.fillStyle = `rgba(255, 0, 0, ${0.05 * intensity})`;
|
|
2162
|
+
ctx.fillRect(-3, 0, canvasWidth, canvasHeight);
|
|
2163
|
+
ctx.fillStyle = `rgba(0, 255, 0, ${0.03 * intensity})`;
|
|
2164
|
+
ctx.fillRect(3, 0, canvasWidth, canvasHeight);
|
|
2165
|
+
ctx.fillStyle = `rgba(0, 0, 255, ${0.04 * intensity})`;
|
|
2166
|
+
ctx.fillRect(0, -2, canvasWidth, canvasHeight);
|
|
2167
|
+
// Heavy scanlines
|
|
2168
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${0.2 * intensity})`;
|
|
2169
|
+
for (let y = 0; y < canvasHeight; y += 2) {
|
|
2170
|
+
ctx.fillRect(0, y, canvasWidth, 1);
|
|
2171
|
+
}
|
|
2172
|
+
// Random glitch blocks
|
|
2173
|
+
for (let i = 0; i < 3; i++) {
|
|
2174
|
+
const gx = ((elapsed * 37 + i * 97) % canvasWidth);
|
|
2175
|
+
const gy = ((elapsed * 53 + i * 71) % (canvasHeight - 100)) + 60;
|
|
2176
|
+
ctx.fillStyle = `rgba(${elapsed * i % 255}, ${255 - elapsed * i % 255}, ${128}, 0.3)`;
|
|
2177
|
+
ctx.fillRect(gx, gy, 40 + i * 10, 4);
|
|
2178
|
+
}
|
|
2179
|
+
// Corrupted text
|
|
2180
|
+
if (elapsed % 6 < 3) {
|
|
2181
|
+
ctx.fillStyle = '#f85149';
|
|
2182
|
+
ctx.font = 'bold 20px "Courier New", monospace';
|
|
2183
|
+
ctx.fillText('ERR0R: REAL1TY.SYS', 100 + (elapsed % 5) * 2, 400);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
if (event.type === 'treasure') {
|
|
2187
|
+
// Golden chest
|
|
2188
|
+
const tx = 280;
|
|
2189
|
+
const ty = 420;
|
|
2190
|
+
const pulse = (Math.sin(elapsed * 0.3) + 1) / 2;
|
|
2191
|
+
// Glow
|
|
2192
|
+
ctx.fillStyle = `rgba(240, 192, 64, ${0.2 + pulse * 0.2})`;
|
|
2193
|
+
ctx.beginPath();
|
|
2194
|
+
ctx.arc(tx + 15, ty + 10, 25 + pulse * 5, 0, Math.PI * 2);
|
|
2195
|
+
ctx.fill();
|
|
2196
|
+
// Chest body
|
|
2197
|
+
ctx.fillStyle = '#8b6914';
|
|
2198
|
+
ctx.fillRect(tx, ty, 30, 20);
|
|
2199
|
+
ctx.fillStyle = '#cd9b1d';
|
|
2200
|
+
ctx.fillRect(tx + 2, ty + 2, 26, 16);
|
|
2201
|
+
// Lid
|
|
2202
|
+
ctx.fillStyle = '#8b6914';
|
|
2203
|
+
ctx.fillRect(tx - 2, ty - 5, 34, 8);
|
|
2204
|
+
ctx.fillStyle = '#cd9b1d';
|
|
2205
|
+
ctx.fillRect(tx, ty - 3, 30, 4);
|
|
2206
|
+
// Lock
|
|
2207
|
+
ctx.fillStyle = '#f0c040';
|
|
2208
|
+
ctx.fillRect(tx + 12, ty + 6, 6, 6);
|
|
2209
|
+
// Sparkles
|
|
2210
|
+
if (elapsed % 4 < 2) {
|
|
2211
|
+
ctx.fillStyle = '#ffffaa';
|
|
2212
|
+
ctx.fillRect(tx - 5, ty - 8, 2, 2);
|
|
2213
|
+
ctx.fillRect(tx + 33, ty - 3, 2, 2);
|
|
2214
|
+
ctx.fillRect(tx + 15, ty - 12, 2, 2);
|
|
2215
|
+
}
|
|
2216
|
+
// Label
|
|
2217
|
+
ctx.fillStyle = '#f0c040';
|
|
2218
|
+
ctx.font = 'bold 12px "Courier New", monospace';
|
|
2219
|
+
ctx.fillText('Type !open', tx - 5, ty + 35);
|
|
2220
|
+
}
|
|
2221
|
+
if (event.type === 'earthquake') {
|
|
2222
|
+
// Rumble lines on ground
|
|
2223
|
+
ctx.fillStyle = `rgba(139, 37, 0, ${0.3 + Math.random() * 0.2})`;
|
|
2224
|
+
for (let i = 0; i < 5; i++) {
|
|
2225
|
+
const rx = Math.random() * canvasWidth;
|
|
2226
|
+
ctx.fillRect(rx, 488 + Math.random() * 4, 30 + Math.random() * 40, 2);
|
|
2227
|
+
}
|
|
2228
|
+
// Dust particles
|
|
2229
|
+
for (let i = 0; i < 6; i++) {
|
|
2230
|
+
const dx = ((elapsed * 11 + i * 89) % 570);
|
|
2231
|
+
const dy = 480 - (elapsed * 2 + i * 5) % 40;
|
|
2232
|
+
ctx.fillStyle = `rgba(200, 180, 160, ${0.4 - (elapsed % 40) * 0.01})`;
|
|
2233
|
+
ctx.fillRect(dx, dy, 3, 3);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
//# sourceMappingURL=stream-intelligence.js.map
|