feat: firework celebration effect on milestone completion (#254)
Some checks failed
CI / validate (pull_request) Failing after 11s
CI / auto-merge (pull_request) Has been skipped

Adds a multi-burst particle firework system to the Nexus 3D scene.
Each firework is a spherical explosion of colored particles with
additive blending and physics-based trajectories (gravity applied
analytically from burst origin).

Triggers:
- `milestone-complete` custom window event (for infra integration)
- chat messages containing "milestone" keyword
- 6 staggered bursts, random positions above the platform, 7-color palette

Fixes #254
This commit is contained in:
Alexander Whitestone
2026-03-24 01:07:38 -04:00
parent 3614886fad
commit e6c747b8cd

123
app.js
View File

@@ -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.
*/