Files
the-nexus/modules/effects.js

212 lines
7.2 KiB
JavaScript
Raw Normal View History

// === 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();