[claude] Re-implement shockwave and fireworks on PR merge (#479) #500
130
app.js
130
app.js
@@ -45,6 +45,17 @@ let chatOpen = true;
|
||||
let loadProgress = 0;
|
||||
let performanceTier = 'high';
|
||||
|
||||
// ═══ CELEBRATIONS STATE ═══
|
||||
const shockwaveRings = [];
|
||||
const fireworkBursts = [];
|
||||
const SHOCKWAVE_DURATION = 2.5;
|
||||
const SHOCKWAVE_MAX_RADIUS = 14;
|
||||
const SHOCKWAVE_RING_COUNT = 3;
|
||||
const FIREWORK_BURST_PARTICLES = 80;
|
||||
const FIREWORK_BURST_DURATION = 2.2;
|
||||
const FIREWORK_GRAVITY = -5.0;
|
||||
const FIREWORK_COLORS = [0xff4466, 0xffaa00, 0x00ffaa, 0x4488ff, 0xff44ff, 0xffff44, 0x00ffff];
|
||||
|
||||
// ═══ NAVIGATION SYSTEM ═══
|
||||
const NAV_MODES = ['walk', 'orbit', 'fly'];
|
||||
let navModeIdx = 0;
|
||||
@@ -141,6 +152,17 @@ async function init() {
|
||||
window.addEventListener('resize', onResize);
|
||||
debugOverlay = document.getElementById('debug-overlay');
|
||||
|
||||
window.addEventListener('pr-notification', (event) => {
|
||||
console.log('[nexus] PR notification:', event.detail);
|
||||
if (event.detail && event.detail.action === 'merged') {
|
||||
triggerMergeFlash();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('milestone-complete', () => {
|
||||
triggerFireworks();
|
||||
});
|
||||
|
||||
updateLoad(100);
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -1480,6 +1502,47 @@ function gameLoop() {
|
||||
core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
|
||||
}
|
||||
|
||||
// Update shockwave rings
|
||||
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.mat.dispose();
|
||||
ring.mesh.geometry.dispose();
|
||||
shockwaveRings.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
const radius = t * SHOCKWAVE_MAX_RADIUS;
|
||||
ring.mesh.scale.setScalar(radius);
|
||||
ring.mat.opacity = (1 - t) * 0.8;
|
||||
}
|
||||
|
||||
// Update firework bursts
|
||||
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.mat.dispose();
|
||||
burst.geo.dispose();
|
||||
fireworkBursts.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
burst.mat.opacity = 1 - t * t;
|
||||
const halfGAge2 = 0.5 * FIREWORK_GRAVITY * age * age;
|
||||
const posArr = burst.geo.attributes.position.array;
|
||||
for (let j = 0; j < FIREWORK_BURST_PARTICLES; j++) {
|
||||
posArr[j * 3] = burst.origins[j * 3] + burst.velocities[j * 3] * age;
|
||||
posArr[j * 3 + 1] = burst.origins[j * 3 + 1] + burst.velocities[j * 3 + 1] * age + halfGAge2;
|
||||
posArr[j * 3 + 2] = burst.origins[j * 3 + 2] + burst.velocities[j * 3 + 2] * age;
|
||||
}
|
||||
burst.geo.attributes.position.needsUpdate = true;
|
||||
}
|
||||
|
||||
composer.render();
|
||||
|
||||
frameCount++;
|
||||
@@ -1679,6 +1742,73 @@ function addAgentLog(agentId, text) {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ CELEBRATIONS ═══
|
||||
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 });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function triggerMergeFlash() {
|
||||
triggerShockwave();
|
||||
triggerFireworks();
|
||||
}
|
||||
|
||||
function triggerHarnessPulse() {
|
||||
if (!harnessPulseMesh) return;
|
||||
harnessPulseMesh.scale.setScalar(0.1);
|
||||
|
||||
Reference in New Issue
Block a user