diff --git a/app.js b/app.js index 23cc7d3..40b33ce 100644 --- a/app.js +++ b/app.js @@ -575,6 +575,92 @@ async function loadSovereigntyStatus() { loadSovereigntyStatus(); +// === METEOR SHOWER === +const METEOR_COLOR = 0xaaddff; +const METEOR_SPEED_BASE = 55; // units/sec +const METEOR_TAIL_BASE = 7; // world units +const METEOR_DURATION_BASE = 1.3; // seconds + +/** @type {Array<{line: THREE.Line, startPos: THREE.Vector3, dir: THREE.Vector3, spawnTime: number, duration: number, speed: number, tailLength: number}>} */ +const activeMeteors = []; + +/** @type {Array<{triggerTime: number, delay: number, showerDir: THREE.Vector3}>} */ +const scheduledMeteors = []; + +// First shower triggers 15–30 s after load so it's visible on first visit; +// subsequent showers every 2–5 minutes. +let nextMeteorShowerAt = 15 + Math.random() * 15; + +/** + * Spawns a single meteor line in world space. + * @param {number} elapsed + * @param {THREE.Vector3} showerDir + */ +function spawnMeteor(elapsed, showerDir) { + const r = 140 + Math.random() * 60; + const phi = Math.random() * Math.PI * 2; + const elevAngle = (Math.random() * 0.4 + 0.1); // 0.1–0.5 rad above equator + const startPos = new THREE.Vector3( + r * Math.cos(elevAngle) * Math.cos(phi), + r * Math.sin(elevAngle) + 20, + r * Math.cos(elevAngle) * Math.sin(phi) + ); + + const dir = showerDir.clone().add( + new THREE.Vector3( + (Math.random() - 0.5) * 0.25, + (Math.random() - 0.5) * 0.25, + (Math.random() - 0.5) * 0.25 + ) + ).normalize(); + + const positions = new Float32Array(6); + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + + const mat = new THREE.LineBasicMaterial({ + color: METEOR_COLOR, + transparent: true, + opacity: 1.0, + depthWrite: false, + }); + + const line = new THREE.Line(geo, mat); + scene.add(line); + + activeMeteors.push({ + line, + startPos, + dir, + spawnTime: elapsed, + duration: METEOR_DURATION_BASE + Math.random() * 0.6, + speed: METEOR_SPEED_BASE + Math.random() * 25, + tailLength: METEOR_TAIL_BASE + Math.random() * 5, + }); +} + +/** + * Schedules a burst of meteors with staggered spawn delays. + * @param {number} elapsed + */ +function triggerMeteorShower(elapsed) { + const angle = Math.random() * Math.PI * 2; + const showerDir = new THREE.Vector3( + Math.cos(angle) * 0.65, + -0.55 - Math.random() * 0.25, + Math.sin(angle) * 0.65 + ).normalize(); + + const count = 8 + Math.floor(Math.random() * 10); + for (let i = 0; i < count; i++) { + scheduledMeteors.push({ + triggerTime: elapsed, + delay: i * (0.18 + Math.random() * 0.28), + showerDir, + }); + } +} + // === ANIMATION LOOP === const clock = new THREE.Clock(); @@ -656,6 +742,45 @@ function animate() { sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22; } + // Trigger meteor shower on schedule + if (elapsed >= nextMeteorShowerAt) { + triggerMeteorShower(elapsed); + nextMeteorShowerAt = elapsed + 120 + Math.random() * 180; // 2–5 min + } + + // Spawn any scheduled meteors whose delay has elapsed + for (let i = scheduledMeteors.length - 1; i >= 0; i--) { + const sm = scheduledMeteors[i]; + if (elapsed >= sm.triggerTime + sm.delay) { + spawnMeteor(elapsed, sm.showerDir); + scheduledMeteors.splice(i, 1); + } + } + + // Animate active meteors + for (let i = activeMeteors.length - 1; i >= 0; i--) { + const m = activeMeteors[i]; + const age = elapsed - m.spawnTime; + if (age >= m.duration) { + scene.remove(m.line); + m.line.geometry.dispose(); + m.line.material.dispose(); + activeMeteors.splice(i, 1); + continue; + } + const headDist = age * m.speed; + const tailDist = Math.max(0, headDist - m.tailLength); + const head = m.startPos.clone().addScaledVector(m.dir, headDist); + const tail = m.startPos.clone().addScaledVector(m.dir, tailDist); + const pos = m.line.geometry.attributes.position.array; + pos[0] = tail.x; pos[1] = tail.y; pos[2] = tail.z; + pos[3] = head.x; pos[4] = head.y; pos[5] = head.z; + m.line.geometry.attributes.position.needsUpdate = true; + // Fade out in last 35% of lifetime + const t = age / m.duration; + m.line.material.opacity = t > 0.65 ? (1 - t) / 0.35 : 1.0; + } + composer.render(); }