feat: firework celebration effect on milestone completion (#254)
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:
123
app.js
123
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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user