diff --git a/app.js b/app.js index 4a44cb8..e4957fc 100644 --- a/app.js +++ b/app.js @@ -1176,6 +1176,34 @@ function animate() { ring.mat.opacity = (1 - t) * 0.9; } + // Animate firework bursts — particles drift outward with gravity, fade out + 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; + } + // Fade out in last 40% of lifetime + burst.mat.opacity = t < 0.6 ? 1.0 : (1.0 - t) / 0.4; + + // Compute positions analytically: p = p0 + v*age + 0.5*g*age^2 + 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; + } + // Animate rune ring — orbit and vertical float for (const rune of runeSprites) { const angle = rune.baseAngle + elapsed * RUNE_ORBIT_SPEED; @@ -1549,9 +1577,17 @@ window.addEventListener('chat-message', (/** @type {CustomEvent} */ event) => { if (event.detail.text.toLowerCase().includes('sovereignty')) { triggerSovereigntyEasterEgg(); } + if (event.detail.text.toLowerCase().includes('milestone')) { + triggerFireworks(); + } } }); +window.addEventListener('milestone-complete', (/** @type {CustomEvent} */ event) => { + console.log('[nexus] Milestone complete:', event.detail); + triggerFireworks(); +}); + window.addEventListener('status-update', (/** @type {CustomEvent} */ event) => { console.log('[hermes] Status update:', event.detail); }); @@ -1668,6 +1704,93 @@ function triggerShockwave() { } } +// === FIREWORK CELEBRATION === +// Multi-burst particle fireworks launched above the scene on milestone completion. + +const FIREWORK_COLORS = [0xff4466, 0xffaa00, 0x00ffaa, 0x4488ff, 0xff44ff, 0xffff44, 0x00ffff]; +const FIREWORK_BURST_PARTICLES = 80; +const FIREWORK_BURST_DURATION = 2.2; // seconds + +/** + * @typedef {{ + * points: THREE.Points, + * geo: THREE.BufferGeometry, + * mat: THREE.PointsMaterial, + * origins: Float32Array, + * velocities: Float32Array, + * startTime: number, + * }} FireworkBurst + */ +/** @type {FireworkBurst[]} */ +const fireworkBursts = []; + +const FIREWORK_GRAVITY = -5.0; // world units per second^2 + +/** + * Creates a single firework burst at the given world position. + * @param {THREE.Vector3} origin + * @param {number} color hex color + */ +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++) { + // Uniform sphere direction + 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 }); +} + +/** + * Launches a full fireworks celebration: several bursts at staggered positions + * and times above the Nexus platform. + */ +function triggerFireworks() { + const burstCount = 6; + for (let i = 0; i < burstCount; 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); + } +} + /** * Triggers a visual flash effect for merge events: stars pulse bright, lines glow. */