feat: Introduce throwable flares and player flashlight overload, enabling monster stunning/teleportation, and remove debug dot.
This commit is contained in:
@@ -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
85
src/Flare.js
Normal 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);
|
||||
}
|
||||
}
|
||||
32
src/Game.js
32
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
12
style.css
12
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;
|
||||
}
|
||||
Reference in New Issue
Block a user