feat: Add player sprinting with stamina, flashlight and environmental shadows, and introduce new monster entities with expanded world furniture and safe zones.

This commit is contained in:
2026-01-03 11:07:34 +00:00
parent 94b6d7ac80
commit d588433d8a
8 changed files with 1197 additions and 16 deletions

View File

@@ -19,10 +19,14 @@
</div> </div>
<div id="hud" style="display: none;"> <div id="hud" style="display: none;">
<div id="battery">Battery: <span id="battery-level">100%</span></div> <div id="battery">Battery: <span id="battery-level">100%</span></div>
<div id="stamina-container">
<div id="stamina-bar"></div>
</div>
</div> </div>
<div id="debug-log" <div id="debug-log"
style="position: absolute; top: 10px; left: 10px; z-index: 10000; color: lime; font-family: monospace; pointer-events: none; background: rgba(0,0,0,0.8); max-height: 50%; overflow-y: auto; font-size: 12px; padding: 10px; width: 300px; display: none;"> style="position: absolute; top: 10px; left: 10px; z-index: 10000; color: lime; font-family: monospace; pointer-events: none; background: rgba(0,0,0,0.8); max-height: 50%; overflow-y: auto; font-size: 12px; padding: 10px; width: 300px; display: none;">
</div> </div>
<div id="test-dot"></div>
</div> </div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>

View File

@@ -1,12 +1,19 @@
import { Graphics } from './Graphics.js'; import { Graphics } from './Graphics.js';
import { World } from './World.js'; import { World } from './World.js';
import { Player } from './Player.js'; import { Player } from './Player.js';
import { Monster } from './Monster.js';
import { Monster2 } from './Monster2.js';
export class Game { export class Game {
constructor() { constructor() {
this.graphics = new Graphics(); this.graphics = new Graphics();
this.world = new World(this.graphics.scene); this.world = new World(this.graphics.scene);
this.player = new Player(this.graphics.camera, this.world.colliders); this.player = new Player(this.graphics.camera, this.world.colliders);
// Monster 1 (The Scuttler)
this.monster = new Monster(this.world.scene, this.player, this.world.colliders, this.player.ctx, this.world.safeZones);
// Monster 2 (The Stalker)
this.monster2 = new Monster2(this.world.scene, this.player, this.world.colliders, this.player.ctx, this.world);
this.isRunning = false; this.isRunning = false;
this.lastTime = 0; this.lastTime = 0;
@@ -40,6 +47,8 @@ export class Game {
if (this.isRunning) { if (this.isRunning) {
this.player.update(dt); this.player.update(dt);
this.world.update(dt, this.player); this.world.update(dt, this.player);
if (this.monster) this.monster.update(dt);
if (this.monster2) this.monster2.update(dt);
} }
this.graphics.render(); this.graphics.render();

View File

@@ -12,6 +12,8 @@ export class Graphics {
// Real Screen Renderer // Real Screen Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: false }); this.renderer = new THREE.WebGLRenderer({ antialias: false });
this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.domElement.id = 'three-canvas'; this.renderer.domElement.id = 'three-canvas';
// Append to the correct container, not body directly // Append to the correct container, not body directly

394
src/Monster.js Normal file
View File

@@ -0,0 +1,394 @@
import * as THREE from 'three';
export class Monster {
constructor(scene, player, colliders, audioCtx, safeZones) {
this.scene = scene;
this.player = player;
this.colliders = colliders;
this.audioCtx = audioCtx;
this.safeZones = safeZones || [];
this.mesh = new THREE.Group();
this.setupVisuals();
this.scene.add(this.mesh);
// AI State
this.state = 'PATROL'; // PATROL, CHASE, JUMPSCARE
this.speed = 1.5;
this.chaseSpeed = 3.5;
this.position = new THREE.Vector3(15, 0, 15);
this.mesh.position.copy(this.position);
this.targetNode = new THREE.Vector3();
this.setNewPatrolTarget();
this.detectionRange = 12;
this.catchRange = 1.5;
this.fov = Math.PI / 1.5; // Wide view
// Animation
this.bobTimer = 0;
this.lastUpdateTime = 0;
// Audio initialization
this.setupAudio();
}
setupAudio() {
if (!this.audioCtx) return;
// Proximity/Breathing Gain
this.breathGain = this.audioCtx.createGain();
this.breathGain.gain.value = 0;
// Spatial Audio
this.panner = this.audioCtx.createPanner();
this.panner.panningModel = 'HRTF';
this.panner.distanceModel = 'exponential';
this.panner.refDistance = 1;
this.panner.maxDistance = 25;
this.panner.rolloffFactor = 1.5;
// Continuous Deep Heavy Breathing (Quiet/Steady)
this.breathGain.connect(this.panner);
this.panner.connect(this.audioCtx.destination);
this.audioStarted = true;
}
setupVisuals() {
// Procedural "Static/Void" Texture for retro look
const size = 128;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#050505';
ctx.fillRect(0, 0, size, size);
for (let i = 0; i < 500; i++) {
ctx.fillStyle = Math.random() > 0.5 ? '#111111' : '#000000';
ctx.fillRect(Math.random() * size, Math.random() * size, 2, 2);
}
const tex = new THREE.CanvasTexture(canvas);
tex.minFilter = THREE.NearestFilter;
tex.magFilter = THREE.NearestFilter;
const mat = new THREE.MeshStandardMaterial({
map: tex,
roughness: 1,
metalness: 0,
color: 0x111111
});
// 1. Torso (Horizontal for crawling)
const torsoGeo = new THREE.BoxGeometry(0.4, 0.3, 1.2);
const torso = new THREE.Mesh(torsoGeo, mat);
torso.position.y = 0.4;
torso.castShadow = true;
this.mesh.add(torso);
// 2. Long spindly "Back" Legs
const legGeo = new THREE.BoxGeometry(0.1, 0.8, 0.1);
const legL = new THREE.Mesh(legGeo, mat);
legL.position.set(-0.25, 0.4, -0.4);
legL.rotation.x = 0.5;
legL.castShadow = true;
this.mesh.add(legL);
this.legL = legL;
const legR = new THREE.Mesh(legGeo, mat);
legR.position.set(0.25, 0.4, -0.4);
legR.rotation.x = 0.5;
legR.castShadow = true;
this.mesh.add(legR);
this.legR = legR;
// 3. Spindly "Front" Arms
const armL = new THREE.Mesh(legGeo, mat);
armL.position.set(-0.25, 0.4, 0.4);
armL.rotation.x = -0.5;
armL.castShadow = true;
this.mesh.add(armL);
this.armL = armL;
const armR = new THREE.Mesh(legGeo, mat);
armR.position.set(0.25, 0.4, 0.4);
armR.rotation.x = -0.5;
armR.castShadow = true;
this.mesh.add(armR);
this.armR = armR;
// 4. Small, unsettling head (Front-mounted)
const headGeo = new THREE.BoxGeometry(0.25, 0.25, 0.3);
const head = new THREE.Mesh(headGeo, mat);
head.position.set(0, 0.5, 0.7);
head.castShadow = true;
this.mesh.add(head);
// Glowy small eyes
const eyeGeo = new THREE.PlaneGeometry(0.04, 0.04);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const eyeL = new THREE.Mesh(eyeGeo, eyeMat);
eyeL.position.set(-0.06, 0.55, 0.86);
const eyeR = new THREE.Mesh(eyeGeo, eyeMat);
eyeR.position.set(0.06, 0.55, 0.86);
this.mesh.add(eyeL);
this.mesh.add(eyeR);
}
setNewPatrolTarget() {
// Random point within basement bounds (-28 to 28)
this.targetNode.set(
(Math.random() - 0.5) * 50,
0,
(Math.random() - 0.5) * 50
);
}
isPlayerSafe() {
const playerPos = this.player.camera.position.clone();
playerPos.y = 0;
for (const zone of this.safeZones) {
if (!zone.active) continue;
const dist = playerPos.distanceTo(zone.position);
if (dist < zone.radius) return true;
}
return false;
}
update(dt) {
if (!this.player) return;
const isSafe = this.isPlayerSafe();
const playerPos = this.player.camera.position.clone();
playerPos.y = 0;
const monsterPos = this.mesh.position.clone();
monsterPos.y = 0;
const distToPlayer = monsterPos.distanceTo(playerPos);
// State Machine
if (this.state === 'PATROL') {
const distToTarget = monsterPos.distanceTo(this.targetNode);
if (distToTarget < 1.0) {
this.setNewPatrolTarget();
}
// Move towards target
const dir = new THREE.Vector3().subVectors(this.targetNode, monsterPos).normalize();
this.mesh.position.add(dir.multiplyScalar(this.speed * dt));
// Rotation
const targetRotation = Math.atan2(dir.x, dir.z);
this.mesh.rotation.y += (targetRotation - this.mesh.rotation.y) * 2 * dt;
// Detection Check
if (distToPlayer < this.detectionRange && !isSafe) {
// If player is flashlighting us, or we see them
if (this.player.flashlightOn || distToPlayer < 5) {
this.state = 'CHASE';
window.log('The entity has spotted you!');
}
}
}
else if (this.state === 'CHASE') {
// Stop chasing if player enters safe zone
if (isSafe) {
this.state = 'PATROL';
this.setNewPatrolTarget();
window.log('It cannot reach you in the light...');
return;
}
const dir = new THREE.Vector3().subVectors(playerPos, monsterPos).normalize();
this.mesh.position.add(dir.multiplyScalar(this.chaseSpeed * dt));
// Intensive Rotation
const targetRotation = Math.atan2(dir.x, dir.z);
this.mesh.rotation.y = targetRotation;
// Lost player?
if (distToPlayer > this.detectionRange * 1.5) {
this.state = 'PATROL';
this.setNewPatrolTarget();
window.log('It lost your trail...');
}
// Catch check
if (distToPlayer < this.catchRange) {
this.state = 'JUMPSCARE';
this.jumpscareTimer = 0;
this.player.lockLook = true; // Lock player input
}
}
else if (this.state === 'JUMPSCARE') {
this.jumpscareTimer += dt;
const camPos = this.player.camera.position.clone();
const monsterPos = this.mesh.position.clone();
// 1. Move/Lunge at camera
const camDir = new THREE.Vector3();
this.player.camera.getWorldDirection(camDir);
const jumpTarget = camPos.clone().add(camDir.multiplyScalar(0.2));
this.mesh.position.lerp(jumpTarget, 15 * dt);
// 2. STARE: Force monster to look at camera
this.mesh.lookAt(camPos);
// 3. SHAKE: Intense high-frequency jitter/shiver
const shakeIntensity = 0.15;
this.mesh.position.x += (Math.random() - 0.5) * shakeIntensity;
this.mesh.position.y += (Math.random() - 0.5) * shakeIntensity;
this.mesh.position.z += (Math.random() - 0.5) * shakeIntensity;
// Rotational shakes for a more "visceral/glitchy" feel
this.mesh.rotation.x += (Math.random() - 0.5) * 0.4;
this.mesh.rotation.y += (Math.random() - 0.5) * 0.4;
this.mesh.rotation.z += (Math.random() - 0.5) * 0.4;
// FORCE CAMERA TO LOOK AT MONSTER (Keep focused)
this.player.camera.lookAt(this.mesh.position);
if (this.jumpscareTimer > 0.8) {
this.onCatchPlayer();
}
}
// Crawling scuttle animation
this.bobTimer += dt * (this.state === 'CHASE' ? 12 : 6);
const bob = Math.sin(this.bobTimer) * 0.05;
this.mesh.position.y = bob;
// Limb scuttle (opposite diagonal movement)
const gait = Math.sin(this.bobTimer);
const lastGait = this.prevGait || 0;
this.prevGait = gait;
this.armL.rotation.x = -0.5 + gait * 0.4;
this.legR.rotation.x = 0.5 + gait * 0.4;
this.armR.rotation.x = -0.5 - gait * 0.4;
this.legL.rotation.x = 0.5 - gait * 0.4;
// Trigger pattering sound at gait extremes
if (Math.sign(gait) !== Math.sign(lastGait)) {
this.playPatteringSound(distToPlayer);
}
// Slight twitching
this.mesh.rotation.z = Math.sin(this.bobTimer * 2) * 0.05;
// Update Audio
this.updateAudio(dt, distToPlayer, monsterPos);
}
playPatteringSound(dist) {
if (!this.audioCtx || dist > 20) return;
const t = this.audioCtx.currentTime;
// Light "patter" noise (Short high-frequency tap) - Increased Volume
const g = this.audioCtx.createGain();
g.gain.setValueAtTime(0.25 * (1 - dist / 20), t); // Increased from 0.08 to 0.25
const osc = this.audioCtx.createOscillator();
osc.type = 'triangle';
osc.frequency.setValueAtTime(400, t);
osc.frequency.exponentialRampToValueAtTime(1200, t + 0.01);
osc.frequency.exponentialRampToValueAtTime(80, t + 0.03);
const filter = this.audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 800;
filter.Q.value = 1;
osc.connect(filter);
filter.connect(g);
g.connect(this.panner);
osc.start(t);
osc.stop(t + 0.04);
}
updateAudio(dt, dist, monsterPos) {
if (!this.audioCtx || !this.player || !this.audioStarted) return;
// Update Panner Position
this.panner.positionX.setTargetAtTime(monsterPos.x, this.audioCtx.currentTime, 0.1);
this.panner.positionY.setTargetAtTime(monsterPos.y + 0.5, this.audioCtx.currentTime, 0.1);
this.panner.positionZ.setTargetAtTime(monsterPos.z, this.audioCtx.currentTime, 0.1);
// Update Listener Position (Player)
const listener = this.audioCtx.listener;
const playerPos = this.player.camera.position;
listener.positionX.setTargetAtTime(playerPos.x, this.audioCtx.currentTime, 0.1);
listener.positionY.setTargetAtTime(playerPos.y, this.audioCtx.currentTime, 0.1);
listener.positionZ.setTargetAtTime(playerPos.z, this.audioCtx.currentTime, 0.1);
// Deep Quiet Heavy Breathing (Slower rhythm)
const breathCycle = Math.sin(this.audioCtx.currentTime * 1.2) * 0.5 + 0.5;
let targetBreath = 0;
if (dist < 15) {
targetBreath = (1 - dist / 15) * 0.3 * breathCycle;
// Randomly trigger a deep breath sound
if (Math.random() < 0.003) {
this.playDeepBreath();
}
}
this.breathGain.gain.setTargetAtTime(targetBreath, this.audioCtx.currentTime, 0.1);
}
playDeepBreath() {
if (!this.audioCtx) return;
const t = this.audioCtx.currentTime;
const source = this.audioCtx.createBufferSource();
const bufferSize = this.audioCtx.sampleRate * 2;
const buffer = this.audioCtx.createBuffer(1, bufferSize, this.audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = (Math.random() * 2 - 1) * 0.1;
source.buffer = buffer;
const lowpass = this.audioCtx.createBiquadFilter();
lowpass.type = 'lowpass';
lowpass.frequency.setValueAtTime(300, t);
lowpass.frequency.exponentialRampToValueAtTime(100, t + 1.5);
const g = this.audioCtx.createGain();
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(0.2, t + 0.5);
g.gain.linearRampToValueAtTime(0, t + 2.0);
source.connect(lowpass);
lowpass.connect(g);
g.connect(this.panner);
source.start(t);
}
onCatchPlayer() {
if (this.isEnding) return;
this.isEnding = true;
window.log('FATAL ERROR: PLAYER_RECOVERY_FAILED');
// Blackout and reload without text
const screen = document.createElement('div');
screen.style.position = 'fixed';
screen.style.top = '0';
screen.style.left = '0';
screen.style.width = '100vw';
screen.style.height = '100vh';
screen.style.backgroundColor = 'black';
screen.style.zIndex = '99999';
document.body.appendChild(screen);
setTimeout(() => {
window.location.reload();
}, 1500);
}
}

330
src/Monster2.js Normal file
View File

@@ -0,0 +1,330 @@
import * as THREE from 'three';
export class Monster2 {
constructor(scene, player, colliders, audioCtx, world) {
this.scene = scene;
this.player = player;
this.colliders = colliders;
this.audioCtx = audioCtx;
this.world = world;
this.safeZones = world ? world.safeZones : [];
this.mesh = new THREE.Group();
this.setupVisuals();
this.scene.add(this.mesh);
// AI State
this.state = 'STALK'; // STALK, FREEZE, JUMPSCARE
this.position = new THREE.Vector3(-15, 0, -15); // Start opposite to player
this.mesh.position.copy(this.position);
this.stalkDistance = 18;
this.minDistance = 12;
this.catchRange = 2.0;
this.moveSpeed = 4.0;
// Stalking logic
this.glitchTimer = 0;
this.isVisibleToPlayer = false;
this.setupAudio();
}
setupVisuals() {
// TV Static Texture Setup
this.staticSize = 64;
this.staticCanvas = document.createElement('canvas');
this.staticCanvas.width = this.staticSize;
this.staticCanvas.height = this.staticSize;
this.staticCtx = this.staticCanvas.getContext('2d');
this.staticTex = new THREE.CanvasTexture(this.staticCanvas);
this.staticTex.minFilter = THREE.NearestFilter;
this.staticTex.magFilter = THREE.NearestFilter;
this.updateStatic(); // Initial draw
const mat = new THREE.MeshStandardMaterial({
map: this.staticTex,
roughness: 0.5,
metalness: 0.5,
emissive: 0x222222, // Subtle glow to make static visible in pitch black
emissiveMap: this.staticTex
});
// 1. Torso (Vertical, extremely slender)
const torsoGeo = new THREE.BoxGeometry(0.2, 1.4, 0.15);
const torso = new THREE.Mesh(torsoGeo, mat);
torso.position.y = 1.8;
torso.castShadow = true;
this.mesh.add(torso);
// 2. Legs (Extremely long)
const legGeo = new THREE.BoxGeometry(0.1, 1.8, 0.1);
const legL = new THREE.Mesh(legGeo, mat);
legL.position.set(-0.1, 0.9, 0);
legL.castShadow = true;
this.mesh.add(legL);
const legR = new THREE.Mesh(legGeo, mat);
legR.position.set(0.1, 0.9, 0);
legR.castShadow = true;
this.mesh.add(legR);
// 3. Arms (Dragging on floor)
const armGeo = new THREE.BoxGeometry(0.08, 2.2, 0.08);
const armL = new THREE.Mesh(armGeo, mat);
armL.position.set(-0.25, 1.5, 0);
armL.rotation.z = 0.05;
armL.castShadow = true;
this.mesh.add(armL);
const armR = new THREE.Mesh(armGeo, mat);
armR.position.set(0.25, 1.5, 0);
armR.rotation.z = -0.05;
armR.castShadow = true;
this.mesh.add(armR);
// 4. Head
const headGeo = new THREE.BoxGeometry(0.2, 0.3, 0.2);
const head = new THREE.Mesh(headGeo, mat);
head.position.y = 2.65;
head.castShadow = true;
this.mesh.add(head);
// Single bright white eye
const eyeGeo = new THREE.PlaneGeometry(0.06, 0.06);
this.eyeMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.9 });
const eye = new THREE.Mesh(eyeGeo, this.eyeMat);
eye.position.set(0, 2.7, 0.11);
this.mesh.add(eye);
// Store material for fade logic
this.mainMat = mat;
}
updateStatic() {
if (!this.staticCtx) return;
const imageData = this.staticCtx.createImageData(this.staticSize, this.staticSize);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const v = Math.random() > 0.5 ? 255 : 0;
data[i] = v; // R
data[i + 1] = v; // G
data[i + 2] = v; // B
data[i + 3] = 255; // A
}
this.staticCtx.putImageData(imageData, 0, 0);
this.staticTex.needsUpdate = true;
}
setupAudio() {
if (!this.audioCtx) return;
this.panner = this.audioCtx.createPanner();
this.panner.panningModel = 'HRTF';
this.panner.distanceModel = 'exponential';
this.panner.refDistance = 2;
this.panner.maxDistance = 30;
this.panner.rolloffFactor = 1.0;
this.panner.connect(this.audioCtx.destination);
// Resonant Crystalline Hum
this.humGain = this.audioCtx.createGain();
this.humGain.gain.value = 0;
const osc1 = this.audioCtx.createOscillator();
const osc2 = this.audioCtx.createOscillator();
osc1.type = 'sine';
osc2.type = 'sine';
osc1.frequency.value = 880; // High A
osc2.frequency.value = 883; // Beating
osc1.connect(this.humGain);
osc2.connect(this.humGain);
this.humGain.connect(this.panner);
osc1.start();
osc2.start();
this.audioStarted = true;
}
isPlayerSafe() {
const playerPos = this.player.camera.position.clone();
playerPos.y = 0;
for (const zone of this.safeZones) {
if (!zone.active) continue;
const dist = playerPos.distanceTo(zone.position);
if (dist < zone.radius) return true;
}
return false;
}
update(dt) {
if (!this.player) return;
const isSafe = this.isPlayerSafe();
const playerPos = this.player.camera.position.clone();
playerPos.y = 0;
const monsterPos = this.mesh.position.clone();
monsterPos.y = 0;
const distToPlayer = monsterPos.distanceTo(playerPos);
const flashlightOn = this.player.flashlightOn;
// 1. SIGNAL PARASITE LOGIC: Fade and Freeze if light is off
let targetOpacity = flashlightOn ? 1.0 : 0.05;
this.mainMat.opacity = THREE.MathUtils.lerp(this.mainMat.opacity, targetOpacity, 5 * dt);
this.mainMat.transparent = this.mainMat.opacity < 1.0;
this.eyeMat.opacity = this.mainMat.opacity;
// 2. AI State Logic
if (this.state === 'STALK') {
if (isSafe) {
this.state = 'FREEZE';
} else if (flashlightOn) {
// Relentlessly glitch towards the signal
this.glitchTimer += dt;
// Faster glitching when light is on
if (this.glitchTimer > 0.3) {
const dir = new THREE.Vector3().subVectors(playerPos, monsterPos).normalize();
this.mesh.position.add(dir.multiplyScalar(this.moveSpeed * dt * 3));
this.glitchTimer = 0;
this.playDraggingSound(distToPlayer);
}
}
// If flashlight is OFF, it stays dormant (no movement)
} else if (this.state === 'FREEZE') {
if (!isSafe) {
this.state = 'STALK';
}
} else if (this.state === 'JUMPSCARE') {
this.updateJumpscare(dt);
}
// 3. SAFE ZONE SABOTAGE: Glitch and Break lights
if (this.world) {
this.safeZones.forEach((zone, index) => {
const distToZone = monsterPos.distanceTo(zone.position);
if (zone.active) {
if (distToZone < 10) {
// Near a light? Glitch it!
this.world.glitchSafeZone(index, 0.4);
}
if (distToZone < 2.5) {
// Too close? BREAK IT!
this.world.breakSafeZone(index);
}
}
});
}
// 4. INTERFERENCE: Flicker player's flashlight when close
if (flashlightOn && distToPlayer < 8 && this.state !== 'JUMPSCARE') {
if (Math.random() < 0.3) {
this.player.flashlight.intensity = Math.random() * 2; // Intense flicker
}
}
// 5. Distance Check for Jumpscare
if (distToPlayer < this.catchRange && this.state !== 'JUMPSCARE' && !isSafe) {
this.state = 'JUMPSCARE';
this.jumpscareTimer = 0;
this.player.lockLook = true;
}
this.mesh.lookAt(playerPos.x, this.mesh.position.y, playerPos.z);
this.updateAudio(distToPlayer, dt);
this.updateStatic();
}
playDraggingSound(dist) {
if (!this.audioCtx || dist > 25) return;
const t = this.audioCtx.currentTime;
const g = this.audioCtx.createGain();
g.gain.setValueAtTime(0.08 * (1 - dist / 25), t); // Reduced from 0.15 to 0.08
g.gain.exponentialRampToValueAtTime(0.01, t + 0.4);
const osc = this.audioCtx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(40, t);
osc.frequency.linearRampToValueAtTime(10, t + 0.3);
const filter = this.audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 100;
osc.connect(filter);
filter.connect(g);
g.connect(this.panner);
osc.start(t);
osc.stop(t + 0.4);
}
updateAudio(dist, dt) {
if (!this.audioCtx || !this.audioStarted) return;
// Update panner
this.panner.positionX.setTargetAtTime(this.mesh.position.x, this.audioCtx.currentTime, 0.1);
this.panner.positionY.setTargetAtTime(this.mesh.position.y + 2, this.audioCtx.currentTime, 0.1);
this.panner.positionZ.setTargetAtTime(this.mesh.position.z, this.audioCtx.currentTime, 0.1);
// Signal Parasite Hum: Faster oscillation when close
let humVol = 0;
if (dist < 20) {
humVol = (1 - dist / 20) * 0.05;
}
this.humGain.gain.setTargetAtTime(humVol, this.audioCtx.currentTime, 0.1);
}
updateJumpscare(dt) {
this.jumpscareTimer += dt;
const camPos = this.player.camera.position.clone();
const camDir = new THREE.Vector3();
this.player.camera.getWorldDirection(camDir);
const jumpTarget = camPos.clone().add(camDir.multiplyScalar(0.1));
this.mesh.position.lerp(jumpTarget, 10 * dt);
this.player.camera.lookAt(this.mesh.position.x, this.mesh.position.y + 2.5, this.mesh.position.z);
// Shake
this.mesh.position.x += (Math.random() - 0.5) * 0.2;
this.mesh.position.y += (Math.random() - 0.5) * 0.2;
if (this.jumpscareTimer > 0.7) {
this.onCatchPlayer();
}
}
onCatchPlayer() {
if (this.isEnding) return;
this.isEnding = true;
// Static sound for jumpscare
if (this.audioCtx) {
const bufferSize = this.audioCtx.sampleRate * 0.5;
const buffer = this.audioCtx.createBuffer(1, bufferSize, this.audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const source = this.audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(this.audioCtx.destination);
source.start();
}
const screen = document.createElement('div');
screen.style.position = 'fixed';
screen.style.top = '0';
screen.style.left = '0';
screen.style.width = '100vw';
screen.style.height = '100vh';
screen.style.backgroundColor = 'black';
screen.style.zIndex = '99999';
document.body.appendChild(screen);
setTimeout(() => {
window.location.reload();
}, 1000);
}
}

View File

@@ -30,10 +30,10 @@ export class Player {
this.adjustBright = false; // 'L' key this.adjustBright = false; // 'L' key
this.velocity = new THREE.Vector3(); this.velocity = new THREE.Vector3();
this.direction = new THREE.Vector3(); this.direction = new THREE.Vector3();
this.direction = new THREE.Vector3();
this.flashlightOn = true; // Started as ON this.flashlightOn = true; // Started as ON
this.battery = 100.0; this.battery = 100.0;
this.battery = 100.0; this.stamina = 100.0;
this.isSprinting = false;
this.baseDrain = 0.5; // Drain per second at base intensity this.baseDrain = 0.5; // Drain per second at base intensity
// Animation // Animation
@@ -48,6 +48,7 @@ export class Player {
this.headBobTimer = 0; this.headBobTimer = 0;
this.baseDrain = 0.5; // Drain per second at base intensity this.baseDrain = 0.5; // Drain per second at base intensity
this.lockLook = false; // To disable controls during jumpscare
this.setupInput(); this.setupInput();
this.setupFlashlight(); this.setupFlashlight();
@@ -93,6 +94,15 @@ export class Player {
this.flashlight.decay = 2.0; // Faster falloff this.flashlight.decay = 2.0; // Faster falloff
this.flashlight.distance = 50; this.flashlight.distance = 50;
this.flashlight.position.set(0, 0, -0.1); // At tip this.flashlight.position.set(0, 0, -0.1); // At tip
// Enable shadows
this.flashlight.castShadow = true;
this.flashlight.shadow.mapSize.width = 512;
this.flashlight.shadow.mapSize.height = 512;
this.flashlight.shadow.camera.near = 0.5;
this.flashlight.shadow.camera.far = 50;
this.flashlight.shadow.bias = -0.001;
// Aim inward to crosshair (Converge) // Aim inward to crosshair (Converge)
// Group is at (0.3, -0.25), so target needs to be (-0.3, 0.25) to hit center 0,0 relative to camera // Group is at (0.3, -0.25), so target needs to be (-0.3, 0.25) to hit center 0,0 relative to camera
this.flashlight.target.position.set(-0.3, 0.25, -20); this.flashlight.target.position.set(-0.3, 0.25, -20);
@@ -123,6 +133,8 @@ export class Player {
case 'KeyF': this.toggleFlashlight(); break; case 'KeyF': this.toggleFlashlight(); break;
case 'KeyK': this.adjustDim = true; break; case 'KeyK': this.adjustDim = true; break;
case 'KeyL': this.adjustBright = true; break; case 'KeyL': this.adjustBright = true; break;
case 'ShiftLeft': this.isSprinting = true; break;
case 'ShiftRight': this.isSprinting = true; break;
} }
}; };
@@ -134,6 +146,8 @@ export class Player {
case 'KeyD': this.moveRight = false; break; case 'KeyD': this.moveRight = false; break;
case 'KeyK': this.adjustDim = false; break; case 'KeyK': this.adjustDim = false; break;
case 'KeyL': this.adjustBright = false; break; case 'KeyL': this.adjustBright = false; break;
case 'ShiftLeft': this.isSprinting = false; break;
case 'ShiftRight': this.isSprinting = false; break;
} }
}; };
@@ -179,7 +193,7 @@ export class Player {
// window.log(`Controls Locked: ${this.controls.isLocked}`); // window.log(`Controls Locked: ${this.controls.isLocked}`);
} }
if (!this.controls.isLocked) return; if (!this.controls.isLocked || this.lockLook) return;
// Friction-like dampening (simple decay) // Friction-like dampening (simple decay)
this.velocity.x -= this.velocity.x * 10.0 * dt; this.velocity.x -= this.velocity.x * 10.0 * dt;
@@ -189,7 +203,25 @@ export class Player {
this.direction.x = Number(this.moveRight) - Number(this.moveLeft); this.direction.x = Number(this.moveRight) - Number(this.moveLeft);
this.direction.normalize(); this.direction.normalize();
const accel = this.speed * 10.0; // Acceleration to reach max speed against friction (assuming friction 10) // Sprint Logic
let currentSpeed = this.speed;
if (this.isSprinting && (this.moveForward || this.moveBackward || this.moveLeft || this.moveRight)) {
if (this.stamina > 0) {
currentSpeed = this.speed * 2.0;
this.stamina = Math.max(0, this.stamina - 30 * dt); // Drain fast
} else {
currentSpeed = this.speed; // No stamina, no run
}
} else {
// Regen
this.stamina = Math.min(100, this.stamina + 15 * dt);
}
// Update UI
const stamBar = document.getElementById('stamina-bar');
if (stamBar) stamBar.style.width = this.stamina + '%';
const accel = currentSpeed * 10.0; // Acceleration to reach max speed against friction (assuming friction 10)
if (this.moveForward || this.moveBackward) this.velocity.z -= this.direction.z * accel * dt; if (this.moveForward || this.moveBackward) this.velocity.z -= this.direction.z * accel * dt;
if (this.moveLeft || this.moveRight) this.velocity.x -= this.direction.x * accel * dt; if (this.moveLeft || this.moveRight) this.velocity.x -= this.direction.x * accel * dt;
@@ -227,11 +259,14 @@ export class Player {
// Audio Only Trigger // Audio Only Trigger
if (this.moveForward || this.moveBackward || this.moveLeft || this.moveRight) { if (this.moveForward || this.moveBackward || this.moveLeft || this.moveRight) {
// Step every 0.6 seconds // Determine if actually running (key held AND stamina available)
const isRunning = this.isSprinting && this.stamina > 0;
const interval = isRunning ? 0.35 : 0.6; // Faster steps when running
this.lastStepTime += dt; this.lastStepTime += dt;
if (this.lastStepTime > 0.6) { if (this.lastStepTime > interval) {
this.lastStepTime = 0; this.lastStepTime = 0;
this.playFootstep(); this.playFootstep(isRunning);
} }
} else { } else {
this.lastStepTime = 0.5; // Ready to step properly next time this.lastStepTime = 0.5; // Ready to step properly next time
@@ -290,11 +325,10 @@ export class Player {
} }
} }
playFootstep() { playFootstep(isRunning = false) {
if (!this.ctx) return; if (!this.ctx) return;
const t = this.ctx.currentTime; const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain(); const gain = this.ctx.createGain();
const filter = this.ctx.createBiquadFilter(); const filter = this.ctx.createBiquadFilter();
@@ -310,10 +344,12 @@ export class Player {
// Filter to make it sound dull (floor/carpet) // Filter to make it sound dull (floor/carpet)
filter.type = 'lowpass'; filter.type = 'lowpass';
filter.frequency.setValueAtTime(400, t); // Low muffled thud // Running sounds heavier/sharper
filter.frequency.setValueAtTime(isRunning ? 600 : 400, t);
// Envelope // Envelope
gain.gain.setValueAtTime(0.3, t); const vol = isRunning ? 0.6 : 0.3;
gain.gain.setValueAtTime(vol, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
// Connect // Connect

View File

@@ -6,6 +6,7 @@ export class World {
this.colliders = []; this.colliders = [];
this.staticObstacles = []; // Track pillars etc for spawning this.staticObstacles = []; // Track pillars etc for spawning
this.dustParticles = null; this.dustParticles = null;
this.safeZones = []; // [{ position: Vector3, radius: number }]
} }
load() { load() {
@@ -28,6 +29,7 @@ export class World {
}); });
const floor = new THREE.Mesh(floorGeo, floorMat); const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2; floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
this.scene.add(floor); this.scene.add(floor);
// Ceiling (Dirty Concrete, Darker) // Ceiling (Dirty Concrete, Darker)
@@ -40,6 +42,7 @@ export class World {
const ceiling = new THREE.Mesh(ceilingGeo, ceilingMat); const ceiling = new THREE.Mesh(ceilingGeo, ceilingMat);
ceiling.rotation.x = Math.PI / 2; ceiling.rotation.x = Math.PI / 2;
ceiling.position.y = 5; // Top of walls ceiling.position.y = 5; // Top of walls
ceiling.receiveShadow = true;
this.scene.add(ceiling); this.scene.add(ceiling);
// Simple walls (Expanded to 60x60 with Moldy Plaster) // Simple walls (Expanded to 60x60 with Moldy Plaster)
@@ -66,6 +69,12 @@ export class World {
this.createFurniture(45); this.createFurniture(45);
this.createDust(); this.createDust();
// Spawn Safe Zones (Fixed points for balance)
this.spawnSafeZone(12, 12);
this.spawnSafeZone(-12, -12);
this.spawnSafeZone(15, -15);
this.spawnSafeZone(-15, 15);
} }
createProceduralTexture(type) { createProceduralTexture(type) {
@@ -149,6 +158,8 @@ export class World {
const wall = new THREE.Mesh(geo, mat); const wall = new THREE.Mesh(geo, mat);
wall.position.set(x, y, z); wall.position.set(x, y, z);
if (rotate) wall.rotation.y = Math.PI / 2; if (rotate) wall.rotation.y = Math.PI / 2;
wall.castShadow = true;
wall.receiveShadow = true;
this.scene.add(wall); this.scene.add(wall);
this.colliders.push(wall); this.colliders.push(wall);
} }
@@ -161,6 +172,8 @@ export class World {
}); });
const pillar = new THREE.Mesh(geo, mat); const pillar = new THREE.Mesh(geo, mat);
pillar.position.set(x, y, z); pillar.position.set(x, y, z);
pillar.castShadow = true;
pillar.receiveShadow = true;
this.scene.add(pillar); this.scene.add(pillar);
this.colliders.push(pillar); this.colliders.push(pillar);
this.staticObstacles.push(pillar); // Add to spawn blocker list this.staticObstacles.push(pillar); // Add to spawn blocker list
@@ -211,7 +224,7 @@ export class World {
if (!valid) continue; // Skip if no spot found if (!valid) continue; // Skip if no spot found
const type = Math.floor(Math.random() * 10); // Increased variety to 10 types const type = Math.floor(Math.random() * 15); // 15 furniture types
const rot = Math.random() * Math.PI * 2; const rot = Math.random() * Math.PI * 2;
const group = new THREE.Group(); const group = new THREE.Group();
@@ -226,9 +239,14 @@ export class World {
else if (type === 4) this.spawnClock(group); else if (type === 4) this.spawnClock(group);
else if (type === 5) this.spawnMannequin(group); else if (type === 5) this.spawnMannequin(group);
else if (type === 6) this.spawnTV(group); else if (type === 6) this.spawnTV(group);
else if (type === 7) this.spawnBed(group); // New else if (type === 7) this.spawnBed(group);
else if (type === 8) this.spawnBookshelf(group); // New else if (type === 8) this.spawnBookshelf(group);
else if (type === 9) this.spawnDrawer(group); // New else if (type === 9) this.spawnDrawer(group);
else if (type === 10) this.spawnBrokenCrate(group);
else if (type === 11) this.spawnOldBarrel(group);
else if (type === 12) this.spawnWornArmchair(group);
else if (type === 13) this.spawnRustyWorkbench(group);
else if (type === 14) this.spawnBrokenMirror(group);
} }
} }
@@ -454,6 +472,302 @@ export class World {
const ant2 = ant.clone(); ant2.position.set(0.1, 1.3, 0); ant2.rotation.z = -0.4; group.add(ant2); const ant2 = ant.clone(); ant2.position.set(0.1, 1.3, 0); ant2.rotation.z = -0.4; group.add(ant2);
} }
// === NEW WORN FURNITURE ===
spawnBrokenCrate(group) {
this.addColliderBox(group, 0.8, 0.6, 0.8);
// Worn, splintered wood
const woodMat = new THREE.MeshStandardMaterial({
map: this.texWood,
color: 0x2a1a0a, // Dark, aged
roughness: 1.0
});
// Main crate body (missing some planks)
const plankGeo = new THREE.BoxGeometry(0.8, 0.08, 0.08);
// Bottom
const bottom = new THREE.Mesh(new THREE.BoxGeometry(0.75, 0.05, 0.75), woodMat);
bottom.position.y = 0.025;
bottom.castShadow = true; bottom.receiveShadow = true;
group.add(bottom);
// Vertical corner posts (some broken)
const postGeo = new THREE.BoxGeometry(0.08, 0.5, 0.08);
const corners = [[-0.35, -0.35], [0.35, -0.35], [-0.35, 0.35], [0.35, 0.35]];
corners.forEach((c, i) => {
if (Math.random() < 0.2) return; // Missing post
const h = 0.3 + Math.random() * 0.2; // Varying height (broken tops)
const post = new THREE.Mesh(new THREE.BoxGeometry(0.08, h, 0.08), woodMat);
post.position.set(c[0], h / 2, c[1]);
post.rotation.z = (Math.random() - 0.5) * 0.1; // Slight lean
post.castShadow = true; post.receiveShadow = true;
group.add(post);
});
// Horizontal slats (some missing)
for (let y = 0.15; y < 0.45; y += 0.15) {
[0, Math.PI / 2].forEach(rot => {
if (Math.random() < 0.3) return; // Missing slat
const slat = new THREE.Mesh(plankGeo, woodMat);
slat.position.set(0, y, rot === 0 ? 0.37 : 0);
slat.rotation.y = rot;
slat.rotation.z = (Math.random() - 0.5) * 0.15; // Warped
slat.castShadow = true; slat.receiveShadow = true;
group.add(slat);
});
}
// Optional: Fallen lid nearby
if (Math.random() < 0.4) {
const lid = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.05, 0.7), woodMat);
lid.position.set(0.5, 0.03, 0.3);
lid.rotation.y = Math.random() * Math.PI;
lid.rotation.x = 0.1;
lid.castShadow = true; lid.receiveShadow = true;
group.add(lid);
}
}
spawnOldBarrel(group) {
this.addColliderBox(group, 0.6, 1.0, 0.6);
// Rusty metal and rotting wood
const metalMat = new THREE.MeshStandardMaterial({
color: 0x4a3520,
roughness: 0.9,
metalness: 0.3
});
const stainMat = new THREE.MeshStandardMaterial({
color: 0x1a1a10,
roughness: 1.0
});
// Main barrel body (cylinder)
const bodyGeo = new THREE.CylinderGeometry(0.28, 0.25, 0.9, 12);
const body = new THREE.Mesh(bodyGeo, metalMat);
body.position.y = 0.45;
body.castShadow = true; body.receiveShadow = true;
group.add(body);
// Metal bands (rusty)
const bandMat = new THREE.MeshStandardMaterial({
color: 0x3a2a1a,
roughness: 0.7,
metalness: 0.6
});
[0.15, 0.5, 0.85].forEach(y => {
if (Math.random() < 0.15) return; // Missing band
const band = new THREE.Mesh(new THREE.TorusGeometry(0.27, 0.02, 8, 16), bandMat);
band.position.y = y;
band.rotation.x = Math.PI / 2;
band.castShadow = true;
group.add(band);
});
// Dent/damage (random scale distortion)
body.scale.x = 0.9 + Math.random() * 0.2;
body.scale.z = 0.9 + Math.random() * 0.2;
// Optional: Tipped over
if (Math.random() < 0.3) {
group.rotation.x = Math.PI / 2 - 0.2;
group.position.y = 0.25;
}
// Stain on floor
const stain = new THREE.Mesh(new THREE.CircleGeometry(0.4, 16), stainMat);
stain.rotation.x = -Math.PI / 2;
stain.position.y = 0.01;
stain.position.z = 0.3;
group.add(stain);
}
spawnWornArmchair(group) {
this.addColliderBox(group, 0.9, 1.0, 0.9);
// Torn, faded fabric
const fabricMat = new THREE.MeshStandardMaterial({
map: this.texFabric,
color: 0x3a3028, // Dirty brown-gray
roughness: 1.0
});
const woodMat = new THREE.MeshStandardMaterial({
map: this.texWood,
color: 0x1f150a,
roughness: 0.9
});
// Legs (wooden, one possibly broken)
const legGeo = new THREE.BoxGeometry(0.08, 0.2, 0.08);
[[-0.35, -0.35], [0.35, -0.35], [-0.35, 0.35], [0.35, 0.35]].forEach((p, i) => {
const h = i === 2 && Math.random() < 0.3 ? 0.1 : 0.2; // One short leg
const leg = new THREE.Mesh(new THREE.BoxGeometry(0.08, h, 0.08), woodMat);
leg.position.set(p[0], h / 2, p[1]);
leg.castShadow = true; leg.receiveShadow = true;
group.add(leg);
});
// Seat cushion (sagging)
const seat = new THREE.Mesh(new THREE.BoxGeometry(0.75, 0.15, 0.7), fabricMat);
seat.position.set(0, 0.27, 0.05);
seat.scale.y = 0.7; // Compressed/worn
seat.castShadow = true; seat.receiveShadow = true;
group.add(seat);
// Back (tilted, torn)
const back = new THREE.Mesh(new THREE.BoxGeometry(0.75, 0.6, 0.12), fabricMat);
back.position.set(0, 0.6, -0.35);
back.rotation.x = 0.15; // Leaning back
back.castShadow = true; back.receiveShadow = true;
group.add(back);
// Armrests (one possibly missing)
const armGeo = new THREE.BoxGeometry(0.1, 0.15, 0.6);
if (Math.random() > 0.2) {
const armL = new THREE.Mesh(armGeo, fabricMat);
armL.position.set(-0.4, 0.42, 0);
armL.castShadow = true; armL.receiveShadow = true;
group.add(armL);
}
if (Math.random() > 0.2) {
const armR = new THREE.Mesh(armGeo, fabricMat);
armR.position.set(0.4, 0.42, 0);
armR.castShadow = true; armR.receiveShadow = true;
group.add(armR);
}
// Random tilt (unstable)
group.rotation.z = (Math.random() - 0.5) * 0.08;
}
spawnRustyWorkbench(group) {
this.addColliderBox(group, 1.5, 1.0, 0.7);
// Rusted metal and old wood
const metalMat = new THREE.MeshStandardMaterial({
color: 0x5a4535,
roughness: 0.8,
metalness: 0.5
});
const woodMat = new THREE.MeshStandardMaterial({
map: this.texWood,
color: 0x2a1a0a,
roughness: 0.95
});
// Metal frame legs
const legGeo = new THREE.BoxGeometry(0.08, 0.8, 0.08);
[[-0.65, -0.25], [0.65, -0.25], [-0.65, 0.25], [0.65, 0.25]].forEach(p => {
const leg = new THREE.Mesh(legGeo, metalMat);
leg.position.set(p[0], 0.4, p[1]);
leg.castShadow = true; leg.receiveShadow = true;
group.add(leg);
});
// Cross braces
const braceGeo = new THREE.BoxGeometry(1.3, 0.04, 0.04);
const brace1 = new THREE.Mesh(braceGeo, metalMat);
brace1.position.set(0, 0.15, -0.25);
brace1.castShadow = true;
group.add(brace1);
const brace2 = brace1.clone();
brace2.position.z = 0.25;
group.add(brace2);
// Wooden top (warped, stained)
const top = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.08, 0.65), woodMat);
top.position.y = 0.84;
top.rotation.z = (Math.random() - 0.5) * 0.03; // Slight warp
top.castShadow = true; top.receiveShadow = true;
group.add(top);
// Random tools/debris on top
if (Math.random() < 0.5) {
const tool = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.05, 0.08), metalMat);
tool.position.set((Math.random() - 0.5) * 0.8, 0.91, (Math.random() - 0.5) * 0.3);
tool.rotation.y = Math.random() * Math.PI;
tool.castShadow = true;
group.add(tool);
}
// Vise or clamp
if (Math.random() < 0.4) {
const vise = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.12, 0.2), metalMat);
vise.position.set(0.6, 0.94, 0);
vise.castShadow = true;
group.add(vise);
}
}
spawnBrokenMirror(group) {
this.addColliderBox(group, 0.8, 1.5, 0.15);
// Ornate but damaged frame
const frameMat = new THREE.MeshStandardMaterial({
color: 0x3d2b1f,
roughness: 0.7,
metalness: 0.2
});
const mirrorMat = new THREE.MeshStandardMaterial({
color: 0x111111,
roughness: 0.05,
metalness: 0.95
});
const crackMat = new THREE.MeshBasicMaterial({
color: 0x222222
});
// Frame
const frameT = new THREE.Mesh(new THREE.BoxGeometry(0.85, 0.08, 0.1), frameMat);
frameT.position.set(0, 1.4, 0);
frameT.castShadow = true; frameT.receiveShadow = true;
group.add(frameT);
const frameB = frameT.clone();
frameB.position.y = 0.1;
group.add(frameB);
const frameL = new THREE.Mesh(new THREE.BoxGeometry(0.08, 1.35, 0.1), frameMat);
frameL.position.set(-0.38, 0.75, 0);
frameL.castShadow = true; frameL.receiveShadow = true;
group.add(frameL);
const frameR = frameL.clone();
frameR.position.x = 0.38;
group.add(frameR);
// Mirror surface (dark, tarnished)
const mirror = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.25, 0.02), mirrorMat);
mirror.position.set(0, 0.75, 0.04);
mirror.receiveShadow = true;
group.add(mirror);
// Cracks (random lines)
for (let i = 0; i < 3 + Math.floor(Math.random() * 4); i++) {
const crack = new THREE.Mesh(
new THREE.BoxGeometry(0.01, 0.2 + Math.random() * 0.4, 0.005),
crackMat
);
crack.position.set(
(Math.random() - 0.5) * 0.5,
0.4 + Math.random() * 0.7,
0.055
);
crack.rotation.z = (Math.random() - 0.5) * 1.5;
group.add(crack);
}
// Leaning against wall or fallen
if (Math.random() < 0.4) {
group.rotation.x = -0.3;
group.position.y = 0.1;
} else {
group.rotation.x = -0.1; // Slight lean
}
}
createDust() { createDust() {
// Create 800 dust particles (Reduced) // Create 800 dust particles (Reduced)
@@ -497,6 +811,69 @@ export class World {
} }
spawnSafeZone(x, z) {
const radius = 3.5;
const color = 0xffccaa; // Warm safety light
// 1. Visual Lamp Fixture
const fixtureGeo = new THREE.CylinderGeometry(0.2, 0.1, 0.4, 8);
const fixtureMat = new THREE.MeshStandardMaterial({ color: 0x222222 });
const fixture = new THREE.Mesh(fixtureGeo, fixtureMat);
fixture.position.set(x, 4.8, z);
this.scene.add(fixture);
const bulbGeo = new THREE.SphereGeometry(0.1, 8, 8);
const bulbMat = new THREE.MeshBasicMaterial({ color: color });
const bulb = new THREE.Mesh(bulbGeo, bulbMat);
bulb.position.set(x, 4.6, z);
this.scene.add(bulb);
// 2. The Safe Light
const light = new THREE.SpotLight(color, 12.0, 20, Math.PI / 3.5, 0.5, 1); // Increased from 4.0 to 12.0
light.position.set(x, 4.5, z);
light.target.position.set(x, 0, z);
light.castShadow = true;
this.scene.add(light);
this.scene.add(light.target);
// Subtle ground glow
const glow = new THREE.PointLight(color, 4.0, 8); // Increased from 1.0 to 4.0
glow.position.set(x, 0.5, z);
this.scene.add(glow);
// 3. Store for AI
this.safeZones.push({
position: new THREE.Vector3(x, 0, z),
radius: radius,
active: true,
light: light,
glow: glow,
baseIntensity: 12.0,
baseGlowIntensity: 4.0
});
}
breakSafeZone(index) {
if (!this.safeZones[index]) return;
const zone = this.safeZones[index];
zone.active = false;
zone.light.visible = false;
zone.glow.visible = false;
window.log('WARNING: LOCAL_POWER_GRID_FAILURE - Safe Zone Lost');
}
glitchSafeZone(index, intensity) {
if (!this.safeZones[index] || !this.safeZones[index].active) return;
const zone = this.safeZones[index];
if (Math.random() < intensity) {
zone.light.intensity = Math.random() * zone.baseIntensity;
zone.glow.intensity = Math.random() * zone.baseGlowIntensity;
} else {
zone.light.intensity = zone.baseIntensity;
zone.glow.intensity = zone.baseGlowIntensity;
}
}
update(dt, player) { update(dt, player) {
if (!this.dustParticles) return; if (!this.dustParticles) return;

View File

@@ -54,4 +54,33 @@ h1 {
bottom: 20px; bottom: 20px;
left: 20px; left: 20px;
font-size: 1.2rem; font-size: 1.2rem;
display: flex;
flex-direction: column;
gap: 10px;
} }
#stamina-container {
width: 200px;
height: 10px;
border: 2px solid #555;
background-color: rgba(0, 0, 0, 0.5);
}
#stamina-bar {
width: 100%;
height: 100%;
background-color: #fff;
transition: width 0.1s linear;
#test-dot {
position: absolute;
top: 50%;
left: 50%;
width: 12px;
height: 12px;
background: red;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
}