2026-03-24 19:58:25 +00:00
|
|
|
// === THE NEXUS — Main Entry Point ===
|
revert: strip Manus damage (nostr, SovOS, gutted app.js) — restore clean split
Reverts to the state of cbfacdf (split app.js into 21 modules, <1000 lines each).
Removes: nostr.js, nostr-panel.js, SovOS.js, RESEARCH_DROP_456.md, core/, data/
Historical archive preserved in .historical/ and branch archive/manus-damage-2026-03-24
Refs #418, #452, #454
2026-03-24 18:18:44 -04:00
|
|
|
// All modules are imported here. This file wires them together.
|
2026-03-24 03:58:50 +00:00
|
|
|
import * as THREE from 'three';
|
2026-03-24 22:16:49 +00:00
|
|
|
import { S } from './modules/state.js';
|
revert: strip Manus damage (nostr, SovOS, gutted app.js) — restore clean split
Reverts to the state of cbfacdf (split app.js into 21 modules, <1000 lines each).
Removes: nostr.js, nostr-panel.js, SovOS.js, RESEARCH_DROP_456.md, core/, data/
Historical archive preserved in .historical/ and branch archive/manus-damage-2026-03-24
Refs #418, #452, #454
2026-03-24 18:18:44 -04:00
|
|
|
import { NEXUS } from './modules/constants.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';
|
|
|
|
|
|
|
|
|
|
// === WIRE UP CROSS-MODULE REFERENCES ===
|
|
|
|
|
setTotalActivityFn(totalActivity);
|
|
|
|
|
setAnimateFn(() => animate());
|
|
|
|
|
setRebuildGravityZonesFn(rebuildGravityZones);
|
|
|
|
|
setRunPortalHealthChecksFn(runPortalHealthChecks);
|
|
|
|
|
|
|
|
|
|
// === ANIMATION LOOP ===
|
|
|
|
|
function animate() {
|
|
|
|
|
requestAnimationFrame(animate);
|
|
|
|
|
animateEnergyBeam();
|
|
|
|
|
const elapsed = clock.getElapsedTime();
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-03-24 22:16:49 +00:00
|
|
|
if (S.isWarping) {
|
revert: strip Manus damage (nostr, SovOS, gutted app.js) — restore clean split
Reverts to the state of cbfacdf (split app.js into 21 modules, <1000 lines each).
Removes: nostr.js, nostr-panel.js, SovOS.js, RESEARCH_DROP_456.md, core/, data/
Historical archive preserved in .historical/ and branch archive/manus-damage-2026-03-24
Refs #418, #452, #454
2026-03-24 18:18:44 -04:00
|
|
|
const warpElapsed = elapsed - S.warpStartTime;
|
|
|
|
|
const progress = Math.min(warpElapsed / WARP_DURATION, 1.0);
|
2026-03-24 22:16:49 +00:00
|
|
|
warpPass.uniforms['time'].value = elapsed;
|
revert: strip Manus damage (nostr, SovOS, gutted app.js) — restore clean split
Reverts to the state of cbfacdf (split app.js into 21 modules, <1000 lines each).
Removes: nostr.js, nostr-panel.js, SovOS.js, RESEARCH_DROP_456.md, core/, data/
Historical archive preserved in .historical/ and branch archive/manus-damage-2026-03-24
Refs #418, #452, #454
2026-03-24 18:18:44 -04:00
|
|
|
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;
|
2026-03-24 22:16:49 +00:00
|
|
|
}
|
|
|
|
|
|
revert: strip Manus damage (nostr, SovOS, gutted app.js) — restore clean split
Reverts to the state of cbfacdf (split app.js into 21 modules, <1000 lines each).
Removes: nostr.js, nostr-panel.js, SovOS.js, RESEARCH_DROP_456.md, core/, data/
Historical archive preserved in .historical/ and branch archive/manus-damage-2026-03-24
Refs #418, #452, #454
2026-03-24 18:18:44 -04:00
|
|
|
// 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();
|
refactor: split app.js (5416 lines) into 21 modules — hard cap 1000 lines/file
app.js: 5416 → 528 lines (entry point, animation loop, event wiring)
modules/state.js: shared mutable state object
modules/constants.js: color palette
modules/matrix-rain.js: matrix rain canvas effect
modules/scene-setup.js: scene, camera, renderer, lighting, stars
modules/platform.js: glass platform, perlin noise, floating island, clouds
modules/heatmap.js: commit heatmap
modules/sigil.js: Timmy sigil
modules/controls.js: mouse, overview, zoom, photo mode
modules/effects.js: energy beam, sovereignty meter, rune ring
modules/earth.js: holographic earth
modules/warp.js: warp tunnel, crystals, lightning
modules/dual-brain.js: dual-brain holographic panel
modules/audio.js: Web Audio, spatial, portal hums
modules/debug.js: debug mode, websocket, session export
modules/celebrations.js: easter egg, shockwave, fireworks
modules/portals.js: portal loading
modules/bookshelves.js: floating bookshelves, spine textures
modules/oath.js: The Oath interactive SOUL.md
modules/panels.js: agent status board, LoRA panel
modules/weather.js: weather system, portal health
modules/extras.js: gravity zones, speech, timelapse, bitcoin
Largest file: 528 lines (app.js). No file exceeds 1000.
All files pass node --check. No refactoring — mechanical split only.
2026-03-24 15:12:15 -04:00
|
|
|
composer.render();
|
revert: strip Manus damage (nostr, SovOS, gutted app.js) — restore clean split
Reverts to the state of cbfacdf (split app.js into 21 modules, <1000 lines each).
Removes: nostr.js, nostr-panel.js, SovOS.js, RESEARCH_DROP_456.md, core/, data/
Historical archive preserved in .historical/ and branch archive/manus-damage-2026-03-24
Refs #418, #452, #454
2026-03-24 18:18:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// === START ===
|
|
|
|
|
animate();
|
|
|
|
|
|
|
|
|
|
// === 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);
|
2026-03-24 22:16:49 +00:00
|
|
|
});
|
2026-03-24 05:14:30 +00:00
|
|
|
|
revert: strip Manus damage (nostr, SovOS, gutted app.js) — restore clean split
Reverts to the state of cbfacdf (split app.js into 21 modules, <1000 lines each).
Removes: nostr.js, nostr-panel.js, SovOS.js, RESEARCH_DROP_456.md, core/, data/
Historical archive preserved in .historical/ and branch archive/manus-damage-2026-03-24
Refs #418, #452, #454
2026-03-24 18:18:44 -04:00
|
|
|
window.addEventListener('pr-notification', (event) => {
|
|
|
|
|
console.log('[hermes] PR notification:', event.detail);
|
|
|
|
|
if (event.detail && event.detail.action === 'merged') {
|
|
|
|
|
triggerMergeFlash();
|
|
|
|
|
}
|
|
|
|
|
});
|