feat: Introduce throwable flares and player flashlight overload, enabling monster stunning/teleportation, and remove debug dot.

This commit is contained in:
2026-01-03 11:24:26 +00:00
parent d588433d8a
commit a74ccd1f6d
7 changed files with 222 additions and 62 deletions

View File

@@ -26,7 +26,6 @@
<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;">
</div>
<div id="test-dot"></div>
</div>
<script type="module" src="/src/main.js"></script>

85
src/Flare.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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