From 78c02169ef6b731cfb96b3cb582a4870f4de9fa5 Mon Sep 17 00:00:00 2001 From: eliazarw Date: Sat, 3 Jan 2026 08:48:35 +0000 Subject: [PATCH] feat: Add ambient audio and dynamic dust particles that react to the player's flashlight. --- src/Game.js | 1 + src/Player.js | 37 ++++++++++++++++ src/World.js | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+) diff --git a/src/Game.js b/src/Game.js index 7574bcc..0598244 100644 --- a/src/Game.js +++ b/src/Game.js @@ -39,6 +39,7 @@ export class Game { if (this.isRunning) { this.player.update(dt); + this.world.update(dt, this.player); } this.graphics.render(); diff --git a/src/Player.js b/src/Player.js index 6eba022..afb6fb6 100644 --- a/src/Player.js +++ b/src/Player.js @@ -72,6 +72,7 @@ export class Player { if (this.ctx && this.ctx.state === 'suspended') { this.ctx.resume(); this.audioEnabled = true; + this.startAmbience(); } switch (event.code) { @@ -279,4 +280,40 @@ export class Player { noise.start(); noise.stop(t + 0.1); } + + startAmbience() { + if (!this.ctx || this.ambienceStarted) return; + this.ambienceStarted = true; + + const t = this.ctx.currentTime; + + // Oscillator 1: The "Hum" (60hz roughly) + const osc1 = this.ctx.createOscillator(); + osc1.type = 'sine'; + osc1.frequency.setValueAtTime(55, t); // Low A (ish) + + // Oscillator 2: The "Detune" (creates beating/unsettling texture) + const osc2 = this.ctx.createOscillator(); + osc2.type = 'triangle'; + osc2.frequency.setValueAtTime(58, t); // Slightly off + + // Filter to keep it dark/muddy + const filter = this.ctx.createBiquadFilter(); + filter.type = 'lowpass'; + filter.frequency.setValueAtTime(120, t); // Very muffled + + // Gain (Volume) + const gain = this.ctx.createGain(); + gain.gain.setValueAtTime(0.3, t); // 30% volume + + // Connect graph + osc1.connect(filter); + osc2.connect(filter); + filter.connect(gain); + gain.connect(this.ctx.destination); + + // Start forever + osc1.start(); + osc2.start(); + } } diff --git a/src/World.js b/src/World.js index 01a858c..c69b36d 100644 --- a/src/World.js +++ b/src/World.js @@ -4,6 +4,7 @@ export class World { constructor(scene) { this.scene = scene; this.colliders = []; + this.dustParticles = null; } load() { @@ -42,6 +43,49 @@ export class World { target.position.set(5, 0.5, -5); this.scene.add(target); this.colliders.push(target); + + this.createDust(); + } + + createDust() { + // Create 2000 dust particles + const count = 2000; + const geometry = new THREE.BufferGeometry(); + const positions = new Float32Array(count * 3); + const colors = new Float32Array(count * 3); // For controlling opacity/brightness per particle + const velocities = []; + + for (let i = 0; i < count; i++) { + positions[i * 3] = (Math.random() - 0.5) * 40; + positions[i * 3 + 1] = Math.random() * 5; + positions[i * 3 + 2] = (Math.random() - 0.5) * 40; + + colors[i * 3] = 0; // Start invisible (black) + colors[i * 3 + 1] = 0; + colors[i * 3 + 2] = 0; + + velocities.push({ + x: (Math.random() - 0.5) * 0.1, + y: (Math.random() - 0.5) * 0.1, + z: (Math.random() - 0.5) * 0.1 + }); + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + + const material = new THREE.PointsMaterial({ + size: 0.05, + vertexColors: true, // IMPORTANT: Use per-particle colors + transparent: true, + opacity: 0.8, + sizeAttenuation: true, + blending: THREE.AdditiveBlending + }); + + this.dustParticles = new THREE.Points(geometry, material); + this.dustParticles.userData = { velocities: velocities }; + this.scene.add(this.dustParticles); } createWall(x, y, z, width, height, rotate = false) { @@ -62,4 +106,74 @@ export class World { this.scene.add(pillar); this.colliders.push(pillar); } + + update(dt, player) { + if (!this.dustParticles) return; + + const positions = this.dustParticles.geometry.attributes.position.array; + const colors = this.dustParticles.geometry.attributes.color.array; + const velocities = this.dustParticles.userData.velocities; + + // Flashlight info + let lightPos, lightDir, lightAngle, lightDist, isLightOn; + if (player && player.flashlight) { + // Get world position and direction of camera/flashlight + lightPos = new THREE.Vector3(); + lightDir = new THREE.Vector3(); + player.camera.getWorldPosition(lightPos); + player.camera.getWorldDirection(lightDir); + + lightAngle = player.flashlight.angle; // Cone half-angle + lightDist = player.flashlight.distance; + isLightOn = player.flashlightOn; + } + + const pPos = new THREE.Vector3(); // Temp vector + + for (let i = 0; i < velocities.length; i++) { + const v = velocities[i]; + + // 1. Move + positions[i * 3] += v.x * dt; + positions[i * 3 + 1] += v.y * dt; + positions[i * 3 + 2] += v.z * dt; + + // Wrap + if (positions[i * 3 + 1] < 0) positions[i * 3 + 1] = 5; + if (positions[i * 3 + 1] > 5) positions[i * 3 + 1] = 0; + if (Math.abs(positions[i * 3]) > 20) positions[i * 3] *= -0.9; + if (Math.abs(positions[i * 3 + 2]) > 20) positions[i * 3 + 2] *= -0.9; + + // 2. Lighting Check + let brightness = 0; + if (isLightOn) { + // Check distance + pPos.set(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]); + const dist = pPos.distanceTo(lightPos); + + if (dist < lightDist) { + // Check angle + // Vector from light to particle + const toPart = pPos.sub(lightPos).normalize(); + const angle = toPart.angleTo(lightDir); + + if (angle < lightAngle) { + // Inside cone! + // Fade out at edges of cone? optional + // Fade out with distance + const atten = 1.0 - (dist / lightDist); + brightness = atten * atten; // clearer near camera + } + } + } + + // Apply color (White * brightness) + colors[i * 3] = brightness; + colors[i * 3 + 1] = brightness; + colors[i * 3 + 2] = brightness; + } + + this.dustParticles.geometry.attributes.position.needsUpdate = true; + this.dustParticles.geometry.attributes.color.needsUpdate = true; + } }