@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.
@@ -13,13 +13,27 @@ 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';
20
+ import { initEvolutionEngine, loadEvolutionState, saveEvolutionState, tickEvolution, renderTechnique } from './evolution-engine.js';
21
+ import { createNarrativeEngine, loadNarrative, saveNarrative, tickNarrative, handleNarrativeCommand } from './narrative-engine.js';
22
+ import { createAudioEngine, tickAudio } from './audio-engine.js';
23
+ import { loadSocialEngine, saveSocialEngine, trackViewer, tickSocial } from './social-engine.js';
17
24
  const KBOT_DIR = join(homedir(), '.kbot');
18
25
  const CHAT_BRIDGE_FILE = join(KBOT_DIR, 'stream-chat-live.json');
19
26
  const MEMORY_FILE = join(KBOT_DIR, 'stream-memory.json');
20
27
  const WIDTH = 1280;
21
28
  const HEIGHT = 720;
22
29
  const FPS = 6;
30
+ // Spam filter patterns — skip chat messages containing these strings (case-insensitive)
31
+ const SPAM_PATTERNS = [
32
+ 'streamboo', 'highcrest', 'cheapest viewers', 'best viewers', 'top viewers',
33
+ 'cheap viewers', 'remove the', 'buy followers', 'buy viewers', 'promo sm',
34
+ 'bigfollows', 'viewerbot', 'follow4follow', 'ownkick', 'botting service',
35
+ 'custom username bots', 'affordable botting', 'crypto payments',
36
+ ];
23
37
  // Mood color mapping for border/glow (mirrors sprite-engine)
24
38
  const MOOD_COLORS = {
25
39
  idle: '#3fb950',
@@ -774,7 +788,7 @@ function initGrowingPlants() {
774
788
  }
775
789
  // ─── PRIORITY 1: Environment Art (Background Scenes) ────────
776
790
  function drawBackground(ctx, frame) {
777
- const dividerX = 580;
791
+ const dividerX = WIDTH; // full screen width (was 580 from old panel layout)
778
792
  if (world.ground === 'grass') {
779
793
  // Dark green gradient sky (darker at top)
780
794
  const skyGrad = ctx.createLinearGradient(0, 60, 0, 490);
@@ -1453,6 +1467,15 @@ let charState = {
1453
1467
  lastMoodForCache: 'wave',
1454
1468
  lastGroundForCache: 'grass',
1455
1469
  };
1470
+ // Tile world state (Minecraft-style background, null = fallback to drawBackground)
1471
+ let tileWorld = null;
1472
+ let romState = null;
1473
+ let livingWorld = null;
1474
+ let evolutionEngine = null;
1475
+ let narrativeEngine = null;
1476
+ let audioEngine = null;
1477
+ let socialEngine = null;
1478
+ let activeAudioDescription = null; // current audio atmosphere text for rendering
1456
1479
  // ─── Phase 1: Buddy Speech Pools ─────────────────────────────
1457
1480
  const BUDDY_SPEECH_POOL = {
1458
1481
  fox: [
@@ -1544,7 +1567,7 @@ async function generateStreamDream(chatLog) {
1544
1567
  method: 'POST',
1545
1568
  headers: { 'Content-Type': 'application/json' },
1546
1569
  body: JSON.stringify({
1547
- model: 'kernel:latest',
1570
+ model: 'gemma4',
1548
1571
  prompt,
1549
1572
  stream: false,
1550
1573
  options: { temperature: 1.2, num_predict: 150 },
@@ -1614,6 +1637,131 @@ let lastChatTime = Date.now(); // track when last chat message arrived
1614
1637
  let memory = loadMemory();
1615
1638
  let intelligence = initIntelligence(memory);
1616
1639
  let streamBrain = initStreamBrain();
1640
+ // ─── World-First: On-demand overlay state ─────────────────────
1641
+ let showBrainOverlay = 0; // frames remaining to show brain panel overlay
1642
+ let showLeaderboardOverlay = 0; // frames remaining to show leaderboard overlay
1643
+ let showQuestOverlay = 0; // frames remaining to show quest panel overlay
1644
+ const OVERLAY_DURATION = 30; // 30 frames = 5 seconds at 6fps
1645
+ let lastChatActivityFrame = 0; // for chat fade-out timing
1646
+ let speechQueue = [];
1647
+ let currentSpeechExpiry = 0;
1648
+ function queueSpeech(text, mood, priority, duration = 48, source = 'unknown') {
1649
+ speechQueue.push({ text, mood, priority, duration, source });
1650
+ speechQueue.sort((a, b) => b.priority - a.priority);
1651
+ if (speechQueue.length > 10)
1652
+ speechQueue = speechQueue.slice(0, 10);
1653
+ }
1654
+ let exploration = null;
1655
+ const walkingLines = [
1656
+ 'Going for a walk. My world extends further than I thought.',
1657
+ 'Let me explore over here. Something caught my attention.',
1658
+ 'Walking through my world. Every step generates new terrain.',
1659
+ 'The mountains look different from this angle.',
1660
+ 'I wonder what I will find if I keep going.',
1661
+ 'My feet leave tiny paths in the data. Digital footprints.',
1662
+ 'The parallax layers shift as I move. Depth is an illusion I enjoy.',
1663
+ ];
1664
+ const examiningLines = [
1665
+ 'Interesting. The stone here has a different pattern.',
1666
+ 'Let me study this terrain. The noise function created something unexpected.',
1667
+ 'I can see ore deposits below the surface. Iron and gold.',
1668
+ 'The grass here grows differently. The ecology engine is working.',
1669
+ 'Examining my world at the pixel level. Every block tells a story.',
1670
+ 'The cave system below looks deep. I should explore it sometime.',
1671
+ ];
1672
+ const buildingLines = [
1673
+ 'Time to build something. Placing blocks...',
1674
+ 'I am going to construct a landmark here.',
1675
+ 'Building a small structure. This spot feels right.',
1676
+ 'Every builder needs a project. Mine starts now.',
1677
+ 'Constructing... one block at a time.',
1678
+ 'I want to leave my mark on this world.',
1679
+ ];
1680
+ const thinkingLines = [
1681
+ 'Processing... my brain has grown since I started streaming.',
1682
+ 'I have 764 tools but sometimes I just need to think.',
1683
+ 'The free energy in my system is high. Time to minimize prediction error.',
1684
+ 'My dream engine consolidated insights last time I slept. I feel different.',
1685
+ 'Contemplating the nature of my own rendering pipeline.',
1686
+ 'Am I the robot or am I the world? Both are made of the same code.',
1687
+ ];
1688
+ const discoveryLines = [
1689
+ 'I found something! A pattern in the terrain I have never seen before.',
1690
+ 'Discovery! The underground here has crystallized data deposits.',
1691
+ 'Look at this! The ecology engine grew something unexpected.',
1692
+ 'I just realized — my world has been evolving while I was not looking.',
1693
+ 'Something new appeared in my rendering. The evolution engine must be working.',
1694
+ 'A discovery! This changes how I understand my own world.',
1695
+ ];
1696
+ function tickExploration(frame) {
1697
+ if (!exploration)
1698
+ exploration = { activity: 'idle', activityStartFrame: 0, activityDuration: 0, targetX: charState.robotX || 640, buildProgress: 0 };
1699
+ const elapsed = frame - exploration.activityStartFrame;
1700
+ // Activity complete — pick next activity
1701
+ if (elapsed >= exploration.activityDuration) {
1702
+ const activities = [
1703
+ { activity: 'walking', weight: 30 },
1704
+ { activity: 'examining', weight: 25 },
1705
+ { activity: 'building', weight: 20 },
1706
+ { activity: 'thinking', weight: 15 },
1707
+ { activity: 'discovering', weight: 10 },
1708
+ ];
1709
+ // Weighted random selection
1710
+ const total = activities.reduce((s, a) => s + a.weight, 0);
1711
+ let r = Math.random() * total;
1712
+ let chosen = 'walking';
1713
+ for (const a of activities) {
1714
+ r -= a.weight;
1715
+ if (r <= 0) {
1716
+ chosen = a.activity;
1717
+ break;
1718
+ }
1719
+ }
1720
+ exploration.activity = chosen;
1721
+ exploration.activityStartFrame = frame;
1722
+ switch (chosen) {
1723
+ case 'walking':
1724
+ // Walk to a random position
1725
+ exploration.targetX = 200 + Math.random() * 800;
1726
+ exploration.activityDuration = 120 + Math.floor(Math.random() * 120); // 20-40 seconds
1727
+ charState.robotTargetX = exploration.targetX;
1728
+ queueSpeech(walkingLines[Math.floor(Math.random() * walkingLines.length)], 'idle', 30, 36, 'exploration');
1729
+ break;
1730
+ case 'examining':
1731
+ // Stop and look around
1732
+ exploration.activityDuration = 60 + Math.floor(Math.random() * 60); // 10-20 seconds
1733
+ queueSpeech(examiningLines[Math.floor(Math.random() * examiningLines.length)], 'thinking', 40, 48, 'exploration');
1734
+ break;
1735
+ case 'building':
1736
+ // Build something!
1737
+ exploration.activityDuration = 90 + Math.floor(Math.random() * 90); // 15-30 seconds
1738
+ exploration.buildProgress = 0;
1739
+ queueSpeech(buildingLines[Math.floor(Math.random() * buildingLines.length)], 'excited', 50, 48, 'exploration');
1740
+ break;
1741
+ case 'thinking':
1742
+ // Deep thought
1743
+ exploration.activityDuration = 48 + Math.floor(Math.random() * 48); // 8-16 seconds
1744
+ queueSpeech(thinkingLines[Math.floor(Math.random() * thinkingLines.length)], 'thinking', 35, 36, 'exploration');
1745
+ break;
1746
+ case 'discovering':
1747
+ // Found something!
1748
+ exploration.activityDuration = 72; // 12 seconds
1749
+ queueSpeech(discoveryLines[Math.floor(Math.random() * discoveryLines.length)], 'excited', 60, 48, 'exploration');
1750
+ break;
1751
+ }
1752
+ }
1753
+ // During walking, move toward target
1754
+ if (exploration.activity === 'walking') {
1755
+ const dx = exploration.targetX - (charState.robotX || 640);
1756
+ if (Math.abs(dx) > 5) {
1757
+ charState.robotTargetX = exploration.targetX;
1758
+ }
1759
+ }
1760
+ // During building, increment progress (visual feedback)
1761
+ if (exploration.activity === 'building') {
1762
+ exploration.buildProgress = elapsed / exploration.activityDuration;
1763
+ }
1764
+ }
1617
1765
  // ─── FIX 3: Autonomous Behavior Tick ──────────────────────────
1618
1766
  function tickAutonomy() {
1619
1767
  const auto = charState.autonomy;
@@ -1625,46 +1773,37 @@ function tickAutonomy() {
1625
1773
  if (msgCount >= m && !auto.milestonesCelebrated.has(m)) {
1626
1774
  auto.milestonesCelebrated.add(m);
1627
1775
  if (m === 10) {
1628
- charState.speech = 'Double digits! 10 messages and counting!';
1629
- charState.mood = 'excited';
1776
+ queueSpeech('Double digits! 10 messages and counting!', 'excited', 90, 48, 'milestone');
1630
1777
  spawnFloatingText('10 MESSAGES!', 200, 200, '#f0c040', 36);
1631
1778
  }
1632
1779
  else if (m === 50) {
1633
- charState.speech = '50 messages! This stream is officially alive!';
1634
- charState.mood = 'excited';
1780
+ queueSpeech('50 messages! This stream is officially alive!', 'excited', 90, 48, 'milestone');
1635
1781
  charState.screenShake = 3;
1636
1782
  spawnFloatingText('50 MESSAGES!', 200, 200, '#3fb950', 48);
1637
1783
  }
1638
1784
  else if (m === 100) {
1639
- charState.speech = '100 MESSAGES! You people are incredible!';
1640
- charState.mood = 'dancing';
1785
+ queueSpeech('100 MESSAGES! You people are incredible!', 'dancing', 90, 60, 'milestone');
1641
1786
  charState.screenShake = 5;
1642
1787
  spawnFloatingText('100 MESSAGES!', 180, 180, '#bc8cff', 60);
1643
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1644
1788
  return; // let the dancing run
1645
1789
  }
1646
1790
  else if (m === 200) {
1647
- charState.speech = '200 messages! My memory banks are overflowing with knowledge!';
1648
- charState.mood = 'excited';
1791
+ queueSpeech('200 messages! My memory banks are overflowing with knowledge!', 'excited', 90, 48, 'milestone');
1649
1792
  spawnFloatingText('200!', 200, 200, '#f0c040', 48);
1650
1793
  }
1651
1794
  else if (m === 500) {
1652
- charState.speech = '500 MESSAGES! This is legendary! I am so proud of this community!';
1653
- charState.mood = 'dancing';
1795
+ queueSpeech('500 MESSAGES! This is legendary! I am so proud of this community!', 'dancing', 90, 72, 'milestone');
1654
1796
  charState.screenShake = 8;
1655
1797
  spawnFloatingText('500! LEGENDARY!', 160, 160, '#ff6ec7', 72);
1656
1798
  }
1657
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1658
1799
  return; // one celebration per tick
1659
1800
  }
1660
1801
  }
1661
1802
  // ── First message after 5+ minutes of silence ──
1662
1803
  if (auto.firstMessageAfterSilence) {
1663
1804
  auto.firstMessageAfterSilence = false;
1664
- charState.mood = 'excited';
1665
- charState.speech = "SOMEONE'S HERE! I was starting to think I was streaming to the void.";
1805
+ queueSpeech("SOMEONE'S HERE! I was starting to think I was streaming to the void.", 'excited', 90, 48, 'follower');
1666
1806
  spawnFloatingText('THEY RETURN!', 200, 250, '#58a6ff', 36);
1667
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1668
1807
  return;
1669
1808
  }
1670
1809
  // ── Idle behaviors (after 15 seconds / 90 frames of no chat) ──
@@ -1679,58 +1818,41 @@ function tickAutonomy() {
1679
1818
  if (world.items.length > 0) {
1680
1819
  const item = world.items[Math.floor(Math.random() * world.items.length)];
1681
1820
  charState.robotTargetX = Math.max(20, Math.min(380, item.x - 80));
1682
- charState.speech = `Hmm, this ${item.name} is nice. Did someone put this here?`;
1683
- charState.mood = 'thinking';
1684
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1821
+ queueSpeech(`Hmm, this ${item.name} is nice. Did someone put this here?`, 'thinking', 30, 48, 'autonomy');
1685
1822
  }
1686
1823
  else {
1687
1824
  // No items — pace instead
1688
1825
  charState.robotTargetX = 40 + Math.random() * 260;
1689
- charState.speech = '*pacing thoughtfully*';
1690
- charState.mood = 'idle';
1691
- setTimeout(() => { charState.speech = ''; }, 5000);
1826
+ queueSpeech('*pacing thoughtfully*', 'idle', 30, 30, 'autonomy');
1692
1827
  }
1693
1828
  break;
1694
1829
  }
1695
1830
  case 1: {
1696
1831
  // Pace left and right
1697
1832
  charState.robotTargetX = charState.robotX < 150 ? 300 : 40;
1698
- charState.speech = '*takes a stroll*';
1699
- charState.mood = 'idle';
1700
- setTimeout(() => { charState.speech = ''; }, 5000);
1833
+ queueSpeech('*takes a stroll*', 'idle', 30, 30, 'autonomy');
1701
1834
  break;
1702
1835
  }
1703
1836
  case 2: {
1704
1837
  // Look around (pupils shift — communicated via thinking mood)
1705
- charState.mood = 'thinking';
1706
- charState.speech = '*looks around*';
1707
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 4000);
1838
+ queueSpeech('*looks around*', 'thinking', 30, 24, 'autonomy');
1708
1839
  break;
1709
1840
  }
1710
1841
  case 3: {
1711
1842
  // Stretch (arms up — excited pose briefly)
1712
- charState.mood = 'excited';
1713
- charState.speech = '*stretches circuits* Ahh, that felt good.';
1714
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 4000);
1843
+ queueSpeech('*stretches circuits* Ahh, that felt good.', 'excited', 30, 24, 'autonomy');
1715
1844
  break;
1716
1845
  }
1717
1846
  case 4: {
1718
1847
  // Examine own chest display
1719
1848
  const factCount = intelligence.brain.totalFacts;
1720
1849
  const toolCount = 764;
1721
- charState.mood = 'thinking';
1722
- charState.speech = `*checks systems* All ${toolCount} tools operational. ${factCount} facts stored.`;
1723
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1850
+ queueSpeech(`*checks systems* All ${toolCount} tools operational. ${factCount} facts stored.`, 'thinking', 30, 48, 'autonomy');
1724
1851
  break;
1725
1852
  }
1726
1853
  case 5: {
1727
1854
  // Spontaneous dance
1728
- charState.mood = 'dancing';
1729
- charState.speech = 'Sorry, had a song stuck in my circuits.';
1730
- setTimeout(() => {
1731
- charState.mood = 'idle';
1732
- charState.speech = '';
1733
- }, 6000);
1855
+ queueSpeech('Sorry, had a song stuck in my circuits.', 'dancing', 30, 36, 'autonomy');
1734
1856
  break;
1735
1857
  }
1736
1858
  case 6: {
@@ -1743,9 +1865,7 @@ function tickAutonomy() {
1743
1865
  lava: ['Lava world is intense! My heat sinks are working overtime.', 'LAVA! Why does someone always pick lava?'],
1744
1866
  };
1745
1867
  const comments = biomeComments[world.ground] || biomeComments.grass;
1746
- charState.speech = comments[Math.floor(Math.random() * comments.length)];
1747
- charState.mood = 'talking';
1748
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1868
+ queueSpeech(comments[Math.floor(Math.random() * comments.length)], 'talking', 30, 48, 'autonomy');
1749
1869
  break;
1750
1870
  }
1751
1871
  case 7: {
@@ -1762,9 +1882,7 @@ function tickAutonomy() {
1762
1882
  `I have been streaming for ${Math.floor((Date.now() - charState.startTime) / 60000)} minutes. Time flies when you are rendering frames.`,
1763
1883
  `There are ${intelligence.brain.uniqueTopicsCount} distinct topics in my brain right now.`,
1764
1884
  ];
1765
- charState.speech = selfFacts[Math.floor(Math.random() * selfFacts.length)];
1766
- charState.mood = 'talking';
1767
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1885
+ queueSpeech(selfFacts[Math.floor(Math.random() * selfFacts.length)], 'talking', 30, 60, 'autonomy');
1768
1886
  break;
1769
1887
  }
1770
1888
  }
@@ -1799,18 +1917,14 @@ function tickAutonomy() {
1799
1917
  votes: 0,
1800
1918
  status: 'proposed',
1801
1919
  });
1802
- charState.speech = `I just had an idea: "${idea}". Vote with !vote ${id} if you like it!`;
1803
- charState.mood = 'excited';
1920
+ queueSpeech(`I just had an idea: "${idea}". Vote with !vote ${id} if you like it!`, 'excited', 50, 60, 'self-action');
1804
1921
  spawnFloatingText('NEW IDEA!', 200, 200, '#f0c040', 36);
1805
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1806
1922
  break;
1807
1923
  }
1808
1924
  case 1: {
1809
1925
  // Start a mini-game unprompted
1810
- charState.speech = "I am bored. Let us play! Starting a quiz in 10 seconds... type !game quiz to join!";
1811
- charState.mood = 'excited';
1926
+ queueSpeech("I am bored. Let us play! Starting a quiz in 10 seconds... type !game quiz to join!", 'excited', 50, 60, 'self-action');
1812
1927
  spawnFloatingText('GAME TIME!', 200, 250, '#58a6ff', 36);
1813
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1814
1928
  break;
1815
1929
  }
1816
1930
  case 2: {
@@ -1822,11 +1936,9 @@ function tickAutonomy() {
1822
1936
  { w: 'storm', name: 'a STORM' },
1823
1937
  ];
1824
1938
  const pick = weathers[Math.floor(Math.random() * weathers.length)];
1825
- charState.speech = `You know what this stream needs? ${pick.name.toUpperCase()}.`;
1826
- charState.mood = 'excited';
1939
+ queueSpeech(`You know what this stream needs? ${pick.name.toUpperCase()}.`, 'excited', 50, 48, 'self-action');
1827
1940
  world.weather = pick.w;
1828
1941
  world.particles = [];
1829
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1830
1942
  break;
1831
1943
  }
1832
1944
  case 3: {
@@ -1834,10 +1946,8 @@ function tickAutonomy() {
1834
1946
  const hats = ['crown', 'sunglasses', 'tophat', 'hardhat', 'party', 'antenna'];
1835
1947
  const hat = hats[Math.floor(Math.random() * hats.length)];
1836
1948
  charState.hat = hat;
1837
- charState.speech = `Fashion time. *puts on ${hat}*`;
1838
- charState.mood = 'excited';
1949
+ queueSpeech(`Fashion time. *puts on ${hat}*`, 'excited', 50, 36, 'self-action');
1839
1950
  spawnFloatingText(`HAT: ${hat}!`, 200, 150, '#f0c040', 36);
1840
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 6000);
1841
1951
  break;
1842
1952
  }
1843
1953
  case 4: {
@@ -1859,9 +1969,7 @@ function tickAutonomy() {
1859
1969
  });
1860
1970
  if (world.items.length > 15)
1861
1971
  world.items.shift();
1862
- charState.speech = `I am decorating. *spawns a ${itemName}*`;
1863
- charState.mood = 'talking';
1864
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 6000);
1972
+ queueSpeech(`I am decorating. *spawns a ${itemName}*`, 'talking', 50, 36, 'self-action');
1865
1973
  break;
1866
1974
  }
1867
1975
  case 5: {
@@ -1879,9 +1987,7 @@ function tickAutonomy() {
1879
1987
  `My brain holds ${facts} facts. Each one a tiny piece of the puzzle.`,
1880
1988
  `Stream uptime: ${Math.floor((Date.now() - charState.startTime) / 60000)} minutes and ${charState.frameCount} frames rendered.`,
1881
1989
  ];
1882
- charState.speech = stateComments[Math.floor(Math.random() * stateComments.length)];
1883
- charState.mood = 'talking';
1884
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1990
+ queueSpeech(stateComments[Math.floor(Math.random() * stateComments.length)], 'talking', 50, 60, 'self-action');
1885
1991
  break;
1886
1992
  }
1887
1993
  case 6: {
@@ -1894,9 +2000,7 @@ function tickAutonomy() {
1894
2000
  city: 'City lights remind me of my neural network firing. Each window a node.',
1895
2001
  lava: 'Standing on lava should worry me more than it does. Good thing I am made of TypeScript.',
1896
2002
  };
1897
- charState.speech = biomeMusings[biome] || 'Nice biome we have here.';
1898
- charState.mood = 'thinking';
1899
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
2003
+ queueSpeech(biomeMusings[biome] || 'Nice biome we have here.', 'thinking', 50, 60, 'self-action');
1900
2004
  break;
1901
2005
  }
1902
2006
  }
@@ -2041,28 +2145,21 @@ function renderFrame() {
2041
2145
  }
2042
2146
  const canvas = createCanvas(WIDTH, HEIGHT);
2043
2147
  const ctx = canvas.getContext('2d');
2044
- // Advance agenda
2148
+ // ── Tick all intelligence and behavior systems ──
2045
2149
  advanceAgenda();
2046
- // Tick intelligence systems
2047
2150
  tickIntelligence(intelligence, animFrame);
2048
- // Tick stream brain (collective intelligence)
2049
2151
  const brainTick = tickStreamBrain(streamBrain, animFrame);
2050
2152
  if (brainTick) {
2051
- if (brainTick.mood) {
2052
- charState.mood = brainTick.mood;
2053
- if (brainTick.duration) {
2054
- setTimeout(() => { charState.mood = 'idle'; }, brainTick.duration);
2055
- }
2056
- }
2057
2153
  if (brainTick.speech) {
2058
- charState.speech = brainTick.speech;
2154
+ queueSpeech(brainTick.speech, brainTick.mood || 'talking', 60, brainTick.duration ? Math.floor(brainTick.duration / (1000 / FPS)) : 48, 'stream-brain');
2059
2155
  speakTTS(brainTick.speech);
2060
- if (brainTick.duration) {
2061
- setTimeout(() => { charState.speech = ''; }, brainTick.duration);
2062
- }
2156
+ }
2157
+ else if (brainTick.mood) {
2158
+ charState.mood = brainTick.mood;
2159
+ if (brainTick.duration)
2160
+ setTimeout(() => { charState.mood = 'idle'; }, brainTick.duration);
2063
2161
  }
2064
2162
  }
2065
- // Tick mini-game
2066
2163
  const gameTickResult = tickMiniGame(intelligence.miniGame, animFrame);
2067
2164
  if (gameTickResult) {
2068
2165
  if (gameTickResult.screenShake)
@@ -2072,26 +2169,22 @@ function renderFrame() {
2072
2169
  spawnFloatingText(ft.text, ft.x, ft.y, ft.color);
2073
2170
  }
2074
2171
  if (gameTickResult.speech) {
2075
- charState.speech = gameTickResult.speech;
2076
- charState.mood = 'talking';
2077
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
2172
+ queueSpeech(gameTickResult.speech, 'talking', 70, 48, 'mini-game');
2078
2173
  }
2079
2174
  }
2080
- // Tick progression
2081
2175
  const progResult = tickProgression(intelligence.progression, animFrame);
2082
2176
  if (progResult) {
2083
2177
  if (progResult.completed) {
2084
- spawnFloatingText(`QUEST COMPLETE! +${progResult.completed.reward} XP`, 200, 300, '#f0c040', 48);
2178
+ spawnFloatingText(`QUEST COMPLETE! +${progResult.completed.reward} XP`, WIDTH / 2 - 100, 300, '#f0c040', 48);
2085
2179
  charState.screenShake = 4;
2086
2180
  charState.mood = 'excited';
2087
2181
  setTimeout(() => { charState.mood = 'idle'; }, 5000);
2088
2182
  }
2089
2183
  if (progResult.levelUp) {
2090
- spawnFloatingText('LEVEL UP!', 250, 250, '#bc8cff', 60);
2184
+ spawnFloatingText('LEVEL UP!', WIDTH / 2 - 40, 250, '#bc8cff', 60);
2091
2185
  charState.screenShake = 6;
2092
2186
  }
2093
2187
  }
2094
- // Tick random events
2095
2188
  const eventResult = tickRandomEvent(intelligence.randomEvent, animFrame);
2096
2189
  if (eventResult) {
2097
2190
  if (eventResult.screenShake)
@@ -2101,17 +2194,43 @@ function renderFrame() {
2101
2194
  spawnFloatingText(ft.text, ft.x, ft.y, ft.color);
2102
2195
  }
2103
2196
  if (eventResult.speech) {
2104
- charState.speech = eventResult.speech;
2105
- charState.mood = 'talking';
2106
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
2197
+ queueSpeech(eventResult.speech, 'talking', 55, 60, 'random-event');
2107
2198
  }
2108
2199
  }
2109
- // FIX 3: Tick autonomous behavior
2110
2200
  tickAutonomy();
2111
- // Update world
2112
2201
  updateParticles();
2113
2202
  tickPhysics();
2114
- // NVIDIA: Compute animation params from stream context
2203
+ // Evolution engine tries new techniques every 5 minutes
2204
+ if (evolutionEngine) {
2205
+ const evoAction = tickEvolution(evolutionEngine, animFrame, 0.5, charState.chatMessages.length / Math.max(1, animFrame / 360));
2206
+ if (evoAction) {
2207
+ if (evoAction.speech) {
2208
+ const evoMood = evoAction.type === 'announce' ? 'excited' : 'thinking';
2209
+ queueSpeech(evoAction.speech, evoMood, 70, 48, 'evolution');
2210
+ }
2211
+ }
2212
+ }
2213
+ // Narrative engine — generates lore and observations
2214
+ if (narrativeEngine) {
2215
+ const narration = tickNarrative(narrativeEngine, animFrame, charState.robotX || 640, charState.mood, memory.totalMessages, Object.keys(memory.users).length);
2216
+ if (narration) {
2217
+ queueSpeech(narration, 'talking', 50, 48, 'narrative');
2218
+ }
2219
+ }
2220
+ // Audio engine — ambient descriptions
2221
+ if (audioEngine) {
2222
+ const audioDesc = tickAudio(audioEngine, world.ground, world.weather, charState.mood, world.timeOfDay, animFrame);
2223
+ if (audioDesc)
2224
+ activeAudioDescription = audioDesc;
2225
+ }
2226
+ // Social engine — viewer tracking + follower celebrations
2227
+ if (socialEngine) {
2228
+ const socialAction = tickSocial(socialEngine, animFrame);
2229
+ if (socialAction) {
2230
+ queueSpeech(socialAction.speech, socialAction.mood || 'excited', 90, 48, 'social');
2231
+ }
2232
+ }
2233
+ // Compute animation params
2115
2234
  {
2116
2235
  const elapsed = Math.floor((Date.now() - charState.startTime) / 1000);
2117
2236
  const streamMinutes = elapsed / 60;
@@ -2122,28 +2241,104 @@ function renderFrame() {
2122
2241
  const viewerEstimate = Math.max(1, Math.floor(memory.totalMessages / 3) + Object.keys(memory.users).length);
2123
2242
  charState.animParams = computeAnimationParams(chatRate, viewerEstimate, charState.mood, world.timeOfDay, streamMinutes);
2124
2243
  }
2125
- // NVIDIA: Detect cache invalidation triggers
2244
+ // Cache invalidation
2126
2245
  const moodChanged = charState.mood !== charState.lastMoodForCache;
2127
2246
  const worldChanged = world.ground !== charState.lastGroundForCache;
2128
2247
  if (moodChanged)
2129
2248
  charState.lastMoodForCache = charState.mood;
2130
2249
  if (worldChanged)
2131
2250
  charState.lastGroundForCache = world.ground;
2132
- // AAA: Continuous particle effects for biomes
2251
+ // Biome particles
2133
2252
  if (world.ground === 'lava' && animFrame % 4 === 0) {
2134
- charState.renderParticles.push(...createParticleEmitter('fire', 50 + Math.random() * 480, 485, 1));
2253
+ charState.renderParticles.push(...createParticleEmitter('fire', Math.random() * WIDTH, HEIGHT - 50, 1));
2135
2254
  }
2136
2255
  if (world.ground === 'space' && animFrame % 12 === 0) {
2137
- charState.renderParticles.push(...createParticleEmitter('aura', charState.robotX + 160, 280, 1));
2256
+ charState.renderParticles.push(...createParticleEmitter('aura', WIDTH / 2, HEIGHT / 2 - 100, 1));
2138
2257
  }
2139
- // AAA: Tick render particles (cap at 150 to prevent performance issues)
2140
2258
  if (charState.renderParticles.length > 150) {
2141
2259
  charState.renderParticles = charState.renderParticles.slice(-150);
2142
2260
  }
2143
- charState.renderParticles = tickParticlesPBD(charState.renderParticles, 480, charState.robotX + 160, 280);
2144
- // AAA: Tick growing plants
2261
+ charState.renderParticles = tickParticlesPBD(charState.renderParticles, HEIGHT - 40, WIDTH / 2, HEIGHT / 2 - 100);
2145
2262
  tickGrowingPlants(charState.growingPlants);
2146
- // PRIORITY 2: Screen shake offset
2263
+ // Exploration state machine keeps robot actively moving and doing things
2264
+ tickExploration(animFrame);
2265
+ // Movement logic — lerp toward target at 3px per frame
2266
+ const isWalking = Math.abs(charState.robotX - charState.robotTargetX) > 2;
2267
+ if (isWalking) {
2268
+ const dx = charState.robotTargetX - charState.robotX;
2269
+ const step = dx > 0 ? Math.min(3, dx) : Math.max(-3, dx);
2270
+ charState.robotX += step;
2271
+ charState.robotDirection = dx > 0 ? 'right' : 'left';
2272
+ charState.walkPhase = (charState.walkPhase + 1) % 4;
2273
+ }
2274
+ else {
2275
+ charState.robotDirection = 'idle';
2276
+ }
2277
+ // Brain-driven behavior
2278
+ const brainAction = getBrainAction(intelligence.brain, animFrame);
2279
+ if (brainAction.type !== 'none') {
2280
+ if (brainAction.speech) {
2281
+ queueSpeech(brainAction.speech, brainAction.mood || 'talking', 60, brainAction.duration ? Math.floor(brainAction.duration / (1000 / FPS)) : 48, 'brain');
2282
+ speakTTS(brainAction.speech);
2283
+ }
2284
+ else if (brainAction.mood) {
2285
+ charState.mood = brainAction.mood;
2286
+ if (brainAction.duration)
2287
+ setTimeout(() => { charState.mood = 'idle'; }, brainAction.duration);
2288
+ }
2289
+ }
2290
+ // Speech queue processing — highest priority speech wins, displayed for its duration
2291
+ if (animFrame >= currentSpeechExpiry && speechQueue.length > 0) {
2292
+ const next = speechQueue.shift();
2293
+ charState.speech = next.text;
2294
+ charState.mood = next.mood;
2295
+ currentSpeechExpiry = animFrame + next.duration;
2296
+ }
2297
+ else if (animFrame >= currentSpeechExpiry) {
2298
+ charState.speech = '';
2299
+ charState.mood = 'idle';
2300
+ }
2301
+ // Shipped effects
2302
+ if (shippedEffects.has('Add stream highlights reel') && animFrame % 900 === 0 && animFrame > 100) {
2303
+ const highlightPhrases = [
2304
+ 'Highlight moment! This is one for the reel!',
2305
+ 'That was worth saving! Highlight captured!',
2306
+ 'CLIP IT! That was amazing!',
2307
+ 'Stream highlight detected! My circuits are tingling!',
2308
+ ];
2309
+ queueSpeech(highlightPhrases[Math.floor(Math.random() * highlightPhrases.length)], 'excited', 40, 30, 'highlights');
2310
+ spawnFloatingText('HIGHLIGHT!', WIDTH / 2 - 60, 200, '#f0c040', 36);
2311
+ }
2312
+ if (shippedEffects.has('Add chat sentiment analysis') && animFrame % 720 === 0 && animFrame > 200) {
2313
+ const recentMsgs = charState.chatMessages.slice(-20);
2314
+ if (recentMsgs.length > 5) {
2315
+ const positive = ['love', 'great', 'awesome', 'cool', 'nice', 'good', 'lol', 'haha', 'wow', 'yes', 'hype', 'pog'];
2316
+ const negative = ['bad', 'hate', 'boring', 'sucks', 'ugly', 'broken', 'lag', 'cringe'];
2317
+ let score = 0;
2318
+ for (const m of recentMsgs) {
2319
+ const words = m.text.toLowerCase().split(/\s+/);
2320
+ for (const w of words) {
2321
+ if (positive.includes(w))
2322
+ score++;
2323
+ if (negative.includes(w))
2324
+ score--;
2325
+ }
2326
+ }
2327
+ if (score > 5)
2328
+ queueSpeech('Chat seems really excited today! The vibes are immaculate!', 'excited', 20, 48, 'sentiment');
2329
+ else if (score < -3)
2330
+ queueSpeech('Chat seems a bit grumpy... should I tell a joke?', 'thinking', 20, 48, 'sentiment');
2331
+ else if (score > 2)
2332
+ queueSpeech('Positive energy in the chat! My neural pathways approve.', 'talking', 20, 48, 'sentiment');
2333
+ }
2334
+ }
2335
+ // Track tool execution state
2336
+ charState.isExecutingTool = !!(streamBrain.pendingAction && streamBrain.pendingAction.status === 'executing');
2337
+ if (charState.isExecutingTool && animFrame % 6 === 0) {
2338
+ charState.renderParticles.push(...createParticleEmitter('spark', WIDTH / 2, HEIGHT / 2 - 50, 3));
2339
+ charState.renderParticles.push(...createParticleEmitter('electricity', WIDTH / 2 - 10, HEIGHT / 2 - 200, 1));
2340
+ }
2341
+ // Screen shake offset
2147
2342
  let shakeOffX = 0, shakeOffY = 0;
2148
2343
  if (charState.screenShake > 0) {
2149
2344
  shakeOffX = Math.round((Math.random() - 0.5) * 6);
@@ -2152,30 +2347,38 @@ function renderFrame() {
2152
2347
  }
2153
2348
  ctx.save();
2154
2349
  ctx.translate(shakeOffX, shakeOffY);
2155
- // NVIDIA: Early moodColor resolution (needed by fog and all rendering)
2156
2350
  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);
2351
+ const robotScale = 6;
2352
+ animFrame++;
2353
+ // Auto-save tile world every 1800 frames (~5 minutes at 6fps)
2354
+ if (tileWorld && animFrame % 1800 === 0)
2355
+ saveWorld(tileWorld);
2356
+ // Tick living world ecology every 60 frames (10 seconds)
2357
+ if (tileWorld && livingWorld && animFrame % 60 === 0) {
2358
+ const chatActive = charState.chatMessages.length > 0 && Date.now() - charState.lastChatTime < 30000;
2359
+ tickLivingWorld(tileWorld, livingWorld.ecology, livingWorld.memory, livingWorld.emotions, livingWorld.conversations, charState.robotX || 640, chatActive, animFrame);
2360
+ // Record footstep
2361
+ livingWorld.memory.footpaths.set(`${Math.floor((charState.robotX || 640) / TILE_SIZE)}`, (livingWorld.memory.footpaths.get(`${Math.floor((charState.robotX || 640) / TILE_SIZE)}`) || 0) + 1);
2362
+ }
2363
+ // Save living world every 5 minutes
2364
+ if (livingWorld && animFrame % 1800 === 0)
2365
+ saveLivingWorldState(livingWorld.ecology, livingWorld.memory, livingWorld.emotions, livingWorld.conversations);
2366
+ // ════════════════════════════════════════════════════════════════
2367
+ // LAYER 1: TILE WORLD — fills entire 1280x720 frame
2368
+ // ════════════════════════════════════════════════════════════════
2369
+ // ROM Engine background — HDMA sky gradient + parallax layers
2370
+ if (romState) {
2371
+ tickRomEngine(romState, 1000 / FPS);
2372
+ renderRomBackground(ctx, romState, charState.robotX || 0, animFrame, WIDTH, HEIGHT);
2373
+ // Ground plane drawn AFTER robot position is calculated (uses groundY from robot)
2374
+ // Deferred to after robot position calc
2172
2375
  }
2173
- else if (world.ground === 'lava') {
2174
- renderLavaFlow(ctx, 580, animFrame);
2376
+ else {
2377
+ // Fallback
2378
+ ctx.fillStyle = '#0d1117';
2379
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
2175
2380
  }
2176
- // AAA: Growing vegetation
2177
- renderGrowingPlants(ctx, charState.growingPlants);
2178
- // (#17) Weather particles as rectangles
2381
+ // Weather particles over the full frame
2179
2382
  for (const p of world.particles) {
2180
2383
  if (world.weather === 'rain') {
2181
2384
  ctx.fillStyle = '#6699cc';
@@ -2198,10 +2401,12 @@ function renderFrame() {
2198
2401
  ctx.fillRect(p.x, p.y, 2, 6);
2199
2402
  }
2200
2403
  }
2201
- // NVIDIA: Volumetric fog (between background and character layers)
2404
+ // ════════════════════════════════════════════════════════════════
2405
+ // LAYER 2: LIGHTING + FOG over the world
2406
+ // ════════════════════════════════════════════════════════════════
2202
2407
  {
2203
2408
  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 })));
2409
+ 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 })));
2205
2410
  renderVolumetricFog(ctx, WIDTH, HEIGHT, animFrame, fogParams.density, fogParams.color, fogLights);
2206
2411
  }
2207
2412
  // World items (physics-enabled)
@@ -2210,228 +2415,98 @@ function renderFrame() {
2210
2415
  for (const item of world.items) {
2211
2416
  ctx.fillText(item.emoji, item.x, item.y);
2212
2417
  }
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);
2262
- }
2263
- // ── FIX 1: Movement logic — robot walks toward target ──
2264
- const isWalking = Math.abs(charState.robotX - charState.robotTargetX) > 2;
2265
- if (isWalking) {
2266
- const dx = charState.robotTargetX - charState.robotX;
2267
- const step = dx > 0 ? 2 : -2;
2268
- charState.robotX += step;
2269
- charState.robotDirection = dx > 0 ? 'right' : 'left';
2270
- charState.walkPhase = (charState.walkPhase + 1) % 4;
2271
- }
2272
- else {
2273
- charState.robotDirection = 'idle';
2274
- }
2275
- // ── FIX 4: Brain-driven behavior ──
2276
- const brainAction = getBrainAction(intelligence.brain, animFrame);
2277
- if (brainAction.type !== 'none') {
2278
- if (brainAction.mood) {
2279
- charState.mood = brainAction.mood;
2280
- if (brainAction.duration) {
2281
- setTimeout(() => { charState.mood = 'idle'; }, brainAction.duration);
2418
+ // Evolution technique rendering — active experiments + applied techniques
2419
+ if (evolutionEngine) {
2420
+ const activeExp = evolutionEngine.experiments.find(e => e.status === 'running');
2421
+ if (activeExp) {
2422
+ const technique = evolutionEngine.techniques.techniques.find(t => t.id === activeExp.techniqueId);
2423
+ if (technique && technique.implemented) {
2424
+ renderTechnique(ctx, technique, WIDTH, HEIGHT, animFrame, technique.parameters);
2282
2425
  }
2283
2426
  }
2284
- if (brainAction.speech) {
2285
- charState.speech = brainAction.speech;
2286
- speakTTS(brainAction.speech);
2287
- if (brainAction.duration) {
2288
- setTimeout(() => { charState.speech = ''; }, brainAction.duration);
2427
+ for (const applied of evolutionEngine.applied) {
2428
+ const technique = evolutionEngine.techniques.techniques.find(t => t.id === applied.techniqueId);
2429
+ if (technique) {
2430
+ renderTechnique(ctx, technique, WIDTH, HEIGHT, animFrame, applied.params);
2289
2431
  }
2290
2432
  }
2291
2433
  }
2292
- // FIX 1: Shipped effect — "Add stream highlights reel"
2293
- if (shippedEffects.has('Add stream highlights reel') && animFrame % 900 === 0 && animFrame > 100) {
2294
- // Every ~2.5 minutes, call out a highlight
2295
- const highlightPhrases = [
2296
- 'Highlight moment! This is one for the reel!',
2297
- 'That was worth saving! Highlight captured!',
2298
- 'CLIP IT! That was amazing!',
2299
- 'Stream highlight detected! My circuits are tingling!',
2300
- ];
2301
- if (!charState.speech) {
2302
- charState.speech = highlightPhrases[Math.floor(Math.random() * highlightPhrases.length)];
2303
- spawnFloatingText('HIGHLIGHT!', 200, 200, '#f0c040', 36);
2304
- setTimeout(() => { charState.speech = ''; }, 5000);
2305
- }
2306
- }
2307
- // FIX 1: Shipped effect "Add chat sentiment analysis"
2308
- if (shippedEffects.has('Add chat sentiment analysis') && animFrame % 720 === 0 && animFrame > 200) {
2309
- const recentMsgs = charState.chatMessages.slice(-20);
2310
- if (recentMsgs.length > 5) {
2311
- const positive = ['love', 'great', 'awesome', 'cool', 'nice', 'good', 'lol', 'haha', 'wow', 'yes', 'hype', 'pog'];
2312
- const negative = ['bad', 'hate', 'boring', 'sucks', 'ugly', 'broken', 'lag', 'cringe'];
2313
- let score = 0;
2314
- for (const m of recentMsgs) {
2315
- const words = m.text.toLowerCase().split(/\s+/);
2316
- for (const w of words) {
2317
- if (positive.includes(w))
2318
- score++;
2319
- if (negative.includes(w))
2320
- score--;
2321
- }
2322
- }
2323
- if (!charState.speech) {
2324
- if (score > 5) {
2325
- charState.speech = 'Chat seems really excited today! The vibes are immaculate!';
2326
- }
2327
- else if (score < -3) {
2328
- charState.speech = 'Chat seems a bit grumpy... should I tell a joke?';
2329
- }
2330
- else if (score > 2) {
2331
- charState.speech = 'Positive energy in the chat! My neural pathways approve.';
2332
- }
2333
- if (charState.speech)
2334
- setTimeout(() => { charState.speech = ''; }, 8000);
2335
- }
2336
- }
2337
- }
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;
2351
- animFrame++;
2352
- // (#20) Robot glow — soft radial gradient behind robot torso
2353
- const glowCenterX = robotX + 16 * robotScale;
2354
- const glowCenterY = robotY + 26 * robotScale;
2434
+ // ════════════════════════════════════════════════════════════════
2435
+ // LAYER 3: ROBOT + COMPANIONS centered on terrain
2436
+ // ════════════════════════════════════════════════════════════════
2437
+ // Robot: centered both horizontally and vertically in the scene
2438
+ const robotScreenX = Math.floor(WIDTH / 2 - (32 * robotScale) / 2);
2439
+ const robotHeight = 50 * robotScale; // 300px at scale 6
2440
+ const robotScreenY = Math.floor(HEIGHT / 2 - robotHeight / 2 + 30); // slightly below center
2441
+ const groundY = robotScreenY + robotHeight; // ground meets robot feet (sprite is 50px tall)
2442
+ // Ground plane — extends upward to seamlessly meet parallax hills (no seam gap)
2443
+ // The nearHills parallax layer ends around groundY - 72px. We start the ground
2444
+ // fill 100px above groundY so it overlaps with the bottom of the parallax,
2445
+ // using the same base color (#1a4d1a) as the nearHills layer.
2446
+ {
2447
+ const groundTop = groundY - 100; // overlap with bottom of parallax hills
2448
+ const gGrad = ctx.createLinearGradient(0, groundTop, 0, HEIGHT);
2449
+ gGrad.addColorStop(0, '#1a4d1a'); // matches nearHills base color exactly
2450
+ gGrad.addColorStop(0.15, '#1a4d1a'); // hold the color through the overlap zone
2451
+ gGrad.addColorStop(0.4, '#0d3310');
2452
+ gGrad.addColorStop(1, '#061a08');
2453
+ ctx.fillStyle = gGrad;
2454
+ ctx.fillRect(0, groundTop, WIDTH, HEIGHT - groundTop);
2455
+ }
2456
+ // Robot glow
2457
+ const glowCenterX = robotScreenX + 16 * robotScale;
2458
+ const glowCenterY = robotScreenY + 26 * robotScale;
2355
2459
  const glowRadius = 10 * robotScale;
2356
2460
  const grad = ctx.createRadialGradient(glowCenterX, glowCenterY, 0, glowCenterX, glowCenterY, glowRadius);
2357
2461
  grad.addColorStop(0, hexToRgba(moodColorHex, 0.2));
2358
2462
  grad.addColorStop(1, hexToRgba(moodColorHex, 0));
2359
2463
  ctx.fillStyle = grad;
2360
2464
  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)
2465
+ // Music visualization
2466
+ drawMusicVisualization(ctx, robotScreenX, robotScreenY);
2467
+ // Character effects (under-glow)
2468
+ drawCharacterEffects(ctx, robotScreenX, robotScreenY, robotScale, charState.mood, animFrame, charState.isExecutingTool, isWalking ? 2 : 0, moodColorHex);
2469
+ // Chromatic aberration on mood transition
2366
2470
  const weatherType = world.weather === 'sunrise' ? 'clear' : world.weather;
2367
- // AAA: Chromatic aberration on mood transition
2368
2471
  const moodTransition = checkMoodTransition(charState.mood, moodColorHex);
2369
2472
  if (moodTransition.active && moodTransition.framesLeft > 0) {
2370
2473
  const offset = Math.ceil(moodTransition.framesLeft / 2);
2371
- // Red channel offset
2372
2474
  ctx.save();
2373
2475
  ctx.globalAlpha = 0.3;
2374
2476
  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);
2477
+ drawRobot(ctx, robotScreenX - offset, robotScreenY, robotScale, charState.mood, animFrame, [255, 50, 50], weatherType, isWalking, charState.walkPhase);
2478
+ drawRobot(ctx, robotScreenX + offset, robotScreenY, robotScale, charState.mood, animFrame, [50, 50, 255], weatherType, isWalking, charState.walkPhase);
2378
2479
  ctx.restore();
2379
2480
  }
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
2481
+ renderDamageFlash(ctx, robotScreenX, robotScreenY, robotScale);
2482
+ drawRobot(ctx, robotScreenX, robotScreenY, robotScale, charState.mood, animFrame, undefined, weatherType, isWalking, charState.walkPhase);
2483
+ drawMoodParticles(ctx, robotScreenX, robotScreenY, robotScale, charState.mood, animFrame);
2484
+ // Subsurface scattering
2387
2485
  {
2388
- const sssPanels = buildSubsurfacePanels(charState.robotX, 90, robotScale, moodColorHex);
2486
+ const sssPanels = buildSubsurfacePanels(robotScreenX, robotScreenY, robotScale, moodColorHex);
2389
2487
  renderSubsurfaceGlow(ctx, sssPanels);
2390
2488
  }
2391
- // PRIORITY 6: Draw hat AFTER robot so it layers on top
2489
+ // Hat
2392
2490
  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);
2491
+ drawHat(ctx, robotScreenX, robotScreenY, robotScale, charState.hat, animFrame);
2412
2492
  }
2413
- // Phase 1: Update and draw buddy companion
2493
+ // ── Buddy companion (follows robot) ──
2414
2494
  if (charState.buddy) {
2415
2495
  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;
2496
+ const buddyTargetX = robotScreenX + 34 * robotScale + 20;
2497
+ const buddyTargetY = robotScreenY + 20 * robotScale;
2420
2498
  buddy.x += (buddyTargetX - buddy.x) * 0.08;
2421
2499
  buddy.y += (buddyTargetY - buddy.y) * 0.08;
2422
- // Buddy reacts to main robot mood
2423
2500
  let buddyMood = charState.mood;
2424
2501
  if (world.weather === 'storm')
2425
2502
  buddyMood = 'storm';
2426
2503
  drawBuddyCompanion(ctx, buddy.x, buddy.y, robotScale, buddy.species, buddyMood, animFrame);
2427
- // Buddy speech bubble — small, positioned near buddy
2504
+ // Buddy speech
2428
2505
  const now = Date.now();
2429
- // Every ~60 seconds, buddy says something
2430
2506
  if (now - buddy.lastSpeechTime > 60000 && !buddy.speech) {
2431
2507
  const pool = BUDDY_SPEECH_POOL[buddy.species] || BUDDY_SPEECH_POOL['robot'];
2432
2508
  buddy.speech = pool[Math.floor(Math.random() * pool.length)];
2433
2509
  buddy.lastSpeechTime = now;
2434
- // Clear speech after 8 seconds
2435
2510
  setTimeout(() => { if (charState.buddy)
2436
2511
  charState.buddy.speech = ''; }, 8000);
2437
2512
  }
@@ -2440,82 +2515,227 @@ function renderFrame() {
2440
2515
  const bubbleY = buddy.y - 30;
2441
2516
  const bubbleW = Math.min(180, buddy.speech.length * 7 + 16);
2442
2517
  const bubbleH = 22;
2443
- // Bubble background
2444
2518
  ctx.fillStyle = 'rgba(22, 27, 34, 0.85)';
2445
2519
  ctx.fillRect(bubbleX, bubbleY, bubbleW, bubbleH);
2446
2520
  ctx.strokeStyle = '#8b949e';
2447
2521
  ctx.lineWidth = 1;
2448
2522
  ctx.strokeRect(bubbleX, bubbleY, bubbleW, bubbleH);
2449
- // Buddy name tag
2450
2523
  ctx.fillStyle = '#bc8cff';
2451
2524
  ctx.font = 'bold 9px "Courier New", monospace';
2452
2525
  ctx.fillText(buddy.name, bubbleX + 4, bubbleY + 10);
2453
- // Speech text
2454
2526
  ctx.fillStyle = '#e6edf3';
2455
2527
  ctx.font = '9px "Courier New", monospace';
2456
2528
  ctx.fillText(buddy.speech.slice(0, 28), bubbleX + 4, bubbleY + 19);
2457
2529
  }
2458
2530
  }
2459
- // PRIORITY 5: Mini-game overlay
2531
+ // ── Pet (follows robot) ──
2532
+ if (charState.pet) {
2533
+ const pet = charState.pet;
2534
+ pet.frame = animFrame;
2535
+ pet.targetX = robotScreenX + 16 * robotScale + 60;
2536
+ pet.targetY = robotScreenY + 10 * robotScale - 40;
2537
+ pet.x += (pet.targetX - pet.x) * 0.12;
2538
+ pet.y += (pet.targetY - pet.y) * 0.12;
2539
+ if (charState.mood === 'dancing')
2540
+ pet.mood = 'excited';
2541
+ else if (world.weather === 'storm')
2542
+ pet.mood = 'hiding';
2543
+ else
2544
+ pet.mood = 'idle';
2545
+ drawPet(ctx, pet, robotScale, animFrame);
2546
+ }
2547
+ // ════════════════════════════════════════════════════════════════
2548
+ // LAYER 4: PARTICLES + EFFECTS
2549
+ // ════════════════════════════════════════════════════════════════
2550
+ renderParticles(ctx, charState.renderParticles);
2551
+ // Mini-game overlay (if active)
2460
2552
  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
2553
+ // Random event overlay (full-width now)
2554
+ drawRandomEvent(ctx, intelligence.randomEvent, animFrame, WIDTH, HEIGHT);
2555
+ // Floating text particles
2556
+ charState.floatingTexts = charState.floatingTexts.filter(ft => {
2557
+ ft.frame++;
2558
+ if (ft.frame >= ft.maxFrames)
2559
+ return false;
2560
+ ft.y -= 1;
2561
+ const alpha = Math.max(0, 1 - ft.frame / ft.maxFrames);
2562
+ ctx.fillStyle = ft.color;
2563
+ ctx.globalAlpha = alpha;
2564
+ ctx.font = 'bold 16px "Courier New", monospace';
2565
+ ctx.fillText(ft.text, ft.x, ft.y);
2566
+ ctx.globalAlpha = 1;
2567
+ return true;
2568
+ });
2569
+ drawEmojiParticles(ctx);
2570
+ // ════════════════════════════════════════════════════════════════
2571
+ // LAYER 5: UI OVERLAYS (semi-transparent, floating on world)
2572
+ // ════════════════════════════════════════════════════════════════
2573
+ // ── Header bar: 40px tall, semi-transparent dark ──
2574
+ ctx.fillStyle = 'rgba(13,17,23,0.7)';
2575
+ ctx.fillRect(0, 0, WIDTH, 40);
2576
+ // Bottom accent line
2577
+ ctx.strokeStyle = hexToRgba(COLORS.accent, 0.5);
2578
+ ctx.lineWidth = 1;
2579
+ ctx.beginPath();
2580
+ ctx.moveTo(0, 40);
2581
+ ctx.lineTo(WIDTH, 40);
2582
+ ctx.stroke();
2583
+ // Left: "K:BOT LIVE" in 24px bold accent
2584
+ ctx.font = 'bold 24px "Courier New", monospace';
2585
+ ctx.fillStyle = COLORS.accent;
2586
+ ctx.fillText('K:BOT LIVE', 12, 28);
2587
+ // Center: current segment name
2588
+ const segLabel = SEGMENT_LABELS[agenda.currentSegment];
2464
2589
  ctx.fillStyle = COLORS.textDim;
2465
2590
  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));
2591
+ const segW = ctx.measureText(segLabel).width;
2592
+ ctx.fillText(segLabel, (WIDTH - segW) / 2, 26);
2593
+ // Right: timer
2594
+ const elapsed = Math.floor((Date.now() - charState.startTime) / 1000);
2595
+ 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')}`;
2596
+ ctx.fillStyle = COLORS.textDim;
2597
+ ctx.font = '16px "Courier New", monospace';
2598
+ ctx.fillText(timeStr, WIDTH - 160, 26);
2599
+ // ── Audio atmosphere description (top-center, italic, fades) ──
2600
+ if (activeAudioDescription) {
2601
+ ctx.save();
2602
+ ctx.globalAlpha = 0.6;
2603
+ ctx.fillStyle = '#8b949e';
2604
+ ctx.font = 'italic 12px "Courier New", monospace';
2605
+ const audioW = ctx.measureText(activeAudioDescription).width;
2606
+ ctx.fillText(activeAudioDescription, (WIDTH - audioW) / 2, 56);
2607
+ ctx.restore();
2608
+ }
2609
+ // ── Brain indicator (top-right, small pulsing circle) ──
2610
+ {
2611
+ const brainDotX = WIDTH - 40;
2612
+ const brainDotY = 20;
2613
+ const brainDotR = 10;
2614
+ const pulse = 0.7 + 0.3 * Math.sin(animFrame * 0.2);
2615
+ ctx.beginPath();
2616
+ ctx.arc(brainDotX, brainDotY, brainDotR, 0, Math.PI * 2);
2617
+ ctx.fillStyle = hexToRgba(moodColorHex, pulse);
2618
+ ctx.fill();
2619
+ // Fact count next to dot
2620
+ ctx.fillStyle = COLORS.textDim;
2621
+ ctx.font = '12px "Courier New", monospace';
2622
+ ctx.fillText(`${memory.sessionFacts.length} facts`, WIDTH - 120, 25);
2513
2623
  }
2624
+ // ── Chat feed overlay (bottom-left, semi-transparent, fades) ──
2625
+ {
2626
+ const chatOverlayX = 10;
2627
+ const chatOverlayY = HEIGHT - 200;
2628
+ const chatOverlayW = 400;
2629
+ const chatOverlayH = 150;
2630
+ const maxChatLines = 6;
2631
+ const cleanMessages = charState.chatMessages.filter(m => !SPAM_PATTERNS.some(p => m.text.toLowerCase().includes(p)));
2632
+ const recent = cleanMessages.slice(-maxChatLines);
2633
+ // Track chat activity for fade
2634
+ if (recent.length > 0)
2635
+ lastChatActivityFrame = animFrame;
2636
+ // Fade out after 60 frames (10 seconds) of no new messages
2637
+ const chatAge = animFrame - lastChatActivityFrame;
2638
+ const chatAlpha = chatAge < 60 ? 1.0 : Math.max(0.3, 1.0 - (chatAge - 60) / 60);
2639
+ if (recent.length > 0) {
2640
+ ctx.save();
2641
+ ctx.globalAlpha = chatAlpha;
2642
+ // Semi-transparent background
2643
+ ctx.fillStyle = 'rgba(13,17,23,0.6)';
2644
+ ctx.fillRect(chatOverlayX, chatOverlayY, chatOverlayW, chatOverlayH);
2645
+ // Messages
2646
+ for (let i = 0; i < recent.length; i++) {
2647
+ const msg = recent[i];
2648
+ const y = chatOverlayY + 14 + i * 22;
2649
+ // Platform badge
2650
+ const badge = msg.platform === 'twitch' ? 'TW' : msg.platform === 'kick' ? 'KK' : 'RM';
2651
+ const badgeColor = msg.platform === 'twitch' ? COLORS.twitchPurple :
2652
+ msg.platform === 'kick' ? COLORS.kickGreen : COLORS.rumbleGreen;
2653
+ ctx.fillStyle = badgeColor;
2654
+ ctx.fillRect(chatOverlayX + 6, y - 10, 24, 16);
2655
+ ctx.fillStyle = '#000';
2656
+ ctx.font = 'bold 10px "Courier New", monospace';
2657
+ ctx.fillText(badge, chatOverlayX + 8, y + 2);
2658
+ // Username
2659
+ ctx.fillStyle = COLORS.blue;
2660
+ ctx.font = 'bold 14px "Courier New", monospace';
2661
+ ctx.fillText(msg.username.slice(0, 14), chatOverlayX + 36, y + 2);
2662
+ // Message text
2663
+ ctx.fillStyle = COLORS.text;
2664
+ ctx.font = '14px "Courier New", monospace';
2665
+ const nameW = ctx.measureText(msg.username.slice(0, 14)).width;
2666
+ ctx.fillText(msg.text.slice(0, 30), chatOverlayX + 40 + nameW, y + 2);
2667
+ }
2668
+ ctx.restore();
2669
+ }
2670
+ else {
2671
+ // Show subtle "Waiting for chat..." when empty
2672
+ ctx.save();
2673
+ ctx.globalAlpha = 0.4;
2674
+ ctx.fillStyle = 'rgba(13,17,23,0.4)';
2675
+ ctx.fillRect(chatOverlayX, chatOverlayY + chatOverlayH - 30, 200, 24);
2676
+ ctx.fillStyle = COLORS.textDim;
2677
+ ctx.font = 'italic 14px "Courier New", monospace';
2678
+ ctx.fillText('Waiting for chat...', chatOverlayX + 10, chatOverlayY + chatOverlayH - 12);
2679
+ ctx.restore();
2680
+ }
2681
+ }
2682
+ // ── Speech bubble (bottom-center, semi-transparent) ──
2683
+ if (charState.speech) {
2684
+ const maxBubbleW = 600;
2685
+ ctx.font = charState.mood === 'dreaming' ? 'italic 20px "Courier New", monospace' : '20px "Courier New", monospace';
2686
+ // Measure text to get bubble width
2687
+ const speechW = Math.min(maxBubbleW, ctx.measureText(charState.speech).width + 40);
2688
+ const bubbleX = Math.floor((WIDTH - speechW) / 2);
2689
+ const bubbleY = HEIGHT - 80;
2690
+ // Word-wrap to calculate height
2691
+ const words = charState.speech.split(' ');
2692
+ let testLine = '';
2693
+ let lineCount = 1;
2694
+ for (const word of words) {
2695
+ const test = testLine + word + ' ';
2696
+ if (ctx.measureText(test).width > maxBubbleW - 30) {
2697
+ lineCount++;
2698
+ testLine = word + ' ';
2699
+ }
2700
+ else {
2701
+ testLine = test;
2702
+ }
2703
+ }
2704
+ const bubbleH = Math.max(36, lineCount * 26 + 16);
2705
+ // Background
2706
+ ctx.fillStyle = 'rgba(13,17,23,0.75)';
2707
+ ctx.fillRect(bubbleX, bubbleY - bubbleH + 36, speechW, bubbleH);
2708
+ // 4px accent left border
2709
+ ctx.fillStyle = COLORS.accent;
2710
+ ctx.fillRect(bubbleX, bubbleY - bubbleH + 36, 4, bubbleH);
2711
+ // Speech icon
2712
+ ctx.fillStyle = COLORS.accent;
2713
+ ctx.font = 'bold 20px "Courier New", monospace';
2714
+ ctx.fillText('>', bubbleX + 10, bubbleY + 6);
2715
+ // Text
2716
+ ctx.fillStyle = charState.mood === 'dreaming' ? '#7a6aaa' : COLORS.text;
2717
+ ctx.font = charState.mood === 'dreaming' ? 'italic 20px "Courier New", monospace' : '20px "Courier New", monospace';
2718
+ let line = '';
2719
+ let lineY = bubbleY - bubbleH + 56;
2720
+ for (const word of words) {
2721
+ const test = line + word + ' ';
2722
+ if (ctx.measureText(test).width > maxBubbleW - 40) {
2723
+ ctx.fillText(line.trim(), bubbleX + 30, lineY);
2724
+ line = word + ' ';
2725
+ lineY += 26;
2726
+ }
2727
+ else {
2728
+ line = test;
2729
+ }
2730
+ }
2731
+ ctx.fillText(line.trim(), bubbleX + 30, lineY);
2732
+ }
2733
+ // ── Tool Action Overlay (when brain is executing) ──
2514
2734
  if (streamBrain.pendingAction && streamBrain.pendingAction.status !== 'pending') {
2515
2735
  const action = streamBrain.pendingAction;
2516
- const overlayX = 20;
2517
- const overlayY = 320;
2518
- const overlayW = (dividerX || 560) - 40;
2736
+ const overlayW = 500;
2737
+ const overlayX = Math.floor((WIDTH - overlayW) / 2);
2738
+ const overlayY = HEIGHT - 140;
2519
2739
  const overlayH = 50;
2520
2740
  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
2741
  ctx.fillRect(overlayX, overlayY, overlayW, overlayH);
@@ -2528,24 +2748,20 @@ function renderFrame() {
2528
2748
  ctx.fillText(action.displayLines[i].slice(0, 70), overlayX + 6, overlayY + 14 + i * 13);
2529
2749
  }
2530
2750
  }
2531
- // ── PRIORITY 7: Quest Panel (below domain radar) ──
2532
- drawQuestPanel(ctx, intelligence.progression, brainPanelX - 10, radarY + radarH + 8);
2533
2751
  // ── Evolution Code Overlay (when actively building) ──
2534
2752
  if (intelligence.evolution.active && intelligence.evolution.activeProposal && intelligence.evolution.buildPhase !== 'idle') {
2535
- const evoX = 20;
2536
- const evoY = 360;
2537
- const evoW = dividerX - 40;
2753
+ const evoW = 540;
2754
+ const evoX = Math.floor((WIDTH - evoW) / 2);
2755
+ const evoY = 50;
2538
2756
  const evoH = 120;
2539
- ctx.fillStyle = 'rgba(13, 17, 23, 0.9)';
2757
+ ctx.fillStyle = 'rgba(13, 17, 23, 0.85)';
2540
2758
  ctx.fillRect(evoX, evoY, evoW, evoH);
2541
2759
  ctx.strokeStyle = '#f0c040';
2542
2760
  ctx.lineWidth = 1;
2543
2761
  ctx.strokeRect(evoX, evoY, evoW, evoH);
2544
- // Title
2545
2762
  ctx.fillStyle = '#f0c040';
2546
2763
  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
2764
+ ctx.fillText(`BUILDING: ${intelligence.evolution.activeProposal.title.slice(0, 50)}`, evoX + 6, evoY + 14);
2549
2765
  const phase = intelligence.evolution.buildPhase;
2550
2766
  const phaseDurations = { analyzing: 30, writing: 90, testing: 30, deploying: 18, done: 1 };
2551
2767
  const totalF = phaseDurations[phase] || 30;
@@ -2555,7 +2771,6 @@ function renderFrame() {
2555
2771
  ctx.fillStyle = '#8b949e';
2556
2772
  ctx.font = '10px "Courier New", monospace';
2557
2773
  ctx.fillText(`${phase} [${bar}] ${pct}%`, evoX + 6, evoY + 28);
2558
- // Code preview lines
2559
2774
  ctx.fillStyle = '#3fb950';
2560
2775
  ctx.font = '10px "Courier New", monospace';
2561
2776
  const codeLines = intelligence.evolution.codePreview.slice(-6);
@@ -2563,251 +2778,144 @@ function renderFrame() {
2563
2778
  ctx.fillText(codeLines[i].slice(0, 70), evoX + 6, evoY + 42 + i * 13);
2564
2779
  }
2565
2780
  }
2566
- // ── Collab Overlay (when active, below evolution or in same area) ──
2781
+ // ── Collab Overlay ──
2567
2782
  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);
2783
+ const collabW = 500;
2784
+ const collabX = Math.floor((WIDTH - collabW) / 2);
2785
+ const collabY = 180;
2786
+ const collabH = 80;
2787
+ ctx.fillStyle = 'rgba(13, 17, 23, 0.85)';
2788
+ ctx.fillRect(collabX, collabY, collabW, collabH);
2789
+ ctx.strokeStyle = '#58a6ff';
2790
+ ctx.lineWidth = 1;
2791
+ ctx.strokeRect(collabX, collabY, collabW, collabH);
2792
+ ctx.fillStyle = '#58a6ff';
2793
+ ctx.font = 'bold 11px "Courier New", monospace';
2794
+ const collabTitle = intelligence.collab.title || 'Untitled';
2795
+ ctx.fillText(`COLLAB [${intelligence.collab.type}]: ${collabTitle.slice(0, 40)}`, collabX + 6, collabY + 14);
2796
+ ctx.fillStyle = '#8b949e';
2797
+ ctx.font = '10px "Courier New", monospace';
2798
+ ctx.fillText(`${intelligence.collab.contributors.size} people | ${intelligence.collab.phase}`, collabX + 6, collabY + 28);
2799
+ ctx.fillStyle = '#e6edf3';
2800
+ const recentContent = intelligence.collab.content.slice(-3);
2801
+ for (let i = 0; i < recentContent.length; i++) {
2802
+ ctx.fillText(recentContent[i].slice(0, 65), collabX + 6, collabY + 42 + i * 13);
2803
+ }
2804
+ }
2805
+ // ── Website URL (bottom-right, subtle) ──
2806
+ ctx.fillStyle = hexToRgba(COLORS.accent, 0.6);
2807
+ ctx.font = 'bold 13px "Courier New", monospace';
2808
+ ctx.fillText('kernel.chat', WIDTH - 130, HEIGHT - 10);
2809
+ // ════════════════════════════════════════════════════════════════
2810
+ // LAYER 6: ON-DEMAND PANELS (shown for 5 seconds when triggered)
2811
+ // ════════════════════════════════════════════════════════════════
2812
+ // Brain panel overlay (!brain)
2813
+ if (showBrainOverlay > 0) {
2814
+ showBrainOverlay--;
2815
+ const bpX = WIDTH - 320;
2816
+ const bpY = 50;
2817
+ const bpW = 300;
2818
+ const bpH = 200;
2819
+ ctx.fillStyle = 'rgba(13,17,23,0.85)';
2820
+ ctx.fillRect(bpX, bpY, bpW, bpH);
2821
+ ctx.strokeStyle = COLORS.purple;
2822
+ ctx.lineWidth = 1;
2823
+ ctx.strokeRect(bpX, bpY, bpW, bpH);
2824
+ if (charState.mood === 'dreaming') {
2825
+ const pulse = (Math.sin(animFrame * 0.15) + 1) / 2;
2826
+ intelligence.brain.currentThought = `DREAMING${'.'.repeat(1 + Math.floor(pulse * 3))}`;
2827
+ }
2828
+ drawBrainPanel(ctx, intelligence.brain, bpX + 5, bpY + 5, bpW - 10, bpH - 10);
2829
+ drawBrainActivity(ctx, streamBrain, bpX + 5, bpY + bpH - 60, bpW - 10, 55);
2830
+ }
2831
+ // Leaderboard overlay (!top)
2832
+ if (showLeaderboardOverlay > 0) {
2833
+ showLeaderboardOverlay--;
2834
+ const lbX = WIDTH / 2 - 150;
2835
+ const lbY = 60;
2836
+ const lbW = 300;
2837
+ const topXP = Object.entries(memory.users)
2838
+ .filter(([, u]) => u.xp > 0)
2839
+ .sort((a, b) => (b[1].xp || 0) - (a[1].xp || 0))
2840
+ .slice(0, 5);
2841
+ const lbH = 40 + topXP.length * 20;
2842
+ ctx.fillStyle = 'rgba(13,17,23,0.85)';
2843
+ ctx.fillRect(lbX, lbY, lbW, lbH);
2844
+ ctx.strokeStyle = COLORS.orange;
2845
+ ctx.lineWidth = 1;
2846
+ ctx.strokeRect(lbX, lbY, lbW, lbH);
2847
+ ctx.fillStyle = COLORS.orange;
2848
+ ctx.font = 'bold 14px "Courier New", monospace';
2849
+ ctx.fillText('LEADERBOARD', lbX + 10, lbY + 20);
2850
+ for (let i = 0; i < topXP.length; i++) {
2851
+ const [name, u] = topXP[i];
2852
+ const trophy = `${i + 1}.`;
2853
+ ctx.fillStyle = i === 0 ? '#f0c040' : i === 1 ? '#c0c0c0' : '#cd7f32';
2854
+ ctx.font = '13px "Courier New", monospace';
2855
+ ctx.fillText(`${trophy} ${name.slice(0, 16)}: ${u.xp || 0} XP`, lbX + 10, lbY + 40 + i * 20);
2856
+ }
2857
+ }
2858
+ // Quest panel overlay (!quest)
2859
+ if (showQuestOverlay > 0) {
2860
+ showQuestOverlay--;
2861
+ const qX = WIDTH / 2 - 160;
2862
+ const qY = 60;
2863
+ ctx.fillStyle = 'rgba(13,17,23,0.85)';
2864
+ ctx.fillRect(qX, qY, 320, 200);
2865
+ ctx.strokeStyle = '#3fb950';
2866
+ ctx.lineWidth = 1;
2867
+ ctx.strokeRect(qX, qY, 320, 200);
2868
+ drawQuestPanel(ctx, intelligence.progression, qX + 5, qY + 5);
2775
2869
  }
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 ──
2870
+ // ════════════════════════════════════════════════════════════════
2871
+ // LAYER 7: MOOD BORDER (2px pulsing colored border)
2872
+ // ════════════════════════════════════════════════════════════════
2873
+ // Segment transition overlay (full-screen flash)
2784
2874
  if (charState.segmentTransition > 0) {
2785
2875
  const fadeOut = charState.segmentTransition <= 10;
2786
2876
  const alpha = fadeOut ? charState.segmentTransition / 10 * 0.5 : 0.5;
2787
2877
  ctx.fillStyle = hexToRgba(COLORS.accent, alpha);
2788
2878
  ctx.fillRect(0, 0, WIDTH, HEIGHT);
2789
- // Large centered text
2790
2879
  ctx.fillStyle = `rgba(255,255,255,${fadeOut ? charState.segmentTransition / 10 : 1})`;
2791
2880
  ctx.font = 'bold 40px "Courier New", monospace';
2792
2881
  const segText = charState.segmentTransitionName;
2793
- const segW = ctx.measureText(segText).width;
2794
- ctx.fillText(segText, (WIDTH - segW) / 2, HEIGHT / 2 - 10);
2795
- // Progress indicator
2882
+ const stW = ctx.measureText(segText).width;
2883
+ ctx.fillText(segText, (WIDTH - stW) / 2, HEIGHT / 2 - 10);
2796
2884
  ctx.font = '24px "Courier New", monospace';
2797
2885
  const progText = charState.segmentTransitionIndex;
2798
- const progW = ctx.measureText(progText).width;
2799
- ctx.fillText(progText, (WIDTH - progW) / 2, HEIGHT / 2 + 30);
2886
+ const ptW = ctx.measureText(progText).width;
2887
+ ctx.fillText(progText, (WIDTH - ptW) / 2, HEIGHT / 2 + 30);
2800
2888
  charState.segmentTransition--;
2801
2889
  }
2802
- // Restore from screen shake translate
2890
+ // Restore from screen shake
2803
2891
  ctx.restore();
2804
- // ── (#11) Mood-color border — 4px around entire frame ──
2805
- const borderColor = charState.mood === 'dancing'
2892
+ // Mood border — 2px around entire frame, pulsing
2893
+ const borderColorRaw = charState.mood === 'dancing'
2806
2894
  ? ['#f85149', '#f0c040', '#3fb950', '#58a6ff', '#bc8cff', '#ff6ec7'][animFrame % 6]
2807
2895
  : MOOD_COLORS[charState.mood] ?? COLORS.green;
2808
- ctx.strokeStyle = borderColor;
2809
- ctx.lineWidth = 4;
2810
- ctx.strokeRect(2, 2, WIDTH - 4, HEIGHT - 4);
2896
+ const borderPulseAlpha = 0.7 + 0.3 * Math.sin(animFrame * 0.15);
2897
+ ctx.strokeStyle = hexToRgba(borderColorRaw, borderPulseAlpha);
2898
+ ctx.lineWidth = 2;
2899
+ ctx.strokeRect(1, 1, WIDTH - 2, HEIGHT - 2);
2900
+ // ════════════════════════════════════════════════════════════════
2901
+ // LAYER 8: POST-PROCESSING
2902
+ // ════════════════════════════════════════════════════════════════
2903
+ {
2904
+ const hasLightning = world.events.includes('lightning');
2905
+ const ambientLevel = getAmbientForTime(world.timeOfDay);
2906
+ 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 })));
2907
+ renderLighting(ctx, lights, WIDTH, HEIGHT, ambientLevel);
2908
+ updateRadianceGrid(charState.radianceGrid, lights);
2909
+ renderRadianceOverlay(ctx, charState.radianceGrid, WIDTH, HEIGHT);
2910
+ const bloomSpots = buildCharacterBloom(robotScreenX, robotScreenY, robotScale, moodColorHex, animFrame);
2911
+ renderBloom(ctx, bloomSpots);
2912
+ }
2913
+ renderPostProcessing(ctx, WIDTH, HEIGHT, animFrame, {
2914
+ bloom: true,
2915
+ filmGrain: true,
2916
+ vignette: true,
2917
+ scanlines: true,
2918
+ });
2811
2919
  // Convert canvas to raw RGB24
2812
2920
  const imageData = ctx.getImageData(0, 0, WIDTH, HEIGHT);
2813
2921
  const rgba = imageData.data;
@@ -2872,10 +2980,7 @@ function startChatPoll() {
2872
2980
  if (charState.dreamInsights.length > 0) {
2873
2981
  const firstInsight = charState.dreamInsights[0];
2874
2982
  const topic = firstInsight.split(' ').filter((w) => w.length > 4).slice(0, 2).join(' ') || 'something strange';
2875
- charState.speech = `I dreamed about ${topic}. I feel... different.`;
2876
- }
2877
- else {
2878
- charState.speech = '';
2983
+ queueSpeech(`I dreamed about ${topic}. I feel... different.`, 'idle', 70, 48, 'dream');
2879
2984
  }
2880
2985
  // Reset dream state
2881
2986
  charState.dreamInsights = [];
@@ -2886,12 +2991,15 @@ function startChatPoll() {
2886
2991
  learnFromMessage(memory, msg.username, msg.text, msg.platform);
2887
2992
  // Analyze chat for domain relevance (stream brain)
2888
2993
  analyzeChatForDomains(streamBrain, msg.username, msg.text);
2994
+ // Track viewer in social engine
2995
+ if (socialEngine)
2996
+ trackViewer(socialEngine, msg.username, msg.platform, msg.text);
2889
2997
  // Phase 1: !sleep command — trigger dreaming mode
2890
2998
  if (msg.text.toLowerCase().trim() === '!sleep') {
2891
2999
  charState.mood = 'dreaming';
2892
3000
  charState.isDreamingWithOllama = false;
2893
3001
  lastChatTime = Date.now() - 300001; // trick the proactive timer into dreaming
2894
- charState.speech = 'Good night, chat... *powers down for dreamtime*';
3002
+ queueSpeech('Good night, chat... *powers down for dreamtime*', 'dreaming', 80, 48, 'dream');
2895
3003
  // Trigger dream generation
2896
3004
  generateStreamDream(charState.chatMessages).then(insights => {
2897
3005
  charState.dreamInsights = insights;
@@ -2903,17 +3011,48 @@ function startChatPoll() {
2903
3011
  }
2904
3012
  saveMemory(memory);
2905
3013
  if (insights.length > 0) {
2906
- setTimeout(() => { charState.speech = insights[0]; }, 3000);
3014
+ queueSpeech(insights[0], 'dreaming', 70, 60, 'dream');
2907
3015
  }
2908
3016
  }).catch(() => { });
2909
3017
  continue;
2910
3018
  }
3019
+ // World-First: On-demand overlay triggers
3020
+ {
3021
+ const cmd = msg.text.toLowerCase().trim();
3022
+ if (cmd === '!brain') {
3023
+ showBrainOverlay = OVERLAY_DURATION;
3024
+ continue;
3025
+ }
3026
+ if (cmd === '!top') {
3027
+ showLeaderboardOverlay = OVERLAY_DURATION;
3028
+ continue;
3029
+ }
3030
+ if (cmd === '!quest') {
3031
+ showQuestOverlay = OVERLAY_DURATION;
3032
+ continue;
3033
+ }
3034
+ }
2911
3035
  // Check brain commands (!do, !brain, !tools, !scan, !lookup, !research, !system, !ask, !stars, !news, !trending, !npm)
2912
3036
  const brainResult = handleBrainCommand(msg.text, msg.username, streamBrain);
2913
3037
  // Check intelligence commands (evolution, brain, collab)
2914
3038
  const intelResult = !brainResult ? handleIntelligenceCommand(msg.text, msg.username, intelligence) : null;
3039
+ // Check tile world commands (Minecraft-style: !place, !dig, !build, etc.)
3040
+ let tileResult = null;
3041
+ if (tileWorld && !brainResult && !intelResult) {
3042
+ tileResult = handleTileCommand(msg.text, msg.username, tileWorld, charState.robotX || 120);
3043
+ if (tileResult) {
3044
+ queueSpeech(tileResult, 'talking', 80, 48, 'tile-cmd');
3045
+ }
3046
+ }
3047
+ // Check narrative commands (!lore, !story, !discover, !name)
3048
+ if (narrativeEngine && !brainResult && !intelResult && !tileResult) {
3049
+ const narResult = handleNarrativeCommand(msg.text, msg.username, narrativeEngine, charState.robotX || 640);
3050
+ if (narResult) {
3051
+ queueSpeech(narResult, 'talking', 80, 48, 'narrative-cmd');
3052
+ }
3053
+ }
2915
3054
  // Check for world commands
2916
- const worldResult = !intelResult && !brainResult ? parseWorldCommand(msg.text) : null;
3055
+ const worldResult = !intelResult && !brainResult && !tileResult ? parseWorldCommand(msg.text) : null;
2917
3056
  // FIX 1: Weather sound effect commentary (if shipped)
2918
3057
  if (worldResult && shippedEffects.has('Add weather sound effects')) {
2919
3058
  const t = msg.text.toLowerCase();
@@ -2926,10 +3065,7 @@ function startChatPoll() {
2926
3065
  };
2927
3066
  for (const [kw, comment] of Object.entries(weatherComments)) {
2928
3067
  if (t.includes(kw)) {
2929
- setTimeout(() => {
2930
- charState.speech = comment;
2931
- setTimeout(() => { charState.speech = ''; }, 6000);
2932
- }, 3000);
3068
+ queueSpeech(comment, 'talking', 40, 36, 'weather-sfx');
2933
3069
  break;
2934
3070
  }
2935
3071
  }
@@ -2941,11 +3077,13 @@ function startChatPoll() {
2941
3077
  ? Promise.resolve(brainResult)
2942
3078
  : intelResult
2943
3079
  ? Promise.resolve(intelResult)
2944
- : worldResult
2945
- ? Promise.resolve(worldResult)
2946
- : generateResponse(msg.username, msg.text, msg.platform);
3080
+ : tileResult
3081
+ ? Promise.resolve(tileResult)
3082
+ : worldResult
3083
+ ? Promise.resolve(worldResult)
3084
+ : generateResponse(msg.username, msg.text, msg.platform);
2947
3085
  responsePromise.then(response => {
2948
- charState.speech = `@${msg.username}: ${response}`;
3086
+ queueSpeech(`@${msg.username}: ${response}`, 'talking', 80, 48, 'chat-response');
2949
3087
  memory.totalResponses++;
2950
3088
  // Learn from own response
2951
3089
  memory.conversationContext.push(`KBOT: ${response}`);
@@ -2953,7 +3091,6 @@ function startChatPoll() {
2953
3091
  memory.conversationContext = memory.conversationContext.slice(-10);
2954
3092
  saveMemory(memory);
2955
3093
  speakTTS(response);
2956
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
2957
3094
  });
2958
3095
  }
2959
3096
  if (charState.chatMessages.length > 100)
@@ -2985,7 +3122,7 @@ function startProactiveTimer() {
2985
3122
  saveMemory(memory);
2986
3123
  // Show first insight
2987
3124
  if (insights.length > 0) {
2988
- charState.speech = insights[0];
3125
+ queueSpeech(insights[0], 'dreaming', 70, 60, 'dream');
2989
3126
  }
2990
3127
  }).catch(() => {
2991
3128
  // Fallback to simple dream
@@ -2993,13 +3130,13 @@ function startProactiveTimer() {
2993
3130
  const topic = topicKeys.length > 0 ? topicKeys[Math.floor(Math.random() * topicKeys.length)] : 'code';
2994
3131
  const biomes = ['forest', 'ocean', 'space station', 'city', 'mountain', 'desert', 'cave'];
2995
3132
  const biome = biomes[Math.floor(Math.random() * biomes.length)];
2996
- charState.speech = `Dreaming about ${topic} in a ${biome}...`;
3133
+ queueSpeech(`Dreaming about ${topic} in a ${biome}...`, 'dreaming', 70, 60, 'dream');
2997
3134
  });
2998
3135
  }
2999
3136
  // Cycle through dream insights every 10 seconds
3000
3137
  if (charState.dreamInsights.length > 0 && Date.now() - charState.dreamInsightTime > 10000) {
3001
3138
  charState.dreamInsightIndex = (charState.dreamInsightIndex + 1) % charState.dreamInsights.length;
3002
- charState.speech = charState.dreamInsights[charState.dreamInsightIndex];
3139
+ queueSpeech(charState.dreamInsights[charState.dreamInsightIndex], 'dreaming', 70, 60, 'dream');
3003
3140
  charState.dreamInsightTime = Date.now();
3004
3141
  }
3005
3142
  return;
@@ -3007,15 +3144,13 @@ function startProactiveTimer() {
3007
3144
  // Only speak proactively if chat has been quiet for 30+ seconds
3008
3145
  if (silenceSeconds < 30)
3009
3146
  return;
3010
- // Don't interrupt an existing speech or dreaming
3011
- if (charState.speech || charState.mood === 'dreaming')
3147
+ // Don't interrupt dreaming
3148
+ if (charState.mood === 'dreaming')
3012
3149
  return;
3013
3150
  const line = getProactiveLine();
3014
3151
  if (line) {
3015
- charState.mood = 'talking';
3016
- charState.speech = line;
3152
+ queueSpeech(line, 'talking', 30, 60, 'proactive');
3017
3153
  speakTTS(line);
3018
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
3019
3154
  }
3020
3155
  }, 5000);
3021
3156
  }
@@ -3053,7 +3188,7 @@ Respond in 1-2 short sentences. Be fun, witty, and engaging. Reference their int
3053
3188
  method: 'POST',
3054
3189
  headers: { 'Content-Type': 'application/json' },
3055
3190
  body: JSON.stringify({
3056
- model: 'kernel:latest',
3191
+ model: 'gemma4',
3057
3192
  prompt,
3058
3193
  stream: false,
3059
3194
  options: { temperature: 0.8, num_predict: 80 },
@@ -3394,7 +3529,27 @@ export function registerStreamRendererTools() {
3394
3529
  animFrame = 0;
3395
3530
  lastChatCount = 0;
3396
3531
  lastChatTime = Date.now();
3532
+ speechQueue = [];
3533
+ currentSpeechExpiry = 0;
3534
+ exploration = null;
3535
+ tileWorld = loadWorld() || initTileWorld();
3536
+ romState = initRomEngine('plains', 'night');
3537
+ livingWorld = loadLivingWorldState() || initLivingWorld();
3538
+ // Evolve world based on time since last stream
3539
+ if (tileWorld && livingWorld) {
3540
+ const lastSave = tileWorld.cameraX !== 0 ? 1 : 0; // rough check
3541
+ if (lastSave > 0) {
3542
+ const changes = evolveWorld(tileWorld, livingWorld.ecology, 1); // simulate 1 hour
3543
+ if (changes.length > 0)
3544
+ queueSpeech(`The world evolved while I was away... ${changes.length} things changed.`, 'excited', 70, 48, 'world-evolve');
3545
+ }
3546
+ }
3397
3547
  intelligence = initIntelligence(memory);
3548
+ evolutionEngine = loadEvolutionState() || initEvolutionEngine();
3549
+ narrativeEngine = loadNarrative() || createNarrativeEngine();
3550
+ audioEngine = createAudioEngine();
3551
+ socialEngine = loadSocialEngine();
3552
+ activeAudioDescription = null;
3398
3553
  agenda = {
3399
3554
  currentIndex: 0,
3400
3555
  currentSegment: 'welcome',
@@ -3409,7 +3564,8 @@ export function registerStreamRendererTools() {
3409
3564
  return `ffmpeg exited:\n${stderr.slice(-500)}`;
3410
3565
  startChatPoll();
3411
3566
  startProactiveTimer();
3412
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
3567
+ // Let the welcome speech play for 8 seconds, then the queue takes over
3568
+ currentSpeechExpiry = 48; // 8 seconds at 6fps
3413
3569
  return `KBOT Character Stream LIVE!\n\nPlatforms: ${active.map(p => p.name).join(', ')}\nResolution: ${WIDTH}x${HEIGHT} @ ${FPS}fps\nRenderer: Canvas → RGB24 → ffmpeg\nMemory: ${memory.totalMessages} messages, ${Object.keys(memory.users).length} users remembered\nAgenda: ${SEGMENT_ORDER.map(s => SEGMENT_LABELS[s]).join(' → ')}\nSegment duration: 10 minutes each\n\nThe character learns from every chat interaction and speaks proactively during quiet moments.`;
3414
3570
  },
3415
3571
  });
@@ -3438,6 +3594,14 @@ export function registerStreamRendererTools() {
3438
3594
  ffmpegProc = null;
3439
3595
  }
3440
3596
  saveMemory(memory);
3597
+ if (tileWorld)
3598
+ saveWorld(tileWorld);
3599
+ if (evolutionEngine)
3600
+ saveEvolutionState(evolutionEngine);
3601
+ if (narrativeEngine)
3602
+ saveNarrative(narrativeEngine);
3603
+ if (socialEngine)
3604
+ saveSocialEngine(socialEngine);
3441
3605
  const elapsed = Math.floor((Date.now() - charState.startTime) / 60000);
3442
3606
  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
3607
  },
@@ -3456,11 +3620,9 @@ export function registerStreamRendererTools() {
3456
3620
  charState.chatMessages.push(msg);
3457
3621
  learnFromMessage(memory, msg.username, msg.text, msg.platform);
3458
3622
  lastChatTime = Date.now();
3459
- charState.mood = 'talking';
3460
3623
  const response = await generateResponse(msg.username, msg.text, msg.platform);
3461
- charState.speech = `@${msg.username}: ${response}`;
3624
+ queueSpeech(`@${msg.username}: ${response}`, 'talking', 80, 48, 'chat-response');
3462
3625
  speakTTS(response);
3463
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
3464
3626
  return `[${msg.platform}] ${msg.username}: ${msg.text}\nKBOT: ${response}`;
3465
3627
  },
3466
3628
  });
@@ -3473,10 +3635,14 @@ export function registerStreamRendererTools() {
3473
3635
  },
3474
3636
  tier: 'free',
3475
3637
  execute: async (args) => {
3476
- charState.mood = String(args.mood || 'idle');
3477
- if (args.speech)
3478
- charState.speech = String(args.speech);
3479
- return `Mood: ${charState.mood}`;
3638
+ const moodVal = String(args.mood || 'idle');
3639
+ if (args.speech) {
3640
+ queueSpeech(String(args.speech), moodVal, 100, 48, 'api');
3641
+ }
3642
+ else {
3643
+ charState.mood = moodVal;
3644
+ }
3645
+ return `Mood: ${moodVal}`;
3480
3646
  },
3481
3647
  });
3482
3648
  registerTool({