diff --git a/index.html b/index.html index b3e0cb0..df89084 100644 --- a/index.html +++ b/index.html @@ -26,7 +26,6 @@
- diff --git a/src/Flare.js b/src/Flare.js new file mode 100644 index 0000000..1772e24 --- /dev/null +++ b/src/Flare.js @@ -0,0 +1,85 @@ +import * as THREE from 'three'; + +export class Flare { + constructor(scene, playerPos, direction, colliders) { + this.scene = scene; + this.colliders = colliders; + + // Flare Mesh + const geo = new THREE.CylinderGeometry(0.02, 0.02, 0.15, 8); + const mat = new THREE.MeshStandardMaterial({ + color: 0xff0000, + emissive: 0xff0000, + emissiveIntensity: 2.0 + }); + this.mesh = new THREE.Mesh(geo, mat); + this.mesh.rotation.x = Math.PI / 2; + + // Stats + this.position = playerPos.clone().add(direction.clone().multiplyScalar(0.5)); + this.mesh.position.copy(this.position); + this.velocity = direction.clone().multiplyScalar(10.0); + this.gravity = -9.8; + this.friction = 0.98; + this.bounce = 0.5; + this.lifetime = 60.0; // 60 seconds + this.active = true; + + // Light + this.light = new THREE.PointLight(0xff3333, 5.0, 10); + this.light.position.copy(this.position); + + this.scene.add(this.mesh); + this.scene.add(this.light); + + // Safe zone properties for AI + this.radius = 2.5; + this.isFlare = true; // Identify as flare + } + + update(dt) { + if (!this.active) return; + + this.lifetime -= dt; + if (this.lifetime <= 0) { + this.destroy(); + return; + } + + // Physics + this.velocity.y += this.gravity * dt; + this.position.add(this.velocity.clone().multiplyScalar(dt)); + + // Ground collision (Floor is at y=0) + if (this.position.y < 0.05) { + this.position.y = 0.05; + this.velocity.y *= -this.bounce; + this.velocity.x *= this.friction; + this.velocity.z *= this.friction; + } + + // Wall collisions (simplified) + this.colliders.forEach(col => { + const box = new THREE.Box3().setFromObject(col); + if (box.containsPoint(this.position)) { + // Bounce back + this.velocity.multiplyScalar(-this.bounce); + this.position.add(this.velocity.clone().multiplyScalar(dt * 2)); + } + }); + + this.mesh.position.copy(this.position); + this.light.position.copy(this.position); + + // Dim light near end + if (this.lifetime < 5) { + this.light.intensity = (this.lifetime / 5) * 5.0; + } + } + + destroy() { + this.active = false; + this.scene.remove(this.mesh); + this.scene.remove(this.light); + } +} diff --git a/src/Game.js b/src/Game.js index 550408d..1c2a487 100644 --- a/src/Game.js +++ b/src/Game.js @@ -3,6 +3,7 @@ import { World } from './World.js'; import { Player } from './Player.js'; import { Monster } from './Monster.js'; import { Monster2 } from './Monster2.js'; +import { Flare } from './Flare.js'; export class Game { constructor() { @@ -18,6 +19,10 @@ export class Game { this.isRunning = false; this.lastTime = 0; this.setupUI(); + window.log('GAME_STATE: INITIALIZED'); + + this.activeFlares = []; + window.addEventListener('throwFlare', (e) => this.spawnFlare(e.detail.position, e.detail.direction)); } setupUI() { @@ -30,6 +35,7 @@ export class Game { startScreen.style.display = 'none'; if (hud) hud.style.display = 'block'; this.isRunning = true; + window.log('GAME_STATE: RUNNING'); }); } } @@ -47,11 +53,33 @@ export class Game { if (this.isRunning) { this.player.update(dt); this.world.update(dt, this.player); - if (this.monster) this.monster.update(dt); - if (this.monster2) this.monster2.update(dt); + + // Update Flares + this.activeFlares = this.activeFlares.filter(f => f.active); + this.activeFlares.forEach(f => f.update(dt)); + + // Combined Safe Zones for Monsters + const combinedSafeZones = [...this.world.safeZones, ...this.activeFlares]; + + if (this.monster) { + this.monster.safeZones = combinedSafeZones; + this.monster.update(dt); + } + if (this.monster2) { + // Monster 2 ignores flares (electronic signal parasite) + // It still respects world.safeZones (which are electronic lights) + this.monster2.update(dt); + } } this.graphics.render(); requestAnimationFrame(this.loop.bind(this)); } + + spawnFlare(pos, dir) { + if (!this.isRunning) return; + const flare = new Flare(this.graphics.scene, pos, dir, this.world.colliders); + this.activeFlares.push(flare); + window.log('FLARE_DEPLOYED - Temporary Safe Zone Established'); + } } diff --git a/src/Monster.js b/src/Monster.js index 4c90ce2..a160172 100644 --- a/src/Monster.js +++ b/src/Monster.js @@ -13,16 +13,18 @@ export class Monster { this.scene.add(this.mesh); // AI State - this.state = 'PATROL'; // PATROL, CHASE, JUMPSCARE - this.speed = 1.5; - this.chaseSpeed = 3.5; + this.state = 'PATROL'; // PATROL, CHASE, JUMPSCARE, STUNNED + this.target = null; + this.patrolSpeed = 1.5; // Renamed from 'speed' + this.chaseSpeed = 4.5; // Increased from 3.5 + this.stunTimer = 0; 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.detectionRange = 15; // Increased from 12 this.catchRange = 1.5; this.fov = Math.PI / 1.5; // Wide view @@ -161,6 +163,22 @@ export class Monster { update(dt) { if (!this.player) return; + if (this.state === 'STUNNED') { + this.stunTimer -= dt; + // Retreat while stunned + const retreatDir = new THREE.Vector3().subVectors(this.mesh.position, this.player.camera.position).normalize(); + retreatDir.y = 0; + this.mesh.position.add(retreatDir.multiplyScalar(this.patrolSpeed * dt)); + + if (this.stunTimer <= 0) { + this.state = 'PATROL'; + this.setNewPatrolTarget(); + } + this.updateAnimation(dt); + this.updateAudio(dt); + return; + } + const isSafe = this.isPlayerSafe(); const playerPos = this.player.camera.position.clone(); playerPos.y = 0; @@ -178,7 +196,7 @@ export class Monster { // Move towards target const dir = new THREE.Vector3().subVectors(this.targetNode, monsterPos).normalize(); - this.mesh.position.add(dir.multiplyScalar(this.speed * dt)); + this.mesh.position.add(dir.multiplyScalar(this.patrolSpeed * dt)); // Rotation const targetRotation = Math.atan2(dir.x, dir.z); @@ -391,4 +409,13 @@ export class Monster { window.location.reload(); }, 1500); } + + onOverload(playerPosition) { + const dist = this.mesh.position.distanceTo(playerPosition); + if (dist < 12) { // Within overload range + this.state = 'STUNNED'; + this.stunTimer = 4.0; // 4 seconds of retreat/stun + window.log('CRITICAL: ENTITY_SENSORS_OVERLOADED - RETREATING'); + } + } } diff --git a/src/Monster2.js b/src/Monster2.js index 1032918..f30af61 100644 --- a/src/Monster2.js +++ b/src/Monster2.js @@ -28,6 +28,9 @@ export class Monster2 { this.isVisibleToPlayer = false; this.setupAudio(); + + // Listen for player overload pulse + window.addEventListener('flashlightOverload', (e) => this.onOverload(e.detail.position)); } setupVisuals() { @@ -327,4 +330,18 @@ export class Monster2 { window.location.reload(); }, 1000); } + + onOverload(playerPosition) { + const dist = this.mesh.position.distanceTo(playerPosition); + if (dist < 10) { // Within overload range + window.log('CRITICAL_SIGNAL_ERROR: PARASITE_SHATTERED'); + // Respawn far away + this.mesh.position.set( + (Math.random() - 0.5) * 50, + 0, + (Math.random() - 0.5) * 50 + ); + this.state = 'STALK'; + } + } } diff --git a/src/Player.js b/src/Player.js index 24e991d..60f3f3c 100644 --- a/src/Player.js +++ b/src/Player.js @@ -16,6 +16,8 @@ export class Player { // Init controls try { this.controls = new PointerLockControls(camera, document.body); + this.controls.addEventListener('lock', () => window.log('POINTER_LOCKED - Input Active')); + this.controls.addEventListener('unlock', () => window.log('POINTER_UNLOCKED - Input Passive')); window.log('PointerLockControls initialized'); } catch (e) { window.log(`ERROR initializing controls: ${e.message}`); @@ -44,14 +46,16 @@ export class Player { this.ctx = new (window.AudioContext || window.webkitAudioContext)(); this.audioEnabled = false; - // Animation - this.headBobTimer = 0; - - this.baseDrain = 0.5; // Drain per second at base intensity this.lockLook = false; // To disable controls during jumpscare + this.baseDrain = 0.5; // Drain per second at base intensity this.setupInput(); this.setupFlashlight(); + + // Survival tools state + this.lastOverloadTime = 0; + this.overloadCooldown = 5000; // 5 seconds + this.flashLight = null; // Temporary point light for overload } setupFlashlight() { @@ -118,11 +122,9 @@ export class Player { setupInput() { const onKeyDown = (event) => { - // Resume Audio Context on first interaction if (this.ctx && this.ctx.state === 'suspended') { this.ctx.resume(); this.audioEnabled = true; - this.startAmbience(); } switch (event.code) { @@ -133,6 +135,8 @@ export class Player { case 'KeyF': this.toggleFlashlight(); break; case 'KeyK': this.adjustDim = true; break; case 'KeyL': this.adjustBright = true; break; + case 'KeyR': this.handleOverload(); break; // Flashlight Overload + case 'KeyQ': this.handleFlare(); break; // Throw Flare case 'ShiftLeft': this.isSprinting = true; break; case 'ShiftRight': this.isSprinting = true; break; } @@ -157,7 +161,7 @@ export class Player { toggleFlashlight() { if (!this.controls) return; - if (!this.controls.isLocked) return; + // Removed strict isLocked check for better reliability if (this.battery <= 0 && this.flashlightOn === false) { window.log('Cannot turn on: Battery empty'); @@ -186,14 +190,7 @@ export class Player { } update(dt) { - if (!this.controls) return; - - // Debug logging for lock state (once per second roughly) - if (Math.random() < 0.01) { - // window.log(`Controls Locked: ${this.controls.isLocked}`); - } - - if (!this.controls.isLocked || this.lockLook) return; + if (!this.controls || this.lockLook) return; // Friction-like dampening (simple decay) this.velocity.x -= this.velocity.x * 10.0 * dt; @@ -362,38 +359,57 @@ export class Player { } startAmbience() { - if (!this.ctx || this.ambienceStarted) return; - this.ambienceStarted = true; + // Ambience removed per user request + } - const t = this.ctx.currentTime; + handleOverload() { + if (!this.controls || this.lockLook) return; + if (!this.flashlightOn || this.battery < 25) { + window.log('NOT_ENOUGH_ENERGY_FOR_OVERLOAD'); + return; + } - // Oscillator 1: The "Hum" (60hz roughly) - const osc1 = this.ctx.createOscillator(); - osc1.type = 'sine'; - osc1.frequency.setValueAtTime(55, t); // Low A (ish) + const now = Date.now(); + if (now - this.lastOverloadTime < this.overloadCooldown) { + window.log('OVERLOAD_COOLDOWN_ACTIVE'); + return; + } - // Oscillator 2: The "Detune" (creates beating/unsettling texture) - const osc2 = this.ctx.createOscillator(); - osc2.type = 'triangle'; - osc2.frequency.setValueAtTime(58, t); // Slightly off + this.battery -= 25; + this.lastOverloadTime = now; + window.log('CRITICAL_SIGNAL_BURST_TRIGGERED'); - // Filter to keep it dark/muddy - const filter = this.ctx.createBiquadFilter(); - filter.type = 'lowpass'; - filter.frequency.setValueAtTime(120, t); // Very muffled + // Create the flash + if (!this.flashLight) { + this.flashLight = new THREE.PointLight(0xffffff, 50, 15); + this.camera.add(this.flashLight); + } + this.flashLight.visible = true; + this.flashLight.intensity = 50; - // Gain (Volume) - const gain = this.ctx.createGain(); - gain.gain.setValueAtTime(0.3, t); // 30% volume + // Visual "blind" effect (could be expanded) + setTimeout(() => { + if (this.flashLight) { + this.flashLight.visible = false; + } + }, 500); - // Connect graph - osc1.connect(filter); - osc2.connect(filter); - filter.connect(gain); - gain.connect(this.ctx.destination); + // Emit event for monsters to react + const event = new CustomEvent('flashlightOverload', { + detail: { position: this.camera.position.clone() } + }); + window.dispatchEvent(event); + } - // Start forever - osc1.start(); - osc2.start(); + handleFlare() { + if (!this.controls || this.lockLook) return; + // This will be handled in Game.js by listening for an event + const event = new CustomEvent('throwFlare', { + detail: { + position: this.camera.position.clone(), + direction: new THREE.Vector3().set(0, 0, -1).applyQuaternion(this.camera.quaternion) + } + }); + window.dispatchEvent(event); } } diff --git a/style.css b/style.css index b095bc0..8a84cf3 100644 --- a/style.css +++ b/style.css @@ -71,16 +71,4 @@ h1 { 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; - } \ No newline at end of file +} \ No newline at end of file