Compare commits
9 Commits
feat/issue
...
reference/
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c18fbf0d1 | |||
| b4f6ff5222 | |||
|
|
4379f70352 | ||
|
|
979c7cf96b | ||
|
|
b1cc4c05da | ||
| 7b54b22df1 | |||
| 09c83e8734 | |||
| 6db2871785 | |||
| c0a673038b |
@@ -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
|
||||
|
||||
7
.historical/manus-commits-2026-03-24.log
Normal file
7
.historical/manus-commits-2026-03-24.log
Normal 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)
|
||||
1161
.historical/manus-full-diff-2026-03-24.patch
Normal file
1161
.historical/manus-full-diff-2026-03-24.patch
Normal file
File diff suppressed because it is too large
Load Diff
538
app.js
538
app.js
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
12
modules/core/scene.js
Normal 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';
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
@@ -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}¤t=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
11
modules/effects.js
vendored
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
12
package-lock.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "the-nexus",
|
||||
"version": "1.0.67",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "the-nexus",
|
||||
"version": "1.0.67"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user