@kernel.chat/kbot 3.87.0 → 3.93.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.
@@ -13,13 +13,23 @@ import { createCanvas } from 'canvas';
13
13
  import { drawRobot, drawMoodParticles, drawHat, drawPet, drawBuddyCompanion } from './sprite-engine.js';
14
14
  import { initIntelligence, tickIntelligence, handleIntelligenceCommand, drawBrainPanel, getBrainAction, tickMiniGame, drawMiniGameOverlay, tickProgression, updateQuestProgress, drawQuestPanel, tickRandomEvent, drawRandomEvent, shippedEffects, extraJokeResponses, multiLanguageGreetings } from './stream-intelligence.js';
15
15
  import { initStreamBrain, analyzeChatForDomains, tickStreamBrain, handleBrainCommand, drawBrainActivity } from './stream-brain.js';
16
- import { renderLighting, renderBloom, renderPostProcessing, renderSky, renderParticles, tickParticlesPBD, createParticleEmitter, drawCharacterEffects, checkMoodTransition, renderDamageFlash, buildCharacterLights, buildCharacterBloom, getAmbientForTime, renderAnimatedWater, renderLavaFlow, buildParallaxLayers, renderParallaxLayers, tickGrowingPlants, renderGrowingPlants, createRadianceGrid, updateRadianceGrid, renderRadianceOverlay, renderSubsurfaceGlow, buildSubsurfacePanels, createFrameCache, renderVolumetricFog, getFogParams, computeAnimationParams } from './render-engine.js';
16
+ import { renderLighting, renderBloom, renderPostProcessing, renderParticles, tickParticlesPBD, createParticleEmitter, drawCharacterEffects, checkMoodTransition, renderDamageFlash, buildCharacterLights, buildCharacterBloom, getAmbientForTime, buildParallaxLayers, tickGrowingPlants, createRadianceGrid, updateRadianceGrid, renderRadianceOverlay, renderSubsurfaceGlow, buildSubsurfacePanels, createFrameCache, renderVolumetricFog, getFogParams, computeAnimationParams } from './render-engine.js';
17
+ import { initTileWorld, handleTileCommand, saveWorld, loadWorld, TILE_SIZE } from './tile-world.js';
18
+ import { initRomEngine, renderRomBackground, tickRomEngine } from './rom-engine.js';
19
+ import { initLivingWorld, tickLivingWorld, saveLivingWorldState, loadLivingWorldState, evolveWorld } from './living-world.js';
17
20
  const KBOT_DIR = join(homedir(), '.kbot');
18
21
  const CHAT_BRIDGE_FILE = join(KBOT_DIR, 'stream-chat-live.json');
19
22
  const MEMORY_FILE = join(KBOT_DIR, 'stream-memory.json');
20
23
  const WIDTH = 1280;
21
24
  const HEIGHT = 720;
22
25
  const FPS = 6;
26
+ // Spam filter patterns — skip chat messages containing these strings (case-insensitive)
27
+ const SPAM_PATTERNS = [
28
+ 'streamboo', 'highcrest', 'cheapest viewers', 'best viewers', 'top viewers',
29
+ 'cheap viewers', 'remove the', 'buy followers', 'buy viewers', 'promo sm',
30
+ 'bigfollows', 'viewerbot', 'follow4follow', 'ownkick', 'botting service',
31
+ 'custom username bots', 'affordable botting', 'crypto payments',
32
+ ];
23
33
  // Mood color mapping for border/glow (mirrors sprite-engine)
24
34
  const MOOD_COLORS = {
25
35
  idle: '#3fb950',
@@ -774,7 +784,7 @@ function initGrowingPlants() {
774
784
  }
775
785
  // ─── PRIORITY 1: Environment Art (Background Scenes) ────────
776
786
  function drawBackground(ctx, frame) {
777
- const dividerX = 580;
787
+ const dividerX = WIDTH; // full screen width (was 580 from old panel layout)
778
788
  if (world.ground === 'grass') {
779
789
  // Dark green gradient sky (darker at top)
780
790
  const skyGrad = ctx.createLinearGradient(0, 60, 0, 490);
@@ -1453,6 +1463,10 @@ let charState = {
1453
1463
  lastMoodForCache: 'wave',
1454
1464
  lastGroundForCache: 'grass',
1455
1465
  };
1466
+ // Tile world state (Minecraft-style background, null = fallback to drawBackground)
1467
+ let tileWorld = null;
1468
+ let romState = null;
1469
+ let livingWorld = null;
1456
1470
  // ─── Phase 1: Buddy Speech Pools ─────────────────────────────
1457
1471
  const BUDDY_SPEECH_POOL = {
1458
1472
  fox: [
@@ -1544,7 +1558,7 @@ async function generateStreamDream(chatLog) {
1544
1558
  method: 'POST',
1545
1559
  headers: { 'Content-Type': 'application/json' },
1546
1560
  body: JSON.stringify({
1547
- model: 'kernel:latest',
1561
+ model: 'gemma4',
1548
1562
  prompt,
1549
1563
  stream: false,
1550
1564
  options: { temperature: 1.2, num_predict: 150 },
@@ -1614,6 +1628,12 @@ let lastChatTime = Date.now(); // track when last chat message arrived
1614
1628
  let memory = loadMemory();
1615
1629
  let intelligence = initIntelligence(memory);
1616
1630
  let streamBrain = initStreamBrain();
1631
+ // ─── World-First: On-demand overlay state ─────────────────────
1632
+ let showBrainOverlay = 0; // frames remaining to show brain panel overlay
1633
+ let showLeaderboardOverlay = 0; // frames remaining to show leaderboard overlay
1634
+ let showQuestOverlay = 0; // frames remaining to show quest panel overlay
1635
+ const OVERLAY_DURATION = 30; // 30 frames = 5 seconds at 6fps
1636
+ let lastChatActivityFrame = 0; // for chat fade-out timing
1617
1637
  // ─── FIX 3: Autonomous Behavior Tick ──────────────────────────
1618
1638
  function tickAutonomy() {
1619
1639
  const auto = charState.autonomy;
@@ -2041,28 +2061,23 @@ function renderFrame() {
2041
2061
  }
2042
2062
  const canvas = createCanvas(WIDTH, HEIGHT);
2043
2063
  const ctx = canvas.getContext('2d');
2044
- // Advance agenda
2064
+ // ── Tick all intelligence and behavior systems ──
2045
2065
  advanceAgenda();
2046
- // Tick intelligence systems
2047
2066
  tickIntelligence(intelligence, animFrame);
2048
- // Tick stream brain (collective intelligence)
2049
2067
  const brainTick = tickStreamBrain(streamBrain, animFrame);
2050
2068
  if (brainTick) {
2051
2069
  if (brainTick.mood) {
2052
2070
  charState.mood = brainTick.mood;
2053
- if (brainTick.duration) {
2071
+ if (brainTick.duration)
2054
2072
  setTimeout(() => { charState.mood = 'idle'; }, brainTick.duration);
2055
- }
2056
2073
  }
2057
2074
  if (brainTick.speech) {
2058
2075
  charState.speech = brainTick.speech;
2059
2076
  speakTTS(brainTick.speech);
2060
- if (brainTick.duration) {
2077
+ if (brainTick.duration)
2061
2078
  setTimeout(() => { charState.speech = ''; }, brainTick.duration);
2062
- }
2063
2079
  }
2064
2080
  }
2065
- // Tick mini-game
2066
2081
  const gameTickResult = tickMiniGame(intelligence.miniGame, animFrame);
2067
2082
  if (gameTickResult) {
2068
2083
  if (gameTickResult.screenShake)
@@ -2077,21 +2092,19 @@ function renderFrame() {
2077
2092
  setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
2078
2093
  }
2079
2094
  }
2080
- // Tick progression
2081
2095
  const progResult = tickProgression(intelligence.progression, animFrame);
2082
2096
  if (progResult) {
2083
2097
  if (progResult.completed) {
2084
- spawnFloatingText(`QUEST COMPLETE! +${progResult.completed.reward} XP`, 200, 300, '#f0c040', 48);
2098
+ spawnFloatingText(`QUEST COMPLETE! +${progResult.completed.reward} XP`, WIDTH / 2 - 100, 300, '#f0c040', 48);
2085
2099
  charState.screenShake = 4;
2086
2100
  charState.mood = 'excited';
2087
2101
  setTimeout(() => { charState.mood = 'idle'; }, 5000);
2088
2102
  }
2089
2103
  if (progResult.levelUp) {
2090
- spawnFloatingText('LEVEL UP!', 250, 250, '#bc8cff', 60);
2104
+ spawnFloatingText('LEVEL UP!', WIDTH / 2 - 40, 250, '#bc8cff', 60);
2091
2105
  charState.screenShake = 6;
2092
2106
  }
2093
2107
  }
2094
- // Tick random events
2095
2108
  const eventResult = tickRandomEvent(intelligence.randomEvent, animFrame);
2096
2109
  if (eventResult) {
2097
2110
  if (eventResult.screenShake)
@@ -2106,12 +2119,10 @@ function renderFrame() {
2106
2119
  setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
2107
2120
  }
2108
2121
  }
2109
- // FIX 3: Tick autonomous behavior
2110
2122
  tickAutonomy();
2111
- // Update world
2112
2123
  updateParticles();
2113
2124
  tickPhysics();
2114
- // NVIDIA: Compute animation params from stream context
2125
+ // Compute animation params
2115
2126
  {
2116
2127
  const elapsed = Math.floor((Date.now() - charState.startTime) / 1000);
2117
2128
  const streamMinutes = elapsed / 60;
@@ -2122,145 +2133,35 @@ function renderFrame() {
2122
2133
  const viewerEstimate = Math.max(1, Math.floor(memory.totalMessages / 3) + Object.keys(memory.users).length);
2123
2134
  charState.animParams = computeAnimationParams(chatRate, viewerEstimate, charState.mood, world.timeOfDay, streamMinutes);
2124
2135
  }
2125
- // NVIDIA: Detect cache invalidation triggers
2136
+ // Cache invalidation
2126
2137
  const moodChanged = charState.mood !== charState.lastMoodForCache;
2127
2138
  const worldChanged = world.ground !== charState.lastGroundForCache;
2128
2139
  if (moodChanged)
2129
2140
  charState.lastMoodForCache = charState.mood;
2130
2141
  if (worldChanged)
2131
2142
  charState.lastGroundForCache = world.ground;
2132
- // AAA: Continuous particle effects for biomes
2143
+ // Biome particles
2133
2144
  if (world.ground === 'lava' && animFrame % 4 === 0) {
2134
- charState.renderParticles.push(...createParticleEmitter('fire', 50 + Math.random() * 480, 485, 1));
2145
+ charState.renderParticles.push(...createParticleEmitter('fire', Math.random() * WIDTH, HEIGHT - 50, 1));
2135
2146
  }
2136
2147
  if (world.ground === 'space' && animFrame % 12 === 0) {
2137
- charState.renderParticles.push(...createParticleEmitter('aura', charState.robotX + 160, 280, 1));
2148
+ charState.renderParticles.push(...createParticleEmitter('aura', WIDTH / 2, HEIGHT / 2 - 100, 1));
2138
2149
  }
2139
- // AAA: Tick render particles (cap at 150 to prevent performance issues)
2140
2150
  if (charState.renderParticles.length > 150) {
2141
2151
  charState.renderParticles = charState.renderParticles.slice(-150);
2142
2152
  }
2143
- charState.renderParticles = tickParticlesPBD(charState.renderParticles, 480, charState.robotX + 160, 280);
2144
- // AAA: Tick growing plants
2153
+ charState.renderParticles = tickParticlesPBD(charState.renderParticles, HEIGHT - 40, WIDTH / 2, HEIGHT / 2 - 100);
2145
2154
  tickGrowingPlants(charState.growingPlants);
2146
- // PRIORITY 2: Screen shake offset
2147
- let shakeOffX = 0, shakeOffY = 0;
2148
- if (charState.screenShake > 0) {
2149
- shakeOffX = Math.round((Math.random() - 0.5) * 6);
2150
- shakeOffY = Math.round((Math.random() - 0.5) * 4);
2151
- charState.screenShake--;
2152
- }
2153
- ctx.save();
2154
- ctx.translate(shakeOffX, shakeOffY);
2155
- // NVIDIA: Early moodColor resolution (needed by fog and all rendering)
2156
- const moodColorHex = MOOD_COLORS[charState.mood] ?? COLORS.green;
2157
- // Background — AAA: base fill + procedural sky
2158
- ctx.fillStyle = world.events.includes('lightning') ? '#ffffff' : getWorldBg();
2159
- ctx.fillRect(0, 0, WIDTH, HEIGHT);
2160
- // AAA: Procedural sky rendering (replaces flat gradient for sky area)
2161
- renderSky(ctx, WIDTH, HEIGHT, world.timeOfDay, world.weather, animFrame, 580);
2162
- // AAA: Parallax layers (rebuild if biome changed)
2163
- if (charState.parallaxLayers.length === 0) {
2164
- charState.parallaxLayers = buildParallaxLayers(world.ground, 580);
2165
- }
2166
- renderParallaxLayers(ctx, charState.parallaxLayers, charState.robotX, animFrame);
2167
- // Draw full animated background scene (biome-specific details on top of parallax)
2168
- drawBackground(ctx, animFrame);
2169
- // AAA: Advanced water/lava rendering for specific biomes
2170
- if (world.ground === 'ocean') {
2171
- renderAnimatedWater(ctx, 580, animFrame);
2172
- }
2173
- else if (world.ground === 'lava') {
2174
- renderLavaFlow(ctx, 580, animFrame);
2175
- }
2176
- // AAA: Growing vegetation
2177
- renderGrowingPlants(ctx, charState.growingPlants);
2178
- // (#17) Weather particles as rectangles
2179
- for (const p of world.particles) {
2180
- if (world.weather === 'rain') {
2181
- ctx.fillStyle = '#6699cc';
2182
- ctx.fillRect(p.x, p.y, 2, 8);
2183
- }
2184
- else if (world.weather === 'snow') {
2185
- ctx.fillStyle = '#ffffff';
2186
- ctx.fillRect(p.x, p.y, 4, 4);
2187
- }
2188
- else if (world.weather === 'storm') {
2189
- ctx.fillStyle = '#aaccff';
2190
- ctx.fillRect(p.x, p.y, 2, 12);
2191
- }
2192
- else if (world.weather === 'stars') {
2193
- ctx.fillStyle = '#ffffaa';
2194
- ctx.fillRect(p.x, p.y, 2, 2);
2195
- }
2196
- else {
2197
- ctx.fillStyle = '#6699cc';
2198
- ctx.fillRect(p.x, p.y, 2, 6);
2199
- }
2200
- }
2201
- // NVIDIA: Volumetric fog (between background and character layers)
2155
+ // Autonomous pacing when idle, periodically pick a new target and walk there
2202
2156
  {
2203
- const fogParams = getFogParams(world.ground, world.timeOfDay);
2204
- const fogLights = buildCharacterLights(charState.robotX, 90, 10, moodColorHex, animFrame, world.events.includes('lightning'), world.items.map(i => ({ x: i.x, y: i.y, emoji: i.emoji, name: i.name })));
2205
- renderVolumetricFog(ctx, WIDTH, HEIGHT, animFrame, fogParams.density, fogParams.color, fogLights);
2206
- }
2207
- // World items (physics-enabled)
2208
- ctx.fillStyle = COLORS.text;
2209
- ctx.font = '18px "Courier New", monospace';
2210
- for (const item of world.items) {
2211
- ctx.fillText(item.emoji, item.x, item.y);
2212
- }
2213
- // ── Header bar ──
2214
- ctx.fillStyle = COLORS.bgPanel;
2215
- ctx.fillRect(0, 0, WIDTH, 60);
2216
- // Header border
2217
- ctx.strokeStyle = COLORS.accent;
2218
- ctx.lineWidth = 2;
2219
- ctx.beginPath();
2220
- ctx.moveTo(0, 60);
2221
- ctx.lineTo(WIDTH, 60);
2222
- ctx.stroke();
2223
- // Title
2224
- ctx.fillStyle = COLORS.accent;
2225
- ctx.font = 'bold 28px "Courier New", "Courier", monospace';
2226
- ctx.fillText('K : B O T L I V E', 40, 40);
2227
- // Current segment badge
2228
- const segLabel = SEGMENT_LABELS[agenda.currentSegment];
2229
- const segElapsed = Math.floor((Date.now() - agenda.segmentStartTime) / 1000);
2230
- const segRemaining = Math.max(0, Math.floor((SEGMENT_DURATION_MS - (Date.now() - agenda.segmentStartTime)) / 1000));
2231
- const segTimeStr = `${Math.floor(segRemaining / 60)}:${String(segRemaining % 60).padStart(2, '0')}`;
2232
- ctx.fillStyle = COLORS.accent;
2233
- ctx.font = 'bold 14px "Courier New", monospace';
2234
- const segText = `[ ${segLabel} ${segTimeStr} ]`;
2235
- ctx.fillText(segText, 330, 40);
2236
- // Viewers counter (proxy from chat message count)
2237
- const viewerEstimate = Math.max(1, Math.floor(memory.totalMessages / 3) + Object.keys(memory.users).length);
2238
- ctx.fillStyle = COLORS.red;
2239
- ctx.font = 'bold 14px "Courier New", monospace';
2240
- ctx.fillText(`VIEWERS: ~${viewerEstimate}`, WIDTH - 280, 22);
2241
- // Timer
2242
- const elapsed = Math.floor((Date.now() - charState.startTime) / 1000);
2243
- const timeStr = `${String(Math.floor(elapsed / 3600)).padStart(2, '0')}:${String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0')}:${String(elapsed % 60).padStart(2, '0')}`;
2244
- ctx.fillStyle = COLORS.textDim;
2245
- ctx.font = '20px "Courier New", monospace';
2246
- ctx.fillText(timeStr, WIDTH - 140, 38);
2247
- // Platform indicators
2248
- ctx.font = 'bold 14px "Courier New", monospace';
2249
- const platforms = [
2250
- { name: 'TWITCH', color: COLORS.twitchPurple, x: 460 },
2251
- { name: 'RUMBLE', color: COLORS.rumbleGreen, x: 580 },
2252
- { name: 'KICK', color: COLORS.kickGreen, x: 700 },
2253
- ];
2254
- for (const p of platforms) {
2255
- // Dot
2256
- ctx.fillStyle = p.color;
2257
- ctx.beginPath();
2258
- ctx.arc(p.x, 33, 5, 0, Math.PI * 2);
2259
- ctx.fill();
2260
- ctx.fillStyle = COLORS.text;
2261
- ctx.fillText(p.name, p.x + 12, 38);
2157
+ const currentlyWalking = Math.abs(charState.robotX - charState.robotTargetX) > 2;
2158
+ if (charState.mood === 'idle' && !currentlyWalking && animFrame % 300 === 0 && animFrame > 60) {
2159
+ // Every ~50 seconds (300 frames at 6fps), stroll to a new position
2160
+ charState.robotTargetX = charState.robotX + (Math.random() > 0.5 ? 100 : -100);
2161
+ charState.robotTargetX = Math.max(200, Math.min(1000, charState.robotTargetX));
2162
+ }
2262
2163
  }
2263
- // ── FIX 1: Movement logic — robot walks toward target ──
2164
+ // Movement logic
2264
2165
  const isWalking = Math.abs(charState.robotX - charState.robotTargetX) > 2;
2265
2166
  if (isWalking) {
2266
2167
  const dx = charState.robotTargetX - charState.robotX;
@@ -2272,26 +2173,23 @@ function renderFrame() {
2272
2173
  else {
2273
2174
  charState.robotDirection = 'idle';
2274
2175
  }
2275
- // ── FIX 4: Brain-driven behavior ──
2176
+ // Brain-driven behavior
2276
2177
  const brainAction = getBrainAction(intelligence.brain, animFrame);
2277
2178
  if (brainAction.type !== 'none') {
2278
2179
  if (brainAction.mood) {
2279
2180
  charState.mood = brainAction.mood;
2280
- if (brainAction.duration) {
2181
+ if (brainAction.duration)
2281
2182
  setTimeout(() => { charState.mood = 'idle'; }, brainAction.duration);
2282
- }
2283
2183
  }
2284
2184
  if (brainAction.speech) {
2285
2185
  charState.speech = brainAction.speech;
2286
2186
  speakTTS(brainAction.speech);
2287
- if (brainAction.duration) {
2187
+ if (brainAction.duration)
2288
2188
  setTimeout(() => { charState.speech = ''; }, brainAction.duration);
2289
- }
2290
2189
  }
2291
2190
  }
2292
- // FIX 1: Shipped effect — "Add stream highlights reel"
2191
+ // Shipped effects
2293
2192
  if (shippedEffects.has('Add stream highlights reel') && animFrame % 900 === 0 && animFrame > 100) {
2294
- // Every ~2.5 minutes, call out a highlight
2295
2193
  const highlightPhrases = [
2296
2194
  'Highlight moment! This is one for the reel!',
2297
2195
  'That was worth saving! Highlight captured!',
@@ -2300,11 +2198,10 @@ function renderFrame() {
2300
2198
  ];
2301
2199
  if (!charState.speech) {
2302
2200
  charState.speech = highlightPhrases[Math.floor(Math.random() * highlightPhrases.length)];
2303
- spawnFloatingText('HIGHLIGHT!', 200, 200, '#f0c040', 36);
2201
+ spawnFloatingText('HIGHLIGHT!', WIDTH / 2 - 60, 200, '#f0c040', 36);
2304
2202
  setTimeout(() => { charState.speech = ''; }, 5000);
2305
2203
  }
2306
2204
  }
2307
- // FIX 1: Shipped effect — "Add chat sentiment analysis"
2308
2205
  if (shippedEffects.has('Add chat sentiment analysis') && animFrame % 720 === 0 && animFrame > 200) {
2309
2206
  const recentMsgs = charState.chatMessages.slice(-20);
2310
2207
  if (recentMsgs.length > 5) {
@@ -2321,117 +2218,176 @@ function renderFrame() {
2321
2218
  }
2322
2219
  }
2323
2220
  if (!charState.speech) {
2324
- if (score > 5) {
2221
+ if (score > 5)
2325
2222
  charState.speech = 'Chat seems really excited today! The vibes are immaculate!';
2326
- }
2327
- else if (score < -3) {
2223
+ else if (score < -3)
2328
2224
  charState.speech = 'Chat seems a bit grumpy... should I tell a joke?';
2329
- }
2330
- else if (score > 2) {
2225
+ else if (score > 2)
2331
2226
  charState.speech = 'Positive energy in the chat! My neural pathways approve.';
2332
- }
2333
2227
  if (charState.speech)
2334
2228
  setTimeout(() => { charState.speech = ''; }, 8000);
2335
2229
  }
2336
2230
  }
2337
2231
  }
2338
- // ── Main layout: Robot (left) | Chat (right) ──
2339
- const dividerX = 580;
2340
- // Divider line
2341
- ctx.strokeStyle = COLORS.border;
2342
- ctx.lineWidth = 1;
2343
- ctx.beginPath();
2344
- ctx.moveTo(dividerX, 70);
2345
- ctx.lineTo(dividerX, HEIGHT - 120);
2346
- ctx.stroke();
2347
- // ── Robot area (left side) Pixel Art Sprite ──
2348
- const robotScale = 10;
2349
- const robotX = charState.robotX; // FIX 1: use dynamic position
2350
- const robotY = 90;
2232
+ // Track tool execution state
2233
+ charState.isExecutingTool = !!(streamBrain.pendingAction && streamBrain.pendingAction.status === 'executing');
2234
+ if (charState.isExecutingTool && animFrame % 6 === 0) {
2235
+ charState.renderParticles.push(...createParticleEmitter('spark', WIDTH / 2, HEIGHT / 2 - 50, 3));
2236
+ charState.renderParticles.push(...createParticleEmitter('electricity', WIDTH / 2 - 10, HEIGHT / 2 - 200, 1));
2237
+ }
2238
+ // Screen shake offset
2239
+ let shakeOffX = 0, shakeOffY = 0;
2240
+ if (charState.screenShake > 0) {
2241
+ shakeOffX = Math.round((Math.random() - 0.5) * 6);
2242
+ shakeOffY = Math.round((Math.random() - 0.5) * 4);
2243
+ charState.screenShake--;
2244
+ }
2245
+ ctx.save();
2246
+ ctx.translate(shakeOffX, shakeOffY);
2247
+ const moodColorHex = MOOD_COLORS[charState.mood] ?? COLORS.green;
2248
+ const robotScale = 6;
2351
2249
  animFrame++;
2352
- // (#20) Robot glow soft radial gradient behind robot torso
2353
- const glowCenterX = robotX + 16 * robotScale;
2354
- const glowCenterY = robotY + 26 * robotScale;
2250
+ // Auto-save tile world every 1800 frames (~5 minutes at 6fps)
2251
+ if (tileWorld && animFrame % 1800 === 0)
2252
+ saveWorld(tileWorld);
2253
+ // Tick living world ecology every 60 frames (10 seconds)
2254
+ if (tileWorld && livingWorld && animFrame % 60 === 0) {
2255
+ const chatActive = charState.chatMessages.length > 0 && Date.now() - charState.lastChatTime < 30000;
2256
+ tickLivingWorld(tileWorld, livingWorld.ecology, livingWorld.memory, livingWorld.emotions, livingWorld.conversations, charState.robotX || 640, chatActive, animFrame);
2257
+ // Record footstep
2258
+ livingWorld.memory.footpaths.set(`${Math.floor((charState.robotX || 640) / TILE_SIZE)}`, (livingWorld.memory.footpaths.get(`${Math.floor((charState.robotX || 640) / TILE_SIZE)}`) || 0) + 1);
2259
+ }
2260
+ // Save living world every 5 minutes
2261
+ if (livingWorld && animFrame % 1800 === 0)
2262
+ saveLivingWorldState(livingWorld.ecology, livingWorld.memory, livingWorld.emotions, livingWorld.conversations);
2263
+ // ════════════════════════════════════════════════════════════════
2264
+ // LAYER 1: TILE WORLD — fills entire 1280x720 frame
2265
+ // ════════════════════════════════════════════════════════════════
2266
+ // ROM Engine background — HDMA sky gradient + parallax layers
2267
+ if (romState) {
2268
+ tickRomEngine(romState, 1000 / FPS);
2269
+ renderRomBackground(ctx, romState, charState.robotX || 0, animFrame, WIDTH, HEIGHT);
2270
+ // Ground plane drawn AFTER robot position is calculated (uses groundY from robot)
2271
+ // Deferred to after robot position calc
2272
+ }
2273
+ else {
2274
+ // Fallback
2275
+ ctx.fillStyle = '#0d1117';
2276
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
2277
+ }
2278
+ // Weather particles over the full frame
2279
+ for (const p of world.particles) {
2280
+ if (world.weather === 'rain') {
2281
+ ctx.fillStyle = '#6699cc';
2282
+ ctx.fillRect(p.x, p.y, 2, 8);
2283
+ }
2284
+ else if (world.weather === 'snow') {
2285
+ ctx.fillStyle = '#ffffff';
2286
+ ctx.fillRect(p.x, p.y, 4, 4);
2287
+ }
2288
+ else if (world.weather === 'storm') {
2289
+ ctx.fillStyle = '#aaccff';
2290
+ ctx.fillRect(p.x, p.y, 2, 12);
2291
+ }
2292
+ else if (world.weather === 'stars') {
2293
+ ctx.fillStyle = '#ffffaa';
2294
+ ctx.fillRect(p.x, p.y, 2, 2);
2295
+ }
2296
+ else {
2297
+ ctx.fillStyle = '#6699cc';
2298
+ ctx.fillRect(p.x, p.y, 2, 6);
2299
+ }
2300
+ }
2301
+ // ════════════════════════════════════════════════════════════════
2302
+ // LAYER 2: LIGHTING + FOG over the world
2303
+ // ════════════════════════════════════════════════════════════════
2304
+ {
2305
+ const fogParams = getFogParams(world.ground, world.timeOfDay);
2306
+ const fogLights = buildCharacterLights(WIDTH / 2, HEIGHT / 2 - 100, robotScale, moodColorHex, animFrame, world.events.includes('lightning'), world.items.map(i => ({ x: i.x, y: i.y, emoji: i.emoji, name: i.name })));
2307
+ renderVolumetricFog(ctx, WIDTH, HEIGHT, animFrame, fogParams.density, fogParams.color, fogLights);
2308
+ }
2309
+ // World items (physics-enabled)
2310
+ ctx.fillStyle = COLORS.text;
2311
+ ctx.font = '18px "Courier New", monospace';
2312
+ for (const item of world.items) {
2313
+ ctx.fillText(item.emoji, item.x, item.y);
2314
+ }
2315
+ // ════════════════════════════════════════════════════════════════
2316
+ // LAYER 3: ROBOT + COMPANIONS — centered on terrain
2317
+ // ════════════════════════════════════════════════════════════════
2318
+ // Robot: centered both horizontally and vertically in the scene
2319
+ const robotScreenX = Math.floor(WIDTH / 2 - (32 * robotScale) / 2);
2320
+ const robotHeight = 50 * robotScale; // 300px at scale 6
2321
+ const robotScreenY = Math.floor(HEIGHT / 2 - robotHeight / 2 + 30); // slightly below center
2322
+ const groundY = robotScreenY + robotHeight; // ground meets robot feet (sprite is 50px tall)
2323
+ // Ground plane — extends upward to seamlessly meet parallax hills (no seam gap)
2324
+ // The nearHills parallax layer ends around groundY - 72px. We start the ground
2325
+ // fill 100px above groundY so it overlaps with the bottom of the parallax,
2326
+ // using the same base color (#1a4d1a) as the nearHills layer.
2327
+ {
2328
+ const groundTop = groundY - 100; // overlap with bottom of parallax hills
2329
+ const gGrad = ctx.createLinearGradient(0, groundTop, 0, HEIGHT);
2330
+ gGrad.addColorStop(0, '#1a4d1a'); // matches nearHills base color exactly
2331
+ gGrad.addColorStop(0.15, '#1a4d1a'); // hold the color through the overlap zone
2332
+ gGrad.addColorStop(0.4, '#0d3310');
2333
+ gGrad.addColorStop(1, '#061a08');
2334
+ ctx.fillStyle = gGrad;
2335
+ ctx.fillRect(0, groundTop, WIDTH, HEIGHT - groundTop);
2336
+ }
2337
+ // Robot glow
2338
+ const glowCenterX = robotScreenX + 16 * robotScale;
2339
+ const glowCenterY = robotScreenY + 26 * robotScale;
2355
2340
  const glowRadius = 10 * robotScale;
2356
2341
  const grad = ctx.createRadialGradient(glowCenterX, glowCenterY, 0, glowCenterX, glowCenterY, glowRadius);
2357
2342
  grad.addColorStop(0, hexToRgba(moodColorHex, 0.2));
2358
2343
  grad.addColorStop(1, hexToRgba(moodColorHex, 0));
2359
2344
  ctx.fillStyle = grad;
2360
2345
  ctx.fillRect(glowCenterX - glowRadius, glowCenterY - glowRadius, glowRadius * 2, glowRadius * 2);
2361
- // FIX 1: Music visualization behind robot (if shipped)
2362
- drawMusicVisualization(ctx, robotX, robotY);
2363
- // AAA: Character effects — eye glow bleed, mood aura (BEFORE robot for under-glow)
2364
- drawCharacterEffects(ctx, robotX, robotY, robotScale, charState.mood, animFrame, charState.isExecutingTool, isWalking ? 2 : 0, moodColorHex);
2365
- // Draw the pixel art robot (FIX 5: pass weather, walking state)
2346
+ // Music visualization
2347
+ drawMusicVisualization(ctx, robotScreenX, robotScreenY);
2348
+ // Character effects (under-glow)
2349
+ drawCharacterEffects(ctx, robotScreenX, robotScreenY, robotScale, charState.mood, animFrame, charState.isExecutingTool, isWalking ? 2 : 0, moodColorHex);
2350
+ // Chromatic aberration on mood transition
2366
2351
  const weatherType = world.weather === 'sunrise' ? 'clear' : world.weather;
2367
- // AAA: Chromatic aberration on mood transition
2368
2352
  const moodTransition = checkMoodTransition(charState.mood, moodColorHex);
2369
2353
  if (moodTransition.active && moodTransition.framesLeft > 0) {
2370
2354
  const offset = Math.ceil(moodTransition.framesLeft / 2);
2371
- // Red channel offset
2372
2355
  ctx.save();
2373
2356
  ctx.globalAlpha = 0.3;
2374
2357
  ctx.globalCompositeOperation = 'lighter';
2375
- drawRobot(ctx, robotX - offset, robotY, robotScale, charState.mood, animFrame, [255, 50, 50], weatherType, isWalking, charState.walkPhase);
2376
- // Blue channel offset
2377
- drawRobot(ctx, robotX + offset, robotY, robotScale, charState.mood, animFrame, [50, 50, 255], weatherType, isWalking, charState.walkPhase);
2358
+ drawRobot(ctx, robotScreenX - offset, robotScreenY, robotScale, charState.mood, animFrame, [255, 50, 50], weatherType, isWalking, charState.walkPhase);
2359
+ drawRobot(ctx, robotScreenX + offset, robotScreenY, robotScale, charState.mood, animFrame, [50, 50, 255], weatherType, isWalking, charState.walkPhase);
2378
2360
  ctx.restore();
2379
2361
  }
2380
- // AAA: Damage flash check
2381
- renderDamageFlash(ctx, robotX, robotY, robotScale);
2382
- drawRobot(ctx, robotX, robotY, robotScale, charState.mood, animFrame, undefined, weatherType, isWalking, charState.walkPhase);
2383
- drawMoodParticles(ctx, robotX, robotY, robotScale, charState.mood, animFrame);
2384
- // AAA: Render advanced particles
2385
- renderParticles(ctx, charState.renderParticles);
2386
- // NVIDIA: Subsurface scattering on translucent panels
2362
+ renderDamageFlash(ctx, robotScreenX, robotScreenY, robotScale);
2363
+ drawRobot(ctx, robotScreenX, robotScreenY, robotScale, charState.mood, animFrame, undefined, weatherType, isWalking, charState.walkPhase);
2364
+ drawMoodParticles(ctx, robotScreenX, robotScreenY, robotScale, charState.mood, animFrame);
2365
+ // Subsurface scattering
2387
2366
  {
2388
- const sssPanels = buildSubsurfacePanels(charState.robotX, 90, robotScale, moodColorHex);
2367
+ const sssPanels = buildSubsurfacePanels(robotScreenX, robotScreenY, robotScale, moodColorHex);
2389
2368
  renderSubsurfaceGlow(ctx, sssPanels);
2390
2369
  }
2391
- // PRIORITY 6: Draw hat AFTER robot so it layers on top
2370
+ // Hat
2392
2371
  if (charState.hat !== 'none') {
2393
- drawHat(ctx, robotX, robotY, robotScale, charState.hat, animFrame);
2394
- }
2395
- // PRIORITY 4: Update and draw pet
2396
- if (charState.pet) {
2397
- const pet = charState.pet;
2398
- pet.frame = animFrame;
2399
- // Pet follows robot with slight delay (lerp toward robot position + offset)
2400
- pet.targetX = robotX + 16 * robotScale + 60;
2401
- pet.targetY = robotY + 10 * robotScale - 40;
2402
- pet.x += (pet.targetX - pet.x) * 0.12;
2403
- pet.y += (pet.targetY - pet.y) * 0.12;
2404
- // Pet mood matches some robot states
2405
- if (charState.mood === 'dancing')
2406
- pet.mood = 'excited';
2407
- else if (world.weather === 'storm')
2408
- pet.mood = 'hiding';
2409
- else
2410
- pet.mood = 'idle';
2411
- drawPet(ctx, pet, robotScale, animFrame);
2372
+ drawHat(ctx, robotScreenX, robotScreenY, robotScale, charState.hat, animFrame);
2412
2373
  }
2413
- // Phase 1: Update and draw buddy companion
2374
+ // ── Buddy companion (follows robot) ──
2414
2375
  if (charState.buddy) {
2415
2376
  const buddy = charState.buddy;
2416
- const robotScale = 10;
2417
- // Buddy follows robot with lerp (offset to the right and slightly below)
2418
- const buddyTargetX = charState.robotX + 34 * robotScale + 20;
2419
- const buddyTargetY = robotY + 20 * robotScale;
2377
+ const buddyTargetX = robotScreenX + 34 * robotScale + 20;
2378
+ const buddyTargetY = robotScreenY + 20 * robotScale;
2420
2379
  buddy.x += (buddyTargetX - buddy.x) * 0.08;
2421
2380
  buddy.y += (buddyTargetY - buddy.y) * 0.08;
2422
- // Buddy reacts to main robot mood
2423
2381
  let buddyMood = charState.mood;
2424
2382
  if (world.weather === 'storm')
2425
2383
  buddyMood = 'storm';
2426
2384
  drawBuddyCompanion(ctx, buddy.x, buddy.y, robotScale, buddy.species, buddyMood, animFrame);
2427
- // Buddy speech bubble — small, positioned near buddy
2385
+ // Buddy speech
2428
2386
  const now = Date.now();
2429
- // Every ~60 seconds, buddy says something
2430
2387
  if (now - buddy.lastSpeechTime > 60000 && !buddy.speech) {
2431
2388
  const pool = BUDDY_SPEECH_POOL[buddy.species] || BUDDY_SPEECH_POOL['robot'];
2432
2389
  buddy.speech = pool[Math.floor(Math.random() * pool.length)];
2433
2390
  buddy.lastSpeechTime = now;
2434
- // Clear speech after 8 seconds
2435
2391
  setTimeout(() => { if (charState.buddy)
2436
2392
  charState.buddy.speech = ''; }, 8000);
2437
2393
  }
@@ -2440,82 +2396,217 @@ function renderFrame() {
2440
2396
  const bubbleY = buddy.y - 30;
2441
2397
  const bubbleW = Math.min(180, buddy.speech.length * 7 + 16);
2442
2398
  const bubbleH = 22;
2443
- // Bubble background
2444
2399
  ctx.fillStyle = 'rgba(22, 27, 34, 0.85)';
2445
2400
  ctx.fillRect(bubbleX, bubbleY, bubbleW, bubbleH);
2446
2401
  ctx.strokeStyle = '#8b949e';
2447
2402
  ctx.lineWidth = 1;
2448
2403
  ctx.strokeRect(bubbleX, bubbleY, bubbleW, bubbleH);
2449
- // Buddy name tag
2450
2404
  ctx.fillStyle = '#bc8cff';
2451
2405
  ctx.font = 'bold 9px "Courier New", monospace';
2452
2406
  ctx.fillText(buddy.name, bubbleX + 4, bubbleY + 10);
2453
- // Speech text
2454
2407
  ctx.fillStyle = '#e6edf3';
2455
2408
  ctx.font = '9px "Courier New", monospace';
2456
2409
  ctx.fillText(buddy.speech.slice(0, 28), bubbleX + 4, bubbleY + 19);
2457
2410
  }
2458
2411
  }
2459
- // PRIORITY 5: Mini-game overlay
2412
+ // ── Pet (follows robot) ──
2413
+ if (charState.pet) {
2414
+ const pet = charState.pet;
2415
+ pet.frame = animFrame;
2416
+ pet.targetX = robotScreenX + 16 * robotScale + 60;
2417
+ pet.targetY = robotScreenY + 10 * robotScale - 40;
2418
+ pet.x += (pet.targetX - pet.x) * 0.12;
2419
+ pet.y += (pet.targetY - pet.y) * 0.12;
2420
+ if (charState.mood === 'dancing')
2421
+ pet.mood = 'excited';
2422
+ else if (world.weather === 'storm')
2423
+ pet.mood = 'hiding';
2424
+ else
2425
+ pet.mood = 'idle';
2426
+ drawPet(ctx, pet, robotScale, animFrame);
2427
+ }
2428
+ // ════════════════════════════════════════════════════════════════
2429
+ // LAYER 4: PARTICLES + EFFECTS
2430
+ // ════════════════════════════════════════════════════════════════
2431
+ renderParticles(ctx, charState.renderParticles);
2432
+ // Mini-game overlay (if active)
2460
2433
  drawMiniGameOverlay(ctx, intelligence.miniGame, animFrame);
2461
- // PRIORITY 8: Random event overlay
2462
- drawRandomEvent(ctx, intelligence.randomEvent, animFrame, dividerX, HEIGHT);
2463
- // (#10) Stats overlay on right side of robot area
2434
+ // Random event overlay (full-width now)
2435
+ drawRandomEvent(ctx, intelligence.randomEvent, animFrame, WIDTH, HEIGHT);
2436
+ // Floating text particles
2437
+ charState.floatingTexts = charState.floatingTexts.filter(ft => {
2438
+ ft.frame++;
2439
+ if (ft.frame >= ft.maxFrames)
2440
+ return false;
2441
+ ft.y -= 1;
2442
+ const alpha = Math.max(0, 1 - ft.frame / ft.maxFrames);
2443
+ ctx.fillStyle = ft.color;
2444
+ ctx.globalAlpha = alpha;
2445
+ ctx.font = 'bold 16px "Courier New", monospace';
2446
+ ctx.fillText(ft.text, ft.x, ft.y);
2447
+ ctx.globalAlpha = 1;
2448
+ return true;
2449
+ });
2450
+ drawEmojiParticles(ctx);
2451
+ // ════════════════════════════════════════════════════════════════
2452
+ // LAYER 5: UI OVERLAYS (semi-transparent, floating on world)
2453
+ // ════════════════════════════════════════════════════════════════
2454
+ // ── Header bar: 40px tall, semi-transparent dark ──
2455
+ ctx.fillStyle = 'rgba(13,17,23,0.7)';
2456
+ ctx.fillRect(0, 0, WIDTH, 40);
2457
+ // Bottom accent line
2458
+ ctx.strokeStyle = hexToRgba(COLORS.accent, 0.5);
2459
+ ctx.lineWidth = 1;
2460
+ ctx.beginPath();
2461
+ ctx.moveTo(0, 40);
2462
+ ctx.lineTo(WIDTH, 40);
2463
+ ctx.stroke();
2464
+ // Left: "K:BOT LIVE" in 24px bold accent
2465
+ ctx.font = 'bold 24px "Courier New", monospace';
2466
+ ctx.fillStyle = COLORS.accent;
2467
+ ctx.fillText('K:BOT LIVE', 12, 28);
2468
+ // Center: current segment name
2469
+ const segLabel = SEGMENT_LABELS[agenda.currentSegment];
2464
2470
  ctx.fillStyle = COLORS.textDim;
2465
2471
  ctx.font = '14px "Courier New", monospace';
2466
- const statsX = dividerX - 160;
2467
- const statsY = robotY + 20;
2468
- ctx.fillText(`Messages: ${memory.totalMessages}`, statsX, statsY);
2469
- ctx.fillText(`Users: ${Object.keys(memory.users).length}`, statsX, statsY + 18);
2470
- const topTopic = Object.entries(memory.topics).sort((a, b) => b[1] - a[1])[0];
2471
- if (topTopic)
2472
- ctx.fillText(`Hot topic: ${topTopic[0]}`, statsX, statsY + 36);
2473
- // (#15) XP Leaderboard top 3 chatters by XP
2474
- const topXP = Object.entries(memory.users)
2475
- .filter(([, u]) => u.xp > 0)
2476
- .sort((a, b) => (b[1].xp || 0) - (a[1].xp || 0))
2477
- .slice(0, 3);
2478
- if (topXP.length > 0) {
2479
- ctx.fillStyle = COLORS.orange;
2480
- ctx.font = 'bold 13px "Courier New", monospace';
2481
- ctx.fillText('LEADERBOARD', statsX, statsY + 62);
2482
- for (let i = 0; i < topXP.length; i++) {
2483
- const [name, u] = topXP[i];
2484
- const trophy = i === 0 ? '1.' : i === 1 ? '2.' : '3.';
2485
- ctx.fillStyle = i === 0 ? '#f0c040' : i === 1 ? '#c0c0c0' : '#cd7f32';
2486
- ctx.fillText(`${trophy} ${name.slice(0, 12)}: ${u.xp || 0} XP`, statsX, statsY + 80 + i * 16);
2487
- }
2488
- }
2489
- // ── Brain Panel (below leaderboard, bottom-left) — FIX 2: bigger, more readable ──
2490
- const brainPanelX = statsX - 40;
2491
- const brainPanelY = statsY + 140;
2492
- const brainPanelW = 260;
2493
- const brainPanelH = 160;
2494
- // Phase 1: Override brain thought during dreaming
2495
- if (charState.mood === 'dreaming') {
2496
- const pulse = (Math.sin(animFrame * 0.15) + 1) / 2;
2497
- intelligence.brain.currentThought = `DREAMING${'.'.repeat(1 + Math.floor(pulse * 3))}`;
2498
- }
2499
- drawBrainPanel(ctx, intelligence.brain, brainPanelX, brainPanelY, brainPanelW, brainPanelH);
2500
- // ── Domain Radar (stream brain collective intelligence) ──
2501
- const radarX = brainPanelX;
2502
- const radarY = brainPanelY + brainPanelH + 4;
2503
- const radarW = brainPanelW;
2504
- const radarH = 130;
2505
- drawBrainActivity(ctx, streamBrain, radarX, radarY, radarW, radarH);
2506
- // ── Tool Action Overlay (when brain is executing) ──
2507
- // AAA: Track tool execution state for character effects
2508
- charState.isExecutingTool = !!(streamBrain.pendingAction && streamBrain.pendingAction.status === 'executing');
2509
- // AAA: Spawn sparks during tool execution
2510
- if (charState.isExecutingTool && animFrame % 6 === 0) {
2511
- charState.renderParticles.push(...createParticleEmitter('spark', charState.robotX + 160, 250, 3));
2512
- charState.renderParticles.push(...createParticleEmitter('electricity', charState.robotX + 150, 90 - 30, 1));
2472
+ const segW = ctx.measureText(segLabel).width;
2473
+ ctx.fillText(segLabel, (WIDTH - segW) / 2, 26);
2474
+ // Right: timer
2475
+ const elapsed = Math.floor((Date.now() - charState.startTime) / 1000);
2476
+ const timeStr = `${String(Math.floor(elapsed / 3600)).padStart(2, '0')}:${String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0')}:${String(elapsed % 60).padStart(2, '0')}`;
2477
+ ctx.fillStyle = COLORS.textDim;
2478
+ ctx.font = '16px "Courier New", monospace';
2479
+ ctx.fillText(timeStr, WIDTH - 160, 26);
2480
+ // ── Brain indicator (top-right, small pulsing circle) ──
2481
+ {
2482
+ const brainDotX = WIDTH - 40;
2483
+ const brainDotY = 20;
2484
+ const brainDotR = 10;
2485
+ const pulse = 0.7 + 0.3 * Math.sin(animFrame * 0.2);
2486
+ ctx.beginPath();
2487
+ ctx.arc(brainDotX, brainDotY, brainDotR, 0, Math.PI * 2);
2488
+ ctx.fillStyle = hexToRgba(moodColorHex, pulse);
2489
+ ctx.fill();
2490
+ // Fact count next to dot
2491
+ ctx.fillStyle = COLORS.textDim;
2492
+ ctx.font = '12px "Courier New", monospace';
2493
+ ctx.fillText(`${memory.sessionFacts.length} facts`, WIDTH - 120, 25);
2513
2494
  }
2495
+ // ── Chat feed overlay (bottom-left, semi-transparent, fades) ──
2496
+ {
2497
+ const chatOverlayX = 10;
2498
+ const chatOverlayY = HEIGHT - 200;
2499
+ const chatOverlayW = 400;
2500
+ const chatOverlayH = 150;
2501
+ const maxChatLines = 6;
2502
+ const cleanMessages = charState.chatMessages.filter(m => !SPAM_PATTERNS.some(p => m.text.toLowerCase().includes(p)));
2503
+ const recent = cleanMessages.slice(-maxChatLines);
2504
+ // Track chat activity for fade
2505
+ if (recent.length > 0)
2506
+ lastChatActivityFrame = animFrame;
2507
+ // Fade out after 60 frames (10 seconds) of no new messages
2508
+ const chatAge = animFrame - lastChatActivityFrame;
2509
+ const chatAlpha = chatAge < 60 ? 1.0 : Math.max(0.3, 1.0 - (chatAge - 60) / 60);
2510
+ if (recent.length > 0) {
2511
+ ctx.save();
2512
+ ctx.globalAlpha = chatAlpha;
2513
+ // Semi-transparent background
2514
+ ctx.fillStyle = 'rgba(13,17,23,0.6)';
2515
+ ctx.fillRect(chatOverlayX, chatOverlayY, chatOverlayW, chatOverlayH);
2516
+ // Messages
2517
+ for (let i = 0; i < recent.length; i++) {
2518
+ const msg = recent[i];
2519
+ const y = chatOverlayY + 14 + i * 22;
2520
+ // Platform badge
2521
+ const badge = msg.platform === 'twitch' ? 'TW' : msg.platform === 'kick' ? 'KK' : 'RM';
2522
+ const badgeColor = msg.platform === 'twitch' ? COLORS.twitchPurple :
2523
+ msg.platform === 'kick' ? COLORS.kickGreen : COLORS.rumbleGreen;
2524
+ ctx.fillStyle = badgeColor;
2525
+ ctx.fillRect(chatOverlayX + 6, y - 10, 24, 16);
2526
+ ctx.fillStyle = '#000';
2527
+ ctx.font = 'bold 10px "Courier New", monospace';
2528
+ ctx.fillText(badge, chatOverlayX + 8, y + 2);
2529
+ // Username
2530
+ ctx.fillStyle = COLORS.blue;
2531
+ ctx.font = 'bold 14px "Courier New", monospace';
2532
+ ctx.fillText(msg.username.slice(0, 14), chatOverlayX + 36, y + 2);
2533
+ // Message text
2534
+ ctx.fillStyle = COLORS.text;
2535
+ ctx.font = '14px "Courier New", monospace';
2536
+ const nameW = ctx.measureText(msg.username.slice(0, 14)).width;
2537
+ ctx.fillText(msg.text.slice(0, 30), chatOverlayX + 40 + nameW, y + 2);
2538
+ }
2539
+ ctx.restore();
2540
+ }
2541
+ else {
2542
+ // Show subtle "Waiting for chat..." when empty
2543
+ ctx.save();
2544
+ ctx.globalAlpha = 0.4;
2545
+ ctx.fillStyle = 'rgba(13,17,23,0.4)';
2546
+ ctx.fillRect(chatOverlayX, chatOverlayY + chatOverlayH - 30, 200, 24);
2547
+ ctx.fillStyle = COLORS.textDim;
2548
+ ctx.font = 'italic 14px "Courier New", monospace';
2549
+ ctx.fillText('Waiting for chat...', chatOverlayX + 10, chatOverlayY + chatOverlayH - 12);
2550
+ ctx.restore();
2551
+ }
2552
+ }
2553
+ // ── Speech bubble (bottom-center, semi-transparent) ──
2554
+ if (charState.speech) {
2555
+ const maxBubbleW = 600;
2556
+ ctx.font = charState.mood === 'dreaming' ? 'italic 20px "Courier New", monospace' : '20px "Courier New", monospace';
2557
+ // Measure text to get bubble width
2558
+ const speechW = Math.min(maxBubbleW, ctx.measureText(charState.speech).width + 40);
2559
+ const bubbleX = Math.floor((WIDTH - speechW) / 2);
2560
+ const bubbleY = HEIGHT - 80;
2561
+ // Word-wrap to calculate height
2562
+ const words = charState.speech.split(' ');
2563
+ let testLine = '';
2564
+ let lineCount = 1;
2565
+ for (const word of words) {
2566
+ const test = testLine + word + ' ';
2567
+ if (ctx.measureText(test).width > maxBubbleW - 30) {
2568
+ lineCount++;
2569
+ testLine = word + ' ';
2570
+ }
2571
+ else {
2572
+ testLine = test;
2573
+ }
2574
+ }
2575
+ const bubbleH = Math.max(36, lineCount * 26 + 16);
2576
+ // Background
2577
+ ctx.fillStyle = 'rgba(13,17,23,0.75)';
2578
+ ctx.fillRect(bubbleX, bubbleY - bubbleH + 36, speechW, bubbleH);
2579
+ // 4px accent left border
2580
+ ctx.fillStyle = COLORS.accent;
2581
+ ctx.fillRect(bubbleX, bubbleY - bubbleH + 36, 4, bubbleH);
2582
+ // Speech icon
2583
+ ctx.fillStyle = COLORS.accent;
2584
+ ctx.font = 'bold 20px "Courier New", monospace';
2585
+ ctx.fillText('>', bubbleX + 10, bubbleY + 6);
2586
+ // Text
2587
+ ctx.fillStyle = charState.mood === 'dreaming' ? '#7a6aaa' : COLORS.text;
2588
+ ctx.font = charState.mood === 'dreaming' ? 'italic 20px "Courier New", monospace' : '20px "Courier New", monospace';
2589
+ let line = '';
2590
+ let lineY = bubbleY - bubbleH + 56;
2591
+ for (const word of words) {
2592
+ const test = line + word + ' ';
2593
+ if (ctx.measureText(test).width > maxBubbleW - 40) {
2594
+ ctx.fillText(line.trim(), bubbleX + 30, lineY);
2595
+ line = word + ' ';
2596
+ lineY += 26;
2597
+ }
2598
+ else {
2599
+ line = test;
2600
+ }
2601
+ }
2602
+ ctx.fillText(line.trim(), bubbleX + 30, lineY);
2603
+ }
2604
+ // ── Tool Action Overlay (when brain is executing) ──
2514
2605
  if (streamBrain.pendingAction && streamBrain.pendingAction.status !== 'pending') {
2515
2606
  const action = streamBrain.pendingAction;
2516
- const overlayX = 20;
2517
- const overlayY = 320;
2518
- const overlayW = (dividerX || 560) - 40;
2607
+ const overlayW = 500;
2608
+ const overlayX = Math.floor((WIDTH - overlayW) / 2);
2609
+ const overlayY = HEIGHT - 140;
2519
2610
  const overlayH = 50;
2520
2611
  ctx.fillStyle = action.status === 'executing' ? 'rgba(240, 192, 64, 0.15)' : action.status === 'complete' ? 'rgba(63, 185, 80, 0.15)' : 'rgba(248, 81, 73, 0.15)';
2521
2612
  ctx.fillRect(overlayX, overlayY, overlayW, overlayH);
@@ -2528,24 +2619,20 @@ function renderFrame() {
2528
2619
  ctx.fillText(action.displayLines[i].slice(0, 70), overlayX + 6, overlayY + 14 + i * 13);
2529
2620
  }
2530
2621
  }
2531
- // ── PRIORITY 7: Quest Panel (below domain radar) ──
2532
- drawQuestPanel(ctx, intelligence.progression, brainPanelX - 10, radarY + radarH + 8);
2533
2622
  // ── Evolution Code Overlay (when actively building) ──
2534
2623
  if (intelligence.evolution.active && intelligence.evolution.activeProposal && intelligence.evolution.buildPhase !== 'idle') {
2535
- const evoX = 20;
2536
- const evoY = 360;
2537
- const evoW = dividerX - 40;
2624
+ const evoW = 540;
2625
+ const evoX = Math.floor((WIDTH - evoW) / 2);
2626
+ const evoY = 50;
2538
2627
  const evoH = 120;
2539
- ctx.fillStyle = 'rgba(13, 17, 23, 0.9)';
2628
+ ctx.fillStyle = 'rgba(13, 17, 23, 0.85)';
2540
2629
  ctx.fillRect(evoX, evoY, evoW, evoH);
2541
2630
  ctx.strokeStyle = '#f0c040';
2542
2631
  ctx.lineWidth = 1;
2543
2632
  ctx.strokeRect(evoX, evoY, evoW, evoH);
2544
- // Title
2545
2633
  ctx.fillStyle = '#f0c040';
2546
2634
  ctx.font = 'bold 11px "Courier New", monospace';
2547
- ctx.fillText(`BUILDING: ${intelligence.evolution.activeProposal.title.slice(0, 40)}`, evoX + 6, evoY + 14);
2548
- // Phase + progress bar
2635
+ ctx.fillText(`BUILDING: ${intelligence.evolution.activeProposal.title.slice(0, 50)}`, evoX + 6, evoY + 14);
2549
2636
  const phase = intelligence.evolution.buildPhase;
2550
2637
  const phaseDurations = { analyzing: 30, writing: 90, testing: 30, deploying: 18, done: 1 };
2551
2638
  const totalF = phaseDurations[phase] || 30;
@@ -2555,7 +2642,6 @@ function renderFrame() {
2555
2642
  ctx.fillStyle = '#8b949e';
2556
2643
  ctx.font = '10px "Courier New", monospace';
2557
2644
  ctx.fillText(`${phase} [${bar}] ${pct}%`, evoX + 6, evoY + 28);
2558
- // Code preview lines
2559
2645
  ctx.fillStyle = '#3fb950';
2560
2646
  ctx.font = '10px "Courier New", monospace';
2561
2647
  const codeLines = intelligence.evolution.codePreview.slice(-6);
@@ -2563,251 +2649,144 @@ function renderFrame() {
2563
2649
  ctx.fillText(codeLines[i].slice(0, 70), evoX + 6, evoY + 42 + i * 13);
2564
2650
  }
2565
2651
  }
2566
- // ── Collab Overlay (when active, below evolution or in same area) ──
2652
+ // ── Collab Overlay ──
2567
2653
  if (intelligence.collab.active) {
2568
- const collabY = (intelligence.evolution.active ? 490 : 360);
2569
- if (collabY < 490) {
2570
- const collabX = 20;
2571
- const collabW = dividerX - 40;
2572
- const collabH = 80;
2573
- ctx.fillStyle = 'rgba(13, 17, 23, 0.85)';
2574
- ctx.fillRect(collabX, collabY, collabW, collabH);
2575
- ctx.strokeStyle = '#58a6ff';
2576
- ctx.lineWidth = 1;
2577
- ctx.strokeRect(collabX, collabY, collabW, collabH);
2578
- ctx.fillStyle = '#58a6ff';
2579
- ctx.font = 'bold 11px "Courier New", monospace';
2580
- const collabTitle = intelligence.collab.title || 'Untitled';
2581
- ctx.fillText(`COLLAB [${intelligence.collab.type}]: ${collabTitle.slice(0, 35)}`, collabX + 6, collabY + 14);
2582
- ctx.fillStyle = '#8b949e';
2583
- ctx.font = '10px "Courier New", monospace';
2584
- ctx.fillText(`${intelligence.collab.contributors.size} people | ${intelligence.collab.phase}`, collabX + 6, collabY + 28);
2585
- ctx.fillStyle = '#e6edf3';
2586
- const recentContent = intelligence.collab.content.slice(-3);
2587
- for (let i = 0; i < recentContent.length; i++) {
2588
- ctx.fillText(recentContent[i].slice(0, 65), collabX + 6, collabY + 42 + i * 13);
2589
- }
2590
- }
2591
- }
2592
- // ── Chat area (right side) ──
2593
- ctx.fillStyle = COLORS.text;
2594
- ctx.font = 'bold 18px "Courier New", monospace';
2595
- ctx.fillText('Chat', dividerX + 20, 90);
2596
- // Chat border
2597
- ctx.strokeStyle = COLORS.border;
2598
- ctx.strokeRect(dividerX + 10, 100, WIDTH - dividerX - 30, HEIGHT - 230);
2599
- ctx.fillStyle = COLORS.bgChat;
2600
- ctx.fillRect(dividerX + 11, 101, WIDTH - dividerX - 32, HEIGHT - 232);
2601
- // Chat messages
2602
- ctx.font = '16px "Courier New", monospace';
2603
- const chatY = 125;
2604
- const maxChatLines = 18;
2605
- const recent = charState.chatMessages.slice(-maxChatLines);
2606
- for (let i = 0; i < recent.length; i++) {
2607
- const msg = recent[i];
2608
- const y = chatY + i * 24;
2609
- // FIX 1: Chat message slide-in animation (if "Add chat message animations" is shipped)
2610
- let slideOffsetX = 0;
2611
- if (shippedEffects.has('Add chat message animations')) {
2612
- // Newest messages slide in from right; older messages are settled
2613
- const msgAge = recent.length - i; // 1 for newest, higher for older
2614
- if (msgAge <= 2) {
2615
- // Recent: slide in over a few frames (approximate via age)
2616
- slideOffsetX = Math.max(0, (3 - msgAge) * 40);
2617
- }
2618
- }
2619
- // FIX 1: Loyalty badge dot (if "Build viewer loyalty badges" is shipped)
2620
- if (shippedEffects.has('Build viewer loyalty badges')) {
2621
- const user = memory.users[msg.username];
2622
- if (user) {
2623
- const msgCount = user.messageCount || 0;
2624
- let dotColor = '#8b949e'; // grey for newcomers
2625
- if (msgCount >= 50)
2626
- dotColor = '#f0c040'; // gold
2627
- else if (msgCount >= 10)
2628
- dotColor = '#c0c0c0'; // silver
2629
- else if (msgCount >= 3)
2630
- dotColor = '#cd7f32'; // bronze
2631
- ctx.fillStyle = dotColor;
2632
- ctx.beginPath();
2633
- ctx.arc(dividerX + 15 + slideOffsetX, y - 3, 4, 0, Math.PI * 2);
2634
- ctx.fill();
2635
- }
2636
- }
2637
- // Platform badge
2638
- const badge = msg.platform === 'twitch' ? 'TW' : msg.platform === 'kick' ? 'KK' : 'RM';
2639
- const badgeColor = msg.platform === 'twitch' ? COLORS.twitchPurple :
2640
- msg.platform === 'kick' ? COLORS.kickGreen : COLORS.rumbleGreen;
2641
- ctx.fillStyle = badgeColor;
2642
- ctx.fillRect(dividerX + 20 + slideOffsetX, y - 12, 28, 18);
2643
- ctx.fillStyle = '#000';
2644
- ctx.font = 'bold 12px "Courier New", monospace';
2645
- ctx.fillText(badge, dividerX + 22 + slideOffsetX, y + 2);
2646
- // Username
2647
- ctx.fillStyle = COLORS.blue;
2648
- ctx.font = 'bold 15px "Courier New", monospace';
2649
- ctx.fillText(msg.username, dividerX + 55 + slideOffsetX, y + 2);
2650
- // Message
2651
- ctx.fillStyle = COLORS.text;
2652
- ctx.font = '15px "Courier New", monospace';
2653
- const nameWidth = ctx.measureText(msg.username).width;
2654
- const msgText = msg.text.slice(0, 40);
2655
- ctx.fillText(msgText, dividerX + 60 + nameWidth + slideOffsetX, y + 2);
2656
- }
2657
- // FIX 1: Draw emoji reaction particles (if shipped)
2658
- drawEmojiParticles(ctx);
2659
- if (recent.length === 0) {
2660
- ctx.fillStyle = COLORS.textDim;
2661
- ctx.font = 'italic 16px "Courier New", monospace';
2662
- ctx.fillText('Waiting for chat...', dividerX + 30, chatY + 10);
2663
- }
2664
- // ── Speech bubble (bottom) — (#13) larger: 150px height, 24px font ──
2665
- const speechBubbleHeight = 150;
2666
- const speechY = HEIGHT - speechBubbleHeight - 20; // leave 20px for ticker
2667
- ctx.fillStyle = COLORS.bgPanel;
2668
- ctx.fillRect(0, speechY, WIDTH, speechBubbleHeight);
2669
- // (#13) 6px colored left border in accent color
2670
- ctx.fillStyle = COLORS.accent;
2671
- ctx.fillRect(0, speechY, 6, speechBubbleHeight);
2672
- // Top border
2673
- ctx.strokeStyle = COLORS.accent;
2674
- ctx.lineWidth = 2;
2675
- ctx.beginPath();
2676
- ctx.moveTo(0, speechY);
2677
- ctx.lineTo(WIDTH, speechY);
2678
- ctx.stroke();
2679
- // Speech icon
2680
- ctx.fillStyle = COLORS.accent;
2681
- ctx.font = 'bold 24px "Courier New", monospace';
2682
- ctx.fillText('>', 20, speechY + 40);
2683
- // Speech text — (#13) 24px font
2684
- if (charState.speech) {
2685
- // Phase 1: Dreamy color when in dream mode
2686
- ctx.fillStyle = charState.mood === 'dreaming' ? '#7a6aaa' : COLORS.text;
2687
- ctx.font = charState.mood === 'dreaming' ? 'italic 24px "Courier New", monospace' : '24px "Courier New", monospace';
2688
- // Word wrap
2689
- const words = charState.speech.split(' ');
2690
- let line = '';
2691
- let lineY = speechY + 40;
2692
- for (const word of words) {
2693
- const test = line + word + ' ';
2694
- if (ctx.measureText(test).width > WIDTH - 80) {
2695
- ctx.fillText(line.trim(), 50, lineY);
2696
- line = word + ' ';
2697
- lineY += 32;
2698
- if (lineY > speechY + speechBubbleHeight - 20)
2699
- break;
2700
- }
2701
- else {
2702
- line = test;
2703
- }
2704
- }
2705
- ctx.fillText(line.trim(), 50, lineY);
2706
- }
2707
- else {
2708
- ctx.fillStyle = COLORS.textDim;
2709
- ctx.font = 'italic 20px "Courier New", monospace';
2710
- ctx.fillText('...', 50, speechY + 40);
2711
- }
2712
- // ── (#14) Inner Monologue Ticker — 20px strip at very bottom ──
2713
- const tickerY = HEIGHT - 20;
2714
- ctx.fillStyle = '#0d1117';
2715
- ctx.fillRect(0, tickerY, WIDTH, 20);
2716
- // Update ticker thought every ~30 seconds
2717
- if (Date.now() > charState.tickerChangeTime) {
2718
- charState.tickerIndex = (charState.tickerIndex + 1) % INNER_THOUGHTS.length;
2719
- charState.tickerChangeTime = Date.now() + 30000;
2720
- charState.tickerOffset = WIDTH; // reset scroll to off-screen right
2721
- }
2722
- const thought = INNER_THOUGHTS[charState.tickerIndex];
2723
- ctx.fillStyle = '#ffb000'; // amber
2724
- ctx.font = '14px "Courier New", monospace';
2725
- charState.tickerOffset -= 2; // scroll left
2726
- const textW = ctx.measureText(thought).width;
2727
- if (charState.tickerOffset < -textW)
2728
- charState.tickerOffset = WIDTH;
2729
- ctx.fillText(thought, charState.tickerOffset, tickerY + 15);
2730
- // ── Learning indicator (above ticker) ──
2731
- if (memory.totalMessages > 0) {
2732
- ctx.fillStyle = COLORS.purple;
2733
- ctx.font = '12px "Courier New", monospace';
2734
- ctx.fillText(`brain: ${memory.sessionFacts.length} facts learned`, 20, tickerY - 4);
2735
- }
2736
- // ── Website URL ──
2737
- ctx.fillStyle = COLORS.accent;
2738
- ctx.font = 'bold 14px "Courier New", monospace';
2739
- ctx.fillText('kernel.chat', WIDTH - 140, tickerY - 4);
2740
- // ── PRIORITY 2: Floating text particles ──
2741
- charState.floatingTexts = charState.floatingTexts.filter(ft => {
2742
- ft.frame++;
2743
- if (ft.frame >= ft.maxFrames)
2744
- return false;
2745
- // Move upward, fade out
2746
- ft.y -= 1;
2747
- const alpha = Math.max(0, 1 - ft.frame / ft.maxFrames);
2748
- ctx.fillStyle = ft.color;
2749
- ctx.globalAlpha = alpha;
2750
- ctx.font = 'bold 16px "Courier New", monospace';
2751
- ctx.fillText(ft.text, ft.x, ft.y);
2752
- ctx.globalAlpha = 1;
2753
- return true;
2754
- });
2755
- // ── AAA: Dynamic Lighting Engine ──
2756
- {
2757
- const robotScale = 10;
2758
- const hasLightning = world.events.includes('lightning');
2759
- const ambientLevel = getAmbientForTime(world.timeOfDay);
2760
- const lights = buildCharacterLights(charState.robotX, 90, robotScale, moodColorHex, animFrame, hasLightning, world.items.map(i => ({ x: i.x, y: i.y, emoji: i.emoji, name: i.name })));
2761
- renderLighting(ctx, lights, WIDTH, HEIGHT, ambientLevel);
2762
- }
2763
- // ── NVIDIA: Radiance Grid — ambient light propagation ──
2764
- {
2765
- const hasLightning = world.events.includes('lightning');
2766
- const lights = buildCharacterLights(charState.robotX, 90, 10, moodColorHex, animFrame, hasLightning, world.items.map(i => ({ x: i.x, y: i.y, emoji: i.emoji, name: i.name })));
2767
- updateRadianceGrid(charState.radianceGrid, lights);
2768
- renderRadianceOverlay(ctx, charState.radianceGrid, WIDTH, HEIGHT);
2769
- }
2770
- // ── AAA: Bloom Effect ──
2771
- {
2772
- const robotScale = 10;
2773
- const bloomSpots = buildCharacterBloom(charState.robotX, 90, robotScale, moodColorHex, animFrame);
2774
- renderBloom(ctx, bloomSpots);
2654
+ const collabW = 500;
2655
+ const collabX = Math.floor((WIDTH - collabW) / 2);
2656
+ const collabY = 180;
2657
+ const collabH = 80;
2658
+ ctx.fillStyle = 'rgba(13, 17, 23, 0.85)';
2659
+ ctx.fillRect(collabX, collabY, collabW, collabH);
2660
+ ctx.strokeStyle = '#58a6ff';
2661
+ ctx.lineWidth = 1;
2662
+ ctx.strokeRect(collabX, collabY, collabW, collabH);
2663
+ ctx.fillStyle = '#58a6ff';
2664
+ ctx.font = 'bold 11px "Courier New", monospace';
2665
+ const collabTitle = intelligence.collab.title || 'Untitled';
2666
+ ctx.fillText(`COLLAB [${intelligence.collab.type}]: ${collabTitle.slice(0, 40)}`, collabX + 6, collabY + 14);
2667
+ ctx.fillStyle = '#8b949e';
2668
+ ctx.font = '10px "Courier New", monospace';
2669
+ ctx.fillText(`${intelligence.collab.contributors.size} people | ${intelligence.collab.phase}`, collabX + 6, collabY + 28);
2670
+ ctx.fillStyle = '#e6edf3';
2671
+ const recentContent = intelligence.collab.content.slice(-3);
2672
+ for (let i = 0; i < recentContent.length; i++) {
2673
+ ctx.fillText(recentContent[i].slice(0, 65), collabX + 6, collabY + 42 + i * 13);
2674
+ }
2675
+ }
2676
+ // ── Website URL (bottom-right, subtle) ──
2677
+ ctx.fillStyle = hexToRgba(COLORS.accent, 0.6);
2678
+ ctx.font = 'bold 13px "Courier New", monospace';
2679
+ ctx.fillText('kernel.chat', WIDTH - 130, HEIGHT - 10);
2680
+ // ════════════════════════════════════════════════════════════════
2681
+ // LAYER 6: ON-DEMAND PANELS (shown for 5 seconds when triggered)
2682
+ // ════════════════════════════════════════════════════════════════
2683
+ // Brain panel overlay (!brain)
2684
+ if (showBrainOverlay > 0) {
2685
+ showBrainOverlay--;
2686
+ const bpX = WIDTH - 320;
2687
+ const bpY = 50;
2688
+ const bpW = 300;
2689
+ const bpH = 200;
2690
+ ctx.fillStyle = 'rgba(13,17,23,0.85)';
2691
+ ctx.fillRect(bpX, bpY, bpW, bpH);
2692
+ ctx.strokeStyle = COLORS.purple;
2693
+ ctx.lineWidth = 1;
2694
+ ctx.strokeRect(bpX, bpY, bpW, bpH);
2695
+ if (charState.mood === 'dreaming') {
2696
+ const pulse = (Math.sin(animFrame * 0.15) + 1) / 2;
2697
+ intelligence.brain.currentThought = `DREAMING${'.'.repeat(1 + Math.floor(pulse * 3))}`;
2698
+ }
2699
+ drawBrainPanel(ctx, intelligence.brain, bpX + 5, bpY + 5, bpW - 10, bpH - 10);
2700
+ drawBrainActivity(ctx, streamBrain, bpX + 5, bpY + bpH - 60, bpW - 10, 55);
2701
+ }
2702
+ // Leaderboard overlay (!top)
2703
+ if (showLeaderboardOverlay > 0) {
2704
+ showLeaderboardOverlay--;
2705
+ const lbX = WIDTH / 2 - 150;
2706
+ const lbY = 60;
2707
+ const lbW = 300;
2708
+ const topXP = Object.entries(memory.users)
2709
+ .filter(([, u]) => u.xp > 0)
2710
+ .sort((a, b) => (b[1].xp || 0) - (a[1].xp || 0))
2711
+ .slice(0, 5);
2712
+ const lbH = 40 + topXP.length * 20;
2713
+ ctx.fillStyle = 'rgba(13,17,23,0.85)';
2714
+ ctx.fillRect(lbX, lbY, lbW, lbH);
2715
+ ctx.strokeStyle = COLORS.orange;
2716
+ ctx.lineWidth = 1;
2717
+ ctx.strokeRect(lbX, lbY, lbW, lbH);
2718
+ ctx.fillStyle = COLORS.orange;
2719
+ ctx.font = 'bold 14px "Courier New", monospace';
2720
+ ctx.fillText('LEADERBOARD', lbX + 10, lbY + 20);
2721
+ for (let i = 0; i < topXP.length; i++) {
2722
+ const [name, u] = topXP[i];
2723
+ const trophy = `${i + 1}.`;
2724
+ ctx.fillStyle = i === 0 ? '#f0c040' : i === 1 ? '#c0c0c0' : '#cd7f32';
2725
+ ctx.font = '13px "Courier New", monospace';
2726
+ ctx.fillText(`${trophy} ${name.slice(0, 16)}: ${u.xp || 0} XP`, lbX + 10, lbY + 40 + i * 20);
2727
+ }
2728
+ }
2729
+ // Quest panel overlay (!quest)
2730
+ if (showQuestOverlay > 0) {
2731
+ showQuestOverlay--;
2732
+ const qX = WIDTH / 2 - 160;
2733
+ const qY = 60;
2734
+ ctx.fillStyle = 'rgba(13,17,23,0.85)';
2735
+ ctx.fillRect(qX, qY, 320, 200);
2736
+ ctx.strokeStyle = '#3fb950';
2737
+ ctx.lineWidth = 1;
2738
+ ctx.strokeRect(qX, qY, 320, 200);
2739
+ drawQuestPanel(ctx, intelligence.progression, qX + 5, qY + 5);
2775
2740
  }
2776
- // ── AAA: Post-processing (replaces old scanlines + vignette) ──
2777
- renderPostProcessing(ctx, WIDTH, HEIGHT, animFrame, {
2778
- bloom: true,
2779
- filmGrain: true,
2780
- vignette: true,
2781
- scanlines: true,
2782
- });
2783
- // ── (#16) Segment transition overlay ──
2741
+ // ════════════════════════════════════════════════════════════════
2742
+ // LAYER 7: MOOD BORDER (2px pulsing colored border)
2743
+ // ════════════════════════════════════════════════════════════════
2744
+ // Segment transition overlay (full-screen flash)
2784
2745
  if (charState.segmentTransition > 0) {
2785
2746
  const fadeOut = charState.segmentTransition <= 10;
2786
2747
  const alpha = fadeOut ? charState.segmentTransition / 10 * 0.5 : 0.5;
2787
2748
  ctx.fillStyle = hexToRgba(COLORS.accent, alpha);
2788
2749
  ctx.fillRect(0, 0, WIDTH, HEIGHT);
2789
- // Large centered text
2790
2750
  ctx.fillStyle = `rgba(255,255,255,${fadeOut ? charState.segmentTransition / 10 : 1})`;
2791
2751
  ctx.font = 'bold 40px "Courier New", monospace';
2792
2752
  const segText = charState.segmentTransitionName;
2793
- const segW = ctx.measureText(segText).width;
2794
- ctx.fillText(segText, (WIDTH - segW) / 2, HEIGHT / 2 - 10);
2795
- // Progress indicator
2753
+ const stW = ctx.measureText(segText).width;
2754
+ ctx.fillText(segText, (WIDTH - stW) / 2, HEIGHT / 2 - 10);
2796
2755
  ctx.font = '24px "Courier New", monospace';
2797
2756
  const progText = charState.segmentTransitionIndex;
2798
- const progW = ctx.measureText(progText).width;
2799
- ctx.fillText(progText, (WIDTH - progW) / 2, HEIGHT / 2 + 30);
2757
+ const ptW = ctx.measureText(progText).width;
2758
+ ctx.fillText(progText, (WIDTH - ptW) / 2, HEIGHT / 2 + 30);
2800
2759
  charState.segmentTransition--;
2801
2760
  }
2802
- // Restore from screen shake translate
2761
+ // Restore from screen shake
2803
2762
  ctx.restore();
2804
- // ── (#11) Mood-color border — 4px around entire frame ──
2805
- const borderColor = charState.mood === 'dancing'
2763
+ // Mood border — 2px around entire frame, pulsing
2764
+ const borderColorRaw = charState.mood === 'dancing'
2806
2765
  ? ['#f85149', '#f0c040', '#3fb950', '#58a6ff', '#bc8cff', '#ff6ec7'][animFrame % 6]
2807
2766
  : MOOD_COLORS[charState.mood] ?? COLORS.green;
2808
- ctx.strokeStyle = borderColor;
2809
- ctx.lineWidth = 4;
2810
- ctx.strokeRect(2, 2, WIDTH - 4, HEIGHT - 4);
2767
+ const borderPulseAlpha = 0.7 + 0.3 * Math.sin(animFrame * 0.15);
2768
+ ctx.strokeStyle = hexToRgba(borderColorRaw, borderPulseAlpha);
2769
+ ctx.lineWidth = 2;
2770
+ ctx.strokeRect(1, 1, WIDTH - 2, HEIGHT - 2);
2771
+ // ════════════════════════════════════════════════════════════════
2772
+ // LAYER 8: POST-PROCESSING
2773
+ // ════════════════════════════════════════════════════════════════
2774
+ {
2775
+ const hasLightning = world.events.includes('lightning');
2776
+ const ambientLevel = getAmbientForTime(world.timeOfDay);
2777
+ const lights = buildCharacterLights(robotScreenX, robotScreenY, robotScale, moodColorHex, animFrame, hasLightning, world.items.map(i => ({ x: i.x, y: i.y, emoji: i.emoji, name: i.name })));
2778
+ renderLighting(ctx, lights, WIDTH, HEIGHT, ambientLevel);
2779
+ updateRadianceGrid(charState.radianceGrid, lights);
2780
+ renderRadianceOverlay(ctx, charState.radianceGrid, WIDTH, HEIGHT);
2781
+ const bloomSpots = buildCharacterBloom(robotScreenX, robotScreenY, robotScale, moodColorHex, animFrame);
2782
+ renderBloom(ctx, bloomSpots);
2783
+ }
2784
+ renderPostProcessing(ctx, WIDTH, HEIGHT, animFrame, {
2785
+ bloom: true,
2786
+ filmGrain: true,
2787
+ vignette: true,
2788
+ scanlines: true,
2789
+ });
2811
2790
  // Convert canvas to raw RGB24
2812
2791
  const imageData = ctx.getImageData(0, 0, WIDTH, HEIGHT);
2813
2792
  const rgba = imageData.data;
@@ -2908,12 +2887,38 @@ function startChatPoll() {
2908
2887
  }).catch(() => { });
2909
2888
  continue;
2910
2889
  }
2890
+ // World-First: On-demand overlay triggers
2891
+ {
2892
+ const cmd = msg.text.toLowerCase().trim();
2893
+ if (cmd === '!brain') {
2894
+ showBrainOverlay = OVERLAY_DURATION;
2895
+ continue;
2896
+ }
2897
+ if (cmd === '!top') {
2898
+ showLeaderboardOverlay = OVERLAY_DURATION;
2899
+ continue;
2900
+ }
2901
+ if (cmd === '!quest') {
2902
+ showQuestOverlay = OVERLAY_DURATION;
2903
+ continue;
2904
+ }
2905
+ }
2911
2906
  // Check brain commands (!do, !brain, !tools, !scan, !lookup, !research, !system, !ask, !stars, !news, !trending, !npm)
2912
2907
  const brainResult = handleBrainCommand(msg.text, msg.username, streamBrain);
2913
2908
  // Check intelligence commands (evolution, brain, collab)
2914
2909
  const intelResult = !brainResult ? handleIntelligenceCommand(msg.text, msg.username, intelligence) : null;
2910
+ // Check tile world commands (Minecraft-style: !place, !dig, !build, etc.)
2911
+ let tileResult = null;
2912
+ if (tileWorld && !brainResult && !intelResult) {
2913
+ tileResult = handleTileCommand(msg.text, msg.username, tileWorld, charState.robotX || 120);
2914
+ if (tileResult) {
2915
+ charState.mood = 'talking';
2916
+ charState.speech = tileResult;
2917
+ setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
2918
+ }
2919
+ }
2915
2920
  // Check for world commands
2916
- const worldResult = !intelResult && !brainResult ? parseWorldCommand(msg.text) : null;
2921
+ const worldResult = !intelResult && !brainResult && !tileResult ? parseWorldCommand(msg.text) : null;
2917
2922
  // FIX 1: Weather sound effect commentary (if shipped)
2918
2923
  if (worldResult && shippedEffects.has('Add weather sound effects')) {
2919
2924
  const t = msg.text.toLowerCase();
@@ -2941,9 +2946,11 @@ function startChatPoll() {
2941
2946
  ? Promise.resolve(brainResult)
2942
2947
  : intelResult
2943
2948
  ? Promise.resolve(intelResult)
2944
- : worldResult
2945
- ? Promise.resolve(worldResult)
2946
- : generateResponse(msg.username, msg.text, msg.platform);
2949
+ : tileResult
2950
+ ? Promise.resolve(tileResult)
2951
+ : worldResult
2952
+ ? Promise.resolve(worldResult)
2953
+ : generateResponse(msg.username, msg.text, msg.platform);
2947
2954
  responsePromise.then(response => {
2948
2955
  charState.speech = `@${msg.username}: ${response}`;
2949
2956
  memory.totalResponses++;
@@ -3053,7 +3060,7 @@ Respond in 1-2 short sentences. Be fun, witty, and engaging. Reference their int
3053
3060
  method: 'POST',
3054
3061
  headers: { 'Content-Type': 'application/json' },
3055
3062
  body: JSON.stringify({
3056
- model: 'kernel:latest',
3063
+ model: 'gemma4',
3057
3064
  prompt,
3058
3065
  stream: false,
3059
3066
  options: { temperature: 0.8, num_predict: 80 },
@@ -3394,6 +3401,18 @@ export function registerStreamRendererTools() {
3394
3401
  animFrame = 0;
3395
3402
  lastChatCount = 0;
3396
3403
  lastChatTime = Date.now();
3404
+ tileWorld = loadWorld() || initTileWorld();
3405
+ romState = initRomEngine('plains', 'night');
3406
+ livingWorld = loadLivingWorldState() || initLivingWorld();
3407
+ // Evolve world based on time since last stream
3408
+ if (tileWorld && livingWorld) {
3409
+ const lastSave = tileWorld.cameraX !== 0 ? 1 : 0; // rough check
3410
+ if (lastSave > 0) {
3411
+ const changes = evolveWorld(tileWorld, livingWorld.ecology, 1); // simulate 1 hour
3412
+ if (changes.length > 0)
3413
+ charState.speech = `The world evolved while I was away... ${changes.length} things changed.`;
3414
+ }
3415
+ }
3397
3416
  intelligence = initIntelligence(memory);
3398
3417
  agenda = {
3399
3418
  currentIndex: 0,
@@ -3438,6 +3457,8 @@ export function registerStreamRendererTools() {
3438
3457
  ffmpegProc = null;
3439
3458
  }
3440
3459
  saveMemory(memory);
3460
+ if (tileWorld)
3461
+ saveWorld(tileWorld);
3441
3462
  const elapsed = Math.floor((Date.now() - charState.startTime) / 60000);
3442
3463
  return `Stream stopped after ${elapsed}m.\nFrames: ${charState.frameCount}\nMessages: ${memory.totalMessages}\nUsers learned: ${Object.keys(memory.users).length}\nFacts: ${memory.sessionFacts.length}\nSegments completed: ${agenda.currentIndex}`;
3443
3464
  },