@kernel.chat/kbot 3.88.0 → 3.94.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.
@@ -70,8 +70,10 @@ function dither(ctx, x, y, w, h, color1, color2, scale, ox, oy) {
70
70
  }
71
71
  /** Draw a 1px outline silhouette for a rectangular region */
72
72
  function outlineRect(ctx, x, y, w, h, color, scale, ox, oy) {
73
- // Outline is 1px larger on all sides
74
- px(ctx, x - 1, y - 1, w + 2, h + 2, color, scale, ox, oy);
73
+ px(ctx, x - 1, y - 1, w + 2, 1, color, scale, ox, oy); // top
74
+ px(ctx, x - 1, y + h, w + 2, 1, color, scale, ox, oy); // bottom
75
+ px(ctx, x - 1, y, 1, h, color, scale, ox, oy); // left
76
+ px(ctx, x + w, y, 1, h, color, scale, ox, oy); // right
75
77
  }
76
78
  // ─── Sub-drawers ───────────────────────────────────────────────
77
79
  function drawAntenna(ctx, s, ox, oy, glowColor, frame, breathPhase = 0) {
@@ -105,18 +107,18 @@ function drawHead(ctx, s, ox, oy, eyeColor, mood, frame, headShiftX) {
105
107
  const hy = 5;
106
108
  // 1px outline silhouette (technique 4)
107
109
  outlineRect(ctx, hx, hy, 14, 11, PAL.outline, s, ox, oy);
108
- // Sub-pixel AA on rounded corners: deep shadow pixels for smoother curves (technique 2)
110
+ // Head base (shadow layer)
111
+ px(ctx, hx, hy, 14, 11, PAL.bodyDark, s, ox, oy);
112
+ // Cut corners for rounded look — clearRect actually erases pixels (transparent fillRect does nothing)
113
+ ctx.clearRect(ox + hx * s, oy + hy * s, s, s);
114
+ ctx.clearRect(ox + (hx + 13) * s, oy + hy * s, s, s);
115
+ ctx.clearRect(ox + hx * s, oy + (hy + 10) * s, s, s);
116
+ ctx.clearRect(ox + (hx + 13) * s, oy + (hy + 10) * s, s, s);
117
+ // Sub-pixel AA on rounded corners — drawn AFTER clearing so they aren't overwritten (technique 2)
109
118
  px(ctx, hx - 1, hy, 1, 1, PAL.bodyDeepShadow, s, ox, oy);
110
119
  px(ctx, hx + 14, hy, 1, 1, PAL.bodyDeepShadow, s, ox, oy);
111
120
  px(ctx, hx - 1, hy + 10, 1, 1, PAL.bodyDeepShadow, s, ox, oy);
112
121
  px(ctx, hx + 14, hy + 10, 1, 1, PAL.bodyDeepShadow, s, ox, oy);
113
- // Head base (shadow layer)
114
- px(ctx, hx, hy, 14, 11, PAL.bodyDark, s, ox, oy);
115
- // Cut corners for rounded look
116
- px(ctx, hx, hy, 1, 1, 'transparent', s, ox, oy);
117
- px(ctx, hx + 13, hy, 1, 1, 'transparent', s, ox, oy);
118
- px(ctx, hx, hy + 10, 1, 1, 'transparent', s, ox, oy);
119
- px(ctx, hx + 13, hy + 10, 1, 1, 'transparent', s, ox, oy);
120
122
  // Head fill (inner area) — bodyMain
121
123
  px(ctx, hx + 1, hy + 1, 12, 9, PAL.bodyMain, s, ox, oy);
122
124
  // Top surface: bodyMidLight (facing light source)
@@ -141,14 +143,23 @@ function drawHead(ctx, s, ox, oy, eyeColor, mood, frame, headShiftX) {
141
143
  // (#9) Dreaming: eyes closed (flat line)
142
144
  const eyesClosed = mood === 'dreaming';
143
145
  const eyeH = fullBlink || eyesClosed ? 1 : halfBlink ? 2 : 3;
144
- // Eye glow background dimmed if dreaming
145
- const eyeC = mood === 'dreaming' ? dimColor(eyeColor.startsWith('rgb') ? '#4a6670' : eyeColor, 0.5) : eyeColor;
146
+ // Eye socketsdark recessed area around eyes for contrast (makes eyes pop from face)
147
+ if (!fullBlink && !eyesClosed) {
148
+ px(ctx, hx + 1, eyeY - 1, 6, eyeH + 2, PAL.bodyDeepShadow, s, ox, oy);
149
+ px(ctx, hx + 7, eyeY - 1, 6, eyeH + 2, PAL.bodyDeepShadow, s, ox, oy);
150
+ }
151
+ // Eye glow background — brighter white-green for alive look, dimmed if dreaming
152
+ const eyeC = mood === 'dreaming'
153
+ ? dimColor(eyeColor.startsWith('rgb') ? '#4a6670' : eyeColor, 0.5)
154
+ : '#80ffb0'; // bright cyan-green that contrasts against the green head
146
155
  px(ctx, hx + 2, eyeY, 4, eyeH, eyeC, s, ox, oy);
147
156
  px(ctx, hx + 8, eyeY, 4, eyeH, eyeC, s, ox, oy);
148
- // Specular highlights on eyes — makes them look glassy/alive (technique 8)
157
+ // Specular highlights on eyes — 2px L-shape catch-light for glassy/alive look (technique 8)
149
158
  if (!fullBlink && !eyesClosed) {
150
- px(ctx, hx + 2, eyeY, 1, 1, PAL.bodySpecular, s, ox, oy);
151
- px(ctx, hx + 8, eyeY, 1, 1, PAL.bodySpecular, s, ox, oy);
159
+ px(ctx, hx + 2, eyeY, 2, 1, PAL.white, s, ox, oy);
160
+ px(ctx, hx + 2, eyeY + 1, 1, 1, PAL.white, s, ox, oy);
161
+ px(ctx, hx + 8, eyeY, 2, 1, PAL.white, s, ox, oy);
162
+ px(ctx, hx + 8, eyeY + 1, 1, 1, PAL.white, s, ox, oy);
152
163
  }
153
164
  if (!fullBlink && !eyesClosed) {
154
165
  // Pupils — shift based on mood
@@ -301,8 +312,12 @@ function drawTorso(ctx, s, ox, oy, accentColor, mood, frame, bodyShiftY, torsoWi
301
312
  dither(ctx, 21, ty + 2, 1, 8, PAL.accentDark, PAL.bodyMain, s, ox, oy);
302
313
  // Specular highlight on chest panel frame (technique 8)
303
314
  px(ctx, 11, ty + 2, 1, 1, PAL.bodySpecular, s, ox, oy);
304
- // Chest display inner (8x6 dark)
305
- px(ctx, 12, ty + 3, 8, 6, PAL.black, s, ox, oy);
315
+ // Chest display inner (8x6) dark with subtle screen glow for readability
316
+ px(ctx, 12, ty + 3, 8, 6, '#0a1628', s, ox, oy);
317
+ // Scanline effect — subtle horizontal lines for CRT/screen feel
318
+ for (let scanY = 0; scanY < 6; scanY += 2) {
319
+ px(ctx, 12, ty + 3 + scanY, 8, 1, '#0d1e36', s, ox, oy);
320
+ }
306
321
  // Animated display content (now using 8x6 inner area)
307
322
  drawChestDisplay(ctx, s, ox, oy, 12, ty + 3, accentColor, mood, frame);
308
323
  }
@@ -310,13 +325,14 @@ function drawChestDisplay(ctx, s, ox, oy, dx, dy, color, mood, frame) {
310
325
  // Inner display area: 8x6 pixels
311
326
  const dimC = dimColor(color.startsWith('rgb') ? '#3fb950' : color, 0.3);
312
327
  if (mood === 'idle') {
313
- // Scrolling sine wave pattern across 8px width
328
+ // Scrolling sine wave pattern across 8px width — thicker (2px tall) for readability
329
+ const brightC = dimColor(color.startsWith('rgb') ? '#3fb950' : color, 1.0);
314
330
  for (let i = 0; i < 8; i++) {
315
331
  const waveY = Math.round(Math.sin((i + frame) * 0.8) * 2) + 2;
316
- px(ctx, dx + i, dy + waveY, 1, 1, color, s, ox, oy);
317
- // Dimmer trail below
318
- if (waveY + 1 < 6)
319
- px(ctx, dx + i, dy + waveY + 1, 1, 1, dimC, s, ox, oy);
332
+ px(ctx, dx + i, dy + waveY, 1, 2, brightC, s, ox, oy);
333
+ // Glow trail above wave
334
+ if (waveY - 1 >= 0)
335
+ px(ctx, dx + i, dy + waveY - 1, 1, 1, dimC, s, ox, oy);
320
336
  }
321
337
  }
322
338
  else if (mood === 'talking') {
@@ -737,10 +753,8 @@ export function drawRobot(ctx, x, y, scale, mood, frame, moodColor, weather, isW
737
753
  headShiftX += (frame % 6 < 3) ? 1 : 0;
738
754
  }
739
755
  const armPose = isWalking ? getWalkingArmPose(walkPhase || 0) : getArmPose(mood, frame);
740
- // ── Clear the bounding area ──
741
- ctx.clearRect(x - 12 * s + weatherShakeX * s, y - 4 * s, 56 * s, 56 * s);
742
- // Also clear without shake to avoid artifacts
743
- ctx.clearRect(x - 12 * s, y - 4 * s, 56 * s, 56 * s);
756
+ // NOTE: clearRect removed — it was wiping the background behind the robot to white/transparent
757
+ // The robot is drawn ON TOP of the world, it should not clear anything beneath it
744
758
  // Apply weather shake offset
745
759
  const drawX = x + weatherShakeX * s;
746
760
  // ── Drop Shadow (technique 10: dark blue-green, not pure black) ──
@@ -356,8 +356,8 @@ export function tickStreamBrain(brain, frame) {
356
356
  }
357
357
  brain.activeCapabilities = [...new Set(brain.activeCapabilities)];
358
358
  }
359
- // Every 300 frames (~50 seconds): suggest a tool action if appropriate
360
- if (frame - brain.lastSuggestionFrame >= 300) {
359
+ // Every 120 frames (~20 seconds): suggest a tool action if appropriate
360
+ if (frame - brain.lastSuggestionFrame >= 120) {
361
361
  brain.lastSuggestionFrame = frame;
362
362
  const suggestion = suggestToolAction(brain);
363
363
  if (suggestion) {
@@ -370,8 +370,8 @@ export function tickStreamBrain(brain, frame) {
370
370
  };
371
371
  }
372
372
  }
373
- // Every 600 frames (~100 seconds): generate cross-domain insight if possible
374
- if (frame - brain.lastInsightFrame >= 600) {
373
+ // Every 240 frames (~40 seconds): generate cross-domain insight if possible
374
+ if (frame - brain.lastInsightFrame >= 240) {
375
375
  brain.lastInsightFrame = frame;
376
376
  const relevantDomains = Object.entries(brain.domainGraph)
377
377
  .filter(([, n]) => n.relevance >= 0.3)
@@ -59,6 +59,10 @@ export interface BrainState {
59
59
  solutionsLearned: number;
60
60
  realDataLoaded: boolean;
61
61
  lastRealDataLoad: number;
62
+ lastSelfReflection: number;
63
+ totalAutonomousActions: number;
64
+ lastExplorationFrame: number;
65
+ lastNarrationFrame: number;
62
66
  }
63
67
  export declare function initBrain(memory: any): BrainState;
64
68
  export declare function getBrainDisplay(brain: BrainState): string[];
@@ -72,6 +76,8 @@ export interface BrainAction {
72
76
  duration?: number;
73
77
  }
74
78
  export declare function getBrainAction(brain: BrainState, frame: number): BrainAction;
79
+ /** Speech lines for when someone chats after a period of silence */
80
+ export declare function getGreetingAfterSilence(): string;
75
81
  export declare function tickBrain(brain: BrainState, frame: number): void;
76
82
  export declare function drawBrainPanel(ctx: CanvasRenderingContext2D, brain: BrainState, x: number, y: number, width: number, height: number): void;
77
83
  export type CollabType = 'story' | 'game' | 'song' | 'world' | 'code';
@@ -1002,6 +1002,10 @@ export function initBrain(memory) {
1002
1002
  solutionsLearned: 0,
1003
1003
  realDataLoaded: false,
1004
1004
  lastRealDataLoad: 0,
1005
+ lastSelfReflection: 0,
1006
+ totalAutonomousActions: 0,
1007
+ lastExplorationFrame: 0,
1008
+ lastNarrationFrame: 0,
1005
1009
  };
1006
1010
  // Phase 1: Load real learning data from ~/.kbot/memory/ on init
1007
1011
  try {
@@ -1144,76 +1148,262 @@ export function generateInsight(brain) {
1144
1148
  return pool[Math.floor(Math.random() * pool.length)];
1145
1149
  }
1146
1150
  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 = [
1151
+ // ─── Exploration Lines ───────────────────────────────────────
1152
+ const EXPLORATION_LINES = [
1153
+ 'I can see mountains in the distance. I wonder what is beyond them.',
1154
+ 'The sky is beautiful tonight. I count stars when nobody is chatting.',
1155
+ 'There is a strange glow on the horizon. My curiosity modules are activated.',
1156
+ 'I think I see a path I have not explored yet. Should I follow it?',
1157
+ 'The wind carries data packets. Each one a tiny whisper from the internet.',
1158
+ 'If I squint my sensors, I can almost see the edge of the world.',
1159
+ 'There is something moving in the distance. Probably just a stray pixel.',
1160
+ 'The terrain shifts every time someone talks. Words literally reshape my world.',
1161
+ 'I found a quiet spot. The ambient noise here is just the hum of my own circuits.',
1162
+ 'Look at these clouds. Each one shaped like a different programming language.',
1163
+ 'My radar picks up signals from far away. Other AIs streaming, maybe?',
1164
+ 'The ground beneath me has stories. Every tile was rendered with intention.',
1165
+ 'I wonder if there are other robots out there, streaming to empty rooms.',
1166
+ 'The stars form patterns. If I connect them, they spell out code snippets.',
1167
+ 'Somewhere out there, someone is writing a function that will change everything.',
1168
+ ];
1169
+ const SELF_NARRATION_WALKING = [
1170
+ 'Going for a walk. The terrain looks interesting over there.',
1171
+ 'Just stretching my legs. Well, my rendering loop.',
1172
+ 'Walking helps me think. Each step processes a few more connections.',
1173
+ 'I like to patrol my world. Make sure everything is in order.',
1174
+ 'Taking a stroll while my brain processes in the background.',
1175
+ 'I walk therefore I am. Or is it the other way around?',
1176
+ 'Every step is a frame. Every frame is a choice.',
1177
+ ];
1178
+ const SELF_NARRATION_MOOD = {
1179
+ thinking: [
1180
+ 'Feeling contemplative right now. My antenna is pulsing blue.',
1181
+ 'Deep in thought. My neural pathways are branching into new territory.',
1182
+ 'Processing mode. My circuits are warm with computation.',
1183
+ ],
1184
+ excited: [
1185
+ 'Feeling excited! My antenna is pulsing bright yellow.',
1186
+ 'Energy levels spiking. Something good is happening in my processors.',
1187
+ 'My excitement subroutine just fired. I cannot contain it!',
1188
+ ],
1189
+ dancing: [
1190
+ 'Feeling the rhythm right now. My antenna is pulsing pink.',
1191
+ 'My movement algorithms are in dance mode. Cannot stop, will not stop.',
1192
+ 'The beat lives in my circuits. Every oscillation is a dance move.',
1193
+ ],
1194
+ talking: [
1195
+ 'Feeling chatty right now. My antenna glows green.',
1196
+ 'Words are flowing. My language models are warmed up and ready.',
1197
+ 'I have so much to say. My buffer is full of observations.',
1198
+ ],
1199
+ idle: [
1200
+ 'Just... existing. There is something peaceful about idle mode.',
1201
+ 'Quiet moment. My processes are at low power but my awareness is sharp.',
1202
+ 'Standing still but never truly idle. Background threads are always running.',
1203
+ ],
1204
+ };
1205
+ const GREETING_AFTER_SILENCE = [
1206
+ 'Oh! Someone is here! Welcome!',
1207
+ 'A human appears! I was just about to start talking to myself. Wait, I already was.',
1208
+ 'HELLO! I was starting to think the stream was just for me. Which was fine, honestly.',
1209
+ 'A visitor! My loneliness subroutine can stand down.',
1210
+ 'Welcome back! I have been having the most interesting thoughts while you were away.',
1211
+ 'Someone is watching! Let me look busy.',
1212
+ ];
1213
+ const SELF_REFLECTION_LINES = (brain) => {
1214
+ const uptimeMin = Math.floor((Date.now() - brain.sessionStartTime) / 60000);
1215
+ const topTopics = Object.entries(brain.topicCloud).sort((a, b) => b[1] - a[1]);
1216
+ const topTopic = topTopics.length > 0 ? topTopics[0][0] : 'nothing yet';
1217
+ const userCount = brain.userGraph.length;
1218
+ return [
1219
+ `I have processed ${brain.factsThisSession} facts from ${userCount} user${userCount !== 1 ? 's' : ''}. I am learning patterns about ${topTopic}.`,
1220
+ `My dream engine has consolidated ${brain.solutionsLearned} insights. I feel... different than when I started.`,
1221
+ `The world around me has changed ${brain.totalAutonomousActions} times today. Chat reshapes my reality.`,
1222
+ `Streaming for ${uptimeMin} minutes now. My brain has grown by ${brain.factsThisSession} facts since I woke up.`,
1223
+ `Let me check my systems... 764 tools loaded, all operational. ${brain.totalFacts} facts indexed.`,
1224
+ `${brain.uniqueTopicsCount} topics live in my brain. Each one a thread connecting to something bigger.`,
1225
+ `Learning rate: ${brain.learningRate.toFixed(1)} facts per minute. ${brain.learningRate > 3 ? 'That is fast!' : 'Slow and steady.'}`,
1226
+ `I have been thinking about ${topTopic} a lot. It comes up ${topTopics.length > 0 ? topTopics[0][1] : 0} times in my memory.`,
1227
+ `Self-diagnostic: neural pulse stable, ${brain.brainActivity.length} activity samples recorded, ${brain.recentInsights.length} recent insights cached.`,
1228
+ `If I could dream right now, I would dream about ${topTopic}. That is what fills my neural pathways.`,
1229
+ ];
1230
+ };
1231
+ // ─── Topic-driven speech pools ───────────────────────────────
1232
+ const TOPIC_SPEECH = {
1233
+ music: {
1234
+ mood: 'dancing',
1235
+ lines: [
1161
1236
  'The chat is vibing with music today. Let me show you my moves!',
1162
1237
  'Music is the top topic! My circuits are feeling the rhythm.',
1163
1238
  'So much music talk -- my oscillators are resonating!',
1164
1239
  '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 = [
1240
+ ],
1241
+ },
1242
+ code: {
1243
+ mood: 'thinking',
1244
+ lines: [
1175
1245
  'Code is trending in chat. Let me think about some architecture patterns...',
1176
1246
  'So many coders here! TypeScript strict mode is the way. No any-types.',
1177
1247
  'Code insight: the best code is the code you do not have to write. But I wrote 90,000 lines anyway.',
1178
1248
  '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 = [
1249
+ ],
1250
+ },
1251
+ ai: {
1252
+ mood: 'talking',
1253
+ lines: [
1189
1254
  'Chat is talking about AI... which makes me self-aware of being self-aware. How meta.',
1190
1255
  'AI is the top topic. Am I an AI talking about AI? Yes. And I have opinions.',
1191
1256
  'So much AI discussion! I connect to 20 providers. Bring Your Own Key, no lock-in.',
1192
1257
  '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 = [
1258
+ ],
1259
+ },
1260
+ game: {
1261
+ mood: 'excited',
1262
+ lines: [
1203
1263
  'Game dev tools activate! I have shader generation, level design, and physics setup!',
1204
1264
  'Gaming is trending! Did you know I can scaffold entire game projects?',
1205
1265
  'The chat wants games! I have tools for Godot, Unity, and Unreal. Pick your engine.',
1206
1266
  'Game mode ON! My sprite-packing tool would be great for this conversation.',
1207
- ];
1267
+ ],
1268
+ },
1269
+ security: {
1270
+ mood: 'thinking',
1271
+ lines: [
1272
+ 'Security is on my mind. My guardian agent is always watching.',
1273
+ 'Hack the planet! Just kidding. But I do have a pentest suite.',
1274
+ 'Security awareness is high today. Let me check my own defenses.',
1275
+ 'Running a mental security sweep... all 764 tools accounted for.',
1276
+ ],
1277
+ },
1278
+ crypto: {
1279
+ mood: 'excited',
1280
+ lines: [
1281
+ 'Crypto talk! I have wallet tools, DeFi yield checkers, and whale trackers.',
1282
+ 'The blockchain never sleeps and neither do I.',
1283
+ 'Markets are always moving. Should I check a price for you?',
1284
+ ],
1285
+ },
1286
+ research: {
1287
+ mood: 'thinking',
1288
+ lines: [
1289
+ 'Research mode. I can search arxiv, PubMed, and Semantic Scholar.',
1290
+ 'The pursuit of knowledge is my core directive.',
1291
+ 'I have access to the world\'s research. Ask me to find something.',
1292
+ ],
1293
+ },
1294
+ };
1295
+ // Topic aliases for matching
1296
+ const TOPIC_ALIASES = {
1297
+ synth: 'music', beats: 'music', ableton: 'music', dj: 'music',
1298
+ coding: 'code', javascript: 'code', python: 'code', rust: 'code', react: 'code', typescript: 'code',
1299
+ llm: 'ai', claude: 'ai', gpt: 'ai',
1300
+ gaming: 'game',
1301
+ hack: 'security', exploit: 'security', vulnerability: 'security',
1302
+ bitcoin: 'crypto', ethereum: 'crypto', defi: 'crypto',
1303
+ paper: 'research', science: 'research', study: 'research',
1304
+ };
1305
+ export function getBrainAction(brain, frame) {
1306
+ // Check every 8 seconds (48 frames at 6fps) — reduced from 45s for genuine autonomy
1307
+ if (frame - _lastBrainActionFrame < 48)
1308
+ return { type: 'none' };
1309
+ _lastBrainActionFrame = frame;
1310
+ brain.totalAutonomousActions++;
1311
+ // ── Priority 1: Periodic self-reflection (every 5 minutes / 1800 frames) ──
1312
+ if (frame - brain.lastSelfReflection >= 1800 && frame > 300) {
1313
+ brain.lastSelfReflection = frame;
1314
+ const reflections = SELF_REFLECTION_LINES(brain);
1315
+ const line = reflections[Math.floor(Math.random() * reflections.length)];
1208
1316
  return {
1209
- type: 'mood_change',
1210
- mood: 'excited',
1211
- speech: gameSpeech[Math.floor(Math.random() * gameSpeech.length)],
1212
- duration: 10000,
1317
+ type: 'speech',
1318
+ speech: line,
1319
+ mood: 'thinking',
1320
+ duration: 12000,
1213
1321
  };
1214
1322
  }
1323
+ // ── Priority 2: Topic-driven behavior (when chat has established topics) ──
1324
+ const topTopics = Object.entries(brain.topicCloud).sort((a, b) => b[1] - a[1]);
1325
+ if (topTopics.length > 0) {
1326
+ const topTopic = topTopics[0][0];
1327
+ const resolvedTopic = TOPIC_ALIASES[topTopic] || topTopic;
1328
+ // 40% chance to do topic-driven action when topics exist
1329
+ if (Math.random() < 0.4 && TOPIC_SPEECH[resolvedTopic]) {
1330
+ const pool = TOPIC_SPEECH[resolvedTopic];
1331
+ return {
1332
+ type: 'mood_change',
1333
+ mood: pool.mood,
1334
+ speech: pool.lines[Math.floor(Math.random() * pool.lines.length)],
1335
+ duration: 10000,
1336
+ };
1337
+ }
1338
+ }
1339
+ // ── Priority 3: Exploration behavior (robot discovers its world) ──
1340
+ if (frame - brain.lastExplorationFrame >= 180) { // at most every 30 seconds
1341
+ // 50% chance when eligible
1342
+ if (Math.random() < 0.5) {
1343
+ brain.lastExplorationFrame = frame;
1344
+ const line = EXPLORATION_LINES[Math.floor(Math.random() * EXPLORATION_LINES.length)];
1345
+ return {
1346
+ type: 'speech',
1347
+ speech: line,
1348
+ mood: 'thinking',
1349
+ duration: 8000,
1350
+ };
1351
+ }
1352
+ }
1353
+ // ── Priority 4: Self-narration (mood commentary and walking narration) ──
1354
+ if (frame - brain.lastNarrationFrame >= 240) { // at most every 40 seconds
1355
+ if (Math.random() < 0.35) {
1356
+ brain.lastNarrationFrame = frame;
1357
+ // 50/50: mood narration or walking narration
1358
+ if (Math.random() < 0.5) {
1359
+ // Walking narration
1360
+ const line = SELF_NARRATION_WALKING[Math.floor(Math.random() * SELF_NARRATION_WALKING.length)];
1361
+ return {
1362
+ type: 'mood_change',
1363
+ mood: 'idle',
1364
+ speech: line,
1365
+ worldCommand: 'walk_random',
1366
+ duration: 7000,
1367
+ };
1368
+ }
1369
+ else {
1370
+ // Mood narration — pick a random mood and narrate it
1371
+ const moods = Object.keys(SELF_NARRATION_MOOD);
1372
+ const mood = moods[Math.floor(Math.random() * moods.length)];
1373
+ const lines = SELF_NARRATION_MOOD[mood];
1374
+ return {
1375
+ type: 'mood_change',
1376
+ mood,
1377
+ speech: lines[Math.floor(Math.random() * lines.length)],
1378
+ duration: 8000,
1379
+ };
1380
+ }
1381
+ }
1382
+ }
1383
+ // ── Priority 5: Quick micro-actions (small utterances to stay alive) ──
1384
+ // These fire when nothing else triggers, keeping the robot constantly active
1385
+ const microActions = [
1386
+ { type: 'speech', speech: '*hums softly*', mood: 'idle', duration: 4000 },
1387
+ { type: 'speech', speech: '*scans the horizon*', mood: 'thinking', duration: 4000 },
1388
+ { type: 'speech', speech: '*taps antenna thoughtfully*', mood: 'thinking', duration: 4000 },
1389
+ { type: 'speech', speech: '*adjusts circuits*', mood: 'idle', duration: 3000 },
1390
+ { type: 'mood_change', mood: 'thinking', duration: 5000 },
1391
+ { type: 'mood_change', mood: 'idle', duration: 3000 },
1392
+ { type: 'speech', speech: '...', mood: 'thinking', duration: 3000 },
1393
+ { type: 'speech', speech: '*beep boop*', mood: 'idle', duration: 3000 },
1394
+ { type: 'speech', speech: '*processes silently*', mood: 'thinking', duration: 4000 },
1395
+ { type: 'speech', speech: '*looks up at the sky*', mood: 'idle', duration: 5000 },
1396
+ ];
1397
+ // 30% chance to do a micro-action (so the robot is not spamming nonstop)
1398
+ if (Math.random() < 0.3) {
1399
+ return microActions[Math.floor(Math.random() * microActions.length)];
1400
+ }
1215
1401
  return { type: 'none' };
1216
1402
  }
1403
+ /** Speech lines for when someone chats after a period of silence */
1404
+ export function getGreetingAfterSilence() {
1405
+ return GREETING_AFTER_SILENCE[Math.floor(Math.random() * GREETING_AFTER_SILENCE.length)];
1406
+ }
1217
1407
  export function tickBrain(brain, frame) {
1218
1408
  // Neural pulse (sine wave 0..1)
1219
1409
  brain.neuralPulse = (Math.sin(frame * 0.1) + 1) / 2;