feat: firework celebration effect on milestone-complete (#241)
Adds a `triggerFireworks()` function that launches 7 coloured firework trails upward from near the island, each exploding into 120 additive- blended particles that fan out with gravity and fade over ~2.8 s. Triggered by the `milestone-complete` window event (dispatched by hermes) and also exposed as `window.triggerFireworks()` for console/testing. Fixes #241 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
155
app.js
155
app.js
@@ -1357,6 +1357,52 @@ function animate() {
|
||||
ring.mat.opacity = (1 - t) * 0.9;
|
||||
}
|
||||
|
||||
// Animate firework launches (trails rising upward)
|
||||
for (let i = fireworkLaunches.length - 1; i >= 0; i--) {
|
||||
const launch = fireworkLaunches[i];
|
||||
const age = elapsed - launch.startTime - launch.delay;
|
||||
if (age < 0) continue;
|
||||
const t = Math.min(age / FIREWORK_RISE_DURATION, 1);
|
||||
if (t >= 1) {
|
||||
scene.remove(launch.mesh);
|
||||
launch.mesh.geometry.dispose();
|
||||
launch.mesh.material.dispose();
|
||||
spawnFireworkBurst(launch.target, launch.color, elapsed);
|
||||
fireworkLaunches.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
const pos = new THREE.Vector3().lerpVectors(launch.origin, launch.target, t);
|
||||
launch.mesh.position.copy(pos);
|
||||
launch.mesh.material.opacity = 0.6 + Math.sin(t * Math.PI) * 0.4;
|
||||
}
|
||||
|
||||
// Animate firework burst particles
|
||||
for (let i = fireworkBursts.length - 1; i >= 0; i--) {
|
||||
const burst = fireworkBursts[i];
|
||||
const age = elapsed - burst.startTime;
|
||||
if (age < 0) continue;
|
||||
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;
|
||||
}
|
||||
const pos = burst.geo.attributes.position.array;
|
||||
const vel = burst.velocities;
|
||||
for (let j = 0; j < FIREWORK_PARTICLE_COUNT; j++) {
|
||||
pos[j * 3] += vel[j * 3];
|
||||
pos[j * 3 + 1] += vel[j * 3 + 1];
|
||||
pos[j * 3 + 2] += vel[j * 3 + 2];
|
||||
vel[j * 3 + 1] -= FIREWORK_GRAVITY; // gravity
|
||||
}
|
||||
burst.geo.attributes.position.needsUpdate = true;
|
||||
// Fade out: bright in middle, dim at end
|
||||
burst.mat.opacity = Math.sin(t * Math.PI) * 0.9 + 0.05;
|
||||
burst.mat.size = 0.28 * (1 - t * 0.5);
|
||||
}
|
||||
|
||||
// Animate rune ring — orbit and vertical float
|
||||
for (const rune of runeSprites) {
|
||||
const angle = rune.baseAngle + elapsed * RUNE_ORBIT_SPEED;
|
||||
@@ -1761,6 +1807,11 @@ window.addEventListener('pr-notification', (/** @type {CustomEvent} */ event) =>
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('milestone-complete', (/** @type {CustomEvent} */ event) => {
|
||||
console.log('[hermes] Milestone complete:', event.detail);
|
||||
triggerFireworks();
|
||||
});
|
||||
|
||||
// === SOVEREIGNTY EASTER EGG ===
|
||||
const SOVEREIGNTY_WORD = 'sovereignty';
|
||||
let sovereigntyBuffer = '';
|
||||
@@ -1866,6 +1917,110 @@ function triggerShockwave() {
|
||||
}
|
||||
}
|
||||
|
||||
// === FIREWORKS CELEBRATION ===
|
||||
const FIREWORK_PARTICLE_COUNT = 120;
|
||||
const FIREWORK_BURST_COUNT = 7;
|
||||
const FIREWORK_GRAVITY = 0.0035;
|
||||
const FIREWORK_RISE_DURATION = 1.2; // seconds for launch trail to rise
|
||||
const FIREWORK_BURST_DURATION = 2.8; // seconds for burst particles to fade
|
||||
|
||||
const FIREWORK_COLORS = [0xff4455, 0xffaa00, 0xffff44, 0x44ff88, 0x4488ff, 0xff44ff, 0x00ffff, 0xffd700, 0xffffff];
|
||||
|
||||
/**
|
||||
* Active launch trails rising toward their burst point.
|
||||
* @type {Array<{mesh: THREE.Mesh, origin: THREE.Vector3, target: THREE.Vector3, color: number, startTime: number, delay: number}>}
|
||||
*/
|
||||
const fireworkLaunches = [];
|
||||
|
||||
/**
|
||||
* Active burst particle systems.
|
||||
* @type {Array<{points: THREE.Points, geo: THREE.BufferGeometry, mat: THREE.PointsMaterial, velocities: Float32Array, startTime: number}>}
|
||||
*/
|
||||
const fireworkBursts = [];
|
||||
|
||||
/**
|
||||
* Spawns a burst of colored particles at the given position.
|
||||
* @param {THREE.Vector3} origin
|
||||
* @param {number} color
|
||||
* @param {number} startTime
|
||||
*/
|
||||
function spawnFireworkBurst(origin, color, startTime) {
|
||||
const geo = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(FIREWORK_PARTICLE_COUNT * 3);
|
||||
const velocities = new Float32Array(FIREWORK_PARTICLE_COUNT * 3);
|
||||
|
||||
for (let i = 0; i < FIREWORK_PARTICLE_COUNT; i++) {
|
||||
positions[i * 3] = origin.x;
|
||||
positions[i * 3 + 1] = origin.y;
|
||||
positions[i * 3 + 2] = origin.z;
|
||||
|
||||
const theta = Math.random() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * Math.random() - 1);
|
||||
const speed = 0.04 + Math.random() * 0.09;
|
||||
velocities[i * 3] = Math.sin(phi) * Math.cos(theta) * speed;
|
||||
velocities[i * 3 + 1] = Math.cos(phi) * speed * 0.8 + 0.015;
|
||||
velocities[i * 3 + 2] = Math.sin(phi) * Math.sin(theta) * speed;
|
||||
}
|
||||
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
|
||||
const mat = new THREE.PointsMaterial({
|
||||
color,
|
||||
size: 0.28,
|
||||
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, velocities, startTime });
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a firework trail that rises to a peak then explodes.
|
||||
* Call to celebrate a milestone completion.
|
||||
*/
|
||||
function triggerFireworks() {
|
||||
const now = clock.getElapsedTime();
|
||||
for (let i = 0; i < FIREWORK_BURST_COUNT; i++) {
|
||||
const x = (Math.random() - 0.5) * 14;
|
||||
const y = 10 + Math.random() * 8;
|
||||
const z = (Math.random() - 0.5) * 14;
|
||||
const color = FIREWORK_COLORS[Math.floor(Math.random() * FIREWORK_COLORS.length)];
|
||||
|
||||
const launchOrigin = new THREE.Vector3(x * 0.3, -1, z * 0.3);
|
||||
const target = new THREE.Vector3(x, y, z);
|
||||
|
||||
// Launch trail: a small bright sphere
|
||||
const trailGeo = new THREE.SphereGeometry(0.12, 6, 6);
|
||||
const trailMat = new THREE.MeshBasicMaterial({
|
||||
color,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
});
|
||||
const trailMesh = new THREE.Mesh(trailGeo, trailMat);
|
||||
trailMesh.position.copy(launchOrigin);
|
||||
scene.add(trailMesh);
|
||||
|
||||
fireworkLaunches.push({
|
||||
mesh: trailMesh,
|
||||
origin: launchOrigin,
|
||||
target,
|
||||
color,
|
||||
startTime: now,
|
||||
delay: i * 0.45,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Expose for external triggers (e.g. hermes events, console testing)
|
||||
window.triggerFireworks = triggerFireworks;
|
||||
|
||||
/**
|
||||
* Triggers a visual flash effect for merge events: stars pulse bright, lines glow.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user