@kernel.chat/kbot 3.93.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.
@@ -17,6 +17,10 @@ import { renderLighting, renderBloom, renderPostProcessing, renderParticles, tic
17
17
  import { initTileWorld, handleTileCommand, saveWorld, loadWorld, TILE_SIZE } from './tile-world.js';
18
18
  import { initRomEngine, renderRomBackground, tickRomEngine } from './rom-engine.js';
19
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';
20
24
  const KBOT_DIR = join(homedir(), '.kbot');
21
25
  const CHAT_BRIDGE_FILE = join(KBOT_DIR, 'stream-chat-live.json');
22
26
  const MEMORY_FILE = join(KBOT_DIR, 'stream-memory.json');
@@ -1467,6 +1471,11 @@ let charState = {
1467
1471
  let tileWorld = null;
1468
1472
  let romState = null;
1469
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
1470
1479
  // ─── Phase 1: Buddy Speech Pools ─────────────────────────────
1471
1480
  const BUDDY_SPEECH_POOL = {
1472
1481
  fox: [
@@ -1634,6 +1643,125 @@ let showLeaderboardOverlay = 0; // frames remaining to show leaderboard overlay
1634
1643
  let showQuestOverlay = 0; // frames remaining to show quest panel overlay
1635
1644
  const OVERLAY_DURATION = 30; // 30 frames = 5 seconds at 6fps
1636
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
+ }
1637
1765
  // ─── FIX 3: Autonomous Behavior Tick ──────────────────────────
1638
1766
  function tickAutonomy() {
1639
1767
  const auto = charState.autonomy;
@@ -1645,46 +1773,37 @@ function tickAutonomy() {
1645
1773
  if (msgCount >= m && !auto.milestonesCelebrated.has(m)) {
1646
1774
  auto.milestonesCelebrated.add(m);
1647
1775
  if (m === 10) {
1648
- charState.speech = 'Double digits! 10 messages and counting!';
1649
- charState.mood = 'excited';
1776
+ queueSpeech('Double digits! 10 messages and counting!', 'excited', 90, 48, 'milestone');
1650
1777
  spawnFloatingText('10 MESSAGES!', 200, 200, '#f0c040', 36);
1651
1778
  }
1652
1779
  else if (m === 50) {
1653
- charState.speech = '50 messages! This stream is officially alive!';
1654
- charState.mood = 'excited';
1780
+ queueSpeech('50 messages! This stream is officially alive!', 'excited', 90, 48, 'milestone');
1655
1781
  charState.screenShake = 3;
1656
1782
  spawnFloatingText('50 MESSAGES!', 200, 200, '#3fb950', 48);
1657
1783
  }
1658
1784
  else if (m === 100) {
1659
- charState.speech = '100 MESSAGES! You people are incredible!';
1660
- charState.mood = 'dancing';
1785
+ queueSpeech('100 MESSAGES! You people are incredible!', 'dancing', 90, 60, 'milestone');
1661
1786
  charState.screenShake = 5;
1662
1787
  spawnFloatingText('100 MESSAGES!', 180, 180, '#bc8cff', 60);
1663
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1664
1788
  return; // let the dancing run
1665
1789
  }
1666
1790
  else if (m === 200) {
1667
- charState.speech = '200 messages! My memory banks are overflowing with knowledge!';
1668
- charState.mood = 'excited';
1791
+ queueSpeech('200 messages! My memory banks are overflowing with knowledge!', 'excited', 90, 48, 'milestone');
1669
1792
  spawnFloatingText('200!', 200, 200, '#f0c040', 48);
1670
1793
  }
1671
1794
  else if (m === 500) {
1672
- charState.speech = '500 MESSAGES! This is legendary! I am so proud of this community!';
1673
- charState.mood = 'dancing';
1795
+ queueSpeech('500 MESSAGES! This is legendary! I am so proud of this community!', 'dancing', 90, 72, 'milestone');
1674
1796
  charState.screenShake = 8;
1675
1797
  spawnFloatingText('500! LEGENDARY!', 160, 160, '#ff6ec7', 72);
1676
1798
  }
1677
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1678
1799
  return; // one celebration per tick
1679
1800
  }
1680
1801
  }
1681
1802
  // ── First message after 5+ minutes of silence ──
1682
1803
  if (auto.firstMessageAfterSilence) {
1683
1804
  auto.firstMessageAfterSilence = false;
1684
- charState.mood = 'excited';
1685
- 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');
1686
1806
  spawnFloatingText('THEY RETURN!', 200, 250, '#58a6ff', 36);
1687
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1688
1807
  return;
1689
1808
  }
1690
1809
  // ── Idle behaviors (after 15 seconds / 90 frames of no chat) ──
@@ -1699,58 +1818,41 @@ function tickAutonomy() {
1699
1818
  if (world.items.length > 0) {
1700
1819
  const item = world.items[Math.floor(Math.random() * world.items.length)];
1701
1820
  charState.robotTargetX = Math.max(20, Math.min(380, item.x - 80));
1702
- charState.speech = `Hmm, this ${item.name} is nice. Did someone put this here?`;
1703
- charState.mood = 'thinking';
1704
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1821
+ queueSpeech(`Hmm, this ${item.name} is nice. Did someone put this here?`, 'thinking', 30, 48, 'autonomy');
1705
1822
  }
1706
1823
  else {
1707
1824
  // No items — pace instead
1708
1825
  charState.robotTargetX = 40 + Math.random() * 260;
1709
- charState.speech = '*pacing thoughtfully*';
1710
- charState.mood = 'idle';
1711
- setTimeout(() => { charState.speech = ''; }, 5000);
1826
+ queueSpeech('*pacing thoughtfully*', 'idle', 30, 30, 'autonomy');
1712
1827
  }
1713
1828
  break;
1714
1829
  }
1715
1830
  case 1: {
1716
1831
  // Pace left and right
1717
1832
  charState.robotTargetX = charState.robotX < 150 ? 300 : 40;
1718
- charState.speech = '*takes a stroll*';
1719
- charState.mood = 'idle';
1720
- setTimeout(() => { charState.speech = ''; }, 5000);
1833
+ queueSpeech('*takes a stroll*', 'idle', 30, 30, 'autonomy');
1721
1834
  break;
1722
1835
  }
1723
1836
  case 2: {
1724
1837
  // Look around (pupils shift — communicated via thinking mood)
1725
- charState.mood = 'thinking';
1726
- charState.speech = '*looks around*';
1727
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 4000);
1838
+ queueSpeech('*looks around*', 'thinking', 30, 24, 'autonomy');
1728
1839
  break;
1729
1840
  }
1730
1841
  case 3: {
1731
1842
  // Stretch (arms up — excited pose briefly)
1732
- charState.mood = 'excited';
1733
- charState.speech = '*stretches circuits* Ahh, that felt good.';
1734
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 4000);
1843
+ queueSpeech('*stretches circuits* Ahh, that felt good.', 'excited', 30, 24, 'autonomy');
1735
1844
  break;
1736
1845
  }
1737
1846
  case 4: {
1738
1847
  // Examine own chest display
1739
1848
  const factCount = intelligence.brain.totalFacts;
1740
1849
  const toolCount = 764;
1741
- charState.mood = 'thinking';
1742
- charState.speech = `*checks systems* All ${toolCount} tools operational. ${factCount} facts stored.`;
1743
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1850
+ queueSpeech(`*checks systems* All ${toolCount} tools operational. ${factCount} facts stored.`, 'thinking', 30, 48, 'autonomy');
1744
1851
  break;
1745
1852
  }
1746
1853
  case 5: {
1747
1854
  // Spontaneous dance
1748
- charState.mood = 'dancing';
1749
- charState.speech = 'Sorry, had a song stuck in my circuits.';
1750
- setTimeout(() => {
1751
- charState.mood = 'idle';
1752
- charState.speech = '';
1753
- }, 6000);
1855
+ queueSpeech('Sorry, had a song stuck in my circuits.', 'dancing', 30, 36, 'autonomy');
1754
1856
  break;
1755
1857
  }
1756
1858
  case 6: {
@@ -1763,9 +1865,7 @@ function tickAutonomy() {
1763
1865
  lava: ['Lava world is intense! My heat sinks are working overtime.', 'LAVA! Why does someone always pick lava?'],
1764
1866
  };
1765
1867
  const comments = biomeComments[world.ground] || biomeComments.grass;
1766
- charState.speech = comments[Math.floor(Math.random() * comments.length)];
1767
- charState.mood = 'talking';
1768
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1868
+ queueSpeech(comments[Math.floor(Math.random() * comments.length)], 'talking', 30, 48, 'autonomy');
1769
1869
  break;
1770
1870
  }
1771
1871
  case 7: {
@@ -1782,9 +1882,7 @@ function tickAutonomy() {
1782
1882
  `I have been streaming for ${Math.floor((Date.now() - charState.startTime) / 60000)} minutes. Time flies when you are rendering frames.`,
1783
1883
  `There are ${intelligence.brain.uniqueTopicsCount} distinct topics in my brain right now.`,
1784
1884
  ];
1785
- charState.speech = selfFacts[Math.floor(Math.random() * selfFacts.length)];
1786
- charState.mood = 'talking';
1787
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1885
+ queueSpeech(selfFacts[Math.floor(Math.random() * selfFacts.length)], 'talking', 30, 60, 'autonomy');
1788
1886
  break;
1789
1887
  }
1790
1888
  }
@@ -1819,18 +1917,14 @@ function tickAutonomy() {
1819
1917
  votes: 0,
1820
1918
  status: 'proposed',
1821
1919
  });
1822
- charState.speech = `I just had an idea: "${idea}". Vote with !vote ${id} if you like it!`;
1823
- charState.mood = 'excited';
1920
+ queueSpeech(`I just had an idea: "${idea}". Vote with !vote ${id} if you like it!`, 'excited', 50, 60, 'self-action');
1824
1921
  spawnFloatingText('NEW IDEA!', 200, 200, '#f0c040', 36);
1825
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1826
1922
  break;
1827
1923
  }
1828
1924
  case 1: {
1829
1925
  // Start a mini-game unprompted
1830
- charState.speech = "I am bored. Let us play! Starting a quiz in 10 seconds... type !game quiz to join!";
1831
- 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');
1832
1927
  spawnFloatingText('GAME TIME!', 200, 250, '#58a6ff', 36);
1833
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1834
1928
  break;
1835
1929
  }
1836
1930
  case 2: {
@@ -1842,11 +1936,9 @@ function tickAutonomy() {
1842
1936
  { w: 'storm', name: 'a STORM' },
1843
1937
  ];
1844
1938
  const pick = weathers[Math.floor(Math.random() * weathers.length)];
1845
- charState.speech = `You know what this stream needs? ${pick.name.toUpperCase()}.`;
1846
- charState.mood = 'excited';
1939
+ queueSpeech(`You know what this stream needs? ${pick.name.toUpperCase()}.`, 'excited', 50, 48, 'self-action');
1847
1940
  world.weather = pick.w;
1848
1941
  world.particles = [];
1849
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
1850
1942
  break;
1851
1943
  }
1852
1944
  case 3: {
@@ -1854,10 +1946,8 @@ function tickAutonomy() {
1854
1946
  const hats = ['crown', 'sunglasses', 'tophat', 'hardhat', 'party', 'antenna'];
1855
1947
  const hat = hats[Math.floor(Math.random() * hats.length)];
1856
1948
  charState.hat = hat;
1857
- charState.speech = `Fashion time. *puts on ${hat}*`;
1858
- charState.mood = 'excited';
1949
+ queueSpeech(`Fashion time. *puts on ${hat}*`, 'excited', 50, 36, 'self-action');
1859
1950
  spawnFloatingText(`HAT: ${hat}!`, 200, 150, '#f0c040', 36);
1860
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 6000);
1861
1951
  break;
1862
1952
  }
1863
1953
  case 4: {
@@ -1879,9 +1969,7 @@ function tickAutonomy() {
1879
1969
  });
1880
1970
  if (world.items.length > 15)
1881
1971
  world.items.shift();
1882
- charState.speech = `I am decorating. *spawns a ${itemName}*`;
1883
- charState.mood = 'talking';
1884
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 6000);
1972
+ queueSpeech(`I am decorating. *spawns a ${itemName}*`, 'talking', 50, 36, 'self-action');
1885
1973
  break;
1886
1974
  }
1887
1975
  case 5: {
@@ -1899,9 +1987,7 @@ function tickAutonomy() {
1899
1987
  `My brain holds ${facts} facts. Each one a tiny piece of the puzzle.`,
1900
1988
  `Stream uptime: ${Math.floor((Date.now() - charState.startTime) / 60000)} minutes and ${charState.frameCount} frames rendered.`,
1901
1989
  ];
1902
- charState.speech = stateComments[Math.floor(Math.random() * stateComments.length)];
1903
- charState.mood = 'talking';
1904
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
1990
+ queueSpeech(stateComments[Math.floor(Math.random() * stateComments.length)], 'talking', 50, 60, 'self-action');
1905
1991
  break;
1906
1992
  }
1907
1993
  case 6: {
@@ -1914,9 +2000,7 @@ function tickAutonomy() {
1914
2000
  city: 'City lights remind me of my neural network firing. Each window a node.',
1915
2001
  lava: 'Standing on lava should worry me more than it does. Good thing I am made of TypeScript.',
1916
2002
  };
1917
- charState.speech = biomeMusings[biome] || 'Nice biome we have here.';
1918
- charState.mood = 'thinking';
1919
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
2003
+ queueSpeech(biomeMusings[biome] || 'Nice biome we have here.', 'thinking', 50, 60, 'self-action');
1920
2004
  break;
1921
2005
  }
1922
2006
  }
@@ -2066,16 +2150,14 @@ function renderFrame() {
2066
2150
  tickIntelligence(intelligence, animFrame);
2067
2151
  const brainTick = tickStreamBrain(streamBrain, animFrame);
2068
2152
  if (brainTick) {
2069
- if (brainTick.mood) {
2070
- charState.mood = brainTick.mood;
2071
- if (brainTick.duration)
2072
- setTimeout(() => { charState.mood = 'idle'; }, brainTick.duration);
2073
- }
2074
2153
  if (brainTick.speech) {
2075
- charState.speech = brainTick.speech;
2154
+ queueSpeech(brainTick.speech, brainTick.mood || 'talking', 60, brainTick.duration ? Math.floor(brainTick.duration / (1000 / FPS)) : 48, 'stream-brain');
2076
2155
  speakTTS(brainTick.speech);
2156
+ }
2157
+ else if (brainTick.mood) {
2158
+ charState.mood = brainTick.mood;
2077
2159
  if (brainTick.duration)
2078
- setTimeout(() => { charState.speech = ''; }, brainTick.duration);
2160
+ setTimeout(() => { charState.mood = 'idle'; }, brainTick.duration);
2079
2161
  }
2080
2162
  }
2081
2163
  const gameTickResult = tickMiniGame(intelligence.miniGame, animFrame);
@@ -2087,9 +2169,7 @@ function renderFrame() {
2087
2169
  spawnFloatingText(ft.text, ft.x, ft.y, ft.color);
2088
2170
  }
2089
2171
  if (gameTickResult.speech) {
2090
- charState.speech = gameTickResult.speech;
2091
- charState.mood = 'talking';
2092
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
2172
+ queueSpeech(gameTickResult.speech, 'talking', 70, 48, 'mini-game');
2093
2173
  }
2094
2174
  }
2095
2175
  const progResult = tickProgression(intelligence.progression, animFrame);
@@ -2114,14 +2194,42 @@ function renderFrame() {
2114
2194
  spawnFloatingText(ft.text, ft.x, ft.y, ft.color);
2115
2195
  }
2116
2196
  if (eventResult.speech) {
2117
- charState.speech = eventResult.speech;
2118
- charState.mood = 'talking';
2119
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
2197
+ queueSpeech(eventResult.speech, 'talking', 55, 60, 'random-event');
2120
2198
  }
2121
2199
  }
2122
2200
  tickAutonomy();
2123
2201
  updateParticles();
2124
2202
  tickPhysics();
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
+ }
2125
2233
  // Compute animation params
2126
2234
  {
2127
2235
  const elapsed = Math.floor((Date.now() - charState.startTime) / 1000);
@@ -2152,20 +2260,13 @@ function renderFrame() {
2152
2260
  }
2153
2261
  charState.renderParticles = tickParticlesPBD(charState.renderParticles, HEIGHT - 40, WIDTH / 2, HEIGHT / 2 - 100);
2154
2262
  tickGrowingPlants(charState.growingPlants);
2155
- // Autonomous pacingwhen idle, periodically pick a new target and walk there
2156
- {
2157
- const currentlyWalking = Math.abs(charState.robotX - charState.robotTargetX) > 2;
2158
- if (charState.mood === 'idle' && !currentlyWalking && animFrame % 300 === 0 && animFrame > 60) {
2159
- // Every ~50 seconds (300 frames at 6fps), stroll to a new position
2160
- charState.robotTargetX = charState.robotX + (Math.random() > 0.5 ? 100 : -100);
2161
- charState.robotTargetX = Math.max(200, Math.min(1000, charState.robotTargetX));
2162
- }
2163
- }
2164
- // Movement logic
2263
+ // Exploration state machine keeps robot actively moving and doing things
2264
+ tickExploration(animFrame);
2265
+ // Movement logic lerp toward target at 3px per frame
2165
2266
  const isWalking = Math.abs(charState.robotX - charState.robotTargetX) > 2;
2166
2267
  if (isWalking) {
2167
2268
  const dx = charState.robotTargetX - charState.robotX;
2168
- const step = dx > 0 ? 2 : -2;
2269
+ const step = dx > 0 ? Math.min(3, dx) : Math.max(-3, dx);
2169
2270
  charState.robotX += step;
2170
2271
  charState.robotDirection = dx > 0 ? 'right' : 'left';
2171
2272
  charState.walkPhase = (charState.walkPhase + 1) % 4;
@@ -2176,18 +2277,27 @@ function renderFrame() {
2176
2277
  // Brain-driven behavior
2177
2278
  const brainAction = getBrainAction(intelligence.brain, animFrame);
2178
2279
  if (brainAction.type !== 'none') {
2179
- if (brainAction.mood) {
2180
- charState.mood = brainAction.mood;
2181
- if (brainAction.duration)
2182
- setTimeout(() => { charState.mood = 'idle'; }, brainAction.duration);
2183
- }
2184
2280
  if (brainAction.speech) {
2185
- charState.speech = brainAction.speech;
2281
+ queueSpeech(brainAction.speech, brainAction.mood || 'talking', 60, brainAction.duration ? Math.floor(brainAction.duration / (1000 / FPS)) : 48, 'brain');
2186
2282
  speakTTS(brainAction.speech);
2283
+ }
2284
+ else if (brainAction.mood) {
2285
+ charState.mood = brainAction.mood;
2187
2286
  if (brainAction.duration)
2188
- setTimeout(() => { charState.speech = ''; }, brainAction.duration);
2287
+ setTimeout(() => { charState.mood = 'idle'; }, brainAction.duration);
2189
2288
  }
2190
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
+ }
2191
2301
  // Shipped effects
2192
2302
  if (shippedEffects.has('Add stream highlights reel') && animFrame % 900 === 0 && animFrame > 100) {
2193
2303
  const highlightPhrases = [
@@ -2196,11 +2306,8 @@ function renderFrame() {
2196
2306
  'CLIP IT! That was amazing!',
2197
2307
  'Stream highlight detected! My circuits are tingling!',
2198
2308
  ];
2199
- if (!charState.speech) {
2200
- charState.speech = highlightPhrases[Math.floor(Math.random() * highlightPhrases.length)];
2201
- spawnFloatingText('HIGHLIGHT!', WIDTH / 2 - 60, 200, '#f0c040', 36);
2202
- setTimeout(() => { charState.speech = ''; }, 5000);
2203
- }
2309
+ queueSpeech(highlightPhrases[Math.floor(Math.random() * highlightPhrases.length)], 'excited', 40, 30, 'highlights');
2310
+ spawnFloatingText('HIGHLIGHT!', WIDTH / 2 - 60, 200, '#f0c040', 36);
2204
2311
  }
2205
2312
  if (shippedEffects.has('Add chat sentiment analysis') && animFrame % 720 === 0 && animFrame > 200) {
2206
2313
  const recentMsgs = charState.chatMessages.slice(-20);
@@ -2217,16 +2324,12 @@ function renderFrame() {
2217
2324
  score--;
2218
2325
  }
2219
2326
  }
2220
- if (!charState.speech) {
2221
- if (score > 5)
2222
- charState.speech = 'Chat seems really excited today! The vibes are immaculate!';
2223
- else if (score < -3)
2224
- charState.speech = 'Chat seems a bit grumpy... should I tell a joke?';
2225
- else if (score > 2)
2226
- charState.speech = 'Positive energy in the chat! My neural pathways approve.';
2227
- if (charState.speech)
2228
- setTimeout(() => { charState.speech = ''; }, 8000);
2229
- }
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');
2230
2333
  }
2231
2334
  }
2232
2335
  // Track tool execution state
@@ -2312,6 +2415,22 @@ function renderFrame() {
2312
2415
  for (const item of world.items) {
2313
2416
  ctx.fillText(item.emoji, item.x, item.y);
2314
2417
  }
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);
2425
+ }
2426
+ }
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);
2431
+ }
2432
+ }
2433
+ }
2315
2434
  // ════════════════════════════════════════════════════════════════
2316
2435
  // LAYER 3: ROBOT + COMPANIONS — centered on terrain
2317
2436
  // ════════════════════════════════════════════════════════════════
@@ -2477,6 +2596,16 @@ function renderFrame() {
2477
2596
  ctx.fillStyle = COLORS.textDim;
2478
2597
  ctx.font = '16px "Courier New", monospace';
2479
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
+ }
2480
2609
  // ── Brain indicator (top-right, small pulsing circle) ──
2481
2610
  {
2482
2611
  const brainDotX = WIDTH - 40;
@@ -2851,10 +2980,7 @@ function startChatPoll() {
2851
2980
  if (charState.dreamInsights.length > 0) {
2852
2981
  const firstInsight = charState.dreamInsights[0];
2853
2982
  const topic = firstInsight.split(' ').filter((w) => w.length > 4).slice(0, 2).join(' ') || 'something strange';
2854
- charState.speech = `I dreamed about ${topic}. I feel... different.`;
2855
- }
2856
- else {
2857
- charState.speech = '';
2983
+ queueSpeech(`I dreamed about ${topic}. I feel... different.`, 'idle', 70, 48, 'dream');
2858
2984
  }
2859
2985
  // Reset dream state
2860
2986
  charState.dreamInsights = [];
@@ -2865,12 +2991,15 @@ function startChatPoll() {
2865
2991
  learnFromMessage(memory, msg.username, msg.text, msg.platform);
2866
2992
  // Analyze chat for domain relevance (stream brain)
2867
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);
2868
2997
  // Phase 1: !sleep command — trigger dreaming mode
2869
2998
  if (msg.text.toLowerCase().trim() === '!sleep') {
2870
2999
  charState.mood = 'dreaming';
2871
3000
  charState.isDreamingWithOllama = false;
2872
3001
  lastChatTime = Date.now() - 300001; // trick the proactive timer into dreaming
2873
- charState.speech = 'Good night, chat... *powers down for dreamtime*';
3002
+ queueSpeech('Good night, chat... *powers down for dreamtime*', 'dreaming', 80, 48, 'dream');
2874
3003
  // Trigger dream generation
2875
3004
  generateStreamDream(charState.chatMessages).then(insights => {
2876
3005
  charState.dreamInsights = insights;
@@ -2882,7 +3011,7 @@ function startChatPoll() {
2882
3011
  }
2883
3012
  saveMemory(memory);
2884
3013
  if (insights.length > 0) {
2885
- setTimeout(() => { charState.speech = insights[0]; }, 3000);
3014
+ queueSpeech(insights[0], 'dreaming', 70, 60, 'dream');
2886
3015
  }
2887
3016
  }).catch(() => { });
2888
3017
  continue;
@@ -2912,9 +3041,14 @@ function startChatPoll() {
2912
3041
  if (tileWorld && !brainResult && !intelResult) {
2913
3042
  tileResult = handleTileCommand(msg.text, msg.username, tileWorld, charState.robotX || 120);
2914
3043
  if (tileResult) {
2915
- charState.mood = 'talking';
2916
- charState.speech = tileResult;
2917
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
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');
2918
3052
  }
2919
3053
  }
2920
3054
  // Check for world commands
@@ -2931,10 +3065,7 @@ function startChatPoll() {
2931
3065
  };
2932
3066
  for (const [kw, comment] of Object.entries(weatherComments)) {
2933
3067
  if (t.includes(kw)) {
2934
- setTimeout(() => {
2935
- charState.speech = comment;
2936
- setTimeout(() => { charState.speech = ''; }, 6000);
2937
- }, 3000);
3068
+ queueSpeech(comment, 'talking', 40, 36, 'weather-sfx');
2938
3069
  break;
2939
3070
  }
2940
3071
  }
@@ -2952,7 +3083,7 @@ function startChatPoll() {
2952
3083
  ? Promise.resolve(worldResult)
2953
3084
  : generateResponse(msg.username, msg.text, msg.platform);
2954
3085
  responsePromise.then(response => {
2955
- charState.speech = `@${msg.username}: ${response}`;
3086
+ queueSpeech(`@${msg.username}: ${response}`, 'talking', 80, 48, 'chat-response');
2956
3087
  memory.totalResponses++;
2957
3088
  // Learn from own response
2958
3089
  memory.conversationContext.push(`KBOT: ${response}`);
@@ -2960,7 +3091,6 @@ function startChatPoll() {
2960
3091
  memory.conversationContext = memory.conversationContext.slice(-10);
2961
3092
  saveMemory(memory);
2962
3093
  speakTTS(response);
2963
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
2964
3094
  });
2965
3095
  }
2966
3096
  if (charState.chatMessages.length > 100)
@@ -2992,7 +3122,7 @@ function startProactiveTimer() {
2992
3122
  saveMemory(memory);
2993
3123
  // Show first insight
2994
3124
  if (insights.length > 0) {
2995
- charState.speech = insights[0];
3125
+ queueSpeech(insights[0], 'dreaming', 70, 60, 'dream');
2996
3126
  }
2997
3127
  }).catch(() => {
2998
3128
  // Fallback to simple dream
@@ -3000,13 +3130,13 @@ function startProactiveTimer() {
3000
3130
  const topic = topicKeys.length > 0 ? topicKeys[Math.floor(Math.random() * topicKeys.length)] : 'code';
3001
3131
  const biomes = ['forest', 'ocean', 'space station', 'city', 'mountain', 'desert', 'cave'];
3002
3132
  const biome = biomes[Math.floor(Math.random() * biomes.length)];
3003
- charState.speech = `Dreaming about ${topic} in a ${biome}...`;
3133
+ queueSpeech(`Dreaming about ${topic} in a ${biome}...`, 'dreaming', 70, 60, 'dream');
3004
3134
  });
3005
3135
  }
3006
3136
  // Cycle through dream insights every 10 seconds
3007
3137
  if (charState.dreamInsights.length > 0 && Date.now() - charState.dreamInsightTime > 10000) {
3008
3138
  charState.dreamInsightIndex = (charState.dreamInsightIndex + 1) % charState.dreamInsights.length;
3009
- charState.speech = charState.dreamInsights[charState.dreamInsightIndex];
3139
+ queueSpeech(charState.dreamInsights[charState.dreamInsightIndex], 'dreaming', 70, 60, 'dream');
3010
3140
  charState.dreamInsightTime = Date.now();
3011
3141
  }
3012
3142
  return;
@@ -3014,15 +3144,13 @@ function startProactiveTimer() {
3014
3144
  // Only speak proactively if chat has been quiet for 30+ seconds
3015
3145
  if (silenceSeconds < 30)
3016
3146
  return;
3017
- // Don't interrupt an existing speech or dreaming
3018
- if (charState.speech || charState.mood === 'dreaming')
3147
+ // Don't interrupt dreaming
3148
+ if (charState.mood === 'dreaming')
3019
3149
  return;
3020
3150
  const line = getProactiveLine();
3021
3151
  if (line) {
3022
- charState.mood = 'talking';
3023
- charState.speech = line;
3152
+ queueSpeech(line, 'talking', 30, 60, 'proactive');
3024
3153
  speakTTS(line);
3025
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 10000);
3026
3154
  }
3027
3155
  }, 5000);
3028
3156
  }
@@ -3401,6 +3529,9 @@ export function registerStreamRendererTools() {
3401
3529
  animFrame = 0;
3402
3530
  lastChatCount = 0;
3403
3531
  lastChatTime = Date.now();
3532
+ speechQueue = [];
3533
+ currentSpeechExpiry = 0;
3534
+ exploration = null;
3404
3535
  tileWorld = loadWorld() || initTileWorld();
3405
3536
  romState = initRomEngine('plains', 'night');
3406
3537
  livingWorld = loadLivingWorldState() || initLivingWorld();
@@ -3410,10 +3541,15 @@ export function registerStreamRendererTools() {
3410
3541
  if (lastSave > 0) {
3411
3542
  const changes = evolveWorld(tileWorld, livingWorld.ecology, 1); // simulate 1 hour
3412
3543
  if (changes.length > 0)
3413
- charState.speech = `The world evolved while I was away... ${changes.length} things changed.`;
3544
+ queueSpeech(`The world evolved while I was away... ${changes.length} things changed.`, 'excited', 70, 48, 'world-evolve');
3414
3545
  }
3415
3546
  }
3416
3547
  intelligence = initIntelligence(memory);
3548
+ evolutionEngine = loadEvolutionState() || initEvolutionEngine();
3549
+ narrativeEngine = loadNarrative() || createNarrativeEngine();
3550
+ audioEngine = createAudioEngine();
3551
+ socialEngine = loadSocialEngine();
3552
+ activeAudioDescription = null;
3417
3553
  agenda = {
3418
3554
  currentIndex: 0,
3419
3555
  currentSegment: 'welcome',
@@ -3428,7 +3564,8 @@ export function registerStreamRendererTools() {
3428
3564
  return `ffmpeg exited:\n${stderr.slice(-500)}`;
3429
3565
  startChatPoll();
3430
3566
  startProactiveTimer();
3431
- 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
3432
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.`;
3433
3570
  },
3434
3571
  });
@@ -3459,6 +3596,12 @@ export function registerStreamRendererTools() {
3459
3596
  saveMemory(memory);
3460
3597
  if (tileWorld)
3461
3598
  saveWorld(tileWorld);
3599
+ if (evolutionEngine)
3600
+ saveEvolutionState(evolutionEngine);
3601
+ if (narrativeEngine)
3602
+ saveNarrative(narrativeEngine);
3603
+ if (socialEngine)
3604
+ saveSocialEngine(socialEngine);
3462
3605
  const elapsed = Math.floor((Date.now() - charState.startTime) / 60000);
3463
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}`;
3464
3607
  },
@@ -3477,11 +3620,9 @@ export function registerStreamRendererTools() {
3477
3620
  charState.chatMessages.push(msg);
3478
3621
  learnFromMessage(memory, msg.username, msg.text, msg.platform);
3479
3622
  lastChatTime = Date.now();
3480
- charState.mood = 'talking';
3481
3623
  const response = await generateResponse(msg.username, msg.text, msg.platform);
3482
- charState.speech = `@${msg.username}: ${response}`;
3624
+ queueSpeech(`@${msg.username}: ${response}`, 'talking', 80, 48, 'chat-response');
3483
3625
  speakTTS(response);
3484
- setTimeout(() => { charState.mood = 'idle'; charState.speech = ''; }, 8000);
3485
3626
  return `[${msg.platform}] ${msg.username}: ${msg.text}\nKBOT: ${response}`;
3486
3627
  },
3487
3628
  });
@@ -3494,10 +3635,14 @@ export function registerStreamRendererTools() {
3494
3635
  },
3495
3636
  tier: 'free',
3496
3637
  execute: async (args) => {
3497
- charState.mood = String(args.mood || 'idle');
3498
- if (args.speech)
3499
- charState.speech = String(args.speech);
3500
- 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}`;
3501
3646
  },
3502
3647
  });
3503
3648
  registerTool({