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