feat: Add ambient audio and dynamic dust particles that react to the player's flashlight.
This commit is contained in:
@@ -39,6 +39,7 @@ export class Game {
|
|||||||
|
|
||||||
if (this.isRunning) {
|
if (this.isRunning) {
|
||||||
this.player.update(dt);
|
this.player.update(dt);
|
||||||
|
this.world.update(dt, this.player);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.graphics.render();
|
this.graphics.render();
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export class Player {
|
|||||||
if (this.ctx && this.ctx.state === 'suspended') {
|
if (this.ctx && this.ctx.state === 'suspended') {
|
||||||
this.ctx.resume();
|
this.ctx.resume();
|
||||||
this.audioEnabled = true;
|
this.audioEnabled = true;
|
||||||
|
this.startAmbience();
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event.code) {
|
switch (event.code) {
|
||||||
@@ -279,4 +280,40 @@ export class Player {
|
|||||||
noise.start();
|
noise.start();
|
||||||
noise.stop(t + 0.1);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/World.js
114
src/World.js
@@ -4,6 +4,7 @@ export class World {
|
|||||||
constructor(scene) {
|
constructor(scene) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.colliders = [];
|
this.colliders = [];
|
||||||
|
this.dustParticles = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
load() {
|
load() {
|
||||||
@@ -42,6 +43,49 @@ export class World {
|
|||||||
target.position.set(5, 0.5, -5);
|
target.position.set(5, 0.5, -5);
|
||||||
this.scene.add(target);
|
this.scene.add(target);
|
||||||
this.colliders.push(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) {
|
createWall(x, y, z, width, height, rotate = false) {
|
||||||
@@ -62,4 +106,74 @@ export class World {
|
|||||||
this.scene.add(pillar);
|
this.scene.add(pillar);
|
||||||
this.colliders.push(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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user