Files
the-nexus/modules/effects.js
Timmy Time 764b617a2a
Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
Staging Smoke Test / smoke-test (push) Failing after 0s
[modularization] Phase 2: Extract data layer — gitea, weather, bitcoin, loaders (#460)
2026-03-24 21:28:03 +00:00

207 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// === 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';
import { fetchSovereigntyStatus } from './data/loaders.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 { score, label, assessmentType } = await fetchSovereigntyStatus();
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();
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();