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.
212 lines
7.2 KiB
JavaScript
212 lines
7.2 KiB
JavaScript
// === ENERGY BEAM + SOVEREIGNTY METER + RUNE RING ===
|
||
import * as THREE from 'three';
|
||
import { NEXUS } from './constants.js';
|
||
import { scene } from './scene-setup.js';
|
||
import { S } from './state.js';
|
||
|
||
// === ENERGY BEAM ===
|
||
const ENERGY_BEAM_RADIUS = 0.2;
|
||
const ENERGY_BEAM_HEIGHT = 50;
|
||
const ENERGY_BEAM_Y = 0;
|
||
const ENERGY_BEAM_X = -10;
|
||
const ENERGY_BEAM_Z = -10;
|
||
|
||
const energyBeamGeometry = new THREE.CylinderGeometry(ENERGY_BEAM_RADIUS, ENERGY_BEAM_RADIUS * 2.5, ENERGY_BEAM_HEIGHT, 32, 16, true);
|
||
export const energyBeamMaterial = new THREE.MeshBasicMaterial({
|
||
color: NEXUS.colors.accent,
|
||
emissive: NEXUS.colors.accent,
|
||
emissiveIntensity: 0.8,
|
||
transparent: true,
|
||
opacity: 0.6,
|
||
blending: THREE.AdditiveBlending,
|
||
side: THREE.DoubleSide,
|
||
depthWrite: false
|
||
});
|
||
const energyBeam = new THREE.Mesh(energyBeamGeometry, energyBeamMaterial);
|
||
energyBeam.position.set(ENERGY_BEAM_X, ENERGY_BEAM_Y + ENERGY_BEAM_HEIGHT / 2, ENERGY_BEAM_Z);
|
||
scene.add(energyBeam);
|
||
|
||
export function animateEnergyBeam() {
|
||
S.energyBeamPulse += 0.02;
|
||
const agentIntensity = S._activeAgentCount === 0 ? 0.1 : Math.min(0.1 + S._activeAgentCount * 0.3, 1.0);
|
||
const pulseEffect = Math.sin(S.energyBeamPulse) * 0.15 * agentIntensity;
|
||
energyBeamMaterial.opacity = agentIntensity * 0.6 + pulseEffect;
|
||
}
|
||
|
||
// === SOVEREIGNTY METER ===
|
||
export const sovereigntyGroup = new THREE.Group();
|
||
sovereigntyGroup.position.set(0, 3.8, 0);
|
||
|
||
const meterBgGeo = new THREE.TorusGeometry(1.6, 0.1, 8, 64);
|
||
const meterBgMat = new THREE.MeshBasicMaterial({ color: 0x0a1828, transparent: true, opacity: 0.5 });
|
||
sovereigntyGroup.add(new THREE.Mesh(meterBgGeo, meterBgMat));
|
||
|
||
function sovereigntyHexColor(score) {
|
||
if (score >= 80) return 0x00ff88;
|
||
if (score >= 40) return 0xffcc00;
|
||
return 0xff4444;
|
||
}
|
||
|
||
function buildScoreArcGeo(score) {
|
||
return new THREE.TorusGeometry(1.6, 0.1, 8, 64, (score / 100) * Math.PI * 2);
|
||
}
|
||
|
||
const scoreArcMat = new THREE.MeshBasicMaterial({
|
||
color: sovereigntyHexColor(S.sovereigntyScore),
|
||
transparent: true,
|
||
opacity: 0.9,
|
||
});
|
||
const scoreArcMesh = new THREE.Mesh(buildScoreArcGeo(S.sovereigntyScore), scoreArcMat);
|
||
scoreArcMesh.rotation.z = Math.PI / 2;
|
||
sovereigntyGroup.add(scoreArcMesh);
|
||
|
||
export const meterLight = new THREE.PointLight(sovereigntyHexColor(S.sovereigntyScore), 0.7, 6);
|
||
sovereigntyGroup.add(meterLight);
|
||
|
||
function buildMeterTexture(score, label, assessmentType) {
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = 256;
|
||
canvas.height = 128;
|
||
const ctx = canvas.getContext('2d');
|
||
const hexStr = score >= 80 ? '#00ff88' : score >= 40 ? '#ffcc00' : '#ff4444';
|
||
ctx.clearRect(0, 0, 256, 128);
|
||
ctx.font = 'bold 52px "Courier New", monospace';
|
||
ctx.fillStyle = hexStr;
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(`${score}%`, 128, 50);
|
||
ctx.font = '16px "Courier New", monospace';
|
||
ctx.fillStyle = '#8899bb';
|
||
ctx.fillText(label.toUpperCase(), 128, 74);
|
||
ctx.font = '11px "Courier New", monospace';
|
||
ctx.fillStyle = '#445566';
|
||
ctx.fillText('SOVEREIGNTY', 128, 94);
|
||
ctx.font = '9px "Courier New", monospace';
|
||
ctx.fillStyle = '#334455';
|
||
ctx.fillText(assessmentType === 'MANUAL' ? 'MANUAL ASSESSMENT' : 'MANUAL ASSESSMENT', 128, 112);
|
||
return new THREE.CanvasTexture(canvas);
|
||
}
|
||
|
||
const meterSpriteMat = new THREE.SpriteMaterial({
|
||
map: buildMeterTexture(S.sovereigntyScore, S.sovereigntyLabel, 'MANUAL'),
|
||
transparent: true,
|
||
depthWrite: false,
|
||
});
|
||
const meterSprite = new THREE.Sprite(meterSpriteMat);
|
||
meterSprite.scale.set(3.2, 1.6, 1);
|
||
sovereigntyGroup.add(meterSprite);
|
||
|
||
scene.add(sovereigntyGroup);
|
||
sovereigntyGroup.traverse(obj => {
|
||
if (obj.isMesh || obj.isSprite) obj.userData.zoomLabel = 'Sovereignty Meter';
|
||
});
|
||
|
||
export async function loadSovereigntyStatus() {
|
||
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 : '';
|
||
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 {
|
||
// defaults already set
|
||
}
|
||
}
|
||
|
||
loadSovereigntyStatus();
|
||
|
||
// === RUNE RING ===
|
||
let RUNE_COUNT = 12;
|
||
const RUNE_RING_RADIUS = 7.0;
|
||
export const RUNE_RING_Y = 1.5;
|
||
export const RUNE_ORBIT_SPEED = 0.08;
|
||
|
||
const ELDER_FUTHARK = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','ᚲ','ᚷ','ᚹ','ᚺ','ᚾ','ᛁ','ᛃ'];
|
||
const RUNE_GLOW_COLORS = ['#00ffcc', '#ff44ff'];
|
||
|
||
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);
|
||
}
|
||
|
||
const runeOrbitRingGeo = new THREE.TorusGeometry(RUNE_RING_RADIUS, 0.03, 6, 64);
|
||
const runeOrbitRingMat = new THREE.MeshBasicMaterial({
|
||
color: 0x224466, transparent: true, opacity: 0.22,
|
||
});
|
||
const runeOrbitRingMesh = new THREE.Mesh(runeOrbitRingGeo, runeOrbitRingMat);
|
||
runeOrbitRingMesh.rotation.x = Math.PI / 2;
|
||
runeOrbitRingMesh.position.y = RUNE_RING_Y;
|
||
scene.add(runeOrbitRingMesh);
|
||
|
||
/** @type {Array<{sprite: THREE.Sprite, baseAngle: number, floatPhase: number, portalOnline: boolean}>} */
|
||
export const runeSprites = [];
|
||
|
||
// portals ref — set from portals module
|
||
let _portalsRef = [];
|
||
export function setPortalsRef(ref) { _portalsRef = ref; }
|
||
export function getPortalsRef() { return _portalsRef; }
|
||
|
||
export function rebuildRuneRing() {
|
||
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;
|
||
|
||
const portalData = _portalsRef.length > 0 ? _portalsRef : null;
|
||
const count = portalData ? portalData.length : RUNE_COUNT;
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const glyph = ELDER_FUTHARK[i % ELDER_FUTHARK.length];
|
||
const color = portalData ? portalData[i].color : RUNE_GLOW_COLORS[i % RUNE_GLOW_COLORS.length];
|
||
const isOnline = portalData ? portalData[i].status === 'online' : true;
|
||
const texture = createRuneTexture(glyph, color);
|
||
|
||
const runeMat = new THREE.SpriteMaterial({
|
||
map: texture,
|
||
transparent: true,
|
||
opacity: isOnline ? 1.0 : 0.15,
|
||
depthWrite: false,
|
||
blending: THREE.AdditiveBlending,
|
||
});
|
||
const sprite = new THREE.Sprite(runeMat);
|
||
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 });
|
||
}
|
||
}
|
||
|
||
rebuildRuneRing();
|