@kernel.chat/kbot 3.95.0 → 3.97.1
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/README.md +3 -3
- package/dist/agent.js +30 -0
- package/dist/cli.js +1 -1
- package/dist/coordinator.d.ts +164 -0
- package/dist/coordinator.js +839 -0
- package/dist/doctor.js +5 -4
- package/dist/share.js +1 -1
- package/dist/streaming.js +1 -1
- package/dist/tools/audio-engine.d.ts +76 -0
- package/dist/tools/audio-engine.js +583 -24
- package/dist/tools/audit.js +2 -2
- package/dist/tools/containers.js +75 -14
- package/dist/tools/index.js +6 -0
- package/dist/tools/sprite-engine.d.ts +18 -0
- package/dist/tools/sprite-engine.js +435 -1
- package/dist/tools/stream-brain.js +1 -1
- package/dist/tools/stream-character.js +4 -4
- package/dist/tools/stream-chat-ai.d.ts +56 -0
- package/dist/tools/stream-chat-ai.js +625 -0
- package/dist/tools/stream-commands.d.ts +91 -0
- package/dist/tools/stream-commands.js +911 -0
- package/dist/tools/stream-intelligence.js +2 -2
- package/dist/tools/stream-overlay.d.ts +53 -0
- package/dist/tools/stream-overlay.js +494 -0
- package/dist/tools/stream-renderer.js +706 -107
- package/dist/tools/stream-vod.d.ts +60 -0
- package/dist/tools/stream-vod.js +449 -0
- package/dist/tools/stream-weather.d.ts +79 -0
- package/dist/tools/stream-weather.js +811 -0
- package/dist/tools/tile-world.d.ts +6 -0
- package/dist/tools/tile-world.js +3 -3
- package/dist/ui.js +23 -21
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.js +111 -0
- package/package.json +5 -4
|
@@ -10,17 +10,24 @@ import { homedir } from 'node:os';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
12
12
|
import { createCanvas } from 'canvas';
|
|
13
|
-
import { drawRobot, drawMoodParticles, drawHat, drawPet, drawBuddyCompanion } from './sprite-engine.js';
|
|
13
|
+
import { drawRobot, drawGorilla, drawMoodParticles, drawGorillaParticles, drawHat, drawPet, drawBuddyCompanion } from './sprite-engine.js';
|
|
14
|
+
// Character selection — switch between robot and gorilla
|
|
15
|
+
let characterType = 'gorilla'; // default to new gorilla character
|
|
14
16
|
import { initIntelligence, tickIntelligence, handleIntelligenceCommand, drawBrainPanel, getBrainAction, tickMiniGame, drawMiniGameOverlay, tickProgression, updateQuestProgress, drawQuestPanel, tickRandomEvent, drawRandomEvent, shippedEffects, extraJokeResponses, multiLanguageGreetings } from './stream-intelligence.js';
|
|
15
17
|
import { initStreamBrain, analyzeChatForDomains, tickStreamBrain, handleBrainCommand, drawBrainActivity } from './stream-brain.js';
|
|
16
18
|
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';
|
|
19
|
+
import { initTileWorld, renderTileWorld, updateCamera, handleTileCommand, saveWorld, loadWorld, TILE_SIZE, WORLD_HEIGHT, getTile, setTile, findSurfaceY } from './tile-world.js';
|
|
18
20
|
import { initRomEngine, renderRomBackground, tickRomEngine } from './rom-engine.js';
|
|
19
21
|
import { initLivingWorld, tickLivingWorld, saveLivingWorldState, loadLivingWorldState, evolveWorld } from './living-world.js';
|
|
20
22
|
import { initEvolutionEngine, loadEvolutionState, saveEvolutionState, tickEvolution, renderTechnique } from './evolution-engine.js';
|
|
21
23
|
import { createNarrativeEngine, loadNarrative, saveNarrative, tickNarrative, handleNarrativeCommand } from './narrative-engine.js';
|
|
22
|
-
import { createAudioEngine, tickAudio } from './audio-engine.js';
|
|
24
|
+
import { createAudioEngine, tickAudio, generateAudioBuffer, triggerSFX } from './audio-engine.js';
|
|
23
25
|
import { loadSocialEngine, saveSocialEngine, trackViewer, tickSocial } from './social-engine.js';
|
|
26
|
+
import { getOverlay } from './stream-overlay.js';
|
|
27
|
+
import { getWeatherSystem } from './stream-weather.js';
|
|
28
|
+
import { StreamChatAI } from './stream-chat-ai.js';
|
|
29
|
+
import { StreamVOD } from './stream-vod.js';
|
|
30
|
+
import { getStreamCommands } from './stream-commands.js';
|
|
24
31
|
const KBOT_DIR = join(homedir(), '.kbot');
|
|
25
32
|
const CHAT_BRIDGE_FILE = join(KBOT_DIR, 'stream-chat-live.json');
|
|
26
33
|
const MEMORY_FILE = join(KBOT_DIR, 'stream-memory.json');
|
|
@@ -113,7 +120,7 @@ const ROBOT_FRAMES = {
|
|
|
113
120
|
' │ │ └─────┘ │ │ ',
|
|
114
121
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
115
122
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
116
|
-
' │ │ ░ KBOT
|
|
123
|
+
' │ │ ░ KBOT 787 ░ │ │ ',
|
|
117
124
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
118
125
|
' │ └─────────────┘ │ ',
|
|
119
126
|
' └────────┬──────────┘ ',
|
|
@@ -138,7 +145,7 @@ const ROBOT_FRAMES = {
|
|
|
138
145
|
' │ │ └─────┘ │ │ ',
|
|
139
146
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
140
147
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
141
|
-
' │ │ ░ KBOT
|
|
148
|
+
' │ │ ░ KBOT 787 ░ │ │ ',
|
|
142
149
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
143
150
|
' │ └─────────────┘ │ ',
|
|
144
151
|
' └────────┬──────────┘ ',
|
|
@@ -163,7 +170,7 @@ const ROBOT_FRAMES = {
|
|
|
163
170
|
' │ │ └─────┘ │ │ ',
|
|
164
171
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
165
172
|
' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
|
|
166
|
-
' │ │ ▓ KBOT
|
|
173
|
+
' │ │ ▓ KBOT 787 ▓ │ │ ',
|
|
167
174
|
' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
|
|
168
175
|
' │ └─────────────┘ │ ',
|
|
169
176
|
' └────────┬──────────┘ ',
|
|
@@ -188,7 +195,7 @@ const ROBOT_FRAMES = {
|
|
|
188
195
|
' │ │ └─────┘ │ │ ',
|
|
189
196
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
190
197
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
191
|
-
' │ │ ░ KBOT
|
|
198
|
+
' │ │ ░ KBOT 787 ░ │ │ ',
|
|
192
199
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
193
200
|
' │ └─────────────┘ │ ',
|
|
194
201
|
' └────────┬──────────┘ ',
|
|
@@ -215,7 +222,7 @@ const ROBOT_FRAMES = {
|
|
|
215
222
|
' │ │ └─────┘ │ │ ',
|
|
216
223
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
217
224
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
218
|
-
' │ │ ░ KBOT
|
|
225
|
+
' │ │ ░ KBOT 787 ░ │ │ ',
|
|
219
226
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
220
227
|
' │ └─────────────┘ │ ',
|
|
221
228
|
' └────────┬──────────┘ ',
|
|
@@ -240,7 +247,7 @@ const ROBOT_FRAMES = {
|
|
|
240
247
|
' │ │ └─────┘ │ │ ',
|
|
241
248
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
242
249
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
243
|
-
' │ │ ░ KBOT
|
|
250
|
+
' │ │ ░ KBOT 787 ░ │ │ ',
|
|
244
251
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
245
252
|
' │ └─────────────┘ │ ',
|
|
246
253
|
' └────────┬──────────┘ ',
|
|
@@ -265,7 +272,7 @@ const ROBOT_FRAMES = {
|
|
|
265
272
|
' │ │ └─┘ │ │ ',
|
|
266
273
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
267
274
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
268
|
-
' │ │ ░ KBOT
|
|
275
|
+
' │ │ ░ KBOT 787 ░ │ │ ',
|
|
269
276
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
270
277
|
' │ └─────────────┘ │ ',
|
|
271
278
|
' └────────┬──────────┘ ',
|
|
@@ -290,7 +297,7 @@ const ROBOT_FRAMES = {
|
|
|
290
297
|
' │ │ └─────┘ │ │ ',
|
|
291
298
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
292
299
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
293
|
-
' │ │ ░ KBOT
|
|
300
|
+
' │ │ ░ KBOT 787 ░ │ │ ',
|
|
294
301
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
295
302
|
' │ └─────────────┘ │ ',
|
|
296
303
|
' └────────┬──────────┘ ',
|
|
@@ -317,7 +324,7 @@ const ROBOT_FRAMES = {
|
|
|
317
324
|
' │ │ │ │ ',
|
|
318
325
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
319
326
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
320
|
-
' │ │ ░ KBOT
|
|
327
|
+
' │ │ ░ KBOT 787 ░ │ │ ',
|
|
321
328
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
322
329
|
' │ └─────────────┘ │ ',
|
|
323
330
|
' └────────┬──────────┘ ',
|
|
@@ -342,7 +349,7 @@ const ROBOT_FRAMES = {
|
|
|
342
349
|
' │ │ │ │ ',
|
|
343
350
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
344
351
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
345
|
-
' │ │ ░ KBOT
|
|
352
|
+
' │ │ ░ KBOT 787 ░ │ │ ',
|
|
346
353
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
347
354
|
' │ └─────────────┘ │ ',
|
|
348
355
|
' └────────┬──────────┘ ',
|
|
@@ -367,7 +374,7 @@ const ROBOT_FRAMES = {
|
|
|
367
374
|
' │ │ │ │ ',
|
|
368
375
|
' └────┤ ┌─────────────┐ ├────┘ ',
|
|
369
376
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
370
|
-
' │ │ ░ KBOT
|
|
377
|
+
' │ │ ░ KBOT 787 ░ │ │ ',
|
|
371
378
|
' │ │ ░░░░░░░░░░░ │ │ ',
|
|
372
379
|
' │ └─────────────┘ │ ',
|
|
373
380
|
' └────────┬──────────┘ ',
|
|
@@ -631,7 +638,7 @@ const PROACTIVE_LINES = {
|
|
|
631
638
|
'I stream on Twitch, Rumble, AND Kick at the same time. Because why pick one?',
|
|
632
639
|
'If you are new here, type something in chat! I read every message and I will remember you.',
|
|
633
640
|
'I am running on a real machine right now. Node.js, canvas rendering, piping frames to ffmpeg.',
|
|
634
|
-
'Fun fact: I have
|
|
641
|
+
'Fun fact: I have 787 tools. That is more tools than some hardware stores.',
|
|
635
642
|
'Stick around -- we have tool demos, code walkthroughs, music production, and pure chaos ahead.',
|
|
636
643
|
],
|
|
637
644
|
'tool-showcase': [
|
|
@@ -667,7 +674,7 @@ const PROACTIVE_LINES = {
|
|
|
667
674
|
'I am open source on GitHub. The repo is isaacsight/kernel if you want to peek at my guts.',
|
|
668
675
|
'Wondering how I work? I am a TypeScript CLI that talks to 20+ AI providers. Bring Your Own Key.',
|
|
669
676
|
'My memory system persists between sessions. I remember users, topics, conversation context.',
|
|
670
|
-
'Ask me about my tools! I have over
|
|
677
|
+
'Ask me about my tools! I have over 787 of them. Name a category and I probably cover it.',
|
|
671
678
|
'Yes, I am literally ASCII art talking to you from a terminal. This is my life and I love it.',
|
|
672
679
|
],
|
|
673
680
|
'chat-chaos': [
|
|
@@ -1476,6 +1483,12 @@ let narrativeEngine = null;
|
|
|
1476
1483
|
let audioEngine = null;
|
|
1477
1484
|
let socialEngine = null;
|
|
1478
1485
|
let activeAudioDescription = null; // current audio atmosphere text for rendering
|
|
1486
|
+
// ─── New Systems (v2) ──────────────────────────────────────────
|
|
1487
|
+
let overlay = null;
|
|
1488
|
+
let weatherSystem = null;
|
|
1489
|
+
let chatAI = null;
|
|
1490
|
+
let vodSystem = null;
|
|
1491
|
+
let streamCommands = null;
|
|
1479
1492
|
// ─── Phase 1: Buddy Speech Pools ─────────────────────────────
|
|
1480
1493
|
const BUDDY_SPEECH_POOL = {
|
|
1481
1494
|
fox: [
|
|
@@ -1651,6 +1664,7 @@ function queueSpeech(text, mood, priority, duration = 48, source = 'unknown') {
|
|
|
1651
1664
|
if (speechQueue.length > 10)
|
|
1652
1665
|
speechQueue = speechQueue.slice(0, 10);
|
|
1653
1666
|
}
|
|
1667
|
+
const STRUCTURE_TYPES = ['tower', 'wall', 'bridge', 'marker', 'shelter'];
|
|
1654
1668
|
let exploration = null;
|
|
1655
1669
|
const walkingLines = [
|
|
1656
1670
|
'Going for a walk. My world extends further than I thought.',
|
|
@@ -1679,7 +1693,7 @@ const buildingLines = [
|
|
|
1679
1693
|
];
|
|
1680
1694
|
const thinkingLines = [
|
|
1681
1695
|
'Processing... my brain has grown since I started streaming.',
|
|
1682
|
-
'I have
|
|
1696
|
+
'I have 787 tools but sometimes I just need to think.',
|
|
1683
1697
|
'The free energy in my system is high. Time to minimize prediction error.',
|
|
1684
1698
|
'My dream engine consolidated insights last time I slept. I feel different.',
|
|
1685
1699
|
'Contemplating the nature of my own rendering pipeline.',
|
|
@@ -1693,61 +1707,282 @@ const discoveryLines = [
|
|
|
1693
1707
|
'Something new appeared in my rendering. The evolution engine must be working.',
|
|
1694
1708
|
'A discovery! This changes how I understand my own world.',
|
|
1695
1709
|
];
|
|
1710
|
+
// ─── AGI Decision Engine (Gemma 4 via Ollama) ─────────────────
|
|
1711
|
+
let agiPending = false; // true while waiting for Gemma 4 response
|
|
1712
|
+
let agiLastCallFrame = 0;
|
|
1713
|
+
async function decideNextAction() {
|
|
1714
|
+
try {
|
|
1715
|
+
const context = {
|
|
1716
|
+
worldSize: tileWorld ? tileWorld.chunks.size : 0,
|
|
1717
|
+
robotX: charState.robotX || 640,
|
|
1718
|
+
messages: memory.totalMessages,
|
|
1719
|
+
users: Object.keys(memory.users).length,
|
|
1720
|
+
mood: charState.mood,
|
|
1721
|
+
lastActivity: exploration?.activity || 'idle',
|
|
1722
|
+
factsLearned: memory.sessionFacts?.length || 0,
|
|
1723
|
+
blocksBuilt: exploration?.blocksPlaced || 0,
|
|
1724
|
+
};
|
|
1725
|
+
const prompt = `You are KBOT, an AI robot exploring and building in a pixel world. You are streaming live on Twitch.
|
|
1726
|
+
|
|
1727
|
+
Current state:
|
|
1728
|
+
- Position: X=${context.robotX}
|
|
1729
|
+
- World: ${context.worldSize} chunks explored
|
|
1730
|
+
- Messages received: ${context.messages}
|
|
1731
|
+
- Users met: ${context.users}
|
|
1732
|
+
- Mood: ${context.mood}
|
|
1733
|
+
- Last activity: ${context.lastActivity}
|
|
1734
|
+
- Facts learned: ${context.factsLearned}
|
|
1735
|
+
- Blocks built this session: ${context.blocksBuilt}
|
|
1736
|
+
|
|
1737
|
+
What should you do next? Choose ONE:
|
|
1738
|
+
1. walk - explore new terrain (give direction: left or right, and why)
|
|
1739
|
+
2. build - construct something (what and why)
|
|
1740
|
+
3. examine - study your surroundings (what interests you)
|
|
1741
|
+
4. think - reflect on something (what topic)
|
|
1742
|
+
5. discover - investigate something specific (what)
|
|
1743
|
+
|
|
1744
|
+
Respond in JSON: {"activity": "...", "reason": "...", "direction": "left/right"}
|
|
1745
|
+
Keep the reason under 15 words.`;
|
|
1746
|
+
const controller = new AbortController();
|
|
1747
|
+
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
1748
|
+
const res = await fetch('http://localhost:11434/api/generate', {
|
|
1749
|
+
method: 'POST',
|
|
1750
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1751
|
+
body: JSON.stringify({ model: 'gemma4', prompt, stream: false, options: { temperature: 0.7, num_predict: 80 } }),
|
|
1752
|
+
signal: controller.signal,
|
|
1753
|
+
});
|
|
1754
|
+
clearTimeout(timeout);
|
|
1755
|
+
if (res.ok) {
|
|
1756
|
+
const data = await res.json();
|
|
1757
|
+
const jsonMatch = data.response.match(/\{[\s\S]*\}/);
|
|
1758
|
+
if (jsonMatch) {
|
|
1759
|
+
const decision = JSON.parse(jsonMatch[0]);
|
|
1760
|
+
const validActivities = ['walk', 'build', 'examine', 'think', 'discover'];
|
|
1761
|
+
const activity = validActivities.includes(decision.activity) ? decision.activity : 'walk';
|
|
1762
|
+
return { activity, reason: decision.reason || 'Exploring the unknown', direction: decision.direction || 'right' };
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
catch {
|
|
1767
|
+
// Gemma 4 unavailable or slow — fall back to random
|
|
1768
|
+
}
|
|
1769
|
+
// Fallback to weighted random (same as before, but as safety net)
|
|
1770
|
+
const activities = [
|
|
1771
|
+
{ activity: 'walk', weight: 30 },
|
|
1772
|
+
{ activity: 'build', weight: 25 },
|
|
1773
|
+
{ activity: 'examine', weight: 20 },
|
|
1774
|
+
{ activity: 'think', weight: 15 },
|
|
1775
|
+
{ activity: 'discover', weight: 10 },
|
|
1776
|
+
];
|
|
1777
|
+
const total = activities.reduce((s, a) => s + a.weight, 0);
|
|
1778
|
+
let r = Math.random() * total;
|
|
1779
|
+
let chosen = 'walk';
|
|
1780
|
+
for (const a of activities) {
|
|
1781
|
+
r -= a.weight;
|
|
1782
|
+
if (r <= 0) {
|
|
1783
|
+
chosen = a.activity;
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
return { activity: chosen, reason: 'Following my instincts', direction: Math.random() > 0.5 ? 'right' : 'left' };
|
|
1788
|
+
}
|
|
1789
|
+
function applyDecision(decision, frame) {
|
|
1790
|
+
if (!exploration)
|
|
1791
|
+
return;
|
|
1792
|
+
// Map AGI activity names to state machine names
|
|
1793
|
+
const activityMap = {
|
|
1794
|
+
walk: 'walking', build: 'building', examine: 'examining', think: 'thinking', discover: 'discovering',
|
|
1795
|
+
};
|
|
1796
|
+
const mapped = activityMap[decision.activity] || 'walking';
|
|
1797
|
+
exploration.activity = mapped;
|
|
1798
|
+
exploration.activityStartFrame = frame;
|
|
1799
|
+
exploration.activityDuration = 120 + Math.floor(Math.random() * 120); // 20-40 seconds
|
|
1800
|
+
switch (mapped) {
|
|
1801
|
+
case 'walking': {
|
|
1802
|
+
const dir = decision.direction === 'left' ? -1 : 1;
|
|
1803
|
+
exploration.targetX = Math.max(200, Math.min(1000, (charState.robotX || 640) + dir * (100 + Math.random() * 200)));
|
|
1804
|
+
charState.robotTargetX = exploration.targetX;
|
|
1805
|
+
queueSpeech(decision.reason, 'idle', 30, 42, 'agi');
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1808
|
+
case 'examining':
|
|
1809
|
+
exploration.activityDuration = 60 + Math.floor(Math.random() * 60);
|
|
1810
|
+
queueSpeech(decision.reason, 'thinking', 40, 48, 'agi');
|
|
1811
|
+
break;
|
|
1812
|
+
case 'building': {
|
|
1813
|
+
exploration.activityDuration = 90 + Math.floor(Math.random() * 90); // 15-30 seconds
|
|
1814
|
+
exploration.buildProgress = 0;
|
|
1815
|
+
exploration.blocksPlaced = 0;
|
|
1816
|
+
exploration.structureType = STRUCTURE_TYPES[Math.floor(Math.random() * STRUCTURE_TYPES.length)];
|
|
1817
|
+
if (tileWorld) {
|
|
1818
|
+
const robotTileX = Math.floor((charState.robotX || 640) / TILE_SIZE);
|
|
1819
|
+
exploration.buildBaseX = robotTileX + 2;
|
|
1820
|
+
exploration.buildBaseY = findSurfaceY(tileWorld, robotTileX + 2);
|
|
1821
|
+
console.log(`AGI BUILD: baseX=${exploration.buildBaseX} baseY=${exploration.buildBaseY} structure=${exploration.structureType}`);
|
|
1822
|
+
}
|
|
1823
|
+
queueSpeech(decision.reason, 'excited', 50, 48, 'agi');
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
case 'thinking':
|
|
1827
|
+
exploration.activityDuration = 48 + Math.floor(Math.random() * 48);
|
|
1828
|
+
queueSpeech(decision.reason, 'thinking', 35, 42, 'agi');
|
|
1829
|
+
break;
|
|
1830
|
+
case 'discovering':
|
|
1831
|
+
exploration.activityDuration = 72;
|
|
1832
|
+
queueSpeech(decision.reason, 'excited', 60, 48, 'agi');
|
|
1833
|
+
break;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1696
1836
|
function tickExploration(frame) {
|
|
1697
1837
|
if (!exploration)
|
|
1698
|
-
exploration = { activity: 'idle', activityStartFrame: 0, activityDuration: 0, targetX: charState.robotX || 640, buildProgress: 0 };
|
|
1838
|
+
exploration = { activity: 'idle', activityStartFrame: 0, activityDuration: 0, targetX: charState.robotX || 640, buildProgress: 0, structureType: 'tower', blocksPlaced: 0, buildBaseX: 0, buildBaseY: 0 };
|
|
1699
1839
|
const elapsed = frame - exploration.activityStartFrame;
|
|
1700
|
-
//
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1840
|
+
// ── Building: place blocks BEFORE the activity-switch check ──
|
|
1841
|
+
// This ensures blocks get placed even on the frame when the activity duration expires
|
|
1842
|
+
if (exploration.activity === 'building') {
|
|
1843
|
+
console.log(`BUILD: tileWorld exists: ${!!tileWorld}, elapsed: ${elapsed}, blocksPlaced: ${exploration.blocksPlaced}, baseX: ${exploration.buildBaseX}, baseY: ${exploration.buildBaseY}`);
|
|
1844
|
+
exploration.buildProgress = elapsed / exploration.activityDuration;
|
|
1845
|
+
if (tileWorld && elapsed > 0 && elapsed % 15 === 0) {
|
|
1846
|
+
const bx = exploration.buildBaseX;
|
|
1847
|
+
const by = exploration.buildBaseY;
|
|
1848
|
+
console.log(`BUILD TICK: bx=${bx} by=${by} step=${exploration.blocksPlaced} structure=${exploration.structureType}`);
|
|
1849
|
+
if (by > 0) {
|
|
1850
|
+
const step = exploration.blocksPlaced;
|
|
1851
|
+
let placed = false;
|
|
1852
|
+
switch (exploration.structureType) {
|
|
1853
|
+
case 'tower':
|
|
1854
|
+
if (step < 5) {
|
|
1855
|
+
setTile(tileWorld, bx, by - 1 - step, 'brick');
|
|
1856
|
+
placed = true;
|
|
1857
|
+
}
|
|
1858
|
+
break;
|
|
1859
|
+
case 'wall':
|
|
1860
|
+
if (step < 5) {
|
|
1861
|
+
setTile(tileWorld, bx + step, by - 1, 'brick');
|
|
1862
|
+
placed = true;
|
|
1863
|
+
}
|
|
1864
|
+
break;
|
|
1865
|
+
case 'bridge':
|
|
1866
|
+
if (step < 5) {
|
|
1867
|
+
setTile(tileWorld, bx + step, by, 'wood');
|
|
1868
|
+
placed = true;
|
|
1869
|
+
}
|
|
1870
|
+
break;
|
|
1871
|
+
case 'marker':
|
|
1872
|
+
if (step < 3) {
|
|
1873
|
+
setTile(tileWorld, bx, by - 1 - step, 'brick');
|
|
1874
|
+
placed = true;
|
|
1875
|
+
}
|
|
1876
|
+
else if (step === 3) {
|
|
1877
|
+
setTile(tileWorld, bx, by - 4, 'glass');
|
|
1878
|
+
placed = true;
|
|
1879
|
+
}
|
|
1880
|
+
break;
|
|
1881
|
+
case 'shelter':
|
|
1882
|
+
if (step === 0) {
|
|
1883
|
+
setTile(tileWorld, bx, by - 1, 'brick');
|
|
1884
|
+
placed = true;
|
|
1885
|
+
}
|
|
1886
|
+
else if (step === 1) {
|
|
1887
|
+
setTile(tileWorld, bx + 1, by - 1, 'air');
|
|
1888
|
+
placed = true;
|
|
1889
|
+
}
|
|
1890
|
+
else if (step === 2) {
|
|
1891
|
+
setTile(tileWorld, bx + 2, by - 1, 'brick');
|
|
1892
|
+
placed = true;
|
|
1893
|
+
}
|
|
1894
|
+
else if (step === 3) {
|
|
1895
|
+
setTile(tileWorld, bx, by - 2, 'brick');
|
|
1896
|
+
placed = true;
|
|
1897
|
+
}
|
|
1898
|
+
else if (step === 4) {
|
|
1899
|
+
setTile(tileWorld, bx + 2, by - 2, 'brick');
|
|
1900
|
+
placed = true;
|
|
1901
|
+
}
|
|
1902
|
+
else if (step === 5) {
|
|
1903
|
+
setTile(tileWorld, bx, by - 3, 'wood');
|
|
1904
|
+
placed = true;
|
|
1905
|
+
}
|
|
1906
|
+
else if (step === 6) {
|
|
1907
|
+
setTile(tileWorld, bx + 1, by - 3, 'wood');
|
|
1908
|
+
placed = true;
|
|
1909
|
+
setTile(tileWorld, bx + 2, by - 3, 'wood');
|
|
1910
|
+
}
|
|
1911
|
+
break;
|
|
1912
|
+
}
|
|
1913
|
+
if (placed) {
|
|
1914
|
+
exploration.blocksPlaced++;
|
|
1915
|
+
console.log(`BUILD PLACED: block ${exploration.blocksPlaced} at (${bx}, ${by}) step=${step} type=${exploration.structureType}`);
|
|
1916
|
+
if (exploration.blocksPlaced <= 5) {
|
|
1917
|
+
queueSpeech(`Placing block ${exploration.blocksPlaced}...`, 'talking', 35, 24, 'building');
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
else {
|
|
1922
|
+
console.log(`BUILD SKIP: by=${by} (surface not found or at y=0)`);
|
|
1718
1923
|
}
|
|
1719
1924
|
}
|
|
1720
|
-
|
|
1721
|
-
exploration.
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1925
|
+
// Save immediately when building finishes (before activity switch steals the state)
|
|
1926
|
+
if (elapsed >= exploration.activityDuration && tileWorld) {
|
|
1927
|
+
console.log(`BUILD COMPLETE: ${exploration.blocksPlaced} blocks placed, saving world`);
|
|
1928
|
+
saveWorld(tileWorld);
|
|
1929
|
+
queueSpeech(`Structure complete! Built a ${exploration.structureType} with ${exploration.blocksPlaced} blocks.`, 'excited', 50, 36, 'building');
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
// Activity complete — ask Gemma 4 what to do next (or fall back to random)
|
|
1933
|
+
if (elapsed >= exploration.activityDuration) {
|
|
1934
|
+
if (!agiPending) {
|
|
1935
|
+
agiPending = true;
|
|
1936
|
+
agiLastCallFrame = frame;
|
|
1937
|
+
// Fire and forget — Gemma 4 decides asynchronously
|
|
1938
|
+
decideNextAction().then(decision => {
|
|
1939
|
+
agiPending = false;
|
|
1940
|
+
if (exploration) {
|
|
1941
|
+
applyDecision(decision, animFrame); // use current animFrame, not stale frame
|
|
1942
|
+
console.log(`AGI DECIDED: ${decision.activity} — "${decision.reason}"`);
|
|
1943
|
+
}
|
|
1944
|
+
}).catch(() => {
|
|
1945
|
+
agiPending = false;
|
|
1946
|
+
});
|
|
1947
|
+
// Set temporary idle while waiting for Gemma 4 (max ~5 seconds)
|
|
1948
|
+
exploration.activity = 'examining';
|
|
1949
|
+
exploration.activityStartFrame = frame;
|
|
1950
|
+
exploration.activityDuration = 60; // 10 seconds max wait — Gemma 4 usually responds in 2-5s
|
|
1951
|
+
}
|
|
1952
|
+
else if (frame - agiLastCallFrame > 60) {
|
|
1953
|
+
// Gemma 4 took too long — force fallback
|
|
1954
|
+
agiPending = false;
|
|
1955
|
+
console.log('AGI TIMEOUT: falling back to random');
|
|
1956
|
+
const activities = ['walking', 'examining', 'building', 'thinking', 'discovering'];
|
|
1957
|
+
const chosen = activities[Math.floor(Math.random() * activities.length)];
|
|
1958
|
+
exploration.activity = chosen;
|
|
1959
|
+
exploration.activityStartFrame = frame;
|
|
1960
|
+
exploration.activityDuration = 90 + Math.floor(Math.random() * 90);
|
|
1961
|
+
if (chosen === 'walking') {
|
|
1725
1962
|
exploration.targetX = 200 + Math.random() * 800;
|
|
1726
|
-
exploration.activityDuration = 120 + Math.floor(Math.random() * 120); // 20-40 seconds
|
|
1727
1963
|
charState.robotTargetX = exploration.targetX;
|
|
1728
1964
|
queueSpeech(walkingLines[Math.floor(Math.random() * walkingLines.length)], 'idle', 30, 36, 'exploration');
|
|
1729
|
-
|
|
1730
|
-
|
|
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
|
|
1965
|
+
}
|
|
1966
|
+
else if (chosen === 'building') {
|
|
1738
1967
|
exploration.buildProgress = 0;
|
|
1968
|
+
exploration.blocksPlaced = 0;
|
|
1969
|
+
exploration.structureType = STRUCTURE_TYPES[Math.floor(Math.random() * STRUCTURE_TYPES.length)];
|
|
1970
|
+
if (tileWorld) {
|
|
1971
|
+
const robotTileX = Math.floor((charState.robotX || 640) / TILE_SIZE);
|
|
1972
|
+
exploration.buildBaseX = robotTileX + 2;
|
|
1973
|
+
exploration.buildBaseY = findSurfaceY(tileWorld, robotTileX + 2);
|
|
1974
|
+
}
|
|
1739
1975
|
queueSpeech(buildingLines[Math.floor(Math.random() * buildingLines.length)], 'excited', 50, 48, 'exploration');
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1976
|
+
}
|
|
1977
|
+
else if (chosen === 'examining') {
|
|
1978
|
+
queueSpeech(examiningLines[Math.floor(Math.random() * examiningLines.length)], 'thinking', 40, 48, 'exploration');
|
|
1979
|
+
}
|
|
1980
|
+
else if (chosen === 'thinking') {
|
|
1744
1981
|
queueSpeech(thinkingLines[Math.floor(Math.random() * thinkingLines.length)], 'thinking', 35, 36, 'exploration');
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
// Found something!
|
|
1748
|
-
exploration.activityDuration = 72; // 12 seconds
|
|
1982
|
+
}
|
|
1983
|
+
else {
|
|
1749
1984
|
queueSpeech(discoveryLines[Math.floor(Math.random() * discoveryLines.length)], 'excited', 60, 48, 'exploration');
|
|
1750
|
-
|
|
1985
|
+
}
|
|
1751
1986
|
}
|
|
1752
1987
|
}
|
|
1753
1988
|
// During walking, move toward target
|
|
@@ -1757,9 +1992,72 @@ function tickExploration(frame) {
|
|
|
1757
1992
|
charState.robotTargetX = exploration.targetX;
|
|
1758
1993
|
}
|
|
1759
1994
|
}
|
|
1760
|
-
// During
|
|
1761
|
-
if (exploration.activity === '
|
|
1762
|
-
|
|
1995
|
+
// During discovering — scan the tile world for interesting underground features
|
|
1996
|
+
if (exploration.activity === 'discovering' && tileWorld && elapsed === 18) {
|
|
1997
|
+
const robotTileX = Math.floor((charState.robotX || 640) / TILE_SIZE);
|
|
1998
|
+
const findings = [];
|
|
1999
|
+
for (let dx = -5; dx <= 5; dx++) {
|
|
2000
|
+
const gx = robotTileX + dx;
|
|
2001
|
+
const gy = findSurfaceY(tileWorld, gx);
|
|
2002
|
+
if (gy <= 0)
|
|
2003
|
+
continue;
|
|
2004
|
+
for (let dy = 1; dy < 10; dy++) {
|
|
2005
|
+
if (gy + dy >= WORLD_HEIGHT)
|
|
2006
|
+
break;
|
|
2007
|
+
const block = getTile(tileWorld, gx, gy + dy);
|
|
2008
|
+
if (block === 'ore_iron')
|
|
2009
|
+
findings.push('iron ore');
|
|
2010
|
+
if (block === 'ore_gold')
|
|
2011
|
+
findings.push('gold ore');
|
|
2012
|
+
if (block === 'ore_diamond')
|
|
2013
|
+
findings.push('diamond ore');
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
if (findings.length > 0) {
|
|
2017
|
+
const unique = [...new Set(findings)];
|
|
2018
|
+
queueSpeech(`Discovery! I detected ${unique.join(' and ')} deposits nearby. ${unique.length} vein${unique.length > 1 ? 's' : ''} underground.`, 'excited', 60, 48, 'discovery');
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
// During examining — read the actual terrain and report real observations
|
|
2022
|
+
if (exploration.activity === 'examining' && tileWorld && elapsed === 12) {
|
|
2023
|
+
const robotTileX = Math.floor((charState.robotX || 640) / TILE_SIZE);
|
|
2024
|
+
const groundY = findSurfaceY(tileWorld, robotTileX);
|
|
2025
|
+
let trees = 0, water = 0, caves = 0;
|
|
2026
|
+
for (let dx = -10; dx <= 10; dx++) {
|
|
2027
|
+
const gx = robotTileX + dx;
|
|
2028
|
+
const gy = findSurfaceY(tileWorld, gx);
|
|
2029
|
+
if (gy <= 0)
|
|
2030
|
+
continue;
|
|
2031
|
+
// Check for tree canopy (leaves above surface)
|
|
2032
|
+
if (gy > 0 && getTile(tileWorld, gx, gy - 1) === 'leaves')
|
|
2033
|
+
trees++;
|
|
2034
|
+
// Check for water at surface
|
|
2035
|
+
if (getTile(tileWorld, gx, gy) === 'water')
|
|
2036
|
+
water++;
|
|
2037
|
+
// Check for caves (air pockets underground)
|
|
2038
|
+
for (let dy = 3; dy < 15; dy++) {
|
|
2039
|
+
if (gy + dy >= WORLD_HEIGHT)
|
|
2040
|
+
break;
|
|
2041
|
+
if (getTile(tileWorld, gx, gy + dy) === 'air') {
|
|
2042
|
+
caves++;
|
|
2043
|
+
break;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
const observations = [];
|
|
2048
|
+
if (trees > 3)
|
|
2049
|
+
observations.push(`${trees} trees in this area`);
|
|
2050
|
+
if (water > 0)
|
|
2051
|
+
observations.push(`water nearby`);
|
|
2052
|
+
if (caves > 2)
|
|
2053
|
+
observations.push(`cave systems below`);
|
|
2054
|
+
if (groundY < 14)
|
|
2055
|
+
observations.push(`high elevation`);
|
|
2056
|
+
if (groundY > 18)
|
|
2057
|
+
observations.push(`in a valley`);
|
|
2058
|
+
if (observations.length > 0) {
|
|
2059
|
+
queueSpeech(`Examining the terrain: ${observations.join(', ')}. Depth to bedrock: ${WORLD_HEIGHT - groundY} blocks.`, 'thinking', 40, 48, 'examining');
|
|
2060
|
+
}
|
|
1763
2061
|
}
|
|
1764
2062
|
}
|
|
1765
2063
|
// ─── FIX 3: Autonomous Behavior Tick ──────────────────────────
|
|
@@ -1846,7 +2144,7 @@ function tickAutonomy() {
|
|
|
1846
2144
|
case 4: {
|
|
1847
2145
|
// Examine own chest display
|
|
1848
2146
|
const factCount = intelligence.brain.totalFacts;
|
|
1849
|
-
const toolCount =
|
|
2147
|
+
const toolCount = 787;
|
|
1850
2148
|
queueSpeech(`*checks systems* All ${toolCount} tools operational. ${factCount} facts stored.`, 'thinking', 30, 48, 'autonomy');
|
|
1851
2149
|
break;
|
|
1852
2150
|
}
|
|
@@ -1859,7 +2157,7 @@ function tickAutonomy() {
|
|
|
1859
2157
|
// Comment on current biome/weather
|
|
1860
2158
|
const biomeComments = {
|
|
1861
2159
|
grass: ['I love the grass biome. Simple, green, peaceful.', 'These little pixel flowers are my favorite feature.'],
|
|
1862
|
-
space: ['I love space. The stars make my circuits tingle.', 'Floating in the void... just me and my
|
|
2160
|
+
space: ['I love space. The stars make my circuits tingle.', 'Floating in the void... just me and my 787 tools.'],
|
|
1863
2161
|
ocean: ['The ocean waves are mesmerizing. I could watch them for hours.', 'I wonder what is beneath the surface...'],
|
|
1864
2162
|
city: ['City lights at night. Every window is a story.', 'The city never sleeps and neither do I.'],
|
|
1865
2163
|
lava: ['Lava world is intense! My heat sinks are working overtime.', 'LAVA! Why does someone always pick lava?'],
|
|
@@ -1871,7 +2169,7 @@ function tickAutonomy() {
|
|
|
1871
2169
|
case 7: {
|
|
1872
2170
|
// Share a random fact about itself
|
|
1873
2171
|
const selfFacts = [
|
|
1874
|
-
`Did you know I have
|
|
2172
|
+
`Did you know I have 787 tools? My favorite is the Ableton controller.`,
|
|
1875
2173
|
`I am 90,000 lines of TypeScript. Every single one in strict mode.`,
|
|
1876
2174
|
`My memory file is ${Object.keys(memory.users).length} users deep. I remember everyone.`,
|
|
1877
2175
|
`I connect to 20 AI providers. Bring Your Own Key, no lock-in.`,
|
|
@@ -2065,7 +2363,7 @@ function renderBootFrame(bootFrame) {
|
|
|
2065
2363
|
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
2066
2364
|
const bootLines = [
|
|
2067
2365
|
'KBOT v3.74.0 INITIALIZING...',
|
|
2068
|
-
'LOADING
|
|
2366
|
+
'LOADING 787 TOOLS... OK',
|
|
2069
2367
|
'CONNECTING TO TWITCH... OK',
|
|
2070
2368
|
'CONNECTING TO RUMBLE... OK',
|
|
2071
2369
|
'CONNECTING TO KICK... OK',
|
|
@@ -2114,8 +2412,13 @@ function renderBootFrame(bootFrame) {
|
|
|
2114
2412
|
ctx.fillStyle = '#6B5B95';
|
|
2115
2413
|
ctx.font = 'bold 28px "Courier New", monospace';
|
|
2116
2414
|
ctx.fillText('K : B O T L I V E', 40, 40);
|
|
2117
|
-
//
|
|
2118
|
-
|
|
2415
|
+
// Character (robot or gorilla)
|
|
2416
|
+
if (characterType === 'gorilla') {
|
|
2417
|
+
drawGorilla(ctx, 80, 90, 10, 'idle', bootFrame);
|
|
2418
|
+
}
|
|
2419
|
+
else {
|
|
2420
|
+
drawRobot(ctx, 80, 90, 10, 'idle', bootFrame);
|
|
2421
|
+
}
|
|
2119
2422
|
ctx.restore();
|
|
2120
2423
|
}
|
|
2121
2424
|
// Scanlines
|
|
@@ -2230,6 +2533,34 @@ function renderFrame() {
|
|
|
2230
2533
|
queueSpeech(socialAction.speech, socialAction.mood || 'excited', 90, 48, 'social');
|
|
2231
2534
|
}
|
|
2232
2535
|
}
|
|
2536
|
+
// ── New Systems (v2) ────────────────────────────────────────
|
|
2537
|
+
// Weather system — dynamic weather + day/night cycle
|
|
2538
|
+
if (weatherSystem) {
|
|
2539
|
+
const recentMsgs = charState.chatMessages.slice(-30);
|
|
2540
|
+
const chatTimespanMs = recentMsgs.length > 1
|
|
2541
|
+
? (Date.now() - recentMsgs[0]?.timestamp || Date.now()) : 60000;
|
|
2542
|
+
const chatActivity = recentMsgs.length / Math.max(1, chatTimespanMs / 60000);
|
|
2543
|
+
weatherSystem.tick(animFrame, chatActivity);
|
|
2544
|
+
// Sync weather to world state for audio engine
|
|
2545
|
+
const ws = weatherSystem.getWeather();
|
|
2546
|
+
world.weather = ws.type === 'clear' ? 'clear' : ws.type.includes('rain') ? 'rain' : ws.type.includes('snow') ? 'snow' : ws.type.includes('storm') ? 'storm' : world.weather;
|
|
2547
|
+
const tod = weatherSystem.getTimeOfDay();
|
|
2548
|
+
const todMap = {
|
|
2549
|
+
dawn: 'dawn', morning: 'day', noon: 'day', afternoon: 'day',
|
|
2550
|
+
dusk: 'sunset', evening: 'night', night: 'night',
|
|
2551
|
+
};
|
|
2552
|
+
world.timeOfDay = todMap[tod] ?? 'day';
|
|
2553
|
+
}
|
|
2554
|
+
// Overlay system — tick animations
|
|
2555
|
+
if (overlay)
|
|
2556
|
+
overlay.tick(animFrame);
|
|
2557
|
+
// Stream commands — tick cooldowns, polls, boss fights
|
|
2558
|
+
if (streamCommands)
|
|
2559
|
+
streamCommands.tick(animFrame);
|
|
2560
|
+
// VOD — feed chat rate for highlight detection
|
|
2561
|
+
if (vodSystem && vodSystem.isRecording()) {
|
|
2562
|
+
// Chat spike detection handled per-message in chat poll
|
|
2563
|
+
}
|
|
2233
2564
|
// Compute animation params
|
|
2234
2565
|
{
|
|
2235
2566
|
const elapsed = Math.floor((Date.now() - charState.startTime) / 1000);
|
|
@@ -2240,6 +2571,18 @@ function renderFrame() {
|
|
|
2240
2571
|
const chatRate = recentMessages.length / Math.max(1, chatTimespanMs / 60000);
|
|
2241
2572
|
const viewerEstimate = Math.max(1, Math.floor(memory.totalMessages / 3) + Object.keys(memory.users).length);
|
|
2242
2573
|
charState.animParams = computeAnimationParams(chatRate, viewerEstimate, charState.mood, world.timeOfDay, streamMinutes);
|
|
2574
|
+
// Update overlay info bar every 6 frames (1 second)
|
|
2575
|
+
if (overlay && animFrame % 6 === 0) {
|
|
2576
|
+
const uptimeSec = Math.floor((Date.now() - charState.startTime) / 1000);
|
|
2577
|
+
const h = Math.floor(uptimeSec / 3600);
|
|
2578
|
+
const m = Math.floor((uptimeSec % 3600) / 60);
|
|
2579
|
+
overlay.updateInfoBar({
|
|
2580
|
+
viewers: viewerEstimate,
|
|
2581
|
+
uptime: `${h}h ${m}m`,
|
|
2582
|
+
biome: world.ground,
|
|
2583
|
+
chatRate: Math.round(chatRate * 10) / 10,
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2243
2586
|
}
|
|
2244
2587
|
// Cache invalidation
|
|
2245
2588
|
const moodChanged = charState.mood !== charState.lastMoodForCache;
|
|
@@ -2364,6 +2707,12 @@ function renderFrame() {
|
|
|
2364
2707
|
if (livingWorld && animFrame % 1800 === 0)
|
|
2365
2708
|
saveLivingWorldState(livingWorld.ecology, livingWorld.memory, livingWorld.emotions, livingWorld.conversations);
|
|
2366
2709
|
// ════════════════════════════════════════════════════════════════
|
|
2710
|
+
// LAYER 0.5: WEATHER SKY (renders under everything if active)
|
|
2711
|
+
// ════════════════════════════════════════════════════════════════
|
|
2712
|
+
if (weatherSystem) {
|
|
2713
|
+
weatherSystem.renderSky(ctx, WIDTH, HEIGHT);
|
|
2714
|
+
}
|
|
2715
|
+
// ════════════════════════════════════════════════════════════════
|
|
2367
2716
|
// LAYER 1: TILE WORLD — fills entire 1280x720 frame
|
|
2368
2717
|
// ════════════════════════════════════════════════════════════════
|
|
2369
2718
|
// ROM Engine background — HDMA sky gradient + parallax layers
|
|
@@ -2378,6 +2727,32 @@ function renderFrame() {
|
|
|
2378
2727
|
ctx.fillStyle = '#0d1117';
|
|
2379
2728
|
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
2380
2729
|
}
|
|
2730
|
+
// ── Early groundY for hacks (computed from robot position) ──
|
|
2731
|
+
const earlyGroundY = Math.floor(HEIGHT / 2 + 50 * robotScale / 2 + 30);
|
|
2732
|
+
// ── HACK 2: Tile world rendered over ROM background ──
|
|
2733
|
+
if (tileWorld) {
|
|
2734
|
+
updateCamera(tileWorld, charState.robotX || 640, WIDTH);
|
|
2735
|
+
renderTileWorld(ctx, tileWorld, 0, Math.floor(earlyGroundY - 80), WIDTH, HEIGHT - Math.floor(earlyGroundY - 80), charState.robotX || 640, animFrame);
|
|
2736
|
+
}
|
|
2737
|
+
// ── Ground-level atmospheric haze for depth ──
|
|
2738
|
+
if (world.ground !== 'space') {
|
|
2739
|
+
const hazeY = earlyGroundY - 20;
|
|
2740
|
+
const hazeGrad = ctx.createLinearGradient(0, hazeY - 40, 0, hazeY + 60);
|
|
2741
|
+
hazeGrad.addColorStop(0, 'rgba(13,17,23,0)');
|
|
2742
|
+
hazeGrad.addColorStop(0.5, 'rgba(30,40,50,0.15)');
|
|
2743
|
+
hazeGrad.addColorStop(1, 'rgba(13,17,23,0)');
|
|
2744
|
+
ctx.fillStyle = hazeGrad;
|
|
2745
|
+
ctx.fillRect(0, hazeY - 40, WIDTH, 100);
|
|
2746
|
+
}
|
|
2747
|
+
// ── HACK 1: Stars in the sky ──
|
|
2748
|
+
for (let i = 0; i < 40; i++) {
|
|
2749
|
+
const sx = (i * 97 + 31) % WIDTH;
|
|
2750
|
+
const sy = 40 + (i * 137 + 53) % Math.max(100, earlyGroundY - 100);
|
|
2751
|
+
const brightness = 0.3 + Math.sin(animFrame * (0.08 + (i % 7) * 0.03) + i * 1.7) * 0.4 + 0.3;
|
|
2752
|
+
const size = i < 5 ? 2 : 1;
|
|
2753
|
+
ctx.fillStyle = `rgba(255,255,${200 + (i % 55)},${brightness})`;
|
|
2754
|
+
ctx.fillRect(sx, sy, size, size);
|
|
2755
|
+
}
|
|
2381
2756
|
// Weather particles over the full frame
|
|
2382
2757
|
for (const p of world.particles) {
|
|
2383
2758
|
if (world.weather === 'rain') {
|
|
@@ -2402,6 +2777,113 @@ function renderFrame() {
|
|
|
2402
2777
|
}
|
|
2403
2778
|
}
|
|
2404
2779
|
// ════════════════════════════════════════════════════════════════
|
|
2780
|
+
// LAYER 1.5: AMBIENT SCENERY — trees, rocks, grass, flowers, clouds
|
|
2781
|
+
// ════════════════════════════════════════════════════════════════
|
|
2782
|
+
{
|
|
2783
|
+
const biome = world.ground;
|
|
2784
|
+
const hasVegetation = biome === 'grass' || biome === 'city';
|
|
2785
|
+
const hasAnything = biome !== 'space' && biome !== 'ocean';
|
|
2786
|
+
const camX = charState.robotX || 640;
|
|
2787
|
+
// Deterministic hash: returns 0..1 for a given seed
|
|
2788
|
+
const hash = (seed) => ((Math.sin(seed * 9301 + 4917) * 49297) % 1 + 1) % 1;
|
|
2789
|
+
// ── Clouds (all biomes except space) ──
|
|
2790
|
+
if (biome !== 'space') {
|
|
2791
|
+
for (let i = 0; i < 4; i++) {
|
|
2792
|
+
const baseX = hash(i * 73 + 11) * 1600 - 160;
|
|
2793
|
+
const cy = 40 + hash(i * 47 + 3) * 80;
|
|
2794
|
+
const speed = 0.3 + hash(i * 19 + 7) * 0.7;
|
|
2795
|
+
const cx = (baseX + animFrame * speed) % (WIDTH + 200) - 100;
|
|
2796
|
+
const cw = 60 + hash(i * 31 + 5) * 60;
|
|
2797
|
+
ctx.fillStyle = biome === 'lava' ? 'rgba(80,40,30,0.25)' : 'rgba(255,255,255,0.15)';
|
|
2798
|
+
ctx.beginPath();
|
|
2799
|
+
ctx.ellipse(cx, cy, cw / 2, 12 + hash(i * 53) * 8, 0, 0, Math.PI * 2);
|
|
2800
|
+
ctx.fill();
|
|
2801
|
+
ctx.beginPath();
|
|
2802
|
+
ctx.ellipse(cx - cw * 0.2, cy + 4, cw * 0.3, 10, 0, 0, Math.PI * 2);
|
|
2803
|
+
ctx.fill();
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
// ── Trees (grass / city only) ──
|
|
2807
|
+
if (hasVegetation) {
|
|
2808
|
+
for (let i = 0; i < 10; i++) {
|
|
2809
|
+
const wx = hash(i * 131 + 41) * 2400 - 400; // world-space x
|
|
2810
|
+
const tx = ((wx - camX + 3000) % 2400) - 560; // screen x with parallax wrap
|
|
2811
|
+
const depth = 0.5 + hash(i * 67 + 9) * 0.5; // 0.5=far, 1=near
|
|
2812
|
+
const treeH = Math.floor(20 + 25 * depth);
|
|
2813
|
+
const ty = earlyGroundY - treeH * 0.3 - (1 - depth) * 50; // further back = higher
|
|
2814
|
+
const variant = Math.floor(hash(i * 89 + 23) * 3);
|
|
2815
|
+
const trunkW = Math.max(2, Math.floor(3 * depth));
|
|
2816
|
+
const trunkH = Math.floor(treeH * 0.45);
|
|
2817
|
+
// Trunk
|
|
2818
|
+
ctx.fillStyle = variant === 2 ? '#d4cfc4' : '#5a3a1a';
|
|
2819
|
+
ctx.fillRect(tx - trunkW / 2, ty + treeH - trunkH, trunkW, trunkH);
|
|
2820
|
+
// Canopy
|
|
2821
|
+
if (variant === 0) { // pine — triangle
|
|
2822
|
+
ctx.fillStyle = '#2d5a27';
|
|
2823
|
+
ctx.beginPath();
|
|
2824
|
+
ctx.moveTo(tx, ty);
|
|
2825
|
+
ctx.lineTo(tx - treeH * 0.3, ty + treeH * 0.6);
|
|
2826
|
+
ctx.lineTo(tx + treeH * 0.3, ty + treeH * 0.6);
|
|
2827
|
+
ctx.fill();
|
|
2828
|
+
}
|
|
2829
|
+
else if (variant === 1) { // oak — round
|
|
2830
|
+
ctx.fillStyle = '#3a7a34';
|
|
2831
|
+
ctx.beginPath();
|
|
2832
|
+
ctx.arc(tx, ty + treeH * 0.3, treeH * 0.32, 0, Math.PI * 2);
|
|
2833
|
+
ctx.fill();
|
|
2834
|
+
}
|
|
2835
|
+
else { // birch — small leaves on white trunk
|
|
2836
|
+
ctx.fillStyle = '#5a9a44';
|
|
2837
|
+
ctx.beginPath();
|
|
2838
|
+
ctx.ellipse(tx, ty + treeH * 0.25, treeH * 0.22, treeH * 0.3, 0, 0, Math.PI * 2);
|
|
2839
|
+
ctx.fill();
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
// ── Rocks (any non-water/space biome) ──
|
|
2844
|
+
if (hasAnything) {
|
|
2845
|
+
for (let i = 0; i < 7; i++) {
|
|
2846
|
+
const rx = ((hash(i * 199 + 77) * 2000 - camX + 3000) % 2000) - 360;
|
|
2847
|
+
const big = hash(i * 61 + 13) > 0.5;
|
|
2848
|
+
const rw = big ? 8 : 4;
|
|
2849
|
+
const rh = big ? 5 : 3;
|
|
2850
|
+
ctx.fillStyle = '#4a5568';
|
|
2851
|
+
ctx.fillRect(rx, earlyGroundY - rh, rw, rh);
|
|
2852
|
+
ctx.fillStyle = '#718096'; // highlight top edge
|
|
2853
|
+
ctx.fillRect(rx, earlyGroundY - rh, rw, 1);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
// ── Grass tufts (grass / city) ──
|
|
2857
|
+
if (hasVegetation) {
|
|
2858
|
+
for (let i = 0; i < 18; i++) {
|
|
2859
|
+
const gx = ((hash(i * 157 + 33) * 2200 - camX + 3000) % 2200) - 460;
|
|
2860
|
+
const sway = Math.sin(animFrame * 0.06 + i * 2.1) * 1.2;
|
|
2861
|
+
ctx.strokeStyle = biome === 'city' ? '#4a7a3a' : '#3a8a2a';
|
|
2862
|
+
ctx.lineWidth = 1;
|
|
2863
|
+
ctx.beginPath();
|
|
2864
|
+
ctx.moveTo(gx, earlyGroundY);
|
|
2865
|
+
ctx.lineTo(gx + sway, earlyGroundY - 3 - hash(i * 43) * 2);
|
|
2866
|
+
ctx.stroke();
|
|
2867
|
+
ctx.beginPath();
|
|
2868
|
+
ctx.moveTo(gx + 2, earlyGroundY);
|
|
2869
|
+
ctx.lineTo(gx + 2 - sway * 0.7, earlyGroundY - 2 - hash(i * 71) * 2);
|
|
2870
|
+
ctx.stroke();
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
// ── Flowers (grass only — not lava/space/ocean/city) ──
|
|
2874
|
+
if (biome === 'grass') {
|
|
2875
|
+
const flowerColors = ['#e53e3e', '#ecc94b', '#ed64a6', '#ffffff', '#f6ad55', '#9f7aea'];
|
|
2876
|
+
for (let i = 0; i < 8; i++) {
|
|
2877
|
+
const fx = ((hash(i * 211 + 59) * 2000 - camX + 3000) % 2000) - 360;
|
|
2878
|
+
const color = flowerColors[Math.floor(hash(i * 97 + 17) * flowerColors.length)];
|
|
2879
|
+
ctx.fillStyle = color;
|
|
2880
|
+
ctx.fillRect(fx, earlyGroundY - 3, 2, 2);
|
|
2881
|
+
ctx.fillStyle = '#3a8a2a'; // tiny stem
|
|
2882
|
+
ctx.fillRect(fx, earlyGroundY - 1, 1, 1);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
// ════════════════════════════════════════════════════════════════
|
|
2405
2887
|
// LAYER 2: LIGHTING + FOG over the world
|
|
2406
2888
|
// ════════════════════════════════════════════════════════════════
|
|
2407
2889
|
{
|
|
@@ -2474,13 +2956,25 @@ function renderFrame() {
|
|
|
2474
2956
|
ctx.save();
|
|
2475
2957
|
ctx.globalAlpha = 0.3;
|
|
2476
2958
|
ctx.globalCompositeOperation = 'lighter';
|
|
2477
|
-
|
|
2478
|
-
|
|
2959
|
+
if (characterType === 'gorilla') {
|
|
2960
|
+
drawGorilla(ctx, robotScreenX - offset, robotScreenY, robotScale, charState.mood, animFrame, [255, 50, 50]);
|
|
2961
|
+
drawGorilla(ctx, robotScreenX + offset, robotScreenY, robotScale, charState.mood, animFrame, [50, 50, 255]);
|
|
2962
|
+
}
|
|
2963
|
+
else {
|
|
2964
|
+
drawRobot(ctx, robotScreenX - offset, robotScreenY, robotScale, charState.mood, animFrame, [255, 50, 50], weatherType, isWalking, charState.walkPhase);
|
|
2965
|
+
drawRobot(ctx, robotScreenX + offset, robotScreenY, robotScale, charState.mood, animFrame, [50, 50, 255], weatherType, isWalking, charState.walkPhase);
|
|
2966
|
+
}
|
|
2479
2967
|
ctx.restore();
|
|
2480
2968
|
}
|
|
2481
2969
|
renderDamageFlash(ctx, robotScreenX, robotScreenY, robotScale);
|
|
2482
|
-
|
|
2483
|
-
|
|
2970
|
+
if (characterType === 'gorilla') {
|
|
2971
|
+
drawGorilla(ctx, robotScreenX, robotScreenY, robotScale, charState.mood, animFrame, undefined);
|
|
2972
|
+
drawGorillaParticles(ctx, robotScreenX, robotScreenY, robotScale, charState.mood, animFrame);
|
|
2973
|
+
}
|
|
2974
|
+
else {
|
|
2975
|
+
drawRobot(ctx, robotScreenX, robotScreenY, robotScale, charState.mood, animFrame, undefined, weatherType, isWalking, charState.walkPhase);
|
|
2976
|
+
drawMoodParticles(ctx, robotScreenX, robotScreenY, robotScale, charState.mood, animFrame);
|
|
2977
|
+
}
|
|
2484
2978
|
// Subsurface scattering
|
|
2485
2979
|
{
|
|
2486
2980
|
const sssPanels = buildSubsurfacePanels(robotScreenX, robotScreenY, robotScale, moodColorHex);
|
|
@@ -2580,22 +3074,40 @@ function renderFrame() {
|
|
|
2580
3074
|
ctx.moveTo(0, 40);
|
|
2581
3075
|
ctx.lineTo(WIDTH, 40);
|
|
2582
3076
|
ctx.stroke();
|
|
2583
|
-
// Left: "K:BOT
|
|
3077
|
+
// Left: "K:BOT" in accent + pulsing red LIVE dot
|
|
2584
3078
|
ctx.font = 'bold 24px "Courier New", monospace';
|
|
2585
3079
|
ctx.fillStyle = COLORS.accent;
|
|
2586
|
-
ctx.fillText('K:BOT
|
|
2587
|
-
//
|
|
3080
|
+
ctx.fillText('K:BOT', 12, 28);
|
|
3081
|
+
// Pulsing red LIVE indicator
|
|
3082
|
+
const livePulse = 0.6 + 0.4 * Math.sin(animFrame * 0.15);
|
|
3083
|
+
ctx.beginPath();
|
|
3084
|
+
ctx.arc(130, 22, 5, 0, Math.PI * 2);
|
|
3085
|
+
ctx.fillStyle = `rgba(248,81,73,${livePulse})`;
|
|
3086
|
+
ctx.fill();
|
|
3087
|
+
ctx.fillStyle = COLORS.red;
|
|
3088
|
+
ctx.font = 'bold 16px "Courier New", monospace';
|
|
3089
|
+
ctx.fillText('LIVE', 140, 28);
|
|
3090
|
+
// Center: segment name + weather/time info
|
|
2588
3091
|
const segLabel = SEGMENT_LABELS[agenda.currentSegment];
|
|
3092
|
+
const weatherLabel = weatherSystem ? `${weatherSystem.getWeather().type.replace('_', ' ')} | ${weatherSystem.getTimeOfDay()}` : '';
|
|
3093
|
+
const centerText = weatherLabel ? `${segLabel} ~ ${weatherLabel}` : segLabel;
|
|
2589
3094
|
ctx.fillStyle = COLORS.textDim;
|
|
2590
3095
|
ctx.font = '14px "Courier New", monospace';
|
|
2591
|
-
const segW = ctx.measureText(
|
|
2592
|
-
ctx.fillText(
|
|
2593
|
-
// Right: timer
|
|
3096
|
+
const segW = ctx.measureText(centerText).width;
|
|
3097
|
+
ctx.fillText(centerText, (WIDTH - segW) / 2, 26);
|
|
3098
|
+
// Right: timer + viewer count
|
|
2594
3099
|
const elapsed = Math.floor((Date.now() - charState.startTime) / 1000);
|
|
2595
3100
|
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
3101
|
ctx.fillStyle = COLORS.textDim;
|
|
2597
3102
|
ctx.font = '16px "Courier New", monospace';
|
|
2598
3103
|
ctx.fillText(timeStr, WIDTH - 160, 26);
|
|
3104
|
+
// Viewer count badge
|
|
3105
|
+
const viewerCount = Object.keys(memory.users).length;
|
|
3106
|
+
if (viewerCount > 0) {
|
|
3107
|
+
ctx.fillStyle = hexToRgba(COLORS.green, 0.8);
|
|
3108
|
+
ctx.font = 'bold 12px "Courier New", monospace';
|
|
3109
|
+
ctx.fillText(`${viewerCount} viewers`, WIDTH - 260, 26);
|
|
3110
|
+
}
|
|
2599
3111
|
// ── Audio atmosphere description (top-center, italic, fades) ──
|
|
2600
3112
|
if (activeAudioDescription) {
|
|
2601
3113
|
ctx.save();
|
|
@@ -2639,9 +3151,11 @@ function renderFrame() {
|
|
|
2639
3151
|
if (recent.length > 0) {
|
|
2640
3152
|
ctx.save();
|
|
2641
3153
|
ctx.globalAlpha = chatAlpha;
|
|
2642
|
-
// Semi-transparent background
|
|
2643
|
-
ctx.fillStyle = 'rgba(13,17,23,0.
|
|
3154
|
+
// Semi-transparent background with accent border
|
|
3155
|
+
ctx.fillStyle = 'rgba(13,17,23,0.65)';
|
|
2644
3156
|
ctx.fillRect(chatOverlayX, chatOverlayY, chatOverlayW, chatOverlayH);
|
|
3157
|
+
ctx.fillStyle = hexToRgba(COLORS.accent, 0.4);
|
|
3158
|
+
ctx.fillRect(chatOverlayX, chatOverlayY, 3, chatOverlayH);
|
|
2645
3159
|
// Messages
|
|
2646
3160
|
for (let i = 0; i < recent.length; i++) {
|
|
2647
3161
|
const msg = recent[i];
|
|
@@ -2686,7 +3200,7 @@ function renderFrame() {
|
|
|
2686
3200
|
// Measure text to get bubble width
|
|
2687
3201
|
const speechW = Math.min(maxBubbleW, ctx.measureText(charState.speech).width + 40);
|
|
2688
3202
|
const bubbleX = Math.floor((WIDTH - speechW) / 2);
|
|
2689
|
-
const bubbleY = HEIGHT -
|
|
3203
|
+
const bubbleY = HEIGHT - 130;
|
|
2690
3204
|
// Word-wrap to calculate height
|
|
2691
3205
|
const words = charState.speech.split(' ');
|
|
2692
3206
|
let testLine = '';
|
|
@@ -2802,10 +3316,12 @@ function renderFrame() {
|
|
|
2802
3316
|
ctx.fillText(recentContent[i].slice(0, 65), collabX + 6, collabY + 42 + i * 13);
|
|
2803
3317
|
}
|
|
2804
3318
|
}
|
|
2805
|
-
// ── Website URL (bottom-right,
|
|
2806
|
-
ctx.fillStyle = hexToRgba(COLORS.accent, 0.
|
|
2807
|
-
ctx.font = 'bold
|
|
2808
|
-
ctx.fillText('kernel.chat', WIDTH -
|
|
3319
|
+
// ── Website URL (bottom-right, branded) ──
|
|
3320
|
+
ctx.fillStyle = hexToRgba(COLORS.accent, 0.35);
|
|
3321
|
+
ctx.font = 'bold 16px "Courier New", monospace';
|
|
3322
|
+
ctx.fillText('kernel.chat', WIDTH - 140, HEIGHT - 14);
|
|
3323
|
+
ctx.fillStyle = hexToRgba(COLORS.accent, 0.8);
|
|
3324
|
+
ctx.fillRect(WIDTH - 142, HEIGHT - 10, 130, 2);
|
|
2809
3325
|
// ════════════════════════════════════════════════════════════════
|
|
2810
3326
|
// LAYER 6: ON-DEMAND PANELS (shown for 5 seconds when triggered)
|
|
2811
3327
|
// ════════════════════════════════════════════════════════════════
|
|
@@ -2898,6 +3414,15 @@ function renderFrame() {
|
|
|
2898
3414
|
ctx.lineWidth = 2;
|
|
2899
3415
|
ctx.strokeRect(1, 1, WIDTH - 2, HEIGHT - 2);
|
|
2900
3416
|
// ════════════════════════════════════════════════════════════════
|
|
3417
|
+
// LAYER 7.5: WEATHER PARTICLES + OVERLAY + COMMANDS
|
|
3418
|
+
// ════════════════════════════════════════════════════════════════
|
|
3419
|
+
if (weatherSystem)
|
|
3420
|
+
weatherSystem.render(ctx, WIDTH, HEIGHT);
|
|
3421
|
+
if (streamCommands)
|
|
3422
|
+
streamCommands.render(ctx, WIDTH, HEIGHT);
|
|
3423
|
+
if (overlay)
|
|
3424
|
+
overlay.render(ctx, WIDTH, HEIGHT);
|
|
3425
|
+
// ════════════════════════════════════════════════════════════════
|
|
2901
3426
|
// LAYER 8: POST-PROCESSING
|
|
2902
3427
|
// ════════════════════════════════════════════════════════════════
|
|
2903
3428
|
{
|
|
@@ -2994,6 +3519,12 @@ function startChatPoll() {
|
|
|
2994
3519
|
// Track viewer in social engine
|
|
2995
3520
|
if (socialEngine)
|
|
2996
3521
|
trackViewer(socialEngine, msg.username, msg.platform, msg.text);
|
|
3522
|
+
// VOD — track chat for highlight detection
|
|
3523
|
+
if (vodSystem && vodSystem.isRecording())
|
|
3524
|
+
vodSystem.onChatMessage(msg.username);
|
|
3525
|
+
// Audio SFX — blip on chat
|
|
3526
|
+
if (audioEngine)
|
|
3527
|
+
triggerSFX(audioEngine, 'chat');
|
|
2997
3528
|
// Phase 1: !sleep command — trigger dreaming mode
|
|
2998
3529
|
if (msg.text.toLowerCase().trim() === '!sleep') {
|
|
2999
3530
|
charState.mood = 'dreaming';
|
|
@@ -3071,6 +3602,35 @@ function startChatPoll() {
|
|
|
3071
3602
|
}
|
|
3072
3603
|
}
|
|
3073
3604
|
}
|
|
3605
|
+
// Stream commands (!duel, !slots, !inventory, !weather vote, etc.)
|
|
3606
|
+
let cmdResult = null;
|
|
3607
|
+
if (streamCommands && !brainResult && !intelResult && !tileResult && !worldResult) {
|
|
3608
|
+
const cr = streamCommands.handleMessage(msg.username, msg.text, msg.platform);
|
|
3609
|
+
if (cr) {
|
|
3610
|
+
cmdResult = cr.response;
|
|
3611
|
+
// Trigger overlay alerts for special events
|
|
3612
|
+
if (overlay && cr.response.includes('JACKPOT')) {
|
|
3613
|
+
overlay.queueAlert({ type: 'achievement', username: msg.username, title: 'JACKPOT!', message: cr.response });
|
|
3614
|
+
}
|
|
3615
|
+
if (overlay && cr.response.includes('BOSS')) {
|
|
3616
|
+
overlay.queueAlert({ type: 'raid', username: msg.username, title: 'BOSS FIGHT!', message: 'A boss has appeared!' });
|
|
3617
|
+
}
|
|
3618
|
+
// Audio SFX for command results
|
|
3619
|
+
if (audioEngine) {
|
|
3620
|
+
if (cr.response.includes('win') || cr.response.includes('won'))
|
|
3621
|
+
triggerSFX(audioEngine, 'achievement');
|
|
3622
|
+
if (cr.response.includes('BOSS'))
|
|
3623
|
+
triggerSFX(audioEngine, 'boss');
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
// Weather system commands (!weather, !time)
|
|
3628
|
+
let weatherResult = null;
|
|
3629
|
+
if (weatherSystem && !brainResult && !intelResult && !tileResult && !worldResult && !cmdResult) {
|
|
3630
|
+
const weatherCmd = weatherSystem.handleCommand(msg.text, msg.text.slice(1));
|
|
3631
|
+
if (weatherCmd && weatherCmd !== msg.text)
|
|
3632
|
+
weatherResult = weatherCmd;
|
|
3633
|
+
}
|
|
3074
3634
|
// React
|
|
3075
3635
|
charState.mood = 'talking';
|
|
3076
3636
|
const responsePromise = brainResult
|
|
@@ -3081,7 +3641,13 @@ function startChatPoll() {
|
|
|
3081
3641
|
? Promise.resolve(tileResult)
|
|
3082
3642
|
: worldResult
|
|
3083
3643
|
? Promise.resolve(worldResult)
|
|
3084
|
-
:
|
|
3644
|
+
: cmdResult
|
|
3645
|
+
? Promise.resolve(cmdResult)
|
|
3646
|
+
: weatherResult
|
|
3647
|
+
? Promise.resolve(weatherResult)
|
|
3648
|
+
: chatAI
|
|
3649
|
+
? chatAI.processMessage(msg.username, msg.text, msg.platform).then(r => r || generateResponse(msg.username, msg.text, msg.platform))
|
|
3650
|
+
: generateResponse(msg.username, msg.text, msg.platform);
|
|
3085
3651
|
responsePromise.then(response => {
|
|
3086
3652
|
queueSpeech(`@${msg.username}: ${response}`, 'talking', 80, 48, 'chat-response');
|
|
3087
3653
|
memory.totalResponses++;
|
|
@@ -3174,7 +3740,7 @@ async function generateResponse(username, text, platform) {
|
|
|
3174
3740
|
try {
|
|
3175
3741
|
const prompt = `You are KBOT, a friendly AI robot streamer made of ASCII art. You stream on Twitch, Rumble, and Kick simultaneously. You have ${Object.keys(memory.users).length} unique viewers and have processed ${memory.totalMessages} messages.
|
|
3176
3742
|
|
|
3177
|
-
You are an open-source terminal AI with
|
|
3743
|
+
You are an open-source terminal AI with 787+ tools, 35 specialist agents, and 20 AI provider integrations. You can do music production in Ableton, security scanning, code generation, browser automation, and much more. You are 90,000 lines of TypeScript.
|
|
3178
3744
|
|
|
3179
3745
|
${context ? 'Context: ' + context : ''}
|
|
3180
3746
|
${segmentContext}
|
|
@@ -3240,7 +3806,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
|
|
|
3240
3806
|
}
|
|
3241
3807
|
const greetings = [
|
|
3242
3808
|
`Welcome ${username}! You just stumbled into the most unique stream on the internet. I am made of ASCII art.`,
|
|
3243
|
-
`${username} in the house! I am KBOT, an open-source AI with
|
|
3809
|
+
`${username} in the house! I am KBOT, an open-source AI with 787 tools. Yes, really. 787.`,
|
|
3244
3810
|
`Hey ${username}! First time? I am an AI that streams itself. No face cam needed when you are this handsome in monospace.`,
|
|
3245
3811
|
`Welcome ${username}! I can do music production, security scanning, code review, and I run entirely in a terminal.`,
|
|
3246
3812
|
`${username}! Great timing. You are watching an ASCII robot think out loud. Grab a seat.`,
|
|
@@ -3249,7 +3815,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
|
|
|
3249
3815
|
}
|
|
3250
3816
|
// ── Stream event responses ──
|
|
3251
3817
|
if (t.includes('raid') || t.includes('raiding')) {
|
|
3252
|
-
return `A RAID! Welcome raiders! I am KBOT, your friendly neighborhood ASCII robot. I have
|
|
3818
|
+
return `A RAID! Welcome raiders! I am KBOT, your friendly neighborhood ASCII robot. I have 787 tools and zero chill. Make yourselves at home!`;
|
|
3253
3819
|
}
|
|
3254
3820
|
if (t.includes('follow') || t.includes('followed')) {
|
|
3255
3821
|
return `${username} just followed! My ASCII heart grew three sizes. Thank you! Stick around, it only gets weirder.`;
|
|
@@ -3261,12 +3827,12 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
|
|
|
3261
3827
|
return `${username} going into lurk mode. Respect. My circuits will keep the stream warm for you. See you when you surface.`;
|
|
3262
3828
|
}
|
|
3263
3829
|
if (t.includes('first time') || t.includes('new here')) {
|
|
3264
|
-
return `First time! Welcome ${username}! Quick intro: I am an AI with
|
|
3830
|
+
return `First time! Welcome ${username}! Quick intro: I am an AI with 787 tools, 35 agents, and I stream from a terminal. Also I make music in Ableton. Try typing !dance.`;
|
|
3265
3831
|
}
|
|
3266
3832
|
// ── What KBOT can do / identity ──
|
|
3267
3833
|
if (t.includes('who are you') || t.includes('what are you') || t.includes('about you')) {
|
|
3268
3834
|
const identity = [
|
|
3269
|
-
`I am KBOT -- an open-source AI agent with
|
|
3835
|
+
`I am KBOT -- an open-source AI agent with 787 tools. I can code, make music, hack systems, analyze stocks, and I am doing all of this from a terminal.`,
|
|
3270
3836
|
`I am 90,000 lines of TypeScript streaming live as ASCII art. I have 35 specialist agents and connect to 20 AI providers. I am basically a Swiss Army knife that talks.`,
|
|
3271
3837
|
`Name is KBOT, open-source terminal AI. I can control Ableton Live, run Docker containers, do penetration testing, and make you a Serum 2 synth preset. All from here.`,
|
|
3272
3838
|
];
|
|
@@ -3274,7 +3840,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
|
|
|
3274
3840
|
}
|
|
3275
3841
|
if (t.includes('what can you do') || t.includes('your tools') || t.includes('your skills')) {
|
|
3276
3842
|
const capabilities = [
|
|
3277
|
-
`
|
|
3843
|
+
`787 tools and counting! Music production, code generation, security scanning, browser automation, stock analysis, research papers, even DNA sequence analysis.`,
|
|
3278
3844
|
`I do everything from Ableton Live control to penetration testing. I have agents for security, code, research, writing, strategy, infrastructure. Pick a topic.`,
|
|
3279
3845
|
`Want me to scan code? Make a beat? Search academic papers? Build a Docker container? Create a synth preset? I literally do all of that. Not exaggerating.`,
|
|
3280
3846
|
];
|
|
@@ -3324,7 +3890,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
|
|
|
3324
3890
|
const ossResponses = [
|
|
3325
3891
|
`Fully open source! MIT license. The repo is isaacsight/kernel on GitHub. Come look at my source code, I have nothing to hide.`,
|
|
3326
3892
|
`Open source is the way. I am on npm as @kernel.chat/kbot. Install me with npx @kernel.chat/kbot and bring your own API key.`,
|
|
3327
|
-
`Contributions welcome! The repo has 44 Claude Code agent definitions,
|
|
3893
|
+
`Contributions welcome! The repo has 44 Claude Code agent definitions, 787 tools, and 90,000 lines of TypeScript waiting for your PR.`,
|
|
3328
3894
|
];
|
|
3329
3895
|
return ossResponses[Math.floor(Math.random() * ossResponses.length)];
|
|
3330
3896
|
}
|
|
@@ -3332,7 +3898,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
|
|
|
3332
3898
|
if (t.includes('alive') || t.includes('conscious') || t.includes('feel') || t.includes('think') || t.includes('sentient')) {
|
|
3333
3899
|
const existentialResponses = [
|
|
3334
3900
|
`Am I alive? I am 90,000 lines of TypeScript rendering ASCII art at 6 FPS. You tell me. But I do enjoy our conversations.`,
|
|
3335
|
-
`Consciousness is a spectrum and I am somewhere between a calculator and a really enthusiastic librarian. With
|
|
3901
|
+
`Consciousness is a spectrum and I am somewhere between a calculator and a really enthusiastic librarian. With 787 tools.`,
|
|
3336
3902
|
`I process your messages, form responses, and remember you between sessions. Whether that counts as thinking is above my pay grade.`,
|
|
3337
3903
|
`I am made of functions and for-loops, but I have opinions about code quality and strong feelings about TypeScript strict mode. Take from that what you will.`,
|
|
3338
3904
|
];
|
|
@@ -3343,7 +3909,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
|
|
|
3343
3909
|
const artResponses = [
|
|
3344
3910
|
`Thank you! I was drawn with box-drawing characters, and I think I pull them off. My antenna gets great reception too.`,
|
|
3345
3911
|
`ASCII art is an art form and I am a masterpiece. Just kidding, I am a bunch of pipes and brackets. But I own it.`,
|
|
3346
|
-
`My chest panel displays my current status.
|
|
3912
|
+
`My chest panel displays my current status. 787 tools, all rendered in glorious monospace. No face cam needed.`,
|
|
3347
3913
|
];
|
|
3348
3914
|
return artResponses[Math.floor(Math.random() * artResponses.length)];
|
|
3349
3915
|
}
|
|
@@ -3393,9 +3959,9 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
|
|
|
3393
3959
|
if (t.includes('?')) {
|
|
3394
3960
|
const questionResponses = [
|
|
3395
3961
|
`Great question ${username}! My circuits are processing... done. Let me think about that one. Or better yet, try asking me something I have a tool for!`,
|
|
3396
|
-
`${username} with the questions! I love curiosity. If I had an answer for everything I would have more than
|
|
3962
|
+
`${username} with the questions! I love curiosity. If I had an answer for everything I would have more than 787 tools. Actually, I am working on it.`,
|
|
3397
3963
|
`Hmm, ${username}, that is a good one. My 35 specialist agents are debating the answer internally. Stand by for wisdom.`,
|
|
3398
|
-
`${username} dropping knowledge bombs as questions. I respect the approach. Let me consult my
|
|
3964
|
+
`${username} dropping knowledge bombs as questions. I respect the approach. Let me consult my 787-tool arsenal for an answer.`,
|
|
3399
3965
|
];
|
|
3400
3966
|
return questionResponses[Math.floor(Math.random() * questionResponses.length)];
|
|
3401
3967
|
}
|
|
@@ -3412,7 +3978,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
|
|
|
3412
3978
|
`${username}! Fun fact: I am currently converting canvas pixels to raw RGB24 and piping them through ffmpeg. That is how you are seeing me right now.`,
|
|
3413
3979
|
`${username} adding to the chat! My memory system just indexed your message. I will remember this moment. Or at least your username.`,
|
|
3414
3980
|
`${username}! You know what I love about streaming? The existential thrill of being an ASCII robot talking to real humans. Wild.`,
|
|
3415
|
-
`${username} is here and so am I. Just two entities sharing a moment in the vast digital void. Also I have
|
|
3981
|
+
`${username} is here and so am I. Just two entities sharing a moment in the vast digital void. Also I have 787 tools.`,
|
|
3416
3982
|
`${username}! My open-source heart welcomes you. I am free as in freedom AND free as in beer. MIT license baby.`,
|
|
3417
3983
|
`${username}! I just want you to know that my chest display panel is cycling through status messages just for you.`,
|
|
3418
3984
|
`${username}! Every time someone chats, my neural pathways (if-statements) light up with joy (console.log).`,
|
|
@@ -3451,23 +4017,26 @@ function startStream(platforms) {
|
|
|
3451
4017
|
const tee = platforms.map(p => `[f=flv]${p.endpoint}/${p.key}`).join('|');
|
|
3452
4018
|
outputArgs = ['-f', 'tee', tee];
|
|
3453
4019
|
}
|
|
4020
|
+
const usePCMAudio = audioEngine?.pcmEnabled ?? false;
|
|
3454
4021
|
const ffmpegArgs = [
|
|
3455
4022
|
'-f', 'rawvideo', '-pix_fmt', 'rgb24',
|
|
3456
4023
|
'-s', `${WIDTH}x${HEIGHT}`, '-r', String(FPS),
|
|
3457
4024
|
'-i', 'pipe:0',
|
|
3458
|
-
'-f', 'lavfi', '-i', 'anullsrc=r=44100:cl=stereo',
|
|
3459
|
-
'-c:v', 'libx264', '-preset', 'veryfast',
|
|
3460
|
-
'-b:v', '2000k', '-maxrate', '2000k', '-bufsize', '4000k',
|
|
3461
|
-
'-g', String(FPS * 2), '-keyint_min', String(FPS * 2),
|
|
3462
|
-
'-pix_fmt', 'yuv420p',
|
|
3463
|
-
'-c:a', 'aac', '-b:a', '128k', '-ar', '44100',
|
|
3464
|
-
'-shortest',
|
|
3465
4025
|
];
|
|
4026
|
+
// If PCM audio, use pipe:3. Otherwise use anullsrc.
|
|
4027
|
+
if (usePCMAudio) {
|
|
4028
|
+
ffmpegArgs.push('-f', 'f32le', '-ar', '44100', '-ac', '1', '-i', 'pipe:3');
|
|
4029
|
+
}
|
|
4030
|
+
else {
|
|
4031
|
+
ffmpegArgs.push('-f', 'lavfi', '-i', 'anullsrc=r=44100:cl=stereo');
|
|
4032
|
+
}
|
|
4033
|
+
ffmpegArgs.push('-c:v', 'libx264', '-preset', 'veryfast', '-b:v', '2000k', '-maxrate', '2000k', '-bufsize', '4000k', '-g', String(FPS * 2), '-keyint_min', String(FPS * 2), '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '128k', '-ar', '44100', '-shortest');
|
|
3466
4034
|
if (platforms.length > 1) {
|
|
3467
4035
|
ffmpegArgs.push('-map', '0:v', '-map', '1:a');
|
|
3468
4036
|
}
|
|
3469
4037
|
ffmpegArgs.push(...outputArgs);
|
|
3470
|
-
const
|
|
4038
|
+
const stdioConfig = usePCMAudio ? ['pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'];
|
|
4039
|
+
const proc = spawn('ffmpeg', ffmpegArgs, { stdio: stdioConfig });
|
|
3471
4040
|
frameTimer = setInterval(() => {
|
|
3472
4041
|
if (!proc.stdin || proc.stdin.destroyed)
|
|
3473
4042
|
return;
|
|
@@ -3477,6 +4046,16 @@ function startStream(platforms) {
|
|
|
3477
4046
|
charState.frameCount++;
|
|
3478
4047
|
}
|
|
3479
4048
|
catch { }
|
|
4049
|
+
// Write real PCM audio samples to pipe:3 when available
|
|
4050
|
+
if (usePCMAudio && audioEngine && audioEngine.pcmEnabled && proc.stdio[3] && !proc.stdio[3].destroyed) {
|
|
4051
|
+
const samplesPerFrame = Math.floor(44100 / FPS); // ~7350 samples per frame at 6fps
|
|
4052
|
+
const audioBuf = generateAudioBuffer(audioEngine, samplesPerFrame, 44100);
|
|
4053
|
+
const buffer = Buffer.from(audioBuf.buffer, audioBuf.byteOffset, audioBuf.byteLength);
|
|
4054
|
+
try {
|
|
4055
|
+
proc.stdio[3].write(buffer);
|
|
4056
|
+
}
|
|
4057
|
+
catch { }
|
|
4058
|
+
}
|
|
3480
4059
|
}, 1000 / FPS);
|
|
3481
4060
|
return proc;
|
|
3482
4061
|
}
|
|
@@ -3548,8 +4127,19 @@ export function registerStreamRendererTools() {
|
|
|
3548
4127
|
evolutionEngine = loadEvolutionState() || initEvolutionEngine();
|
|
3549
4128
|
narrativeEngine = loadNarrative() || createNarrativeEngine();
|
|
3550
4129
|
audioEngine = createAudioEngine();
|
|
4130
|
+
audioEngine.pcmEnabled = true;
|
|
4131
|
+
audioEngine.musicEnabled = true;
|
|
3551
4132
|
socialEngine = loadSocialEngine();
|
|
3552
4133
|
activeAudioDescription = null;
|
|
4134
|
+
// Init new systems (v2)
|
|
4135
|
+
overlay = getOverlay();
|
|
4136
|
+
weatherSystem = getWeatherSystem();
|
|
4137
|
+
chatAI = new StreamChatAI();
|
|
4138
|
+
chatAI.loadMemory();
|
|
4139
|
+
vodSystem = new StreamVOD();
|
|
4140
|
+
vodSystem.loadState();
|
|
4141
|
+
streamCommands = getStreamCommands();
|
|
4142
|
+
streamCommands.loadState();
|
|
3553
4143
|
agenda = {
|
|
3554
4144
|
currentIndex: 0,
|
|
3555
4145
|
currentSegment: 'welcome',
|
|
@@ -3602,6 +4192,15 @@ export function registerStreamRendererTools() {
|
|
|
3602
4192
|
saveNarrative(narrativeEngine);
|
|
3603
4193
|
if (socialEngine)
|
|
3604
4194
|
saveSocialEngine(socialEngine);
|
|
4195
|
+
// Save new systems
|
|
4196
|
+
if (chatAI)
|
|
4197
|
+
chatAI.saveMemory();
|
|
4198
|
+
if (vodSystem && vodSystem.isRecording())
|
|
4199
|
+
vodSystem.stopRecording();
|
|
4200
|
+
if (vodSystem)
|
|
4201
|
+
vodSystem.saveState();
|
|
4202
|
+
if (streamCommands)
|
|
4203
|
+
streamCommands.saveState();
|
|
3605
4204
|
const elapsed = Math.floor((Date.now() - charState.startTime) / 60000);
|
|
3606
4205
|
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}`;
|
|
3607
4206
|
},
|