feat: Add ambient audio and dynamic dust particles that react to the player's flashlight.

This commit is contained in:
2026-01-03 08:48:35 +00:00
parent cf4704c71b
commit 78c02169ef
3 changed files with 152 additions and 0 deletions

View File

@@ -39,6 +39,7 @@ export class Game {
if (this.isRunning) {
this.player.update(dt);
this.world.update(dt, this.player);
}
this.graphics.render();

View File

@@ -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();
}
}

View File

@@ -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;
}
}