Split the monolithic 5393-line app.js into 32 focused ES modules under modules/ with a thin ~330-line orchestrator. No bundler required — runs in-browser via import maps. Module structure: core/ — scene, ticker, state, theme, audio data/ — gitea, weather, bitcoin, loaders terrain/ — stars, clouds, island effects/ — matrix-rain, energy-beam, lightning, shockwave, rune-ring, gravity-zones panels/ — heatmap, sigil, sovereignty, dual-brain, batcave, earth, agent-board, lora-panel portals/ — portal-system, commit-banners narrative/ — bookshelves, oath, chat utils/ — perlin All files pass node --check. No new dependencies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
193 lines
7.3 KiB
JavaScript
193 lines
7.3 KiB
JavaScript
// modules/effects/shockwave.js — Shockwave ripple + fireworks + merge flash
|
|
import * as THREE from 'three';
|
|
import { starMaterial, constellationLines } from '../terrain/stars.js';
|
|
|
|
const SHOCKWAVE_RING_COUNT = 3;
|
|
const SHOCKWAVE_MAX_RADIUS = 14;
|
|
const SHOCKWAVE_DURATION = 2.5;
|
|
|
|
const FIREWORK_COLORS = [0xff4466, 0xffaa00, 0x00ffaa, 0x4488ff, 0xff44ff, 0xffff44, 0x00ffff];
|
|
const FIREWORK_BURST_PARTICLES = 80;
|
|
const FIREWORK_BURST_DURATION = 2.2;
|
|
const FIREWORK_GRAVITY = -5.0;
|
|
|
|
const shockwaveRings = [];
|
|
const fireworkBursts = [];
|
|
|
|
let _scene, _clock;
|
|
|
|
export function init(scene, clock) {
|
|
_scene = scene;
|
|
_clock = clock;
|
|
}
|
|
|
|
export function triggerShockwave() {
|
|
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) {
|
|
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();
|
|
const originalLineColor = constellationLines.material.color.getHex();
|
|
constellationLines.material.color.setHex(0x00ffff);
|
|
constellationLines.material.opacity = 1.0;
|
|
const originalStarColor = starMaterial.color.getHex();
|
|
const originalStarOpacity = starMaterial.opacity;
|
|
starMaterial.color.setHex(0x00ffff);
|
|
starMaterial.opacity = 1.0;
|
|
|
|
const startTime = performance.now();
|
|
const DURATION = 2000;
|
|
function fadeBack() {
|
|
const t = Math.min((performance.now() - startTime) / DURATION, 1);
|
|
const eased = t * t;
|
|
const origStarColor = new THREE.Color(originalStarColor);
|
|
starMaterial.color.setRGB(0 + origStarColor.r * eased, 1.0 + (origStarColor.g - 1.0) * eased, 1.0 + (origStarColor.b - 1.0) * eased);
|
|
starMaterial.opacity = 1.0 + (originalStarOpacity - 1.0) * eased;
|
|
const origLineColor = new THREE.Color(originalLineColor);
|
|
constellationLines.material.color.setRGB(0 + origLineColor.r * eased, 1.0 + (origLineColor.g - 1.0) * eased, 1.0 + origLineColor.b * eased);
|
|
constellationLines.material.opacity = 1.0 + (0.18 - 1.0) * eased;
|
|
if (t < 1) requestAnimationFrame(fadeBack);
|
|
else {
|
|
starMaterial.color.setHex(originalStarColor);
|
|
starMaterial.opacity = originalStarOpacity;
|
|
constellationLines.material.color.setHex(originalLineColor);
|
|
constellationLines.material.opacity = 0.18;
|
|
}
|
|
}
|
|
requestAnimationFrame(fadeBack);
|
|
}
|
|
|
|
export function triggerSovereigntyEasterEgg() {
|
|
const originalLineColor = constellationLines.material.color.getHex();
|
|
constellationLines.material.color.setHex(0xffd700);
|
|
constellationLines.material.opacity = 0.9;
|
|
const originalStarColor = starMaterial.color.getHex();
|
|
const originalStarOpacity = starMaterial.opacity;
|
|
starMaterial.color.setHex(0xffd700);
|
|
starMaterial.opacity = 1.0;
|
|
|
|
const sovereigntyMsg = document.getElementById('sovereignty-msg');
|
|
if (sovereigntyMsg) {
|
|
sovereigntyMsg.classList.remove('visible');
|
|
void sovereigntyMsg.offsetWidth;
|
|
sovereigntyMsg.classList.add('visible');
|
|
}
|
|
|
|
const startTime = performance.now();
|
|
const DURATION = 2500;
|
|
function fadeBack() {
|
|
const t = Math.min((performance.now() - startTime) / DURATION, 1);
|
|
const eased = t * t;
|
|
const origColor = new THREE.Color(originalStarColor);
|
|
starMaterial.color.setRGB(1.0 + (origColor.r - 1.0) * eased, 0.843 + (origColor.g - 0.843) * eased, 0 + origColor.b * eased);
|
|
starMaterial.opacity = 1.0 + (originalStarOpacity - 1.0) * eased;
|
|
const origLineColor = new THREE.Color(originalLineColor);
|
|
constellationLines.material.color.setRGB(1.0 + (origLineColor.r - 1.0) * eased, 0.843 + (origLineColor.g - 0.843) * eased, 0 + origLineColor.b * eased);
|
|
if (t < 1) requestAnimationFrame(fadeBack);
|
|
else {
|
|
starMaterial.color.setHex(originalStarColor);
|
|
starMaterial.opacity = originalStarOpacity;
|
|
constellationLines.material.color.setHex(originalLineColor);
|
|
if (sovereigntyMsg) sovereigntyMsg.classList.remove('visible');
|
|
}
|
|
}
|
|
requestAnimationFrame(fadeBack);
|
|
}
|
|
|
|
export function update(elapsed) {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|