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"
|
<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>
|
||||||
|
|||||||
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 { Player } from './Player.js';
|
||||||
import { Monster } from './Monster.js';
|
import { Monster } from './Monster.js';
|
||||||
import { Monster2 } from './Monster2.js';
|
import { Monster2 } from './Monster2.js';
|
||||||
|
import { Flare } from './Flare.js';
|
||||||
|
|
||||||
export class Game {
|
export class Game {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -18,6 +19,10 @@ export class Game {
|
|||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
this.lastTime = 0;
|
this.lastTime = 0;
|
||||||
this.setupUI();
|
this.setupUI();
|
||||||
|
window.log('GAME_STATE: INITIALIZED');
|
||||||
|
|
||||||
|
this.activeFlares = [];
|
||||||
|
window.addEventListener('throwFlare', (e) => this.spawnFlare(e.detail.position, e.detail.direction));
|
||||||
}
|
}
|
||||||
|
|
||||||
setupUI() {
|
setupUI() {
|
||||||
@@ -30,6 +35,7 @@ export class Game {
|
|||||||
startScreen.style.display = 'none';
|
startScreen.style.display = 'none';
|
||||||
if (hud) hud.style.display = 'block';
|
if (hud) hud.style.display = 'block';
|
||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
|
window.log('GAME_STATE: RUNNING');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,11 +53,33 @@ 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);
|
// 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();
|
this.graphics.render();
|
||||||
requestAnimationFrame(this.loop.bind(this));
|
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);
|
this.scene.add(this.mesh);
|
||||||
|
|
||||||
// AI State
|
// AI State
|
||||||
this.state = 'PATROL'; // PATROL, CHASE, JUMPSCARE
|
this.state = 'PATROL'; // PATROL, CHASE, JUMPSCARE, STUNNED
|
||||||
this.speed = 1.5;
|
this.target = null;
|
||||||
this.chaseSpeed = 3.5;
|
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.position = new THREE.Vector3(15, 0, 15);
|
||||||
this.mesh.position.copy(this.position);
|
this.mesh.position.copy(this.position);
|
||||||
|
|
||||||
this.targetNode = new THREE.Vector3();
|
this.targetNode = new THREE.Vector3();
|
||||||
this.setNewPatrolTarget();
|
this.setNewPatrolTarget();
|
||||||
|
|
||||||
this.detectionRange = 12;
|
this.detectionRange = 15; // Increased from 12
|
||||||
this.catchRange = 1.5;
|
this.catchRange = 1.5;
|
||||||
this.fov = Math.PI / 1.5; // Wide view
|
this.fov = Math.PI / 1.5; // Wide view
|
||||||
|
|
||||||
@@ -161,6 +163,22 @@ export class Monster {
|
|||||||
update(dt) {
|
update(dt) {
|
||||||
if (!this.player) return;
|
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 isSafe = this.isPlayerSafe();
|
||||||
const playerPos = this.player.camera.position.clone();
|
const playerPos = this.player.camera.position.clone();
|
||||||
playerPos.y = 0;
|
playerPos.y = 0;
|
||||||
@@ -178,7 +196,7 @@ export class Monster {
|
|||||||
|
|
||||||
// Move towards target
|
// Move towards target
|
||||||
const dir = new THREE.Vector3().subVectors(this.targetNode, monsterPos).normalize();
|
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
|
// Rotation
|
||||||
const targetRotation = Math.atan2(dir.x, dir.z);
|
const targetRotation = Math.atan2(dir.x, dir.z);
|
||||||
@@ -391,4 +409,13 @@ export class Monster {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 1500);
|
}, 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.isVisibleToPlayer = false;
|
||||||
|
|
||||||
this.setupAudio();
|
this.setupAudio();
|
||||||
|
|
||||||
|
// Listen for player overload pulse
|
||||||
|
window.addEventListener('flashlightOverload', (e) => this.onOverload(e.detail.position));
|
||||||
}
|
}
|
||||||
|
|
||||||
setupVisuals() {
|
setupVisuals() {
|
||||||
@@ -327,4 +330,18 @@ export class Monster2 {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 1000);
|
}, 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
|
// Init controls
|
||||||
try {
|
try {
|
||||||
this.controls = new PointerLockControls(camera, document.body);
|
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');
|
window.log('PointerLockControls initialized');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.log(`ERROR initializing controls: ${e.message}`);
|
window.log(`ERROR initializing controls: ${e.message}`);
|
||||||
@@ -44,14 +46,16 @@ export class Player {
|
|||||||
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
this.audioEnabled = false;
|
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.lockLook = false; // To disable controls during jumpscare
|
||||||
|
this.baseDrain = 0.5; // Drain per second at base intensity
|
||||||
|
|
||||||
this.setupInput();
|
this.setupInput();
|
||||||
this.setupFlashlight();
|
this.setupFlashlight();
|
||||||
|
|
||||||
|
// Survival tools state
|
||||||
|
this.lastOverloadTime = 0;
|
||||||
|
this.overloadCooldown = 5000; // 5 seconds
|
||||||
|
this.flashLight = null; // Temporary point light for overload
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFlashlight() {
|
setupFlashlight() {
|
||||||
@@ -118,11 +122,9 @@ export class Player {
|
|||||||
|
|
||||||
setupInput() {
|
setupInput() {
|
||||||
const onKeyDown = (event) => {
|
const onKeyDown = (event) => {
|
||||||
// Resume Audio Context on first interaction
|
|
||||||
if (this.ctx && this.ctx.state === 'suspended') {
|
if (this.ctx && this.ctx.state === 'suspended') {
|
||||||
this.ctx.resume();
|
this.ctx.resume();
|
||||||
this.audioEnabled = true;
|
this.audioEnabled = true;
|
||||||
this.startAmbience();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event.code) {
|
switch (event.code) {
|
||||||
@@ -133,6 +135,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 'KeyR': this.handleOverload(); break; // Flashlight Overload
|
||||||
|
case 'KeyQ': this.handleFlare(); break; // Throw Flare
|
||||||
case 'ShiftLeft': this.isSprinting = true; break;
|
case 'ShiftLeft': this.isSprinting = true; break;
|
||||||
case 'ShiftRight': this.isSprinting = true; break;
|
case 'ShiftRight': this.isSprinting = true; break;
|
||||||
}
|
}
|
||||||
@@ -157,7 +161,7 @@ export class Player {
|
|||||||
|
|
||||||
toggleFlashlight() {
|
toggleFlashlight() {
|
||||||
if (!this.controls) return;
|
if (!this.controls) return;
|
||||||
if (!this.controls.isLocked) return;
|
// Removed strict isLocked check for better reliability
|
||||||
|
|
||||||
if (this.battery <= 0 && this.flashlightOn === false) {
|
if (this.battery <= 0 && this.flashlightOn === false) {
|
||||||
window.log('Cannot turn on: Battery empty');
|
window.log('Cannot turn on: Battery empty');
|
||||||
@@ -186,14 +190,7 @@ export class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update(dt) {
|
update(dt) {
|
||||||
if (!this.controls) return;
|
if (!this.controls || this.lockLook) 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;
|
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -362,38 +359,57 @@ export class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startAmbience() {
|
startAmbience() {
|
||||||
if (!this.ctx || this.ambienceStarted) return;
|
// Ambience removed per user request
|
||||||
this.ambienceStarted = true;
|
}
|
||||||
|
|
||||||
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 now = Date.now();
|
||||||
const osc1 = this.ctx.createOscillator();
|
if (now - this.lastOverloadTime < this.overloadCooldown) {
|
||||||
osc1.type = 'sine';
|
window.log('OVERLOAD_COOLDOWN_ACTIVE');
|
||||||
osc1.frequency.setValueAtTime(55, t); // Low A (ish)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Oscillator 2: The "Detune" (creates beating/unsettling texture)
|
this.battery -= 25;
|
||||||
const osc2 = this.ctx.createOscillator();
|
this.lastOverloadTime = now;
|
||||||
osc2.type = 'triangle';
|
window.log('CRITICAL_SIGNAL_BURST_TRIGGERED');
|
||||||
osc2.frequency.setValueAtTime(58, t); // Slightly off
|
|
||||||
|
|
||||||
// Filter to keep it dark/muddy
|
// Create the flash
|
||||||
const filter = this.ctx.createBiquadFilter();
|
if (!this.flashLight) {
|
||||||
filter.type = 'lowpass';
|
this.flashLight = new THREE.PointLight(0xffffff, 50, 15);
|
||||||
filter.frequency.setValueAtTime(120, t); // Very muffled
|
this.camera.add(this.flashLight);
|
||||||
|
}
|
||||||
|
this.flashLight.visible = true;
|
||||||
|
this.flashLight.intensity = 50;
|
||||||
|
|
||||||
// Gain (Volume)
|
// Visual "blind" effect (could be expanded)
|
||||||
const gain = this.ctx.createGain();
|
setTimeout(() => {
|
||||||
gain.gain.setValueAtTime(0.3, t); // 30% volume
|
if (this.flashLight) {
|
||||||
|
this.flashLight.visible = false;
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
// Connect graph
|
// Emit event for monsters to react
|
||||||
osc1.connect(filter);
|
const event = new CustomEvent('flashlightOverload', {
|
||||||
osc2.connect(filter);
|
detail: { position: this.camera.position.clone() }
|
||||||
filter.connect(gain);
|
});
|
||||||
gain.connect(this.ctx.destination);
|
window.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
// Start forever
|
handleFlare() {
|
||||||
osc1.start();
|
if (!this.controls || this.lockLook) return;
|
||||||
osc2.start();
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
style.css
14
style.css
@@ -71,16 +71,4 @@ h1 {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
transition: width 0.1s linear;
|
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