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:
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
394
src/Monster.js
Normal 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
330
src/Monster2.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
385
src/World.js
385
src/World.js
@@ -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;
|
||||||
|
|
||||||
|
|||||||
31
style.css
31
style.css
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user