@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.
- package/dist/tools/audio-engine.d.ts +72 -0
- package/dist/tools/audio-engine.js +426 -0
- package/dist/tools/coordination-engine.d.ts +127 -0
- package/dist/tools/coordination-engine.js +543 -0
- package/dist/tools/evolution-engine.d.ts +102 -0
- package/dist/tools/evolution-engine.js +746 -0
- package/dist/tools/foundation-engines.d.ts +111 -0
- package/dist/tools/foundation-engines.js +520 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/living-world.d.ts +161 -0
- package/dist/tools/living-world.js +1054 -0
- package/dist/tools/narrative-engine.d.ts +58 -0
- package/dist/tools/narrative-engine.js +681 -0
- package/dist/tools/render-engine.js +5 -5
- package/dist/tools/research-engine.d.ts +58 -0
- package/dist/tools/research-engine.js +550 -0
- package/dist/tools/rom-engine.d.ts +130 -0
- package/dist/tools/rom-engine.js +1208 -0
- package/dist/tools/social-engine.d.ts +100 -0
- package/dist/tools/social-engine.js +540 -0
- package/dist/tools/sprite-engine.js +40 -26
- package/dist/tools/stream-brain.js +4 -4
- package/dist/tools/stream-intelligence.d.ts +6 -0
- package/dist/tools/stream-intelligence.js +239 -49
- package/dist/tools/stream-renderer.js +805 -639
- package/dist/tools/stream-self-eval.d.ts +96 -0
- package/dist/tools/stream-self-eval.js +764 -0
- package/dist/tools/tile-world.d.ts +40 -0
- package/dist/tools/tile-world.js +1070 -0
- package/package.json +1 -1
|
@@ -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,
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
2061
|
-
|
|
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
|
-
|
|
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`,
|
|
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!',
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2251
|
+
// Biome particles
|
|
2133
2252
|
if (world.ground === 'lava' && animFrame % 4 === 0) {
|
|
2134
|
-
charState.renderParticles.push(...createParticleEmitter('fire',
|
|
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',
|
|
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,
|
|
2144
|
-
// AAA: Tick growing plants
|
|
2261
|
+
charState.renderParticles = tickParticlesPBD(charState.renderParticles, HEIGHT - 40, WIDTH / 2, HEIGHT / 2 - 100);
|
|
2145
2262
|
tickGrowingPlants(charState.growingPlants);
|
|
2146
|
-
//
|
|
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
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
//
|
|
2163
|
-
if (
|
|
2164
|
-
charState.
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
//
|
|
2170
|
-
if (
|
|
2171
|
-
|
|
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
|
|
2174
|
-
|
|
2376
|
+
else {
|
|
2377
|
+
// Fallback
|
|
2378
|
+
ctx.fillStyle = '#0d1117';
|
|
2379
|
+
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
2175
2380
|
}
|
|
2176
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
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
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
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
|
-
//
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
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
|
-
//
|
|
2362
|
-
drawMusicVisualization(ctx,
|
|
2363
|
-
//
|
|
2364
|
-
drawCharacterEffects(ctx,
|
|
2365
|
-
//
|
|
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,
|
|
2376
|
-
|
|
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
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
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(
|
|
2486
|
+
const sssPanels = buildSubsurfacePanels(robotScreenX, robotScreenY, robotScale, moodColorHex);
|
|
2389
2487
|
renderSubsurfaceGlow(ctx, sssPanels);
|
|
2390
2488
|
}
|
|
2391
|
-
//
|
|
2489
|
+
// Hat
|
|
2392
2490
|
if (charState.hat !== 'none') {
|
|
2393
|
-
drawHat(ctx,
|
|
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
|
-
//
|
|
2493
|
+
// ── Buddy companion (follows robot) ──
|
|
2414
2494
|
if (charState.buddy) {
|
|
2415
2495
|
const buddy = charState.buddy;
|
|
2416
|
-
const
|
|
2417
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2462
|
-
drawRandomEvent(ctx, intelligence.randomEvent, animFrame,
|
|
2463
|
-
//
|
|
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
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
const
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
.
|
|
2477
|
-
.
|
|
2478
|
-
|
|
2479
|
-
ctx.
|
|
2480
|
-
|
|
2481
|
-
ctx.fillText(
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
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
|
|
2517
|
-
const
|
|
2518
|
-
const
|
|
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
|
|
2536
|
-
const
|
|
2537
|
-
const
|
|
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.
|
|
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,
|
|
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
|
|
2781
|
+
// ── Collab Overlay ──
|
|
2567
2782
|
if (intelligence.collab.active) {
|
|
2568
|
-
const
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
ctx.
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
//
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
ctx.
|
|
2649
|
-
ctx.
|
|
2650
|
-
|
|
2651
|
-
ctx.
|
|
2652
|
-
ctx.
|
|
2653
|
-
|
|
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
|
-
//
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
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
|
|
2794
|
-
ctx.fillText(segText, (WIDTH -
|
|
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
|
|
2799
|
-
ctx.fillText(progText, (WIDTH -
|
|
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
|
|
2890
|
+
// Restore from screen shake
|
|
2803
2891
|
ctx.restore();
|
|
2804
|
-
//
|
|
2805
|
-
const
|
|
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
|
-
|
|
2809
|
-
ctx.
|
|
2810
|
-
ctx.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
2945
|
-
? Promise.resolve(
|
|
2946
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
3011
|
-
if (charState.
|
|
3147
|
+
// Don't interrupt dreaming
|
|
3148
|
+
if (charState.mood === 'dreaming')
|
|
3012
3149
|
return;
|
|
3013
3150
|
const line = getProactiveLine();
|
|
3014
3151
|
if (line) {
|
|
3015
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3477
|
-
if (args.speech)
|
|
3478
|
-
|
|
3479
|
-
|
|
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({
|