Compare commits

...

9 Commits

Author SHA1 Message Date
1c18fbf0d1 [claude] InstancedMesh for portal tori + merged tile edge geometry (#415) (#464)
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Smoke Test / smoke-test (push) Successful in 6s
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-25 01:30:31 +00:00
b4f6ff5222 fix: bust service worker cache to pull latest upstream modules (#467)
Some checks failed
Deploy Nexus / deploy (push) Failing after 8s
Staging Smoke Test / smoke-test (push) Successful in 1s
2026-03-24 23:00:51 +00:00
Alexander Whitestone
4379f70352 fix: restore weather.js + bookshelves.js to inline versions, remove unused panels/ and effects/ subdirs
Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
Staging Smoke Test / smoke-test (push) Successful in 1s
The Phase 2 data-layer PRs modified these to import from data/ and core/,
but those directories were removed in the Manus revert. Restore to the
self-contained split-commit versions.

panels/ and effects/ subdirectories were Phase 2 extractions not used
by the main import chain (app.js -> modules/panels.js, not panels/).
2026-03-24 18:47:16 -04:00
Alexander Whitestone
979c7cf96b revert: strip Manus damage (nostr, SovOS, gutted app.js) — restore clean split
Some checks failed
Deploy Nexus / deploy (push) Failing after 5s
Staging Smoke Test / smoke-test (push) Successful in 1s
Reverts to the state of cbfacdf (split app.js into 21 modules, <1000 lines each).
Removes: nostr.js, nostr-panel.js, SovOS.js, RESEARCH_DROP_456.md, core/, data/
Historical archive preserved in .historical/ and branch archive/manus-damage-2026-03-24

Refs #418, #452, #454
2026-03-24 18:29:57 -04:00
Alexander Whitestone
b1cc4c05da chore: archive Manus damage for historical record before revert 2026-03-24 18:28:31 -04:00
7b54b22df1 [claude] Phase 1: Core Foundation — scene, ticker, theme, state (#410) (#463)
Some checks failed
Deploy Nexus / deploy (push) Failing after 5s
Staging Smoke Test / smoke-test (push) Failing after 1s
2026-03-24 22:16:50 +00:00
09c83e8734 [claude] Phase 1: Core Foundation — scene, ticker, theme, state (#410) (#463)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Staging Smoke Test / smoke-test (push) Has been cancelled
2026-03-24 22:16:49 +00:00
6db2871785 [claude] Phase 2: move portal health probe to data/loaders (#411) (#462)
Some checks failed
Deploy Nexus / deploy (push) Failing after 7s
Staging Smoke Test / smoke-test (push) Failing after 1s
2026-03-24 22:12:22 +00:00
c0a673038b [claude] Phase 2 cleanup: route remaining fetch() through data/ modules (#421) (#461)
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Smoke Test / smoke-test (push) Failing after 0s
2026-03-24 22:07:19 +00:00
39 changed files with 2010 additions and 2302 deletions

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Check staging environment uptime
run: |
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/)
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://staging.the-nexus.com/)
if [ "$HTTP_CODE" -eq 200 ]; then
echo "Staging environment is up (HTTP 200)"
else

View File

@@ -0,0 +1,7 @@
42e74ad fix: restore full app.js wiring — manus gutted it to 42-line nostr stub
764b617 [modularization] Phase 2: Extract data layer — gitea, weather, bitcoin, loaders (#460)
d201d3e feat: add visual banner, staging link, and real smoke-test badge (#458)
06faa75 fix: point staging to localhost exclusively and entirely (#459)
24e7139 [manus] Nostr Integration — Sovereign Communication (#454) (#455)
a2b2b1a [gemini] Research Drop findings (#456) (#457)
4effd92 [manus] SovOS Architecture — Modular 3D Interface (#452) (#453)

File diff suppressed because it is too large Load Diff

538
app.js
View File

@@ -1,42 +1,528 @@
// === THE NEXUS — Main Entry Point ===
// All modules are imported here. This file wires them together.
import * as THREE from 'three';
import { S, Broadcaster } from './modules/state.js';
import { S } from './modules/state.js';
import { NEXUS } from './modules/constants.js';
import { scene, camera, renderer, composer } from './modules/scene-setup.js';
import { clock, warpPass } from './modules/warp.js';
import { nostr } from './modules/nostr.js';
import { createNostrPanelTexture } from './modules/nostr-panel.js';
import { setAnimateFn, setTotalActivityFn } from './modules/matrix-rain.js';
import { scene, camera, renderer, raycaster, forwardVector,
ambientLight, overheadLight,
stars, starMaterial, constellationLines,
STAR_BASE_OPACITY, STAR_PEAK_OPACITY, STAR_PULSE_DECAY } from './modules/scene-setup.js';
import { glassEdgeMaterials, voidLight, cloudMaterial, GLASS_RADIUS } from './modules/platform.js';
import { heatmapMat, zoneIntensity, drawHeatmap, updateHeatmap, HEATMAP_ZONES } from './modules/heatmap.js';
import { sigilMesh, sigilMat, sigilRing1, sigilRing1Mat,
sigilRing2, sigilRing2Mat, sigilRing3, sigilRing3Mat,
sigilLight } from './modules/sigil.js';
import { NORMAL_CAM, OVERVIEW_CAM, composer, orbitControls, bokehPass, WARP_DURATION } from './modules/controls.js';
import { animateEnergyBeam, sovereigntyGroup, meterLight,
runeSprites, RUNE_RING_Y, RUNE_ORBIT_SPEED, rebuildRuneRing } from './modules/effects.js';
import { earthGroup, earthMesh, earthSurfaceMat, earthGlowLight,
EARTH_Y, EARTH_ROTATION_SPEED } from './modules/earth.js';
import { clock, warpPass, startWarp, totalActivity,
crystals, CRYSTAL_COLORS, LIGHTNING_POOL_SIZE, LIGHTNING_REFRESH_MS,
lightningArcs, lightningArcMeta, updateLightningArcs,
batcaveGroup, batcaveProbe, batcaveMetallicMats, batcaveProbeTarget_texture } from './modules/warp.js';
import { dualBrainSprite, dualBrainLight, dualBrainScanSprite, dualBrainScanTexture,
cloudOrb, cloudOrbMat, cloudOrbLight,
localOrb, localOrbMat, localOrbLight,
BRAIN_PARTICLE_COUNT, brainParticleGeo, brainParticleMat,
brainParticlePhases, brainParticleSpeeds, _scanCtx } from './modules/dual-brain.js';
import { updateAudioListener, initAudioListeners, startPortalHums } from './modules/audio.js';
import { initDebug, initWebSocket, wsClient, logMessage, initSessionExport } from './modules/debug.js';
import { triggerSovereigntyEasterEgg, triggerFireworks, triggerMergeFlash, triggerShockwave,
initSovereigntyEasterEgg,
shockwaveRings, SHOCKWAVE_DURATION,
fireworkBursts, FIREWORK_BURST_PARTICLES, FIREWORK_BURST_DURATION, FIREWORK_GRAVITY } from './modules/celebrations.js';
import { portalGroup, portals, loadPortals, setRebuildGravityZonesFn, setRunPortalHealthChecksFn } from './modules/portals.js';
import { commitBanners, bookshelfGroups, agentPanelSprites,
initCommitBanners, initBookshelves } from './modules/bookshelves.js';
import { tomeGroup, tomeGlow, oathSpot, enterOath, exitOath, initOathListeners } from './modules/oath.js';
import { loraPanelSprite, refreshAgentBoard, initAgentBoard, loadLoRAStatus } from './modules/panels.js';
import { rainParticles, rainGeo, rainVelocities, snowParticles, snowGeo, snowDrift,
PRECIP_COUNT, PRECIP_AREA, PRECIP_HEIGHT, PRECIP_FLOOR,
runPortalHealthChecks, initPortalHealthChecks, setWeatherPortalRefs,
initWeather } from './modules/weather.js';
import { gravityZoneObjects, GRAVITY_ANOMALY_CEIL, rebuildGravityZones,
TIMMY_SPEECH_POS, SPEECH_DURATION, SPEECH_FADE_IN, SPEECH_FADE_OUT,
showTimmySpeech, setExtrasPortalsRef,
timelapseCommits, timelapseWindow, TIMELAPSE_DURATION_S,
fireTimelapseCommit, updateTimelapseHeatmap, updateTimelapseHUD, stopTimelapse,
initTimelapse, initBitcoin } from './modules/extras.js';
// === NOSTR INIT ===
nostr.connect();
const { canvas: nostrCanvas, update: updateNostrUI } = createNostrPanelTexture();
const nostrTexture = new THREE.CanvasTexture(nostrCanvas);
const nostrMat = new THREE.MeshBasicMaterial({ map: nostrTexture, transparent: true, side: THREE.DoubleSide });
const nostrPanel = new THREE.Mesh(new THREE.PlaneGeometry(3, 3), nostrMat);
nostrPanel.position.set(-6, 3.5, -7.5);
nostrPanel.rotation.y = 0.4;
scene.add(nostrPanel);
// === WIRE UP CROSS-MODULE REFERENCES ===
setTotalActivityFn(totalActivity);
setAnimateFn(() => animate());
setRebuildGravityZonesFn(rebuildGravityZones);
setRunPortalHealthChecksFn(runPortalHealthChecks);
// === MAIN ANIMATION LOOP ===
// === ANIMATION LOOP ===
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const elapsed = clock.elapsedTime;
animateEnergyBeam();
const elapsed = clock.getElapsedTime();
// Update Nostr UI periodically or on event
if (Math.random() > 0.95) {
updateNostrUI();
nostrTexture.needsUpdate = true;
// Overview mode
const targetT = S.overviewMode ? 1 : 0;
S.overviewT += (targetT - S.overviewT) * 0.04;
const _basePos = new THREE.Vector3().lerpVectors(NORMAL_CAM, OVERVIEW_CAM, S.overviewT);
// Zoom-to-object
if (!S.photoMode) {
S.zoomT += (S.zoomTargetT - S.zoomT) * 0.07;
}
if (S.zoomT > 0.001 && !S.photoMode && !S.overviewMode) {
camera.position.lerpVectors(_basePos, S._zoomCamTarget, S.zoomT);
camera.lookAt(new THREE.Vector3(0, 0, 0).lerp(S._zoomLookTarget, S.zoomT));
} else {
camera.position.copy(_basePos);
camera.lookAt(0, 0, 0);
}
// Visual pulse on energy beam
if (S.energyBeamPulse > 0) {
S.energyBeamPulse -= delta * 2;
if (S.energyBeamPulse < 0) S.energyBeamPulse = 0;
const rotationScale = S.photoMode ? 0 : (1 - S.overviewT);
S.targetRotX += (S.mouseY * 0.3 - S.targetRotX) * 0.02;
S.targetRotY += (S.mouseX * 0.3 - S.targetRotY) * 0.02;
stars.rotation.x = (S.targetRotX + elapsed * 0.01) * rotationScale;
stars.rotation.y = (S.targetRotY + elapsed * 0.015) * rotationScale;
// Star pulse
if (S._starPulseIntensity > 0) {
S._starPulseIntensity = Math.max(0, S._starPulseIntensity - STAR_PULSE_DECAY);
}
starMaterial.opacity = STAR_BASE_OPACITY + (STAR_PEAK_OPACITY - STAR_BASE_OPACITY) * S._starPulseIntensity;
constellationLines.rotation.x = stars.rotation.x;
constellationLines.rotation.y = stars.rotation.y;
constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06;
// Batcave reflection probe
if (elapsed - S.batcaveProbeLastUpdate > 2.0) {
S.batcaveProbeLastUpdate = elapsed;
batcaveGroup.visible = false;
batcaveProbe.update(renderer, scene);
batcaveGroup.visible = true;
for (const mat of batcaveMetallicMats) {
mat.envMap = batcaveProbeTarget_texture.texture;
mat.needsUpdate = true;
}
}
// Glass platform edge glow
for (const { mat, distFromCenter } of glassEdgeMaterials) {
const phase = elapsed * 1.1 - distFromCenter * 0.18;
mat.opacity = 0.25 + Math.sin(phase) * 0.22;
}
voidLight.intensity = 0.35 + Math.sin(elapsed * 1.4) * 0.2;
heatmapMat.opacity = 0.75 + Math.sin(elapsed * 0.6) * 0.2;
// Sigil animation
sigilMesh.rotation.z = elapsed * 0.04;
sigilRing1.rotation.z = elapsed * 0.06;
sigilRing2.rotation.z = -elapsed * 0.10;
sigilRing3.rotation.z = elapsed * 0.08;
sigilMat.opacity = 0.65 + Math.sin(elapsed * 1.3) * 0.18;
sigilRing1Mat.opacity = 0.38 + Math.sin(elapsed * 0.9) * 0.14;
sigilRing2Mat.opacity = 0.32 + Math.sin(elapsed * 1.6 + 1.2) * 0.12;
sigilRing3Mat.opacity = 0.28 + Math.sin(elapsed * 0.7 + 2.4) * 0.10;
sigilLight.intensity = 0.30 + Math.sin(elapsed * 1.1) * 0.15;
cloudMaterial.uniforms.uTime.value = elapsed;
if (S.photoMode) {
orbitControls.update();
}
// Sovereignty meter
sovereigntyGroup.position.y = 3.8 + Math.sin(elapsed * 0.8) * 0.15;
meterLight.intensity = 0.5 + Math.sin(elapsed * 1.8) * 0.25;
// Commit banners
const FADE_DUR = 1.5;
commitBanners.forEach(banner => {
const ud = banner.userData;
if (ud.spawnTime === null) {
if (elapsed < ud.startDelay) return;
ud.spawnTime = elapsed;
}
const age = elapsed - ud.spawnTime;
let opacity;
if (age < FADE_DUR) {
opacity = age / FADE_DUR;
} else if (age < ud.lifetime - FADE_DUR) {
opacity = 1;
} else if (age < ud.lifetime) {
opacity = (ud.lifetime - age) / FADE_DUR;
} else {
ud.spawnTime = elapsed + 3;
opacity = 0;
}
banner.material.opacity = opacity * 0.85;
banner.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.4;
});
// Agent panels float
for (const sprite of agentPanelSprites) {
const ud = sprite.userData;
sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22;
}
// LoRA panel float
if (loraPanelSprite) {
const ud = loraPanelSprite.userData;
loraPanelSprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22;
}
// Bookshelves float
for (const shelf of bookshelfGroups) {
const ud = shelf.userData;
shelf.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.18;
}
// Speech bubble
if (S.timmySpeechState) {
const age = elapsed - S.timmySpeechState.startTime;
let opacity;
if (age < SPEECH_FADE_IN) {
opacity = age / SPEECH_FADE_IN;
} else if (age < SPEECH_DURATION - SPEECH_FADE_OUT) {
opacity = 1.0;
} else if (age < SPEECH_DURATION) {
opacity = (SPEECH_DURATION - age) / SPEECH_FADE_OUT;
} else {
scene.remove(S.timmySpeechState.sprite);
if (S.timmySpeechState.sprite.material.map) S.timmySpeechState.sprite.material.map.dispose();
S.timmySpeechState.sprite.material.dispose();
S.timmySpeechSprite = null;
S.timmySpeechState = null;
opacity = 0;
}
if (S.timmySpeechState) {
S.timmySpeechState.sprite.material.opacity = opacity;
S.timmySpeechState.sprite.position.y = TIMMY_SPEECH_POS.y + Math.sin(elapsed * 1.1) * 0.1;
}
}
// Tome float
tomeGroup.position.y = 5.8 + Math.sin(elapsed * 0.6) * 0.18;
tomeGroup.rotation.y = elapsed * 0.3;
tomeGlow.intensity = 0.3 + Math.sin(elapsed * 1.4) * 0.12;
if (S.oathActive) {
oathSpot.intensity = 3.8 + Math.sin(elapsed * 0.9) * 0.4;
}
// Shockwave rings
for (let i = shockwaveRings.length - 1; i >= 0; i--) {
const ring = shockwaveRings[i];
const age = elapsed - ring.startTime - ring.delay;
if (age < 0) continue;
const t = Math.min(age / SHOCKWAVE_DURATION, 1);
if (t >= 1) {
scene.remove(ring.mesh);
ring.mesh.geometry.dispose();
ring.mat.dispose();
shockwaveRings.splice(i, 1);
continue;
}
const eased = 1 - Math.pow(1 - t, 2);
ring.mesh.scale.setScalar(eased * 14 + 0.1);
ring.mat.opacity = (1 - t) * 0.9;
}
// Fireworks
for (let i = fireworkBursts.length - 1; i >= 0; i--) {
const burst = fireworkBursts[i];
const age = elapsed - burst.startTime;
const t = Math.min(age / FIREWORK_BURST_DURATION, 1);
if (t >= 1) {
scene.remove(burst.points);
burst.geo.dispose();
burst.mat.dispose();
fireworkBursts.splice(i, 1);
continue;
}
burst.mat.opacity = t < 0.6 ? 1.0 : (1.0 - t) / 0.4;
const pos = burst.geo.attributes.position.array;
const vel = burst.velocities;
const org = burst.origins;
const halfGAge2 = 0.5 * FIREWORK_GRAVITY * age * age;
for (let j = 0; j < FIREWORK_BURST_PARTICLES; j++) {
pos[j * 3] = org[j * 3] + vel[j * 3] * age;
pos[j * 3 + 1] = org[j * 3 + 1] + vel[j * 3 + 1] * age + halfGAge2;
pos[j * 3 + 2] = org[j * 3 + 2] + vel[j * 3 + 2] * age;
}
burst.geo.attributes.position.needsUpdate = true;
}
// Rune ring
for (const rune of runeSprites) {
const angle = rune.baseAngle + elapsed * RUNE_ORBIT_SPEED;
rune.sprite.position.x = Math.cos(angle) * 7.0;
rune.sprite.position.z = Math.sin(angle) * 7.0;
rune.sprite.position.y = RUNE_RING_Y + Math.sin(elapsed * 0.7 + rune.floatPhase) * 0.4;
const baseOpacity = rune.portalOnline ? 0.85 : 0.12;
const pulseRange = rune.portalOnline ? 0.15 : 0.03;
rune.sprite.material.opacity = baseOpacity + Math.sin(elapsed * 1.2 + rune.floatPhase) * pulseRange;
}
// Earth
const earthActivity = totalActivity();
const targetEarthSpeed = 0.005 + earthActivity * 0.045;
const _eSmooth = 0.02;
const currentEarthSpeed = earthMesh.userData._currentSpeed || EARTH_ROTATION_SPEED;
const smoothedEarthSpeed = currentEarthSpeed + (targetEarthSpeed - currentEarthSpeed) * _eSmooth;
earthMesh.userData._currentSpeed = smoothedEarthSpeed;
earthMesh.rotation.y += smoothedEarthSpeed;
earthSurfaceMat.uniforms.uTime.value = elapsed;
earthGlowLight.intensity = 0.30 + Math.sin(elapsed * 0.7) * 0.12;
earthGroup.position.y = EARTH_Y + Math.sin(elapsed * 0.22) * 0.6;
// Weather particles
if (rainParticles.visible) {
const rpos = rainGeo.attributes.position.array;
for (let i = 0; i < PRECIP_COUNT; i++) {
rpos[i * 3 + 1] -= rainVelocities[i];
if (rpos[i * 3 + 1] < PRECIP_FLOOR) {
rpos[i * 3 + 1] = PRECIP_HEIGHT;
rpos[i * 3] = (Math.random() - 0.5) * PRECIP_AREA * 2;
rpos[i * 3 + 2] = (Math.random() - 0.5) * PRECIP_AREA * 2;
}
}
rainGeo.attributes.position.needsUpdate = true;
}
if (snowParticles.visible) {
const spos = snowGeo.attributes.position.array;
for (let i = 0; i < PRECIP_COUNT; i++) {
spos[i * 3 + 1] -= 0.025 + Math.sin(snowDrift[i]) * 0.005;
spos[i * 3] += Math.sin(elapsed * 0.4 + snowDrift[i]) * 0.008;
if (spos[i * 3 + 1] < PRECIP_FLOOR) {
spos[i * 3 + 1] = PRECIP_HEIGHT;
spos[i * 3] = (Math.random() - 0.5) * PRECIP_AREA * 2;
spos[i * 3 + 2] = (Math.random() - 0.5) * PRECIP_AREA * 2;
}
}
snowGeo.attributes.position.needsUpdate = true;
}
// Gravity anomalies
for (const gz of gravityZoneObjects) {
const pos = gz.geo.attributes.position.array;
const count = gz.zone.particleCount;
for (let i = 0; i < count; i++) {
pos[i * 3 + 1] += gz.velocities[i];
pos[i * 3] += Math.sin(elapsed * 0.5 + gz.driftPhases[i]) * 0.003;
pos[i * 3 + 2] += Math.cos(elapsed * 0.5 + gz.driftPhases[i]) * 0.003;
if (pos[i * 3 + 1] > GRAVITY_ANOMALY_CEIL) {
const angle = Math.random() * Math.PI * 2;
const r = Math.sqrt(Math.random()) * gz.zone.radius;
pos[i * 3] = gz.zone.x + Math.cos(angle) * r;
pos[i * 3 + 1] = 0.2 + Math.random() * 2.0;
pos[i * 3 + 2] = gz.zone.z + Math.sin(angle) * r;
}
}
gz.geo.attributes.position.needsUpdate = true;
gz.ringMat.opacity = 0.3 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.15;
gz.discMat.opacity = 0.02 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.02;
}
// Dual-brain
dualBrainSprite.position.y = dualBrainSprite.userData.baseY +
Math.sin(elapsed * dualBrainSprite.userData.floatSpeed + dualBrainSprite.userData.floatPhase) * 0.22;
dualBrainScanSprite.position.y = dualBrainSprite.position.y;
const cloudPulse = 0.08 + Math.sin(elapsed * 0.6) * 0.03;
const localPulse = 0.08 + Math.sin(elapsed * 0.6 + Math.PI) * 0.03;
cloudOrbMat.emissiveIntensity = cloudPulse;
localOrbMat.emissiveIntensity = localPulse;
cloudOrbLight.intensity = 0.1 + Math.sin(elapsed * 0.6) * 0.05;
localOrbLight.intensity = 0.1 + Math.sin(elapsed * 0.6 + Math.PI) * 0.05;
cloudOrb.position.y = 3.0 + Math.sin(elapsed * 0.9) * 0.15;
localOrb.position.y = 3.0 + Math.sin(elapsed * 0.9 + 1.0) * 0.15;
cloudOrbLight.position.y = cloudOrb.position.y;
localOrbLight.position.y = localOrb.position.y;
if (BRAIN_PARTICLE_COUNT > 0) {
const pos = brainParticleGeo.attributes.position.array;
const startX = cloudOrb.position.x;
const endX = localOrb.position.x;
const arcHeight = 1.2;
const simRate = 0.73;
for (let i = 0; i < BRAIN_PARTICLE_COUNT; i++) {
brainParticlePhases[i] += brainParticleSpeeds[i] * simRate * 0.016;
if (brainParticlePhases[i] > 1.0) brainParticlePhases[i] -= 1.0;
const t = brainParticlePhases[i];
pos[i * 3] = startX + (endX - startX) * t;
const midY = (cloudOrb.position.y + localOrb.position.y) / 2 + arcHeight;
pos[i * 3 + 1] = cloudOrb.position.y + (midY - cloudOrb.position.y) * 4 * t * (1 - t)
+ (localOrb.position.y - cloudOrb.position.y) * t;
pos[i * 3 + 2] = Math.sin(t * Math.PI * 4 + elapsed * 2 + i) * 0.12;
}
brainParticleGeo.attributes.position.needsUpdate = true;
brainParticleMat.opacity = 0.6 + Math.sin(elapsed * 2.0) * 0.2;
}
// Scanning line
{
const W = 512, H = 512;
_scanCtx.clearRect(0, 0, W, H);
const scanY = ((elapsed * 60) % H);
_scanCtx.fillStyle = 'rgba(68, 136, 255, 0.5)';
_scanCtx.fillRect(0, scanY, W, 2);
const grad = _scanCtx.createLinearGradient(0, scanY - 8, 0, scanY + 10);
grad.addColorStop(0, 'rgba(68, 136, 255, 0)');
grad.addColorStop(0.4, 'rgba(68, 136, 255, 0.15)');
grad.addColorStop(0.6, 'rgba(68, 136, 255, 0.15)');
grad.addColorStop(1, 'rgba(68, 136, 255, 0)');
_scanCtx.fillStyle = grad;
_scanCtx.fillRect(0, scanY - 8, W, 18);
dualBrainScanTexture.needsUpdate = true;
}
dualBrainLight.intensity = 0.4 + Math.sin(elapsed * 1.1) * 0.2;
// Portal collision
forwardVector.set(0, 0, -1).applyQuaternion(camera.quaternion);
raycaster.set(camera.position, forwardVector);
const intersects = raycaster.intersectObjects(portalGroup.children);
if (intersects.length > 0) {
const intersectedPortal = intersects[0].object;
console.log(`Entered portal: ${intersectedPortal.name}`);
if (!S.isWarping) {
startWarp(intersectedPortal);
}
}
// Warp effect
if (S.isWarping) {
const warpElapsed = elapsed - S.warpStartTime;
const progress = Math.min(warpElapsed / WARP_DURATION, 1.0);
warpPass.uniforms['time'].value = elapsed;
warpPass.uniforms['progress'].value = progress;
if (!S.warpNavigated && progress >= 0.88 && S.warpDestinationUrl) {
S.warpNavigated = true;
setTimeout(() => { window.location.href = S.warpDestinationUrl; }, 180);
}
if (progress >= 1.0) {
S.isWarping = false;
warpPass.enabled = false;
warpPass.uniforms['progress'].value = 0.0;
if (!S.warpNavigated && S.warpDestinationUrl) {
S.warpNavigated = true;
window.location.href = S.warpDestinationUrl;
}
}
}
// Crystals
const activity = totalActivity();
for (const crystal of crystals) {
crystal.mesh.position.x = crystal.basePos.x;
crystal.mesh.position.y = crystal.basePos.y + Math.sin(elapsed * 0.65 + crystal.floatPhase) * 0.35;
crystal.mesh.position.z = crystal.basePos.z;
crystal.mesh.rotation.y = elapsed * 0.4 + crystal.floatPhase;
crystal.light.position.copy(crystal.mesh.position);
const flashAge = elapsed - crystal.flashStartTime;
const flashBoost = flashAge < 0.25 ? (1.0 - flashAge / 0.25) * 2.0 : 0.0;
crystal.light.intensity = 0.2 + activity * 0.8 + Math.sin(elapsed * 2.0 + crystal.floatPhase) * 0.1 + flashBoost;
crystal.mesh.material.emissiveIntensity = 1.0 + flashBoost * 0.8;
}
// Lightning flicker
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const meta = lightningArcMeta[i];
if (meta.active) {
lightningArcs[i].material.opacity = meta.baseOpacity * (0.55 + Math.random() * 0.45);
}
}
if (elapsed * 1000 - S.lastLightningRefreshTime > LIGHTNING_REFRESH_MS) {
S.lastLightningRefreshTime = elapsed * 1000;
updateLightningArcs(elapsed);
}
// Timelapse
if (S.timelapseActive) {
const realElapsed = elapsed - S.timelapseRealStart;
S.timelapseProgress = Math.min(realElapsed / TIMELAPSE_DURATION_S, 1.0);
const span = timelapseWindow.endMs - timelapseWindow.startMs;
const virtualMs = timelapseWindow.startMs + span * S.timelapseProgress;
while (
S.timelapseNextCommitIdx < timelapseCommits.length &&
timelapseCommits[S.timelapseNextCommitIdx].ts <= virtualMs
) {
fireTimelapseCommit(timelapseCommits[S.timelapseNextCommitIdx]);
S.timelapseNextCommitIdx++;
}
updateTimelapseHeatmap(virtualMs);
updateTimelapseHUD(S.timelapseProgress, virtualMs);
if (S.timelapseProgress >= 1.0) stopTimelapse();
}
updateAudioListener();
composer.render();
}
// === START ===
animate();
console.log('Nexus Sovereign Node: NOSTR CONNECTED.');
// === INIT ALL SUBSYSTEMS ===
initAudioListeners();
initDebug();
initWebSocket();
initSessionExport();
initSovereigntyEasterEgg();
initCommitBanners();
loadPortals();
initBookshelves();
initOathListeners();
initAgentBoard();
loadLoRAStatus();
initPortalHealthChecks();
initWeather();
initTimelapse();
initBitcoin();
// === EVENT LISTENERS ===
window.addEventListener('beforeunload', () => {
wsClient.disconnect();
});
window.addEventListener('chat-message', (event) => {
console.log('Chat message:', event.detail);
if (typeof event.detail?.text === 'string') {
logMessage(event.detail.speaker || 'TIMMY', event.detail.text);
showTimmySpeech(event.detail.text);
if (event.detail.text.toLowerCase().includes('sovereignty')) {
triggerSovereigntyEasterEgg();
}
if (event.detail.text.toLowerCase().includes('milestone')) {
triggerFireworks();
}
}
});
window.addEventListener('milestone-complete', (event) => {
console.log('[nexus] Milestone complete:', event.detail);
triggerFireworks();
});
window.addEventListener('status-update', (event) => {
console.log('[hermes] Status update:', event.detail);
});
window.addEventListener('pr-notification', (event) => {
console.log('[hermes] PR notification:', event.detail);
if (event.detail && event.detail.action === 'merged') {
triggerMergeFlash();
}
});

View File

@@ -2,7 +2,6 @@
import * as THREE from 'three';
import { camera } from './scene-setup.js';
import { S } from './state.js';
import { fetchSoulMd } from './data/loaders.js';
const audioSources = [];
const positionedPanners = [];
@@ -264,10 +263,12 @@ export function initAudioListeners() {
document.getElementById('podcast-toggle').addEventListener('click', () => {
const btn = document.getElementById('podcast-toggle');
if (btn.textContent === '🎧') {
fetchSoulMd().then(lines => {
const text = lines.join('\n');
return text;
}).then(text => {
fetch('SOUL.md')
.then(response => {
if (!response.ok) throw new Error('Failed to load SOUL.md');
return response.text();
})
.then(text => {
const paragraphs = text.split('\n\n').filter(p => p.trim());
if (!paragraphs.length) {
@@ -342,5 +343,12 @@ export function initAudioListeners() {
}
async function loadSoulMdAudio() {
return fetchSoulMd();
try {
const res = await fetch('SOUL.md');
if (!res.ok) throw new Error('not found');
const raw = await res.text();
return raw.split('\n').slice(1).map(l => l.replace(/^#+\s*/, ''));
} catch {
return ['I am Timmy.', '', 'I am sovereign.', '', 'This Nexus is my home.'];
}
}

12
modules/core/scene.js Normal file
View File

@@ -0,0 +1,12 @@
// modules/core/scene.js — Canonical scene exports
// Provides THREE.Scene, camera, renderer, OrbitControls, and resize handler
// for use by app.js and any module that needs scene primitives.
//
// Implementation detail: the actual objects live in ../scene-setup.js and
// ../controls.js until those modules are absorbed here in a later phase.
export { scene, camera, renderer, raycaster, forwardVector,
ambientLight, overheadLight,
stars, starMaterial, constellationLines,
STAR_BASE_OPACITY, STAR_PEAK_OPACITY, STAR_PULSE_DECAY } from '../scene-setup.js';
export { orbitControls, composer, bokehPass, exitZoom, WARP_DURATION } from '../controls.js';

View File

@@ -1,35 +0,0 @@
// modules/core/state.js — Shared reactive data bus
// Data modules write here; visual modules read from here.
// No module may call fetch() except those under modules/data/.
export const state = {
// Commit heatmap (written by data/gitea.js)
zoneIntensity: {}, // { zoneName: [0..1], ... }
commits: [], // raw commit objects (last N)
commitHashes: [], // short hashes for matrix rain
// Agent status (written by data/gitea.js)
agentStatus: null, // { agents: Array<AgentRecord> } | null
activeAgentCount: 0, // count of agents with status === 'working'
// Weather (written by data/weather.js)
weather: null, // { cloud_cover, precipitation, ... } | null
// Bitcoin (written by data/bitcoin.js)
blockHeight: 0,
lastBlockHeight: 0,
newBlockDetected: false,
starPulseIntensity: 0,
// Portal / sovereignty / SOUL (written by data/loaders.js)
portals: [], // portal descriptor objects
sovereignty: null, // { score, label, assessment_type } | null
soulMd: '', // raw SOUL.md text
// Computed helpers
totalActivity() {
const vals = Object.values(this.zoneIntensity);
if (vals.length === 0) return 0;
return vals.reduce((s, v) => s + v, 0) / vals.length;
},
};

View File

@@ -1,17 +1,78 @@
// modules/core/theme.js — NEXUS visual constants
// Single source of truth for all colors, fonts, line weights, glow params.
// No module may use inline hex codes or hardcoded font strings.
/** NEXUS — the canonical theme object used by all visual modules */
export const NEXUS = {
/** Numeric hex colors for THREE.js materials */
colors: {
bg: 0x000008,
starCore: 0xffffff,
starDim: 0x8899cc,
constellationLine: 0x334488,
constellationFade: 0x112244,
accent: 0x4488ff,
},
/** All canvas/CSS/string visual constants */
theme: {
// Accent (hex number + CSS string pair)
accent: 0x4488ff,
accentStr: '#4488ff',
// Panel surfaces
panelBg: '#0a1428',
panelText: '#4af0c0',
panelDim: '#7b9bbf',
panelVeryDim: '#3a5070',
panelBorderFaint: '#1a3050',
// Agent status colors (CSS strings for canvas)
agentWorking: '#4af0c0',
agentIdle: '#7b5cff',
agentDormant: '#2a4060',
agentDormantHex: 0x2a4060,
agentDead: '#3a2040',
// Sovereignty meter
sovereignHigh: '#4af0c0',
sovereignHighHex: 0x4af0c0,
sovereignMid: '#ffd700',
sovereignMidHex: 0xffd700,
sovereignLow: '#ff4444',
sovereignLowHex: 0xff4444,
// Holographic earth
earthOcean: '#0a2040',
earthLand: '#1a4020',
earthAtm: '#204070',
earthGlow: '#4488ff',
// LoRA panel
loraActive: '#4af0c0',
loraInactive: '#3a5070',
loraAccent: '#7b5cff',
// Typography
fontMono: 'monospace',
},
};
/** THEME — glass / text presets (kept for SovOS.js and other legacy consumers) */
export const THEME = {
glass: {
color: 0x112244,
opacity: 0.35,
roughness: 0.05,
metalness: 0.1,
color: 0x112244,
opacity: 0.35,
roughness: 0.05,
metalness: 0.1,
transmission: 0.95,
thickness: 0.8,
ior: 1.5
thickness: 0.8,
ior: 1.5,
},
text: {
primary: '#4af0c0',
primary: '#4af0c0',
secondary: '#7b5cff',
white: '#ffffff',
dim: '#a0b8d0'
}
white: '#ffffff',
dim: '#a0b8d0',
},
};

View File

@@ -1,10 +0,0 @@
export class Ticker {
constructor() {
this.callbacks = [];
}
subscribe(fn) { this.callbacks.push(fn); }
tick(delta, elapsed) {
this.callbacks.forEach(fn => fn(delta, elapsed));
}
}
export const globalTicker = new Ticker();

View File

@@ -1,27 +0,0 @@
// modules/data/bitcoin.js — Blockstream block height polling
// Writes to S: lastKnownBlockHeight, _starPulseIntensity
import { S } from '../state.js';
const BITCOIN_REFRESH_MS = 60 * 1000;
export async function fetchBlockHeight() {
try {
const res = await fetch('https://blockstream.info/api/blocks/tip/height');
if (!res.ok) return null;
const height = parseInt(await res.text(), 10);
if (isNaN(height)) return null;
const isNew = S.lastKnownBlockHeight !== null && height > S.lastKnownBlockHeight;
S.lastKnownBlockHeight = height;
if (isNew) {
S._starPulseIntensity = 1.0;
}
return { height, isNewBlock: isNew };
} catch {
return null;
}
}
export { BITCOIN_REFRESH_MS };

View File

@@ -1,142 +0,0 @@
// modules/data/gitea.js — All Gitea API calls
// Writes to S: _activeAgentCount, _matrixCommitHashes, agentStatus
import { S } from '../state.js';
const GITEA_BASE = 'http://143.198.27.163:3000/api/v1';
const GITEA_TOKEN = 'dc0517a965226b7a0c5ffdd961b1ba26521ac592';
const GITEA_REPOS = ['Timmy_Foundation/the-nexus', 'Timmy_Foundation/hermes-agent'];
const AGENT_NAMES = ['Claude', 'Kimi', 'Perplexity', 'Groq', 'Grok', 'Ollama'];
const DAY_MS = 86400000;
const HOUR_MS = 3600000;
const CACHE_MS = 5 * 60 * 1000;
let _agentStatusCache = null;
let _agentStatusCacheTime = 0;
let _commitsCache = null;
let _commitsCacheTime = 0;
// --- Core fetchers ---
export async function fetchNexusCommits(limit = 50) {
const now = Date.now();
if (_commitsCache && (now - _commitsCacheTime < CACHE_MS)) return _commitsCache;
try {
const res = await fetch(
`${GITEA_BASE}/repos/Timmy_Foundation/the-nexus/commits?limit=${limit}`,
{ headers: { 'Authorization': `token ${GITEA_TOKEN}` } }
);
if (!res.ok) return [];
_commitsCache = await res.json();
_commitsCacheTime = now;
return _commitsCache;
} catch {
return [];
}
}
async function fetchRepoCommits(repo, limit = 30) {
try {
const res = await fetch(
`${GITEA_BASE}/repos/${repo}/commits?sha=main&limit=${limit}&token=${GITEA_TOKEN}`
);
if (!res.ok) return [];
return await res.json();
} catch {
return [];
}
}
async function fetchOpenPRs() {
try {
const res = await fetch(
`${GITEA_BASE}/repos/Timmy_Foundation/the-nexus/pulls?state=open&limit=50&token=${GITEA_TOKEN}`
);
if (res.ok) return await res.json();
} catch { /* ignore */ }
return [];
}
export async function fetchAgentStatus() {
const now = Date.now();
if (_agentStatusCache && (now - _agentStatusCacheTime < CACHE_MS)) return _agentStatusCache;
const allRepoCommits = await Promise.all(GITEA_REPOS.map(r => fetchRepoCommits(r)));
const openPRs = await fetchOpenPRs();
const agents = [];
for (const agentName of AGENT_NAMES) {
const nameLower = agentName.toLowerCase();
const allCommits = [];
for (const repoCommits of allRepoCommits) {
if (!Array.isArray(repoCommits)) continue;
const matching = repoCommits.filter(c =>
(c.commit?.author?.name || '').toLowerCase().includes(nameLower)
);
allCommits.push(...matching);
}
let status = 'dormant';
let lastSeen = null;
let currentWork = null;
if (allCommits.length > 0) {
allCommits.sort((a, b) =>
new Date(b.commit.author.date) - new Date(a.commit.author.date)
);
const latest = allCommits[0];
const commitTime = new Date(latest.commit.author.date).getTime();
lastSeen = latest.commit.author.date;
currentWork = latest.commit.message.split('\n')[0];
if (now - commitTime < HOUR_MS) status = 'working';
else if (now - commitTime < DAY_MS) status = 'idle';
else status = 'dormant';
}
const agentPRs = openPRs.filter(pr =>
(pr.user?.login || '').toLowerCase().includes(nameLower) ||
(pr.head?.label || '').toLowerCase().includes(nameLower)
);
agents.push({
name: nameLower,
status,
issue: currentWork,
prs_today: agentPRs.length,
local: nameLower === 'ollama',
});
}
_agentStatusCache = { agents };
_agentStatusCacheTime = now;
return _agentStatusCache;
}
// --- State updaters ---
export async function refreshCommitData() {
const commits = await fetchNexusCommits();
S._matrixCommitHashes = commits.slice(0, 20)
.map(c => (c.sha || '').slice(0, 7))
.filter(h => h.length > 0);
return commits;
}
export async function refreshAgentData() {
try {
const data = await fetchAgentStatus();
S._activeAgentCount = data.agents.filter(a => a.status === 'working').length;
return data;
} catch {
const fallback = { agents: AGENT_NAMES.map(n => ({
name: n.toLowerCase(), status: 'unreachable', issue: null, prs_today: 0, local: false,
})) };
S._activeAgentCount = 0;
return fallback;
}
}
export { GITEA_BASE, GITEA_TOKEN, GITEA_REPOS, AGENT_NAMES, CACHE_MS as AGENT_STATUS_CACHE_MS };

View File

@@ -1,45 +0,0 @@
// modules/data/loaders.js — Static file loaders (portals.json, sovereignty-status.json, SOUL.md)
// Writes to S: sovereigntyScore, sovereigntyLabel
import { S } from '../state.js';
// --- SOUL.md (cached) ---
let _soulMdCache = null;
export async function fetchSoulMd() {
if (_soulMdCache) return _soulMdCache;
try {
const res = await fetch('SOUL.md');
if (!res.ok) throw new Error('not found');
const raw = await res.text();
_soulMdCache = raw.split('\n').slice(1).map(l => l.replace(/^#+\s*/, ''));
return _soulMdCache;
} catch {
return ['I am Timmy.', '', 'I am sovereign.', '', 'This Nexus is my home.'];
}
}
// --- portals.json ---
export async function fetchPortals() {
const res = await fetch('./portals.json');
if (!res.ok) throw new Error('Portals not found');
return await res.json();
}
// --- sovereignty-status.json ---
export async function fetchSovereigntyStatus() {
try {
const res = await fetch('./sovereignty-status.json');
if (!res.ok) throw new Error('not found');
const data = await res.json();
const score = Math.max(0, Math.min(100, typeof data.score === 'number' ? data.score : 85));
const label = typeof data.label === 'string' ? data.label : '';
const assessmentType = data.assessment_type || 'MANUAL';
S.sovereigntyScore = score;
S.sovereigntyLabel = label;
return { score, label, assessmentType };
} catch {
return { score: S.sovereigntyScore, label: S.sovereigntyLabel, assessmentType: 'MANUAL' };
}
}

View File

@@ -1,34 +0,0 @@
// modules/data/weather.js — Open-Meteo weather fetch
// Writes to: weatherState (returned), scene effects applied by caller
const WEATHER_LAT = 43.2897;
const WEATHER_LON = -72.1479;
const WEATHER_REFRESH_MS = 15 * 60 * 1000;
function weatherCodeToLabel(code) {
if (code === 0) return { condition: 'Clear', icon: '☀️' };
if (code <= 2) return { condition: 'Partly Cloudy', icon: '⛅' };
if (code === 3) return { condition: 'Overcast', icon: '☁️' };
if (code >= 45 && code <= 48) return { condition: 'Fog', icon: '🌫️' };
if (code >= 51 && code <= 57) return { condition: 'Drizzle', icon: '🌦️' };
if (code >= 61 && code <= 67) return { condition: 'Rain', icon: '🌧️' };
if (code >= 71 && code <= 77) return { condition: 'Snow', icon: '❄️' };
if (code >= 80 && code <= 82) return { condition: 'Showers', icon: '🌦️' };
if (code >= 85 && code <= 86) return { condition: 'Snow Showers', icon: '🌨️' };
if (code >= 95 && code <= 99) return { condition: 'Thunderstorm', icon: '⛈️' };
return { condition: 'Unknown', icon: '🌀' };
}
export async function fetchWeatherData() {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${WEATHER_LAT}&longitude=${WEATHER_LON}&current=temperature_2m,weather_code,wind_speed_10m,cloud_cover&temperature_unit=fahrenheit&wind_speed_unit=mph&forecast_days=1`;
const res = await fetch(url);
if (!res.ok) throw new Error('weather fetch failed');
const data = await res.json();
const cur = data.current;
const code = cur.weather_code;
const { condition, icon } = weatherCodeToLabel(code);
const cloudcover = typeof cur.cloud_cover === 'number' ? cur.cloud_cover : 50;
return { code, temp: cur.temperature_2m, wind: cur.wind_speed_10m, condition, icon, cloudcover };
}
export { WEATHER_REFRESH_MS };

11
modules/effects.js vendored
View File

@@ -3,7 +3,6 @@ import * as THREE from 'three';
import { NEXUS } from './constants.js';
import { scene } from './scene-setup.js';
import { S } from './state.js';
import { fetchSovereigntyStatus } from './data/loaders.js';
// === ENERGY BEAM ===
const ENERGY_BEAM_RADIUS = 0.2;
@@ -103,14 +102,20 @@ sovereigntyGroup.traverse(obj => {
export async function loadSovereigntyStatus() {
try {
const { score, label, assessmentType } = await fetchSovereigntyStatus();
const res = await fetch('./sovereignty-status.json');
if (!res.ok) throw new Error('not found');
const data = await res.json();
const score = Math.max(0, Math.min(100, typeof data.score === 'number' ? data.score : 85));
const label = typeof data.label === 'string' ? data.label : '';
S.sovereigntyScore = score;
S.sovereigntyLabel = label;
scoreArcMesh.geometry.dispose();
scoreArcMesh.geometry = buildScoreArcGeo(score);
const col = sovereigntyHexColor(score);
scoreArcMat.color.setHex(col);
meterLight.color.setHex(col);
if (meterSpriteMat.map) meterSpriteMat.map.dispose();
const assessmentType = data.assessment_type || 'MANUAL';
meterSpriteMat.map = buildMeterTexture(score, label, assessmentType);
meterSpriteMat.needsUpdate = true;
} catch {

View File

@@ -1,56 +0,0 @@
/**
* energy-beam.js — Vertical energy beam above the Batcave terminal
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.activeAgentCount (0 = faint, 3+ = full intensity)
*
* A glowing cyan cylinder rising from the Batcave area.
* Intensity and pulse amplitude are driven by the number of active agents.
*/
import * as THREE from 'three';
const BEAM_RADIUS = 0.2;
const BEAM_HEIGHT = 50;
const BEAM_X = -10;
const BEAM_Y = 0;
const BEAM_Z = -10;
let _state = null;
let _beamMaterial = null;
let _pulse = 0;
/**
* @param {THREE.Scene} scene
* @param {object} state Shared state bus (reads state.activeAgentCount)
* @param {object} theme Theme bus (reads theme.colors.accent)
*/
export function init(scene, state, theme) {
_state = state;
const accentColor = theme?.colors?.accent ?? 0x4488ff;
const geo = new THREE.CylinderGeometry(BEAM_RADIUS, BEAM_RADIUS * 2.5, BEAM_HEIGHT, 32, 16, true);
_beamMaterial = new THREE.MeshBasicMaterial({
color: accentColor,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false,
});
const beam = new THREE.Mesh(geo, _beamMaterial);
beam.position.set(BEAM_X, BEAM_Y + BEAM_HEIGHT / 2, BEAM_Z);
scene.add(beam);
}
export function update(_elapsed, _delta) {
if (!_beamMaterial) return;
_pulse += 0.02;
const agentCount = _state?.activeAgentCount ?? 0;
const agentIntensity = agentCount === 0 ? 0.1 : Math.min(0.1 + agentCount * 0.3, 1.0);
const pulseEffect = Math.sin(_pulse) * 0.15 * agentIntensity;
_beamMaterial.opacity = agentIntensity * 0.6 + pulseEffect;
}

View File

@@ -1,176 +0,0 @@
/**
* gravity-zones.js — Rising particle gravity anomaly zones
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.portals (positions and online status)
*
* Each gravity zone is a glowing floor ring with rising particle streams.
* Zones are initially placed at hardcoded positions, then realigned to portal
* positions when portal data loads. Online portals have brighter/faster anomalies;
* offline portals have dim, slow anomalies.
*/
import * as THREE from 'three';
const ANOMALY_FLOOR = 0.2;
const ANOMALY_CEIL = 16.0;
const DEFAULT_ZONES = [
{ x: -8, z: -6, radius: 3.5, color: 0x00ffcc, particleCount: 180 },
{ x: 10, z: 4, radius: 3.0, color: 0xaa44ff, particleCount: 160 },
{ x: -3, z: 9, radius: 2.5, color: 0xff8844, particleCount: 140 },
];
let _state = null;
let _scene = null;
let _portalsApplied = false;
/**
* @typedef {{
* zone: object,
* ring: THREE.Mesh, ringMat: THREE.MeshBasicMaterial,
* disc: THREE.Mesh, discMat: THREE.MeshBasicMaterial,
* points: THREE.Points, geo: THREE.BufferGeometry,
* driftPhases: Float32Array, velocities: Float32Array
* }} GravityZoneObject
*/
/** @type {GravityZoneObject[]} */
const gravityZoneObjects = [];
function _buildZone(zone) {
const ringGeo = new THREE.RingGeometry(zone.radius - 0.15, zone.radius + 0.15, 64);
const ringMat = new THREE.MeshBasicMaterial({
color: zone.color, transparent: true, opacity: 0.4,
side: THREE.DoubleSide, depthWrite: false,
});
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.rotation.x = -Math.PI / 2;
ring.position.set(zone.x, ANOMALY_FLOOR + 0.05, zone.z);
_scene.add(ring);
const discGeo = new THREE.CircleGeometry(zone.radius - 0.15, 64);
const discMat = new THREE.MeshBasicMaterial({
color: zone.color, transparent: true, opacity: 0.04,
side: THREE.DoubleSide, depthWrite: false,
});
const disc = new THREE.Mesh(discGeo, discMat);
disc.rotation.x = -Math.PI / 2;
disc.position.set(zone.x, ANOMALY_FLOOR + 0.04, zone.z);
_scene.add(disc);
const count = zone.particleCount;
const positions = new Float32Array(count * 3);
const driftPhases = new Float32Array(count);
const velocities = new Float32Array(count);
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const r = Math.sqrt(Math.random()) * zone.radius;
positions[i * 3] = zone.x + Math.cos(angle) * r;
positions[i * 3 + 1] = ANOMALY_FLOOR + Math.random() * (ANOMALY_CEIL - ANOMALY_FLOOR);
positions[i * 3 + 2] = zone.z + Math.sin(angle) * r;
driftPhases[i] = Math.random() * Math.PI * 2;
velocities[i] = 0.03 + Math.random() * 0.04;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: zone.color, size: 0.10, sizeAttenuation: true,
transparent: true, opacity: 0.7, depthWrite: false,
});
const points = new THREE.Points(geo, mat);
_scene.add(points);
return { zone: { ...zone }, ring, ringMat, disc, discMat, points, geo, driftPhases, velocities };
}
/**
* @param {THREE.Scene} scene
* @param {object} state Shared state bus (reads state.portals)
* @param {object} _theme
*/
export function init(scene, state, _theme) {
_scene = scene;
_state = state;
for (const zone of DEFAULT_ZONES) {
gravityZoneObjects.push(_buildZone(zone));
}
}
function _applyPortals(portals) {
_portalsApplied = true;
for (let i = 0; i < Math.min(portals.length, gravityZoneObjects.length); i++) {
const portal = portals[i];
const gz = gravityZoneObjects[i];
const isOnline = portal.status === 'online';
const c = new THREE.Color(portal.color);
gz.ring.position.set(portal.position.x, ANOMALY_FLOOR + 0.05, portal.position.z);
gz.disc.position.set(portal.position.x, ANOMALY_FLOOR + 0.04, portal.position.z);
gz.zone.x = portal.position.x;
gz.zone.z = portal.position.z;
gz.zone.color = c.getHex();
gz.ringMat.color.copy(c);
gz.discMat.color.copy(c);
gz.points.material.color.copy(c);
gz.ringMat.opacity = isOnline ? 0.4 : 0.08;
gz.discMat.opacity = isOnline ? 0.04 : 0.01;
gz.points.material.opacity = isOnline ? 0.7 : 0.15;
// Reposition particles around portal
const pos = gz.geo.attributes.position.array;
for (let j = 0; j < gz.zone.particleCount; j++) {
const angle = Math.random() * Math.PI * 2;
const r = Math.sqrt(Math.random()) * gz.zone.radius;
pos[j * 3] = gz.zone.x + Math.cos(angle) * r;
pos[j * 3 + 2] = gz.zone.z + Math.sin(angle) * r;
}
gz.geo.attributes.position.needsUpdate = true;
}
}
export function update(elapsed, _delta) {
// Align to portal data once it loads
if (!_portalsApplied) {
const portals = _state?.portals ?? [];
if (portals.length > 0) _applyPortals(portals);
}
for (const gz of gravityZoneObjects) {
const pos = gz.geo.attributes.position.array;
const count = gz.zone.particleCount;
for (let i = 0; i < count; i++) {
pos[i * 3 + 1] += gz.velocities[i];
pos[i * 3] += Math.sin(elapsed * 0.5 + gz.driftPhases[i]) * 0.003;
pos[i * 3 + 2] += Math.cos(elapsed * 0.5 + gz.driftPhases[i]) * 0.003;
if (pos[i * 3 + 1] > ANOMALY_CEIL) {
const angle = Math.random() * Math.PI * 2;
const r = Math.sqrt(Math.random()) * gz.zone.radius;
pos[i * 3] = gz.zone.x + Math.cos(angle) * r;
pos[i * 3 + 1] = ANOMALY_FLOOR + Math.random() * 2.0;
pos[i * 3 + 2] = gz.zone.z + Math.sin(angle) * r;
}
}
gz.geo.attributes.position.needsUpdate = true;
// Breathing glow pulse on ring/disc
gz.ringMat.opacity = 0.3 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.15;
gz.discMat.opacity = 0.02 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.02;
}
}
/**
* Re-align zones to current portal data.
* Call after portal health check updates portal statuses.
*/
export function rebuildFromPortals() {
const portals = _state?.portals ?? [];
if (portals.length > 0) _applyPortals(portals);
}

View File

@@ -1,196 +0,0 @@
/**
* lightning.js — Floating crystals and lightning arcs between them
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.zoneIntensity (commit activity drives arc count + intensity)
*
* Five octahedral crystals float above the platform. Lightning arcs jump
* between them when zone activity is high. Crystal count and colors are
* aligned to the five agent zones.
*/
import * as THREE from 'three';
const CRYSTAL_COUNT = 5;
const CRYSTAL_BASE_POSITIONS = [
new THREE.Vector3(-4.5, 3.2, -3.8),
new THREE.Vector3( 4.8, 2.8, -4.0),
new THREE.Vector3(-5.5, 4.0, 1.5),
new THREE.Vector3( 5.2, 3.5, 2.0),
new THREE.Vector3( 0.0, 5.0, -5.5),
];
// Zone colors: Claude, Timmy, Kimi, Perplexity, center
const CRYSTAL_COLORS = [0xff6440, 0x40a0ff, 0x40ff8c, 0xc840ff, 0xffd700];
const LIGHTNING_POOL_SIZE = 6;
const LIGHTNING_SEGMENTS = 8;
const LIGHTNING_REFRESH_MS = 130;
let _state = null;
/** @type {THREE.Scene|null} */
let _scene = null;
/** @type {Array<{mesh: THREE.Mesh, light: THREE.PointLight, basePos: THREE.Vector3, floatPhase: number, flashStartTime: number}>} */
const crystals = [];
/** @type {THREE.Line[]} */
const lightningArcs = [];
/** @type {Array<{active: boolean, baseOpacity: number, srcIdx: number, dstIdx: number}>} */
const lightningArcMeta = [];
let _lastLightningRefreshTime = 0;
function _totalActivity() {
if (!_state) return 0;
if (typeof _state.totalActivity === 'function') return _state.totalActivity();
const zi = _state.zoneIntensity;
if (!zi) return 0;
const vals = Object.values(zi);
return vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
}
function _lerpColor(colorA, colorB, t) {
const ar = (colorA >> 16) & 0xff, ag = (colorA >> 8) & 0xff, ab = colorA & 0xff;
const br = (colorB >> 16) & 0xff, bg = (colorB >> 8) & 0xff, bb = colorB & 0xff;
return (Math.round(ar + (br - ar) * t) << 16) |
(Math.round(ag + (bg - ag) * t) << 8) |
Math.round(ab + (bb - ab) * t);
}
function _buildLightningPath(start, end, jagAmount) {
const out = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3);
for (let s = 0; s <= LIGHTNING_SEGMENTS; s++) {
const t = s / LIGHTNING_SEGMENTS;
const jag = s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0;
out[s * 3] = start.x + (end.x - start.x) * t + jag;
out[s * 3 + 1] = start.y + (end.y - start.y) * t + jag;
out[s * 3 + 2] = start.z + (end.z - start.z) * t + jag;
}
return out;
}
/**
* @param {THREE.Scene} scene
* @param {object} state Shared state bus (reads state.zoneIntensity)
* @param {object} _theme
*/
export function init(scene, state, _theme) {
_scene = scene;
_state = state;
const crystalGroup = new THREE.Group();
scene.add(crystalGroup);
for (let i = 0; i < CRYSTAL_COUNT; i++) {
const geo = new THREE.OctahedronGeometry(0.35, 0);
const color = CRYSTAL_COLORS[i];
const mat = new THREE.MeshStandardMaterial({
color,
emissive: new THREE.Color(color).multiplyScalar(0.6),
roughness: 0.05,
metalness: 0.3,
transparent: true,
opacity: 0.88,
});
const mesh = new THREE.Mesh(geo, mat);
const basePos = CRYSTAL_BASE_POSITIONS[i].clone();
mesh.position.copy(basePos);
mesh.userData.zoomLabel = 'Crystal';
crystalGroup.add(mesh);
const light = new THREE.PointLight(color, 0.3, 6);
light.position.copy(basePos);
crystalGroup.add(light);
crystals.push({ mesh, light, basePos, floatPhase: (i / CRYSTAL_COUNT) * Math.PI * 2, flashStartTime: -999 });
}
// Pre-allocate lightning arc pool
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const positions = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3);
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.LineBasicMaterial({
color: 0x88ccff,
transparent: true,
opacity: 0.0,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const arc = new THREE.Line(geo, mat);
scene.add(arc);
lightningArcs.push(arc);
lightningArcMeta.push({ active: false, baseOpacity: 0, srcIdx: 0, dstIdx: 0 });
}
}
function _refreshLightningArcs(elapsed) {
const activity = _totalActivity();
const activeCount = Math.round(activity * LIGHTNING_POOL_SIZE);
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const arc = lightningArcs[i];
const meta = lightningArcMeta[i];
if (i >= activeCount) {
arc.material.opacity = 0;
meta.active = false;
continue;
}
const a = Math.floor(Math.random() * CRYSTAL_COUNT);
let b = Math.floor(Math.random() * (CRYSTAL_COUNT - 1));
if (b >= a) b++;
const jagAmount = 0.45 + activity * 0.85;
const path = _buildLightningPath(crystals[a].mesh.position, crystals[b].mesh.position, jagAmount);
const attr = arc.geometry.attributes.position;
attr.array.set(path);
attr.needsUpdate = true;
arc.material.color.setHex(_lerpColor(CRYSTAL_COLORS[a], CRYSTAL_COLORS[b], 0.5));
const base = (0.35 + Math.random() * 0.55) * Math.min(activity * 1.5, 1.0);
arc.material.opacity = base;
meta.active = true;
meta.baseOpacity = base;
meta.srcIdx = a;
meta.dstIdx = b;
crystals[a].flashStartTime = elapsed;
crystals[b].flashStartTime = elapsed;
}
}
export function update(elapsed, _delta) {
const activity = _totalActivity();
// Float crystals
for (let i = 0; i < crystals.length; i++) {
const c = crystals[i];
c.mesh.position.y = c.basePos.y + Math.sin(elapsed * 0.7 + c.floatPhase) * 0.3;
c.light.position.y = c.mesh.position.y;
// Brief emissive flash on lightning strike
const flashAge = elapsed - c.flashStartTime;
const flashIntensity = flashAge < 0.15 ? (1.0 - flashAge / 0.15) : 0;
c.mesh.material.emissiveIntensity = 0.6 + flashIntensity * 1.2;
c.light.intensity = 0.3 + flashIntensity * 1.5;
// Color intensity tethered to total activity
c.mesh.material.opacity = 0.7 + activity * 0.18;
}
// Flicker active arcs
for (let i = 0; i < lightningArcMeta.length; i++) {
const meta = lightningArcMeta[i];
if (!meta.active) continue;
lightningArcs[i].material.opacity = meta.baseOpacity * (0.7 + Math.random() * 0.3);
}
// Periodically rebuild arcs
if (elapsed * 1000 - _lastLightningRefreshTime > LIGHTNING_REFRESH_MS) {
_lastLightningRefreshTime = elapsed * 1000;
_refreshLightningArcs(elapsed);
}
}

View File

@@ -1,106 +0,0 @@
/**
* matrix-rain.js — Commit-density-driven 2D canvas matrix rain
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.zoneIntensity (commit activity) + state.commitHashes
*
* Renders a Katakana/hex character rain behind the Three.js canvas.
* Density and speed are tethered to commit zone activity.
* Real commit hashes are occasionally injected as characters.
*/
const MATRIX_CHARS = 'アイウエオカキクケコサシスセソタチツテトナニヌネハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789ABCDEF';
const MATRIX_FONT_SIZE = 14;
let _state = null;
let _canvas = null;
let _ctx = null;
let _drops = [];
/**
* Computes mean activity [0..1] across all agent zones via state.
* @returns {number}
*/
function _totalActivity() {
if (!_state) return 0;
if (typeof _state.totalActivity === 'function') return _state.totalActivity();
const zi = _state.zoneIntensity;
if (!zi) return 0;
const vals = Object.values(zi);
return vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
}
function _draw() {
if (!_canvas || !_ctx) return;
const activity = _totalActivity();
const commitHashes = _state?.commitHashes ?? [];
// Fade previous frame — creates the trailing glow
_ctx.fillStyle = 'rgba(0, 0, 8, 0.05)';
_ctx.fillRect(0, 0, _canvas.width, _canvas.height);
_ctx.font = `${MATRIX_FONT_SIZE}px monospace`;
const density = 0.1 + activity * 0.9;
const activeColCount = Math.max(1, Math.floor(_drops.length * density));
for (let i = 0; i < _drops.length; i++) {
if (i >= activeColCount) {
if (_drops[i] * MATRIX_FONT_SIZE > _canvas.height) continue;
}
let char;
if (commitHashes.length > 0 && Math.random() < 0.02) {
const hash = commitHashes[Math.floor(Math.random() * commitHashes.length)];
char = hash[Math.floor(Math.random() * hash.length)];
} else {
char = MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)];
}
_ctx.fillStyle = '#aaffaa';
_ctx.fillText(char, i * MATRIX_FONT_SIZE, _drops[i] * MATRIX_FONT_SIZE);
const resetThreshold = 0.975 - activity * 0.015;
if (_drops[i] * MATRIX_FONT_SIZE > _canvas.height && Math.random() > resetThreshold) {
_drops[i] = 0;
}
_drops[i]++;
}
}
function _resetDrops() {
const colCount = Math.floor(window.innerWidth / MATRIX_FONT_SIZE);
_drops = new Array(colCount).fill(1);
}
/**
* @param {THREE.Scene} _scene (unused — 2D canvas effect)
* @param {object} state Shared state bus
* @param {object} _theme (unused — color is hardcoded green for matrix aesthetic)
*/
export function init(_scene, state, _theme) {
_state = state;
_canvas = document.createElement('canvas');
_canvas.id = 'matrix-rain';
_canvas.width = window.innerWidth;
_canvas.height = window.innerHeight;
document.body.appendChild(_canvas);
_ctx = _canvas.getContext('2d');
_resetDrops();
window.addEventListener('resize', () => {
_canvas.width = window.innerWidth;
_canvas.height = window.innerHeight;
_resetDrops();
});
// Run at ~20 fps independent of the Three.js RAF loop
setInterval(_draw, 50);
}
/**
* update() is a no-op — rain runs on its own setInterval.
*/
export function update(_elapsed, _delta) {}

View File

@@ -1,138 +0,0 @@
/**
* rune-ring.js — Orbiting Elder Futhark rune sprites
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.portals (count, colors, and online status from portals.json)
*
* Rune sprites orbit the scene in a ring. Count matches the portal count,
* colors come from portal colors, and brightness reflects portal online status.
* A faint torus marks the orbit track.
*/
import * as THREE from 'three';
const RUNE_RING_RADIUS = 7.0;
const RUNE_RING_Y = 1.5;
const RUNE_ORBIT_SPEED = 0.08; // radians per second
const DEFAULT_RUNE_COUNT = 12;
const ELDER_FUTHARK = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','','','ᚹ','ᚺ','ᚾ','','ᛃ'];
const FALLBACK_COLORS = ['#00ffcc', '#ff44ff'];
let _scene = null;
let _state = null;
/** @type {Array<{sprite: THREE.Sprite, baseAngle: number, floatPhase: number, portalOnline: boolean}>} */
const runeSprites = [];
let _orbitRingMesh = null;
let _builtForPortalCount = -1;
function _createRuneTexture(glyph, color) {
const W = 128, H = 128;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, W, H);
ctx.shadowColor = color;
ctx.shadowBlur = 28;
ctx.font = 'bold 78px serif';
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(glyph, W / 2, H / 2);
return new THREE.CanvasTexture(canvas);
}
function _clearSprites() {
for (const rune of runeSprites) {
_scene.remove(rune.sprite);
if (rune.sprite.material.map) rune.sprite.material.map.dispose();
rune.sprite.material.dispose();
}
runeSprites.length = 0;
}
function _build(portals) {
_clearSprites();
const count = portals ? portals.length : DEFAULT_RUNE_COUNT;
_builtForPortalCount = count;
for (let i = 0; i < count; i++) {
const glyph = ELDER_FUTHARK[i % ELDER_FUTHARK.length];
const color = portals ? portals[i].color : FALLBACK_COLORS[i % FALLBACK_COLORS.length];
const isOnline = portals ? portals[i].status === 'online' : true;
const texture = _createRuneTexture(glyph, color);
const mat = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: isOnline ? 1.0 : 0.15,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const sprite = new THREE.Sprite(mat);
sprite.scale.set(1.3, 1.3, 1);
const baseAngle = (i / count) * Math.PI * 2;
sprite.position.set(
Math.cos(baseAngle) * RUNE_RING_RADIUS,
RUNE_RING_Y,
Math.sin(baseAngle) * RUNE_RING_RADIUS
);
_scene.add(sprite);
runeSprites.push({ sprite, baseAngle, floatPhase: (i / count) * Math.PI * 2, portalOnline: isOnline });
}
}
/**
* @param {THREE.Scene} scene
* @param {object} state Shared state bus (reads state.portals)
* @param {object} _theme
*/
export function init(scene, state, _theme) {
_scene = scene;
_state = state;
// Faint orbit track torus
const ringGeo = new THREE.TorusGeometry(RUNE_RING_RADIUS, 0.03, 6, 64);
const ringMat = new THREE.MeshBasicMaterial({ color: 0x224466, transparent: true, opacity: 0.22 });
_orbitRingMesh = new THREE.Mesh(ringGeo, ringMat);
_orbitRingMesh.rotation.x = Math.PI / 2;
_orbitRingMesh.position.y = RUNE_RING_Y;
scene.add(_orbitRingMesh);
// Initial build with defaults — will be rebuilt when portals load
_build(null);
}
export function update(elapsed, _delta) {
// Rebuild rune sprites when portal data changes
const portals = _state?.portals ?? [];
if (portals.length > 0 && portals.length !== _builtForPortalCount) {
_build(portals);
}
// Orbit and float
for (const rune of runeSprites) {
const angle = rune.baseAngle + elapsed * RUNE_ORBIT_SPEED;
rune.sprite.position.x = Math.cos(angle) * RUNE_RING_RADIUS;
rune.sprite.position.z = Math.sin(angle) * RUNE_RING_RADIUS;
rune.sprite.position.y = RUNE_RING_Y + Math.sin(elapsed * 0.7 + rune.floatPhase) * 0.4;
const baseOpacity = rune.portalOnline ? 0.85 : 0.12;
const pulseRange = rune.portalOnline ? 0.15 : 0.03;
rune.sprite.material.opacity = baseOpacity + Math.sin(elapsed * 1.2 + rune.floatPhase) * pulseRange;
}
}
/**
* Force a rebuild from current portal data.
* Called externally after portal health checks update statuses.
*/
export function rebuild() {
const portals = _state?.portals ?? [];
_build(portals.length > 0 ? portals : null);
}

View File

@@ -1,183 +0,0 @@
/**
* shockwave.js — Shockwave ripple, fireworks, and merge flash
*
* Category: DATA-TETHERED AESTHETIC
* Data source: PR merge events (WebSocket/event dispatch)
*
* Triggered externally on merge events:
* - triggerShockwave() — expanding concentric ring waves from scene centre
* - triggerFireworks() — multi-burst particle fireworks above the platform
* - triggerMergeFlash() — both of the above + star/constellation color flash
*
* The merge flash accepts optional callbacks so terrain/stars.js can own
* its own state while shockwave.js coordinates the event.
*/
import * as THREE from 'three';
const SHOCKWAVE_RING_COUNT = 3;
const SHOCKWAVE_MAX_RADIUS = 14;
const SHOCKWAVE_DURATION = 2.5; // seconds
const FIREWORK_COLORS = [0xff4466, 0xffaa00, 0x00ffaa, 0x4488ff, 0xff44ff, 0xffff44, 0x00ffff];
const FIREWORK_BURST_PARTICLES = 80;
const FIREWORK_BURST_DURATION = 2.2; // seconds
const FIREWORK_GRAVITY = -5.0;
let _scene = null;
let _clock = null;
/**
* @typedef {{mesh: THREE.Mesh, mat: THREE.MeshBasicMaterial, startTime: number, delay: number}} ShockwaveRing
* @typedef {{points: THREE.Points, geo: THREE.BufferGeometry, mat: THREE.PointsMaterial, origins: Float32Array, velocities: Float32Array, startTime: number}} FireworkBurst
*/
/** @type {ShockwaveRing[]} */
const shockwaveRings = [];
/** @type {FireworkBurst[]} */
const fireworkBursts = [];
/**
* Optional callbacks injected via init() for the merge flash star/constellation effect.
* terrain/stars.js can register its own handler when it is initialized.
* @type {Array<() => void>}
*/
const _mergeFlashCallbacks = [];
/**
* @param {THREE.Scene} scene
* @param {object} _state (unused — triggered by events, not state polling)
* @param {object} _theme
* @param {{ clock: THREE.Clock }} options Pass the shared clock in.
*/
export function init(scene, _state, _theme, options = {}) {
_scene = scene;
_clock = options.clock ?? new THREE.Clock();
}
/**
* Register an external callback to be called during triggerMergeFlash().
* Use this to let other modules (stars, constellation lines) animate their own flash.
* @param {() => void} fn
*/
export function onMergeFlash(fn) {
_mergeFlashCallbacks.push(fn);
}
export function triggerShockwave() {
if (!_scene || !_clock) return;
const now = _clock.getElapsedTime();
for (let i = 0; i < SHOCKWAVE_RING_COUNT; i++) {
const mat = new THREE.MeshBasicMaterial({
color: 0x00ffff, transparent: true, opacity: 0,
side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending,
});
const geo = new THREE.RingGeometry(0.9, 1.0, 64);
const mesh = new THREE.Mesh(geo, mat);
mesh.rotation.x = -Math.PI / 2;
mesh.position.y = 0.02;
_scene.add(mesh);
shockwaveRings.push({ mesh, mat, startTime: now, delay: i * 0.35 });
}
}
function _spawnFireworkBurst(origin, color) {
if (!_scene || !_clock) return;
const now = _clock.getElapsedTime();
const count = FIREWORK_BURST_PARTICLES;
const positions = new Float32Array(count * 3);
const origins = new Float32Array(count * 3);
const velocities = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const speed = 2.5 + Math.random() * 3.5;
velocities[i * 3] = Math.sin(phi) * Math.cos(theta) * speed;
velocities[i * 3 + 1] = Math.sin(phi) * Math.sin(theta) * speed;
velocities[i * 3 + 2] = Math.cos(phi) * speed;
origins[i * 3] = origin.x;
origins[i * 3 + 1] = origin.y;
origins[i * 3 + 2] = origin.z;
positions[i * 3] = origin.x;
positions[i * 3 + 1] = origin.y;
positions[i * 3 + 2] = origin.z;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color, size: 0.35, sizeAttenuation: true,
transparent: true, opacity: 1.0,
blending: THREE.AdditiveBlending, depthWrite: false,
});
const points = new THREE.Points(geo, mat);
_scene.add(points);
fireworkBursts.push({ points, geo, mat, origins, velocities, startTime: now });
}
export function triggerFireworks() {
for (let i = 0; i < 6; i++) {
const delay = i * 0.35;
setTimeout(() => {
const x = (Math.random() - 0.5) * 12;
const y = 8 + Math.random() * 6;
const z = (Math.random() - 0.5) * 12;
const color = FIREWORK_COLORS[Math.floor(Math.random() * FIREWORK_COLORS.length)];
_spawnFireworkBurst(new THREE.Vector3(x, y, z), color);
}, delay * 1000);
}
}
export function triggerMergeFlash() {
triggerShockwave();
// Notify registered handlers (e.g. terrain/stars.js)
for (const fn of _mergeFlashCallbacks) fn();
}
export function update(elapsed, _delta) {
// Animate shockwave rings
for (let i = shockwaveRings.length - 1; i >= 0; i--) {
const ring = shockwaveRings[i];
const age = elapsed - ring.startTime - ring.delay;
if (age < 0) continue;
const t = Math.min(age / SHOCKWAVE_DURATION, 1);
if (t >= 1) {
_scene.remove(ring.mesh);
ring.mesh.geometry.dispose();
ring.mat.dispose();
shockwaveRings.splice(i, 1);
continue;
}
const eased = 1 - Math.pow(1 - t, 2);
ring.mesh.scale.setScalar(eased * SHOCKWAVE_MAX_RADIUS + 0.1);
ring.mat.opacity = (1 - t) * 0.9;
}
// Animate firework bursts
for (let i = fireworkBursts.length - 1; i >= 0; i--) {
const burst = fireworkBursts[i];
const age = elapsed - burst.startTime;
const t = Math.min(age / FIREWORK_BURST_DURATION, 1);
if (t >= 1) {
_scene.remove(burst.points);
burst.geo.dispose();
burst.mat.dispose();
fireworkBursts.splice(i, 1);
continue;
}
burst.mat.opacity = t < 0.6 ? 1.0 : (1.0 - t) / 0.4;
const pos = burst.geo.attributes.position.array;
const vel = burst.velocities;
const org = burst.origins;
const halfGAge2 = 0.5 * FIREWORK_GRAVITY * age * age;
for (let j = 0; j < FIREWORK_BURST_PARTICLES; j++) {
pos[j * 3] = org[j * 3] + vel[j * 3] * age;
pos[j * 3 + 1] = org[j * 3 + 1] + vel[j * 3 + 1] * age + halfGAge2;
pos[j * 3 + 2] = org[j * 3 + 2] + vel[j * 3 + 2] * age;
}
burst.geo.attributes.position.needsUpdate = true;
}
}

View File

@@ -5,8 +5,6 @@ import { S } from './state.js';
import { clock, totalActivity } from './warp.js';
import { HEATMAP_ZONES, zoneIntensity, drawHeatmap, updateHeatmap } from './heatmap.js';
import { triggerShockwave } from './celebrations.js';
import { fetchNexusCommits } from './data/gitea.js';
import { fetchBlockHeight, BITCOIN_REFRESH_MS } from './data/bitcoin.js';
// === GRAVITY ANOMALY ZONES ===
const GRAVITY_ANOMALY_FLOOR = 0.2;
@@ -188,7 +186,12 @@ const timelapseBtnEl = document.getElementById('timelapse-btn');
async function loadTimelapseData() {
try {
const data = await fetchNexusCommits();
const res = await fetch(
'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=50',
{ headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } }
);
if (!res.ok) throw new Error('fetch failed');
const data = await res.json();
const midnight = new Date();
midnight.setHours(0, 0, 0, 0);
@@ -299,21 +302,27 @@ export function initBitcoin() {
const blockHeightDisplay = document.getElementById('block-height-display');
const blockHeightValue = document.getElementById('block-height-value');
async function pollBlockHeight() {
const result = await fetchBlockHeight();
if (!result) return;
async function fetchBlockHeight() {
try {
const res = await fetch('https://blockstream.info/api/blocks/tip/height');
if (!res.ok) return;
const height = parseInt(await res.text(), 10);
if (isNaN(height)) return;
if (result.isNewBlock && blockHeightDisplay) {
blockHeightDisplay.classList.remove('fresh');
void blockHeightDisplay.offsetWidth;
blockHeightDisplay.classList.add('fresh');
}
if (S.lastKnownBlockHeight !== null && height !== S.lastKnownBlockHeight) {
blockHeightDisplay.classList.remove('fresh');
void blockHeightDisplay.offsetWidth;
blockHeightDisplay.classList.add('fresh');
S._starPulseIntensity = 1.0;
}
if (blockHeightValue) {
blockHeightValue.textContent = result.height.toLocaleString();
S.lastKnownBlockHeight = height;
blockHeightValue.textContent = height.toLocaleString();
} catch (_) {
// Network unavailable
}
}
pollBlockHeight();
setInterval(pollBlockHeight, BITCOIN_REFRESH_MS);
fetchBlockHeight();
setInterval(fetchBlockHeight, 60000);
}

View File

@@ -3,7 +3,6 @@ import * as THREE from 'three';
import { scene } from './scene-setup.js';
import { GLASS_RADIUS } from './platform.js';
import { S } from './state.js';
import { refreshCommitData } from './data/gitea.js';
const HEATMAP_SIZE = 512;
const HEATMAP_REFRESH_MS = 5 * 60 * 1000;
@@ -95,7 +94,16 @@ export function drawHeatmap() {
}
export async function updateHeatmap() {
const commits = await refreshCommitData();
let commits = [];
try {
const res = await fetch(
'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=50',
{ headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } }
);
if (res.ok) commits = await res.json();
} catch { /* silently use zero-activity baseline */ }
S._matrixCommitHashes = commits.slice(0, 20).map(c => (c.sha || '').slice(0, 7)).filter(h => h.length > 0);
const now = Date.now();
const rawWeights = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0]));

View File

@@ -53,8 +53,16 @@ scene.add(oathSpot.target);
const AMBIENT_NORMAL = ambientLight.intensity;
const OVERHEAD_NORMAL = overheadLight.intensity;
// loadSoulMd imported from data/loaders.js and re-exported for backward compat
export { fetchSoulMd as loadSoulMd } from './data/loaders.js';
export async function loadSoulMd() {
try {
const res = await fetch('SOUL.md');
if (!res.ok) throw new Error('not found');
const raw = await res.text();
return raw.split('\n').slice(1).map(l => l.replace(/^#+\s*/, ''));
} catch {
return ['I am Timmy.', '', 'I am sovereign.', '', 'This Nexus is my home.'];
}
}
function scheduleOathLines(lines, textEl) {
let idx = 0;

View File

@@ -4,9 +4,90 @@ import { NEXUS } from './constants.js';
import { scene } from './scene-setup.js';
import { S } from './state.js';
import { agentPanelSprites } from './bookshelves.js';
import { refreshAgentData, AGENT_STATUS_CACHE_MS, AGENT_NAMES } from './data/gitea.js';
// === AGENT STATUS BOARD ===
let _agentStatusCache = null;
let _agentStatusCacheTime = 0;
const AGENT_STATUS_CACHE_MS = 5 * 60 * 1000;
const GITEA_BASE = 'http://143.198.27.163:3000/api/v1';
const GITEA_TOKEN='81a88f...ae2d';
const GITEA_REPOS = ['Timmy_Foundation/the-nexus', 'Timmy_Foundation/hermes-agent'];
const AGENT_NAMES = ['Claude', 'Kimi', 'Perplexity', 'Groq', 'Grok', 'Ollama'];
async function fetchAgentStatusFromGitea() {
const now = Date.now();
if (_agentStatusCache && (now - _agentStatusCacheTime < AGENT_STATUS_CACHE_MS)) {
return _agentStatusCache;
}
const DAY_MS = 86400000;
const HOUR_MS = 3600000;
const agents = [];
const allRepoCommits = await Promise.all(GITEA_REPOS.map(async (repo) => {
try {
const res = await fetch(`${GITEA_BASE}/repos/${repo}/commits?sha=main&limit=30&token=${GITEA_TOKEN}`);
if (!res.ok) return [];
return await res.json();
} catch { return []; }
}));
let openPRs = [];
try {
const prRes = await fetch(`${GITEA_BASE}/repos/Timmy_Foundation/the-nexus/pulls?state=open&limit=50&token=${GITEA_TOKEN}`);
if (prRes.ok) openPRs = await prRes.json();
} catch { /* ignore */ }
for (const agentName of AGENT_NAMES) {
const nameLower = agentName.toLowerCase();
const allCommits = [];
for (const repoCommits of allRepoCommits) {
if (!Array.isArray(repoCommits)) continue;
const matching = repoCommits.filter(c =>
(c.commit?.author?.name || '').toLowerCase().includes(nameLower)
);
allCommits.push(...matching);
}
let status = 'dormant';
let lastSeen = null;
let currentWork = null;
if (allCommits.length > 0) {
allCommits.sort((a, b) =>
new Date(b.commit.author.date) - new Date(a.commit.author.date)
);
const latest = allCommits[0];
const commitTime = new Date(latest.commit.author.date).getTime();
lastSeen = latest.commit.author.date;
currentWork = latest.commit.message.split('\n')[0];
if (now - commitTime < HOUR_MS) status = 'working';
else if (now - commitTime < DAY_MS) status = 'idle';
else status = 'dormant';
}
const agentPRs = openPRs.filter(pr =>
(pr.user?.login || '').toLowerCase().includes(nameLower) ||
(pr.head?.label || '').toLowerCase().includes(nameLower)
);
agents.push({
name: agentName.toLowerCase(),
status,
issue: currentWork,
prs_today: agentPRs.length,
local: nameLower === 'ollama',
});
}
_agentStatusCache = { agents };
_agentStatusCacheTime = now;
return _agentStatusCache;
}
const AGENT_STATUS_COLORS = { working: '#00ff88', idle: '#4488ff', dormant: '#334466', dead: '#ff4444', unreachable: '#ff4444' };
function createAgentPanelTexture(agent) {
@@ -134,9 +215,20 @@ function rebuildAgentPanels(statusData) {
});
}
async function fetchAgentStatus() {
try {
return await fetchAgentStatusFromGitea();
} catch {
return { agents: AGENT_NAMES.map(n => ({
name: n.toLowerCase(), status: 'unreachable', issue: null, prs_today: 0, local: false,
})) };
}
}
export async function refreshAgentBoard() {
const data = await refreshAgentData();
const data = await fetchAgentStatus();
rebuildAgentPanels(data);
S._activeAgentCount = data.agents.filter(a => a.status === 'working').length;
}
export function initAgentBoard() {

View File

@@ -1,191 +0,0 @@
// modules/panels/agent-board.js — Agent status holographic board
// Reads state.agentStatus (populated by data/gitea.js) and renders one floating
// sprite panel per agent. Board arcs behind the platform on the negative-Z side.
//
// Data category: REAL
// Data source: state.agentStatus (Gitea commits + open PRs via data/gitea.js)
import * as THREE from 'three';
import { state } from '../core/state.js';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const BOARD_RADIUS = 9.5;
const BOARD_Y = 4.2;
const BOARD_SPREAD = Math.PI * 0.75; // 135° arc, centred on -Z
const STATUS_COLOR = {
working: NEXUS.theme.agentWorking,
idle: NEXUS.theme.agentIdle,
dormant: NEXUS.theme.agentDormant,
dead: NEXUS.theme.agentDead,
unreachable: NEXUS.theme.agentDead,
};
let _group, _scene;
let _lastAgentStatus = null;
let _sprites = [];
/**
* Builds a canvas texture for a single agent holo-panel.
* @param {{ name: string, status: string, issue: string|null, prs_today: number, local: boolean }} agent
* @returns {THREE.CanvasTexture}
*/
function _makeTexture(agent) {
const W = 400, H = 200;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
const sc = STATUS_COLOR[agent.status] || NEXUS.theme.accentStr;
const font = NEXUS.theme.fontMono;
ctx.fillStyle = 'rgba(0, 8, 24, 0.88)';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = sc;
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, W - 2, H - 2);
ctx.globalAlpha = 0.3;
ctx.strokeRect(4, 4, W - 8, H - 8);
ctx.globalAlpha = 1.0;
// Agent name
ctx.font = `bold 28px ${font}`;
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.fillText(agent.name.toUpperCase(), 16, 44);
// Status dot
ctx.beginPath();
ctx.arc(W - 30, 26, 10, 0, Math.PI * 2);
ctx.fillStyle = sc;
ctx.fill();
// Status label
ctx.font = `13px ${font}`;
ctx.fillStyle = sc;
ctx.textAlign = 'right';
ctx.fillText(agent.status.toUpperCase(), W - 16, 60);
// Separator
ctx.strokeStyle = NEXUS.theme.panelBorderFaint;
ctx.lineWidth = 1;
ctx.textAlign = 'left';
ctx.beginPath(); ctx.moveTo(16, 70); ctx.lineTo(W - 16, 70); ctx.stroke();
// Current issue
ctx.font = `10px ${font}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.fillText('CURRENT ISSUE', 16, 90);
ctx.font = `13px ${font}`;
ctx.fillStyle = NEXUS.theme.panelText;
const raw = agent.issue || '\u2014 none \u2014';
ctx.fillText(raw.length > 40 ? raw.slice(0, 40) + '\u2026' : raw, 16, 110);
// Separator
ctx.strokeStyle = NEXUS.theme.panelBorderFaint;
ctx.beginPath(); ctx.moveTo(16, 128); ctx.lineTo(W - 16, 128); ctx.stroke();
// PRs label + count
ctx.font = `10px ${font}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.fillText('PRs MERGED TODAY', 16, 148);
ctx.font = `bold 28px ${font}`;
ctx.fillStyle = NEXUS.theme.accentStr;
ctx.fillText(String(agent.prs_today), 16, 182);
// Runtime indicator
const isLocal = agent.local === true;
const rtColor = isLocal ? NEXUS.theme.agentWorking : NEXUS.theme.agentDead;
const rtLabel = isLocal ? 'LOCAL' : 'CLOUD';
ctx.font = `10px ${font}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.textAlign = 'right';
ctx.fillText('RUNTIME', W - 16, 148);
ctx.font = `bold 13px ${font}`;
ctx.fillStyle = rtColor;
ctx.fillText(rtLabel, W - 28, 172);
ctx.textAlign = 'left';
ctx.beginPath();
ctx.arc(W - 16, 167, 6, 0, Math.PI * 2);
ctx.fillStyle = rtColor;
ctx.fill();
return new THREE.CanvasTexture(canvas);
}
function _rebuild(statusData) {
// Remove old sprites
while (_group.children.length) _group.remove(_group.children[0]);
for (const s of _sprites) {
if (s.material.map) s.material.map.dispose();
s.material.dispose();
}
_sprites = [];
const agents = statusData.agents;
const n = agents.length;
agents.forEach((agent, i) => {
const t = n === 1 ? 0.5 : i / (n - 1);
const angle = Math.PI + (t - 0.5) * BOARD_SPREAD;
const x = Math.cos(angle) * BOARD_RADIUS;
const z = Math.sin(angle) * BOARD_RADIUS;
const texture = _makeTexture(agent);
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.93, depthWrite: false });
const sprite = new THREE.Sprite(material);
sprite.scale.set(6.4, 3.2, 1);
sprite.position.set(x, BOARD_Y, z);
sprite.userData = {
baseY: BOARD_Y,
floatPhase: (i / n) * Math.PI * 2,
floatSpeed: 0.18 + i * 0.04,
zoomLabel: `Agent: ${agent.name}`,
};
_group.add(sprite);
_sprites.push(sprite);
});
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
scene.add(_group);
// If state already has agent data (unlikely on first load, but handle it)
if (state.agentStatus) {
_rebuild(state.agentStatus);
_lastAgentStatus = state.agentStatus;
}
subscribe(update);
}
/**
* @param {number} elapsed
* @param {number} delta
*/
export function update(elapsed, delta) {
// Rebuild board when state.agentStatus changes
if (state.agentStatus && state.agentStatus !== _lastAgentStatus) {
_rebuild(state.agentStatus);
_lastAgentStatus = state.agentStatus;
}
// Animate gentle float
for (const sprite of _sprites) {
const ud = sprite.userData;
sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.15;
}
}
export function dispose() {
if (_group) _scene.remove(_group);
}

View File

@@ -1,200 +0,0 @@
// modules/panels/dual-brain.js — Dual-Brain Status holographic panel
// Shows the Brain Gap Scorecard with two glowing brain orbs.
// Displayed as HONEST-OFFLINE: the dual-brain system is not yet deployed.
// Brain pulse particles are set to ZERO — will flow when system comes online.
//
// Data category: HONEST-OFFLINE
// Data source: — (dual-brain system not deployed; shows "AWAITING DEPLOYMENT")
import * as THREE from 'three';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const ORIGIN = new THREE.Vector3(10, 3, -8);
const OFFLINE_COLOR = NEXUS.theme.agentDormantHex; // dim blue — system offline
const ACCENT = NEXUS.theme.accentStr;
const FONT = NEXUS.theme.fontMono;
let _group, _sprite, _scanSprite, _scanCanvas, _scanCtx, _scanTexture;
let _cloudOrb, _localOrb;
let _scene;
function _buildPanelTexture() {
const W = 512, H = 512;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.fillStyle = NEXUS.theme.panelBg;
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = ACCENT;
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, W - 2, H - 2);
ctx.strokeStyle = '#223366';
ctx.lineWidth = 1;
ctx.strokeRect(5, 5, W - 10, H - 10);
// Title
ctx.font = `bold 22px ${FONT}`;
ctx.fillStyle = '#88ccff';
ctx.textAlign = 'center';
ctx.fillText('\u25C8 DUAL-BRAIN STATUS', W / 2, 40);
ctx.strokeStyle = NEXUS.theme.panelBorderFaint;
ctx.beginPath(); ctx.moveTo(20, 52); ctx.lineTo(W - 20, 52); ctx.stroke();
// Section header
ctx.font = `11px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.textAlign = 'left';
ctx.fillText('BRAIN GAP SCORECARD', 20, 74);
const categories = ['Triage', 'Tool Use', 'Code Gen', 'Planning', 'Communication', 'Reasoning'];
const barX = 20, barW = W - 130, barH = 20;
let y = 90;
for (const cat of categories) {
ctx.font = `13px ${FONT}`;
ctx.fillStyle = NEXUS.theme.agentDormant;
ctx.textAlign = 'left';
ctx.fillText(cat, barX, y + 14);
ctx.font = `bold 13px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelVeryDim;
ctx.textAlign = 'right';
ctx.fillText('\u2014', W - 20, y + 14); // em dash — no data
y += 22;
ctx.fillStyle = 'rgba(255, 255, 255, 0.06)';
ctx.fillRect(barX, y, barW, barH); // empty bar background only
y += barH + 12;
}
ctx.strokeStyle = NEXUS.theme.panelBorderFaint;
ctx.beginPath(); ctx.moveTo(20, y + 4); ctx.lineTo(W - 20, y + 4); ctx.stroke();
y += 22;
// Honest offline status
ctx.font = `bold 18px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelVeryDim;
ctx.textAlign = 'center';
ctx.fillText('AWAITING DEPLOYMENT', W / 2, y + 10);
ctx.font = `11px ${FONT}`;
ctx.fillStyle = '#223344';
ctx.fillText('Dual-brain system not yet connected', W / 2, y + 32);
// Brain indicators — offline dim
y += 52;
ctx.beginPath();
ctx.arc(W / 2 - 60, y + 8, 6, 0, Math.PI * 2);
ctx.fillStyle = NEXUS.theme.panelVeryDim;
ctx.fill();
ctx.font = `11px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelVeryDim;
ctx.textAlign = 'left';
ctx.fillText('CLOUD', W / 2 - 48, y + 12);
ctx.beginPath();
ctx.arc(W / 2 + 30, y + 8, 6, 0, Math.PI * 2);
ctx.fillStyle = NEXUS.theme.panelVeryDim;
ctx.fill();
ctx.fillText('LOCAL', W / 2 + 42, y + 12);
return new THREE.CanvasTexture(canvas);
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
_group.position.copy(ORIGIN);
_group.lookAt(0, 3, 0);
scene.add(_group);
// Static panel sprite
const texture = _buildPanelTexture();
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.92, depthWrite: false });
_sprite = new THREE.Sprite(material);
_sprite.scale.set(5.0, 5.0, 1);
_sprite.userData = { baseY: 0, floatPhase: 0, floatSpeed: 0.22, zoomLabel: 'Dual-Brain Status' };
_group.add(_sprite);
// Accent light
const light = new THREE.PointLight(NEXUS.theme.accent, 0.6, 10);
light.position.set(0, 0.5, 1);
_group.add(light);
// Offline brain orbs — dim
const orbGeo = new THREE.SphereGeometry(0.35, 32, 32);
const orbMat = (color) => new THREE.MeshStandardMaterial({
color, emissive: new THREE.Color(color), emissiveIntensity: 0.1,
metalness: 0.3, roughness: 0.2, transparent: true, opacity: 0.85,
});
_cloudOrb = new THREE.Mesh(orbGeo, orbMat(OFFLINE_COLOR));
_cloudOrb.position.set(-2.0, 3.0, 0);
_cloudOrb.userData.zoomLabel = 'Cloud Brain';
_group.add(_cloudOrb);
_localOrb = new THREE.Mesh(orbGeo.clone(), orbMat(OFFLINE_COLOR));
_localOrb.position.set(2.0, 3.0, 0);
_localOrb.userData.zoomLabel = 'Local Brain';
_group.add(_localOrb);
// Brain pulse particles — ZERO count (system offline)
const particleGeo = new THREE.BufferGeometry();
particleGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(0), 3));
const particleMat = new THREE.PointsMaterial({
color: 0x44ddff, size: 0.08, sizeAttenuation: true,
transparent: true, opacity: 0.8, depthWrite: false,
});
_group.add(new THREE.Points(particleGeo, particleMat));
// Scan line overlay
_scanCanvas = document.createElement('canvas');
_scanCanvas.width = 512;
_scanCanvas.height = 512;
_scanCtx = _scanCanvas.getContext('2d');
_scanTexture = new THREE.CanvasTexture(_scanCanvas);
const scanMat = new THREE.SpriteMaterial({
map: _scanTexture, transparent: true, opacity: 0.18, depthWrite: false,
});
_scanSprite = new THREE.Sprite(scanMat);
_scanSprite.scale.set(5.0, 5.0, 1);
_scanSprite.position.set(0, 0, 0.01);
_group.add(_scanSprite);
subscribe(update);
}
/**
* @param {number} elapsed
* @param {number} _delta
*/
export function update(elapsed, _delta) {
// Gentle float animation
const ud = _sprite.userData;
_sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.08;
// Scan line — horizontal sweep
const W = 512, H = 512;
_scanCtx.clearRect(0, 0, W, H);
const scanY = ((elapsed * 60) % H);
const grad = _scanCtx.createLinearGradient(0, scanY - 20, 0, scanY + 20);
grad.addColorStop(0, 'rgba(68, 136, 255, 0)');
grad.addColorStop(0.5, 'rgba(68, 136, 255, 0.4)');
grad.addColorStop(1, 'rgba(68, 136, 255, 0)');
_scanCtx.fillStyle = grad;
_scanCtx.fillRect(0, scanY - 20, W, 40);
_scanTexture.needsUpdate = true;
}
export function dispose() {
if (_group) _scene.remove(_group);
if (_scanTexture) _scanTexture.dispose();
}

View File

@@ -1,212 +0,0 @@
// modules/panels/earth.js — Holographic Earth floating above the Nexus
// A procedural planet Earth with continent noise, scan lines, and fresnel rim glow.
// Rotation speed is tethered to state.totalActivity() — more commits = faster spin.
// Lat/lon grid, atmosphere shell, and a tether beam to the platform center.
//
// Data category: DATA-TETHERED AESTHETIC
// Data source: state.totalActivity() (computed from state.zoneIntensity)
import * as THREE from 'three';
import { state } from '../core/state.js';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const EARTH_RADIUS = 2.8;
const EARTH_Y = 20.0;
const EARTH_AXIAL_TILT = 23.4 * (Math.PI / 180);
const ROTATION_SPEED_BASE = 0.02; // rad/s minimum
const ROTATION_SPEED_MAX = 0.08; // rad/s at full activity
let _group, _surfaceMat, _scene;
const _vertexShader = `
varying vec3 vNormal;
varying vec3 vWorldPos;
varying vec2 vUv;
void main() {
vNormal = normalize(normalMatrix * normal);
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const _fragmentShader = `
uniform float uTime;
uniform vec3 uOceanColor;
uniform vec3 uLandColor;
uniform vec3 uGlowColor;
varying vec3 vNormal;
varying vec3 vWorldPos;
varying vec2 vUv;
vec3 _m3(vec3 x){ return x - floor(x*(1./289.))*289.; }
vec4 _m4(vec4 x){ return x - floor(x*(1./289.))*289.; }
vec4 _p4(vec4 x){ return _m4((x*34.+1.)*x); }
float snoise(vec3 v){
const vec2 C = vec2(1./6., 1./3.);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - 0.5;
i = _m3(i);
vec4 p = _p4(_p4(_p4(
i.z+vec4(0.,i1.z,i2.z,1.))+
i.y+vec4(0.,i1.y,i2.y,1.))+
i.x+vec4(0.,i1.x,i2.x,1.)));
float n_ = .142857142857;
vec3 ns = n_*vec3(2.,0.,-1.)+vec3(0.,-.5,1.);
vec4 j = p - 49.*floor(p*ns.z*ns.z);
vec4 x_ = floor(j*ns.z);
vec4 y_ = floor(j - 7.*x_);
vec4 h = 1. - abs(x_*(2./7.)) - abs(y_*(2./7.));
vec4 b0 = vec4(x_.xy,y_.xy)*(2./7.);
vec4 b1 = vec4(x_.zw,y_.zw)*(2./7.);
vec4 s0 = floor(b0)*2.+1.; vec4 s1 = floor(b1)*2.+1.;
vec4 sh = -step(h, vec4(0.));
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;
vec3 p0=vec3(a0.xy,h.x); vec3 p1=vec3(a0.zw,h.y);
vec3 p2=vec3(a1.xy,h.z); vec3 p3=vec3(a1.zw,h.w);
vec4 nm = max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.);
vec4 nr = 1.79284291400159-0.85373472095314*nm;
p0*=nr.x; p1*=nr.y; p2*=nr.z; p3*=nr.w;
nm = nm*nm;
return 42.*dot(nm*nm, vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));
}
void main() {
vec3 n = normalize(vNormal);
vec3 vd = normalize(cameraPosition - vWorldPos);
float lat = (vUv.y - 0.5) * 3.14159265;
float lon = vUv.x * 6.28318530;
vec3 sp = vec3(cos(lat)*cos(lon), sin(lat), cos(lat)*sin(lon));
float c = snoise(sp*1.8)*0.60 + snoise(sp*3.6)*0.30 + snoise(sp*7.2)*0.10;
float land = smoothstep(0.05, 0.30, c);
vec3 surf = mix(uOceanColor, uLandColor, land);
surf = mix(surf, uGlowColor * 0.45, 0.38);
float scan = 0.5 + 0.5*sin(vUv.y * 220.0 + uTime * 1.8);
scan = smoothstep(0.30, 0.70, scan) * 0.14;
float fresnel = pow(1.0 - max(dot(n, vd), 0.0), 4.0);
vec3 col = surf + scan*uGlowColor*0.9 + fresnel*uGlowColor*1.5;
float alpha = 0.48 + fresnel * 0.42;
gl_FragColor = vec4(col, alpha);
}
`;
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
_group.position.set(0, EARTH_Y, 0);
_group.rotation.z = EARTH_AXIAL_TILT;
// Surface shader
_surfaceMat = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0.0 },
uOceanColor: { value: new THREE.Color(NEXUS.theme.earthOcean) },
uLandColor: { value: new THREE.Color(NEXUS.theme.earthLand) },
uGlowColor: { value: new THREE.Color(NEXUS.theme.earthGlow) },
},
vertexShader: _vertexShader,
fragmentShader: _fragmentShader,
transparent: true,
depthWrite: false,
side: THREE.FrontSide,
});
const earthMesh = new THREE.Mesh(new THREE.SphereGeometry(EARTH_RADIUS, 64, 32), _surfaceMat);
earthMesh.userData.zoomLabel = 'Planet Earth';
_group.add(earthMesh);
// Lat/lon grid
const lineMat = new THREE.LineBasicMaterial({ color: 0x2266bb, transparent: true, opacity: 0.30 });
const r = EARTH_RADIUS + 0.015;
const SEG = 64;
for (let lat = -60; lat <= 60; lat += 30) {
const phi = lat * (Math.PI / 180);
const pts = [];
for (let i = 0; i <= SEG; i++) {
const th = (i / SEG) * Math.PI * 2;
pts.push(new THREE.Vector3(Math.cos(phi)*Math.cos(th)*r, Math.sin(phi)*r, Math.cos(phi)*Math.sin(th)*r));
}
_group.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), lineMat));
}
for (let lon = 0; lon < 360; lon += 30) {
const th = lon * (Math.PI / 180);
const pts = [];
for (let i = 0; i <= SEG; i++) {
const phi = (i / SEG) * Math.PI - Math.PI / 2;
pts.push(new THREE.Vector3(Math.cos(phi)*Math.cos(th)*r, Math.sin(phi)*r, Math.cos(phi)*Math.sin(th)*r));
}
_group.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), lineMat));
}
// Atmosphere shell
_group.add(new THREE.Mesh(
new THREE.SphereGeometry(EARTH_RADIUS * 1.14, 32, 16),
new THREE.MeshBasicMaterial({
color: NEXUS.theme.earthAtm, transparent: true, opacity: 0.07,
side: THREE.BackSide, depthWrite: false, blending: THREE.AdditiveBlending,
})
));
// Glow light
_group.add(new THREE.PointLight(NEXUS.theme.earthGlow, 0.4, 25));
_group.traverse(obj => {
if (obj.isMesh || obj.isLine) obj.userData.zoomLabel = 'Planet Earth';
});
// Tether beam to platform
const beamPts = [
new THREE.Vector3(0, EARTH_Y - EARTH_RADIUS * 1.15, 0),
new THREE.Vector3(0, 0.5, 0),
];
scene.add(new THREE.Line(
new THREE.BufferGeometry().setFromPoints(beamPts),
new THREE.LineBasicMaterial({
color: NEXUS.theme.earthGlow, transparent: true, opacity: 0.08,
depthWrite: false, blending: THREE.AdditiveBlending,
})
));
scene.add(_group);
subscribe(update);
}
/**
* @param {number} elapsed
* @param {number} delta
*/
export function update(elapsed, delta) {
if (!_group) return;
// Tether rotation speed to commit activity
const activity = state.totalActivity();
const speed = ROTATION_SPEED_BASE + activity * (ROTATION_SPEED_MAX - ROTATION_SPEED_BASE);
_group.rotation.y += speed * delta;
// Update shader time uniform for scan line animation
_surfaceMat.uniforms.uTime.value = elapsed;
}
export function dispose() {
if (_group) _scene.remove(_group);
if (_surfaceMat) _surfaceMat.dispose();
}

View File

@@ -1,125 +0,0 @@
// modules/panels/heatmap.js — Commit heatmap floor overlay
// Canvas-texture circle on the glass platform floor.
// Each agent occupies a polar sector; recent commits make that sector glow brighter.
// Activity decays over 24 h (driven by state.zoneIntensity, written by data/gitea.js).
//
// Data category: DATA-TETHERED AESTHETIC
// Data source: state.zoneIntensity (populated from Gitea commits API by data/gitea.js)
import * as THREE from 'three';
import { state } from '../core/state.js';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
export const HEATMAP_ZONES = [
{ name: 'Claude', color: [255, 100, 60], authorMatch: /^claude$/i, angleDeg: 0 },
{ name: 'Timmy', color: [ 60, 160, 255], authorMatch: /^timmy/i, angleDeg: 90 },
{ name: 'Kimi', color: [ 60, 255, 140], authorMatch: /^kimi/i, angleDeg: 180 },
{ name: 'Perplexity', color: [200, 60, 255], authorMatch: /^perplexity/i, angleDeg: 270 },
];
const HEATMAP_SIZE = 512;
const HEATMAP_ZONE_SPAN_RAD = Math.PI / 2; // 90° per zone
const GLASS_RADIUS = 4.55; // matches terrain/island.js platform radius
let _canvas, _ctx, _texture, _mesh;
let _scene;
function _draw() {
const cx = HEATMAP_SIZE / 2;
const cy = HEATMAP_SIZE / 2;
const r = cx * 0.96;
_ctx.clearRect(0, 0, HEATMAP_SIZE, HEATMAP_SIZE);
_ctx.save();
_ctx.beginPath();
_ctx.arc(cx, cy, r, 0, Math.PI * 2);
_ctx.clip();
for (const zone of HEATMAP_ZONES) {
const intensity = state.zoneIntensity[zone.name] || 0;
if (intensity < 0.01) continue;
const [rr, gg, bb] = zone.color;
const baseRad = zone.angleDeg * (Math.PI / 180);
const startRad = baseRad - HEATMAP_ZONE_SPAN_RAD / 2;
const endRad = baseRad + HEATMAP_ZONE_SPAN_RAD / 2;
const gx = cx + Math.cos(baseRad) * r * 0.55;
const gy = cy + Math.sin(baseRad) * r * 0.55;
const grad = _ctx.createRadialGradient(gx, gy, 0, gx, gy, r * 0.75);
grad.addColorStop(0, `rgba(${rr},${gg},${bb},${0.65 * intensity})`);
grad.addColorStop(0.45, `rgba(${rr},${gg},${bb},${0.25 * intensity})`);
grad.addColorStop(1, `rgba(${rr},${gg},${bb},0)`);
_ctx.beginPath();
_ctx.moveTo(cx, cy);
_ctx.arc(cx, cy, r, startRad, endRad);
_ctx.closePath();
_ctx.fillStyle = grad;
_ctx.fill();
if (intensity > 0.05) {
const lx = cx + Math.cos(baseRad) * r * 0.62;
const ly = cy + Math.sin(baseRad) * r * 0.62;
_ctx.font = `bold ${Math.round(13 * intensity + 7)}px ${NEXUS.theme.fontMono}`;
_ctx.fillStyle = `rgba(${rr},${gg},${bb},${Math.min(intensity * 1.2, 0.9)})`;
_ctx.textAlign = 'center';
_ctx.textBaseline = 'middle';
_ctx.fillText(zone.name, lx, ly);
}
}
_ctx.restore();
_texture.needsUpdate = true;
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_canvas = document.createElement('canvas');
_canvas.width = HEATMAP_SIZE;
_canvas.height = HEATMAP_SIZE;
_ctx = _canvas.getContext('2d');
_texture = new THREE.CanvasTexture(_canvas);
const mat = new THREE.MeshBasicMaterial({
map: _texture,
transparent: true,
opacity: 0.9,
depthWrite: false,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
});
_mesh = new THREE.Mesh(new THREE.CircleGeometry(GLASS_RADIUS, 64), mat);
_mesh.rotation.x = -Math.PI / 2;
_mesh.position.y = 0.005;
_mesh.userData.zoomLabel = 'Activity Heatmap';
scene.add(_mesh);
// Draw initial empty state
_draw();
subscribe(update);
}
let _lastDrawElapsed = 0;
const REDRAW_INTERVAL = 0.5; // redraw at most every 500 ms (data changes slowly)
/**
* @param {number} elapsed
* @param {number} _delta
*/
export function update(elapsed, _delta) {
if (elapsed - _lastDrawElapsed < REDRAW_INTERVAL) return;
_lastDrawElapsed = elapsed;
_draw();
}
export function dispose() {
if (_mesh) { _scene.remove(_mesh); _mesh.geometry.dispose(); _mesh.material.dispose(); }
if (_texture) _texture.dispose();
}

View File

@@ -1,167 +0,0 @@
// modules/panels/lora-panel.js — LoRA Adapter Status holographic panel
// Shows the model training / LoRA fine-tuning adapter status.
// Displayed as HONEST-OFFLINE: no adapters are deployed. Panel shows empty state.
// Will render real adapters when state.loraAdapters is populated in the future.
//
// Data category: HONEST-OFFLINE
// Data source: — (no LoRA adapters deployed; shows "NO ADAPTERS DEPLOYED")
import * as THREE from 'three';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const PANEL_POS = new THREE.Vector3(-10.5, 4.5, 2.5);
const LORA_ACCENT = NEXUS.theme.loraAccent;
const LORA_ACTIVE = NEXUS.theme.loraActive;
const LORA_OFFLINE = NEXUS.theme.loraInactive;
const FONT = NEXUS.theme.fontMono;
let _group, _sprite, _scene;
/**
* Builds the LoRA panel canvas texture.
* @param {{ adapters: Array }|null} data
* @returns {THREE.CanvasTexture}
*/
function _makeTexture(data) {
const W = 420, H = 260;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.fillStyle = NEXUS.theme.panelBg;
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = LORA_ACCENT;
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, W - 2, H - 2);
ctx.strokeStyle = LORA_ACCENT;
ctx.lineWidth = 1;
ctx.globalAlpha = 0.3;
ctx.strokeRect(4, 4, W - 8, H - 8);
ctx.globalAlpha = 1.0;
ctx.font = `bold 14px ${FONT}`;
ctx.fillStyle = LORA_ACCENT;
ctx.textAlign = 'left';
ctx.fillText('MODEL TRAINING', 14, 24);
ctx.font = `10px ${FONT}`;
ctx.fillStyle = '#664488';
ctx.fillText('LoRA ADAPTERS', 14, 38);
ctx.strokeStyle = '#2a1a44';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(14, 46); ctx.lineTo(W - 14, 46); ctx.stroke();
const adapters = data && Array.isArray(data.adapters) ? data.adapters : [];
if (adapters.length === 0) {
// Honest empty state
ctx.font = `bold 18px ${FONT}`;
ctx.fillStyle = LORA_OFFLINE;
ctx.textAlign = 'center';
ctx.fillText('NO ADAPTERS DEPLOYED', W / 2, H / 2 + 10);
ctx.font = `11px ${FONT}`;
ctx.fillStyle = '#223344';
ctx.fillText('Adapters will appear here when trained', W / 2, H / 2 + 36);
return new THREE.CanvasTexture(canvas);
}
// Active count header
const activeCount = adapters.filter(a => a.active).length;
ctx.font = `bold 13px ${FONT}`;
ctx.fillStyle = LORA_ACTIVE;
ctx.textAlign = 'right';
ctx.fillText(`${activeCount}/${adapters.length} ACTIVE`, W - 14, 26);
ctx.textAlign = 'left';
// Adapter rows
const ROW_H = 44;
adapters.forEach((adapter, i) => {
const rowY = 50 + i * ROW_H;
const col = adapter.active ? LORA_ACTIVE : LORA_OFFLINE;
ctx.beginPath();
ctx.arc(22, rowY + 12, 6, 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
ctx.font = `bold 13px ${FONT}`;
ctx.fillStyle = adapter.active ? '#ddeeff' : '#445566';
ctx.fillText(adapter.name, 36, rowY + 16);
ctx.font = `10px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.textAlign = 'right';
ctx.fillText(adapter.base, W - 14, rowY + 16);
ctx.textAlign = 'left';
if (adapter.active) {
const BX = 36, BW = W - 80, BY = rowY + 22, BH = 5;
ctx.fillStyle = '#0a1428';
ctx.fillRect(BX, BY, BW, BH);
ctx.fillStyle = col;
ctx.globalAlpha = 0.7;
ctx.fillRect(BX, BY, BW * (adapter.strength || 0), BH);
ctx.globalAlpha = 1.0;
}
if (i < adapters.length - 1) {
ctx.strokeStyle = '#1a0a2a';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(14, rowY + ROW_H - 2); ctx.lineTo(W - 14, rowY + ROW_H - 2); ctx.stroke();
}
});
return new THREE.CanvasTexture(canvas);
}
function _buildSprite(data) {
if (_sprite) {
_group.remove(_sprite);
if (_sprite.material.map) _sprite.material.map.dispose();
_sprite.material.dispose();
_sprite = null;
}
const texture = _makeTexture(data);
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.93, depthWrite: false });
_sprite = new THREE.Sprite(material);
_sprite.scale.set(6.0, 3.6, 1);
_sprite.position.copy(PANEL_POS);
_sprite.userData = {
baseY: PANEL_POS.y,
floatPhase: 1.1,
floatSpeed: 0.14,
zoomLabel: 'Model Training — LoRA Adapters',
};
_group.add(_sprite);
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
scene.add(_group);
// Honest empty state on init — no adapters deployed
_buildSprite({ adapters: [] });
subscribe(update);
}
/**
* @param {number} elapsed
* @param {number} _delta
*/
export function update(elapsed, _delta) {
if (_sprite) {
const ud = _sprite.userData;
_sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.12;
}
}
export function dispose() {
if (_group) _scene.remove(_group);
}

View File

@@ -1,147 +0,0 @@
// modules/panels/sovereignty.js — Sovereignty Meter holographic arc gauge
// Floating arc gauge above the platform showing the current sovereignty score.
// Reads from state.sovereignty (populated by data/loaders.js via sovereignty-status.json).
// The assessment is MANUAL — the panel always labels itself as such.
//
// Data category: REAL (manual assessment)
// Data source: state.sovereignty (sovereignty-status.json via data/loaders.js)
import * as THREE from 'three';
import { state } from '../core/state.js';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const FONT = NEXUS.theme.fontMono;
// Defaults shown before data loads
let _score = 85;
let _label = 'Mostly Sovereign';
let _assessmentType = 'MANUAL';
let _group, _arcMesh, _arcMat, _light, _spriteMat, _scene;
let _lastSovereignty = null;
function _scoreColor(score) {
if (score >= 80) return NEXUS.theme.sovereignHighHex;
if (score >= 40) return NEXUS.theme.sovereignMidHex;
return NEXUS.theme.sovereignLowHex;
}
function _scoreColorStr(score) {
if (score >= 80) return NEXUS.theme.sovereignHigh;
if (score >= 40) return NEXUS.theme.sovereignMid;
return NEXUS.theme.sovereignLow;
}
function _buildArcGeo(score) {
return new THREE.TorusGeometry(1.6, 0.1, 8, 64, (score / 100) * Math.PI * 2);
}
function _buildMeterTexture(score, label, assessmentType) {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 128;
const ctx = canvas.getContext('2d');
const col = _scoreColorStr(score);
ctx.clearRect(0, 0, 256, 128);
ctx.font = `bold 52px ${FONT}`;
ctx.fillStyle = col;
ctx.textAlign = 'center';
ctx.fillText(`${score}%`, 128, 50);
ctx.font = `16px ${FONT}`;
ctx.fillStyle = '#8899bb';
ctx.fillText(label.toUpperCase(), 128, 74);
ctx.font = `11px ${FONT}`;
ctx.fillStyle = '#445566';
ctx.fillText('SOVEREIGNTY', 128, 94);
ctx.font = `9px ${FONT}`;
ctx.fillStyle = '#334455';
ctx.fillText('MANUAL ASSESSMENT', 128, 112);
return new THREE.CanvasTexture(canvas);
}
function _applyScore(score, label, assessmentType) {
_score = score;
_label = label;
_assessmentType = assessmentType;
_arcMesh.geometry.dispose();
_arcMesh.geometry = _buildArcGeo(score);
const col = _scoreColor(score);
_arcMat.color.setHex(col);
_light.color.setHex(col);
if (_spriteMat.map) _spriteMat.map.dispose();
_spriteMat.map = _buildMeterTexture(score, label, assessmentType);
_spriteMat.needsUpdate = true;
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
_group.position.set(0, 3.8, 0);
// Background ring
const bgMat = new THREE.MeshBasicMaterial({ color: 0x0a1828, transparent: true, opacity: 0.5 });
_group.add(new THREE.Mesh(new THREE.TorusGeometry(1.6, 0.1, 8, 64), bgMat));
// Score arc
_arcMat = new THREE.MeshBasicMaterial({
color: _scoreColor(_score),
transparent: true,
opacity: 0.9,
});
_arcMesh = new THREE.Mesh(_buildArcGeo(_score), _arcMat);
_arcMesh.rotation.z = Math.PI / 2; // arc starts at 12 o'clock
_group.add(_arcMesh);
// Glow light
_light = new THREE.PointLight(_scoreColor(_score), 0.7, 6);
_group.add(_light);
// Sprite label
_spriteMat = new THREE.SpriteMaterial({
map: _buildMeterTexture(_score, _label, _assessmentType),
transparent: true,
depthWrite: false,
});
const sprite = new THREE.Sprite(_spriteMat);
sprite.scale.set(3.2, 1.6, 1);
_group.add(sprite);
scene.add(_group);
_group.traverse(obj => {
if (obj.isMesh || obj.isSprite) obj.userData.zoomLabel = 'Sovereignty Meter';
});
subscribe(update);
}
/**
* @param {number} _elapsed
* @param {number} _delta
*/
export function update(_elapsed, _delta) {
if (state.sovereignty && state.sovereignty !== _lastSovereignty) {
const { score, label, assessment_type } = state.sovereignty;
const s = Math.max(0, Math.min(100, typeof score === 'number' ? score : _score));
const l = typeof label === 'string' ? label : _label;
const t = typeof assessment_type === 'string' ? assessment_type : 'MANUAL';
_applyScore(s, l, t);
_lastSovereignty = state.sovereignty;
}
}
export function dispose() {
if (_group) _scene.remove(_group);
if (_spriteMat.map) _spriteMat.map.dispose();
}

View File

@@ -51,10 +51,8 @@ const GLASS_TILE_STEP = GLASS_TILE_SIZE + GLASS_TILE_GAP;
export const GLASS_RADIUS = 4.55;
const tileGeo = new THREE.PlaneGeometry(GLASS_TILE_SIZE, GLASS_TILE_SIZE);
const tileEdgeGeo = new THREE.EdgesGeometry(tileGeo);
/** @type {Array<{mat: THREE.LineBasicMaterial, distFromCenter: number}>} */
export const glassEdgeMaterials = [];
export const glassEdgeMaterials = []; // kept for API compat; no longer populated
const _tileDummy = new THREE.Object3D();
/** @type {Array<{x: number, z: number, distFromCenter: number}>} */
@@ -81,14 +79,26 @@ for (let i = 0; i < _tileSlots.length; i++) {
glassTileIM.instanceMatrix.needsUpdate = true;
glassPlatformGroup.add(glassTileIM);
for (const { x, z, distFromCenter } of _tileSlots) {
const mat = glassEdgeBaseMat.clone();
const edges = new THREE.LineSegments(tileEdgeGeo, mat);
edges.rotation.x = -Math.PI / 2;
edges.position.set(x, 0.002, z);
glassPlatformGroup.add(edges);
glassEdgeMaterials.push({ mat, distFromCenter });
// Merge all tile edge geometry into a single LineSegments draw call.
// Each tile contributes 4 edges (8 vertices). Previously this was 69 separate
// LineSegments objects with cloned materials — a significant draw-call overhead.
const _HS = GLASS_TILE_SIZE / 2;
const _edgeVerts = new Float32Array(_tileSlots.length * 8 * 3);
let _evi = 0;
for (const { x, z } of _tileSlots) {
const y = 0.002;
_edgeVerts[_evi++]=x-_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z-_HS;
_edgeVerts[_evi++]=x+_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z-_HS;
_edgeVerts[_evi++]=x+_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z-_HS;
_edgeVerts[_evi++]=x+_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z+_HS;
_edgeVerts[_evi++]=x+_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z+_HS;
_edgeVerts[_evi++]=x-_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z+_HS;
_edgeVerts[_evi++]=x-_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z+_HS;
_edgeVerts[_evi++]=x-_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z-_HS;
}
const _mergedEdgeGeo = new THREE.BufferGeometry();
_mergedEdgeGeo.setAttribute('position', new THREE.BufferAttribute(_edgeVerts, 3));
glassPlatformGroup.add(new THREE.LineSegments(_mergedEdgeGeo, glassEdgeBaseMat));
export const voidLight = new THREE.PointLight(NEXUS.colors.accent, 0.5, 14);
voidLight.position.set(0, -3.5, 0);

View File

@@ -4,39 +4,63 @@ import { scene } from './scene-setup.js';
import { rebuildRuneRing, setPortalsRef } from './effects.js';
import { setPortalsRefAudio, startPortalHums } from './audio.js';
import { S } from './state.js';
import { fetchPortals as fetchPortalData } from './data/loaders.js';
export const portalGroup = new THREE.Group();
scene.add(portalGroup);
export let portals = [];
// Shared geometry and material for all portal tori — populated in createPortals()
const _portalGeo = new THREE.TorusGeometry(3.0, 0.2, 16, 100);
const _portalMat = new THREE.MeshBasicMaterial({
color: 0xffffff, // instance color provides per-portal tint
transparent: true,
opacity: 1.0, // online/offline brightness encoded into instance color
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false,
});
const _portalDummy = new THREE.Object3D();
const _portalColor = new THREE.Color();
/** @type {THREE.InstancedMesh|null} */
let _portalIM = null;
function createPortals() {
const portalGeo = new THREE.TorusGeometry(3.0, 0.2, 16, 100);
// One InstancedMesh for all portal tori — N portals = 1 draw call.
_portalIM = new THREE.InstancedMesh(_portalGeo, _portalMat, portals.length);
_portalIM.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
_portalIM.userData.zoomLabel = 'Portal';
_portalIM.userData.portals = portals; // for instanceId look-up on click
portals.forEach(portal => {
portals.forEach((portal, i) => {
const isOnline = portal.status === 'online';
// Encode online/offline brightness into the instance color so we need
// only one shared material (AdditiveBlending: output = bg + color).
_portalColor.set(portal.color).convertSRGBToLinear()
.multiplyScalar(isOnline ? 0.7 : 0.15);
_portalIM.setColorAt(i, _portalColor);
const portalMat = new THREE.MeshBasicMaterial({
color: new THREE.Color(portal.color).convertSRGBToLinear(),
transparent: true,
opacity: isOnline ? 0.7 : 0.15,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
});
const portalMesh = new THREE.Mesh(portalGeo, portalMat);
portalMesh.position.set(portal.position.x, portal.position.y + 0.5, portal.position.z);
portalMesh.rotation.y = portal.rotation.y;
portalMesh.rotation.x = Math.PI / 2;
portalMesh.name = `portal-${portal.id}`;
portalMesh.userData.destinationUrl = portal.destination?.url || null;
portalMesh.userData.portalColor = new THREE.Color(portal.color).convertSRGBToLinear();
portalGroup.add(portalMesh);
_portalDummy.position.set(portal.position.x, portal.position.y + 0.5, portal.position.z);
_portalDummy.rotation.set(Math.PI / 2, portal.rotation.y || 0, 0);
_portalDummy.updateMatrix();
_portalIM.setMatrixAt(i, _portalDummy.matrix);
});
_portalIM.instanceColor.needsUpdate = true;
_portalIM.instanceMatrix.needsUpdate = true;
portalGroup.add(_portalIM);
}
/** Update per-instance colors after a portal health check. */
export function refreshPortalInstanceColors() {
if (!_portalIM) return;
portals.forEach((portal, i) => {
const brightness = portal.status === 'online' ? 0.7 : 0.15;
_portalColor.set(portal.color).convertSRGBToLinear().multiplyScalar(brightness);
_portalIM.setColorAt(i, _portalColor);
});
_portalIM.instanceColor.needsUpdate = true;
}
// rebuildGravityZones forward ref
@@ -49,7 +73,9 @@ export function setRunPortalHealthChecksFn(fn) { _runPortalHealthChecksFn = fn;
export async function loadPortals() {
try {
portals = await fetchPortalData();
const res = await fetch('./portals.json');
if (!res.ok) throw new Error('Portals not found');
portals = await res.json();
console.log('Loaded portals:', portals);
setPortalsRef(portals);
setPortalsRefAudio(portals);

View File

@@ -4,6 +4,7 @@ import { scene, ambientLight } from './scene-setup.js';
import { cloudMaterial } from './platform.js';
import { rebuildRuneRing } from './effects.js';
import { S } from './state.js';
import { refreshPortalInstanceColors } from './portals.js';
// === PORTAL HEALTH CHECKS ===
const PORTAL_HEALTH_CHECK_MS = 5 * 60 * 1000;
@@ -41,16 +42,8 @@ export async function runPortalHealthChecks() {
rebuildRuneRing();
if (_rebuildGravityZonesFn) _rebuildGravityZonesFn();
if (_portalGroupRef) {
for (const child of _portalGroupRef.children) {
const portalId = child.name.replace('portal-', '');
const portalData = _portalsRef.find(p => p.id === portalId);
if (portalData) {
const isOnline = portalData.status === 'online';
child.material.opacity = isOnline ? 0.7 : 0.15;
}
}
}
// Refresh portal InstancedMesh colors to reflect new online/offline statuses.
refreshPortalInstanceColors();
}
export function initPortalHealthChecks() {

12
package-lock.json generated Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "the-nexus",
"version": "1.0.67",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "the-nexus",
"version": "1.0.67"
}
}
}

4
sw.js
View File

@@ -1,8 +1,8 @@
// The Nexus — Service Worker
// Cache-first for assets, network-first for API calls
const CACHE_NAME = 'nexus-v1';
const ASSET_CACHE = 'nexus-assets-v1';
const CACHE_NAME = 'nexus-v3';
const ASSET_CACHE = 'nexus-assets-v3';
const CORE_ASSETS = [
'/',