First commit
This commit is contained in:
21
index.html
21
index.html
@@ -11,21 +11,34 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="game-container"></div>
|
<div id="game-container"></div>
|
||||||
|
|
||||||
<div id="ui-layer">
|
<div id="ui-container">
|
||||||
<div id="start-screen">
|
<div id="loading-screen">
|
||||||
<h1>ECHOES</h1>
|
<h1>INITIALIZING...</h1>
|
||||||
<p>Click to Start</p>
|
<div class="loader"></div>
|
||||||
|
</div>
|
||||||
|
<div id="start-screen" style="display: none;">
|
||||||
|
<h1>SUBJECT #096 CONTAINMENT</h1>
|
||||||
|
<p>CLICK TO BEGIN</p>
|
||||||
<p class="controls">WASD to Move | Mouse to Look | F Flashlight | P Debug</p>
|
<p class="controls">WASD to Move | Mouse to Look | F Flashlight | P Debug</p>
|
||||||
</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="flares">Flares: <span id="flare-count">3</span></div>
|
||||||
<div id="stamina-container">
|
<div id="stamina-container">
|
||||||
<div id="stamina-bar"></div>
|
<div id="stamina-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="heartbeat">
|
||||||
|
<svg viewBox="0 0 100 100" class="heart-icon">
|
||||||
|
<path
|
||||||
|
d="M50 88.9L16.7 55.6C7.2 46.1 7.2 30.9 16.7 21.4s24.7-9.5 33.3 0C58.6 11.9 74.1 11.9 83.3 21.4s9.5 24.7 0 34.2L50 88.9z" />
|
||||||
|
</svg>
|
||||||
|
</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="crosshair"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
|||||||
55
src/Flare.js
55
src/Flare.js
@@ -1,22 +1,28 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
export class Flare {
|
export class Flare {
|
||||||
constructor(scene, playerPos, direction, colliders) {
|
// NEW: Shared assets
|
||||||
this.scene = scene;
|
static geometry = new THREE.CylinderGeometry(0.02, 0.02, 0.15, 8);
|
||||||
this.colliders = colliders;
|
static material = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xff0000,
|
||||||
|
emissive: 0xff0000,
|
||||||
|
emissiveIntensity: 2.0
|
||||||
|
});
|
||||||
|
|
||||||
// Flare Mesh
|
// NEW: Temp vectors for GC reduction
|
||||||
const geo = new THREE.CylinderGeometry(0.02, 0.02, 0.15, 8);
|
static tempVec = new THREE.Vector3();
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
|
||||||
color: 0xff0000,
|
constructor(scene, playerPos, direction, colliderBoxes) {
|
||||||
emissive: 0xff0000,
|
this.scene = scene;
|
||||||
emissiveIntensity: 2.0
|
this.colliderBoxes = colliderBoxes;
|
||||||
});
|
|
||||||
this.mesh = new THREE.Mesh(geo, mat);
|
// Flare Mesh (Shared)
|
||||||
|
this.mesh = new THREE.Mesh(Flare.geometry, Flare.material);
|
||||||
this.mesh.rotation.x = Math.PI / 2;
|
this.mesh.rotation.x = Math.PI / 2;
|
||||||
|
this.mesh.castShadow = true;
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
this.position = playerPos.clone().add(direction.clone().multiplyScalar(0.5));
|
this.position = playerPos.clone().add(Flare.tempVec.copy(direction).multiplyScalar(0.5));
|
||||||
this.mesh.position.copy(this.position);
|
this.mesh.position.copy(this.position);
|
||||||
this.velocity = direction.clone().multiplyScalar(10.0);
|
this.velocity = direction.clone().multiplyScalar(10.0);
|
||||||
this.gravity = -9.8;
|
this.gravity = -9.8;
|
||||||
@@ -24,6 +30,7 @@ export class Flare {
|
|||||||
this.bounce = 0.5;
|
this.bounce = 0.5;
|
||||||
this.lifetime = 60.0; // 60 seconds
|
this.lifetime = 60.0; // 60 seconds
|
||||||
this.active = true;
|
this.active = true;
|
||||||
|
this.isSleeping = false; // NEW: Physics sleep
|
||||||
|
|
||||||
// Light
|
// Light
|
||||||
this.light = new THREE.PointLight(0xff3333, 5.0, 10);
|
this.light = new THREE.PointLight(0xff3333, 5.0, 10);
|
||||||
@@ -38,7 +45,13 @@ export class Flare {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update(dt) {
|
update(dt) {
|
||||||
if (!this.active) return;
|
if (!this.active || this.isSleeping) {
|
||||||
|
if (this.active) {
|
||||||
|
this.lifetime -= dt;
|
||||||
|
if (this.lifetime <= 0) this.destroy();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.lifetime -= dt;
|
this.lifetime -= dt;
|
||||||
if (this.lifetime <= 0) {
|
if (this.lifetime <= 0) {
|
||||||
@@ -48,7 +61,7 @@ export class Flare {
|
|||||||
|
|
||||||
// Physics
|
// Physics
|
||||||
this.velocity.y += this.gravity * dt;
|
this.velocity.y += this.gravity * dt;
|
||||||
this.position.add(this.velocity.clone().multiplyScalar(dt));
|
this.position.add(Flare.tempVec.copy(this.velocity).multiplyScalar(dt));
|
||||||
|
|
||||||
// Ground collision (Floor is at y=0)
|
// Ground collision (Floor is at y=0)
|
||||||
if (this.position.y < 0.05) {
|
if (this.position.y < 0.05) {
|
||||||
@@ -56,15 +69,21 @@ export class Flare {
|
|||||||
this.velocity.y *= -this.bounce;
|
this.velocity.y *= -this.bounce;
|
||||||
this.velocity.x *= this.friction;
|
this.velocity.x *= this.friction;
|
||||||
this.velocity.z *= this.friction;
|
this.velocity.z *= this.friction;
|
||||||
|
|
||||||
|
// Sleep check: if almost still on ground
|
||||||
|
if (Math.abs(this.velocity.y) < 0.1 && Flare.tempVec.set(this.velocity.x, 0, this.velocity.z).length() < 0.1) {
|
||||||
|
this.isSleeping = true;
|
||||||
|
this.velocity.set(0, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wall collisions (simplified)
|
// Wall collisions (optimized)
|
||||||
this.colliders.forEach(col => {
|
this.colliderBoxes.forEach(box => {
|
||||||
const box = new THREE.Box3().setFromObject(col);
|
|
||||||
if (box.containsPoint(this.position)) {
|
if (box.containsPoint(this.position)) {
|
||||||
// Bounce back
|
// Bounce back
|
||||||
this.velocity.multiplyScalar(-this.bounce);
|
this.velocity.multiplyScalar(-this.bounce);
|
||||||
this.position.add(this.velocity.clone().multiplyScalar(dt * 2));
|
this.position.add(Flare.tempVec.copy(this.velocity).multiplyScalar(dt * 2));
|
||||||
|
this.isSleeping = false; // Wake up on collision
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
101
src/Game.js
101
src/Game.js
@@ -2,19 +2,22 @@ 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 { Monster } from './Monster.js';
|
||||||
import { Monster2 } from './Monster2.js';
|
import { SCP096 } from './SCP096.js';
|
||||||
import { Flare } from './Flare.js';
|
import { Flare } from './Flare.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, this.world.rechargeStations, this.world.colliderBoxes, this.world.collectableFlares);
|
||||||
|
this.player.world = this.world; // New: Link world for interaction checks
|
||||||
|
// Flashlight is already child of camera, do not reparent
|
||||||
// Monster 1 (The Scuttler)
|
// Monster 1 (The Scuttler)
|
||||||
this.monster = new Monster(this.world.scene, this.player, this.world.colliders, this.player.ctx, this.world.safeZones);
|
this.monster = new Monster(this.world.scene, this.player, this.world.colliders, this.player.ctx, this.world.safeZones);
|
||||||
|
|
||||||
// Monster 2 (The Stalker)
|
// SCP-096 (The Shy Guy)
|
||||||
this.monster2 = new Monster2(this.world.scene, this.player, this.world.colliders, this.player.ctx, this.world);
|
this.scp096 = new SCP096(this.world.scene, this.player, this.world.colliders, this.player.ctx);
|
||||||
|
|
||||||
|
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
this.lastTime = 0;
|
this.lastTime = 0;
|
||||||
@@ -22,7 +25,44 @@ export class Game {
|
|||||||
window.log('GAME_STATE: INITIALIZED');
|
window.log('GAME_STATE: INITIALIZED');
|
||||||
|
|
||||||
this.activeFlares = [];
|
this.activeFlares = [];
|
||||||
|
this.combinedSafeZones = []; // NEW: Persistent array to avoid GC
|
||||||
window.addEventListener('throwFlare', (e) => this.spawnFlare(e.detail.position, e.detail.direction));
|
window.addEventListener('throwFlare', (e) => this.spawnFlare(e.detail.position, e.detail.direction));
|
||||||
|
|
||||||
|
this.heartElement = document.querySelector('.heart-icon');
|
||||||
|
|
||||||
|
// Start initialization immediately
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// Yield to let UI render the loading screen first
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
window.log('GAME_STATE: LOADING_ASSETS');
|
||||||
|
this.world.load();
|
||||||
|
|
||||||
|
this.graphics.scene.add(this.player.getObject());
|
||||||
|
|
||||||
|
// Pre-compile shaders
|
||||||
|
window.log('GAME_STATE: COMPILING_SHADERS');
|
||||||
|
try {
|
||||||
|
this.graphics.renderer.compile(this.graphics.scene, this.graphics.camera);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Shader compilation failed", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ready
|
||||||
|
this.onLoadComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoadComplete() {
|
||||||
|
const loadingScreen = document.getElementById('loading-screen');
|
||||||
|
const startScreen = document.getElementById('start-screen');
|
||||||
|
|
||||||
|
if (loadingScreen) loadingScreen.style.display = 'none';
|
||||||
|
if (startScreen) startScreen.style.display = 'block';
|
||||||
|
|
||||||
|
window.log('GAME_STATE: READY');
|
||||||
}
|
}
|
||||||
|
|
||||||
setupUI() {
|
setupUI() {
|
||||||
@@ -35,14 +75,13 @@ 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;
|
||||||
|
this.startLoop();
|
||||||
window.log('GAME_STATE: RUNNING');
|
window.log('GAME_STATE: RUNNING');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
startLoop() {
|
||||||
this.world.load();
|
|
||||||
this.graphics.scene.add(this.player.getObject());
|
|
||||||
requestAnimationFrame(this.loop.bind(this));
|
requestAnimationFrame(this.loop.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,21 +94,31 @@ export class Game {
|
|||||||
this.world.update(dt, this.player);
|
this.world.update(dt, this.player);
|
||||||
|
|
||||||
// Update Flares
|
// Update Flares
|
||||||
|
const flareCountBefore = this.activeFlares.length;
|
||||||
this.activeFlares = this.activeFlares.filter(f => f.active);
|
this.activeFlares = this.activeFlares.filter(f => f.active);
|
||||||
this.activeFlares.forEach(f => f.update(dt));
|
this.activeFlares.forEach(f => f.update(dt));
|
||||||
|
|
||||||
// Combined Safe Zones for Monsters
|
// Combined Safe Zones for Monsters (Optimized to avoid allocation)
|
||||||
const combinedSafeZones = [...this.world.safeZones, ...this.activeFlares];
|
if (this.activeFlares.length !== flareCountBefore || this.combinedSafeZones.length === 0) {
|
||||||
|
this.combinedSafeZones = [...this.world.safeZones, ...this.activeFlares];
|
||||||
|
if (this.monster) this.monster.safeZones = this.combinedSafeZones;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.monster) {
|
if (this.monster) {
|
||||||
this.monster.safeZones = combinedSafeZones;
|
|
||||||
this.monster.update(dt);
|
this.monster.update(dt);
|
||||||
}
|
}
|
||||||
if (this.monster2) {
|
if (this.scp096) {
|
||||||
// Monster 2 ignores flares (electronic signal parasite)
|
this.scp096.update(dt);
|
||||||
// It still respects world.safeZones (which are electronic lights)
|
|
||||||
this.monster2.update(dt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Heartbeat Monitor
|
||||||
|
// Find closest monster distance
|
||||||
|
let dist1 = 999;
|
||||||
|
if (this.monster && this.monster.mesh) {
|
||||||
|
dist1 = this.player.camera.position.distanceTo(this.monster.mesh.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateHeartbeat(dist1);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.graphics.render();
|
this.graphics.render();
|
||||||
@@ -78,8 +127,30 @@ export class Game {
|
|||||||
|
|
||||||
spawnFlare(pos, dir) {
|
spawnFlare(pos, dir) {
|
||||||
if (!this.isRunning) return;
|
if (!this.isRunning) return;
|
||||||
const flare = new Flare(this.graphics.scene, pos, dir, this.world.colliders);
|
const flare = new Flare(this.graphics.scene, pos, dir, this.world.colliderBoxes);
|
||||||
this.activeFlares.push(flare);
|
this.activeFlares.push(flare);
|
||||||
|
|
||||||
|
// Update combined safe zones immediately so AI reacts
|
||||||
|
this.combinedSafeZones = [...this.world.safeZones, ...this.activeFlares];
|
||||||
|
if (this.monster) this.monster.safeZones = this.combinedSafeZones;
|
||||||
|
|
||||||
window.log('FLARE_DEPLOYED - Temporary Safe Zone Established');
|
window.log('FLARE_DEPLOYED - Temporary Safe Zone Established');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateHeartbeat(distance) {
|
||||||
|
if (!this.heartElement) return;
|
||||||
|
|
||||||
|
// Distance thresholds:
|
||||||
|
// > 15m: Resting (1s pulse)
|
||||||
|
// 10m - 15m: Alert (0.6s pulse)
|
||||||
|
// 5m - 10m: Danger (0.3s pulse)
|
||||||
|
// < 5m: Panic (0.15s pulse)
|
||||||
|
|
||||||
|
let duration = '1s';
|
||||||
|
if (distance < 5) duration = '0.15s';
|
||||||
|
else if (distance < 10) duration = '0.3s';
|
||||||
|
else if (distance < 15) duration = '0.6s';
|
||||||
|
|
||||||
|
this.heartElement.style.animationDuration = duration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export class Graphics {
|
|||||||
constructor() {
|
constructor() {
|
||||||
// Main scene rendering
|
// Main scene rendering
|
||||||
this.scene = new THREE.Scene();
|
this.scene = new THREE.Scene();
|
||||||
this.scene.fog = new THREE.Fog(0x000000, 2, 12);
|
// Fog removed here, handled by World
|
||||||
this.scene.background = new THREE.Color(0x000000);
|
this.scene.background = new THREE.Color(0x000000);
|
||||||
|
|
||||||
this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
|
this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
|
||||||
@@ -13,7 +13,7 @@ export class Graphics {
|
|||||||
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.enabled = true;
|
||||||
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
this.renderer.shadowMap.type = THREE.PCFShadowMap; // Optimized for performance
|
||||||
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
|
||||||
|
|||||||
120
src/Monster.js
120
src/Monster.js
@@ -16,9 +16,9 @@ export class Monster {
|
|||||||
this.state = 'PATROL'; // PATROL, CHASE, JUMPSCARE, STUNNED
|
this.state = 'PATROL'; // PATROL, CHASE, JUMPSCARE, STUNNED
|
||||||
this.target = null;
|
this.target = null;
|
||||||
this.patrolSpeed = 1.5; // Renamed from 'speed'
|
this.patrolSpeed = 1.5; // Renamed from 'speed'
|
||||||
this.chaseSpeed = 4.5; // Increased from 3.5
|
this.chaseSpeed = 3.2; // Reduced from 4.5
|
||||||
this.stunTimer = 0;
|
this.stunTimer = 0;
|
||||||
this.position = new THREE.Vector3(15, 0, 15);
|
this.position = new THREE.Vector3(25, 0, 25); // Moved further from origin (0,0)
|
||||||
this.mesh.position.copy(this.position);
|
this.mesh.position.copy(this.position);
|
||||||
|
|
||||||
this.targetNode = new THREE.Vector3();
|
this.targetNode = new THREE.Vector3();
|
||||||
@@ -34,8 +34,16 @@ export class Monster {
|
|||||||
|
|
||||||
// Audio initialization
|
// Audio initialization
|
||||||
this.setupAudio();
|
this.setupAudio();
|
||||||
|
|
||||||
|
// Listen for player overload pulse
|
||||||
|
// window.addEventListener('flashlightOverload', (e) => this.onOverload(e.detail.position));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Shared temp vectors for GC reduction
|
||||||
|
static tempVec1 = new THREE.Vector3();
|
||||||
|
static tempVec2 = new THREE.Vector3();
|
||||||
|
static tempVec3 = new THREE.Vector3();
|
||||||
|
|
||||||
setupAudio() {
|
setupAudio() {
|
||||||
if (!this.audioCtx) return;
|
if (!this.audioCtx) return;
|
||||||
|
|
||||||
@@ -150,9 +158,10 @@ export class Monster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPlayerSafe() {
|
isPlayerSafe() {
|
||||||
const playerPos = this.player.camera.position.clone();
|
const playerPos = Monster.tempVec1.copy(this.player.camera.position); // Reuse temp vector
|
||||||
playerPos.y = 0;
|
playerPos.y = 0;
|
||||||
for (const zone of this.safeZones) {
|
for (let i = 0; i < this.safeZones.length; i++) {
|
||||||
|
const zone = this.safeZones[i];
|
||||||
if (!zone.active) continue;
|
if (!zone.active) continue;
|
||||||
const dist = playerPos.distanceTo(zone.position);
|
const dist = playerPos.distanceTo(zone.position);
|
||||||
if (dist < zone.radius) return true;
|
if (dist < zone.radius) return true;
|
||||||
@@ -163,10 +172,29 @@ export class Monster {
|
|||||||
update(dt) {
|
update(dt) {
|
||||||
if (!this.player) return;
|
if (!this.player) return;
|
||||||
|
|
||||||
|
// NEW: Hiding Logic
|
||||||
|
if (this.player.isHiding) {
|
||||||
|
if (this.state === 'CHASE') {
|
||||||
|
this.state = 'IDLE';
|
||||||
|
// Reset patrol or do nothing
|
||||||
|
window.log('The entity loses track of you...');
|
||||||
|
}
|
||||||
|
// Just animate IDLE or patrol randomly? Culling will hide it if far.
|
||||||
|
// But we should stop chasing.
|
||||||
|
// Move randomly?
|
||||||
|
// For now, simply RETURN to stop all logic (freeze/idle)
|
||||||
|
// OR finding a new random target would be better.
|
||||||
|
if (this.state !== 'PATROL') {
|
||||||
|
this.state = 'PATROL';
|
||||||
|
this.setNewPatrolTarget();
|
||||||
|
}
|
||||||
|
// Allow basic patrol update
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state === 'STUNNED') {
|
if (this.state === 'STUNNED') {
|
||||||
this.stunTimer -= dt;
|
this.stunTimer -= dt;
|
||||||
// Retreat while stunned
|
// Retreat while stunned
|
||||||
const retreatDir = new THREE.Vector3().subVectors(this.mesh.position, this.player.camera.position).normalize();
|
const retreatDir = Monster.tempVec1.subVectors(this.mesh.position, this.player.camera.position).normalize();
|
||||||
retreatDir.y = 0;
|
retreatDir.y = 0;
|
||||||
this.mesh.position.add(retreatDir.multiplyScalar(this.patrolSpeed * dt));
|
this.mesh.position.add(retreatDir.multiplyScalar(this.patrolSpeed * dt));
|
||||||
|
|
||||||
@@ -180,9 +208,9 @@ export class Monster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isSafe = this.isPlayerSafe();
|
const isSafe = this.isPlayerSafe();
|
||||||
const playerPos = this.player.camera.position.clone();
|
const playerPos = Monster.tempVec1.copy(this.player.camera.position);
|
||||||
playerPos.y = 0;
|
playerPos.y = 0;
|
||||||
const monsterPos = this.mesh.position.clone();
|
const monsterPos = Monster.tempVec2.copy(this.mesh.position);
|
||||||
monsterPos.y = 0;
|
monsterPos.y = 0;
|
||||||
|
|
||||||
const distToPlayer = monsterPos.distanceTo(playerPos);
|
const distToPlayer = monsterPos.distanceTo(playerPos);
|
||||||
@@ -195,7 +223,7 @@ export class Monster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move towards target
|
// Move towards target
|
||||||
const dir = new THREE.Vector3().subVectors(this.targetNode, monsterPos).normalize();
|
const dir = Monster.tempVec3.subVectors(this.targetNode, monsterPos).normalize();
|
||||||
this.mesh.position.add(dir.multiplyScalar(this.patrolSpeed * dt));
|
this.mesh.position.add(dir.multiplyScalar(this.patrolSpeed * dt));
|
||||||
|
|
||||||
// Rotation
|
// Rotation
|
||||||
@@ -220,7 +248,7 @@ export class Monster {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dir = new THREE.Vector3().subVectors(playerPos, monsterPos).normalize();
|
const dir = Monster.tempVec3.subVectors(playerPos, monsterPos).normalize();
|
||||||
this.mesh.position.add(dir.multiplyScalar(this.chaseSpeed * dt));
|
this.mesh.position.add(dir.multiplyScalar(this.chaseSpeed * dt));
|
||||||
|
|
||||||
// Intensive Rotation
|
// Intensive Rotation
|
||||||
@@ -244,13 +272,12 @@ export class Monster {
|
|||||||
else if (this.state === 'JUMPSCARE') {
|
else if (this.state === 'JUMPSCARE') {
|
||||||
this.jumpscareTimer += dt;
|
this.jumpscareTimer += dt;
|
||||||
|
|
||||||
const camPos = this.player.camera.position.clone();
|
const camPos = Monster.tempVec1.copy(this.player.camera.position);
|
||||||
const monsterPos = this.mesh.position.clone();
|
|
||||||
|
|
||||||
// 1. Move/Lunge at camera
|
// 1. Move/Lunge at camera
|
||||||
const camDir = new THREE.Vector3();
|
const camDir = Monster.tempVec2;
|
||||||
this.player.camera.getWorldDirection(camDir);
|
this.player.camera.getWorldDirection(camDir);
|
||||||
const jumpTarget = camPos.clone().add(camDir.multiplyScalar(0.2));
|
const jumpTarget = Monster.tempVec3.copy(camPos).add(camDir.multiplyScalar(0.2));
|
||||||
this.mesh.position.lerp(jumpTarget, 15 * dt);
|
this.mesh.position.lerp(jumpTarget, 15 * dt);
|
||||||
|
|
||||||
// 2. STARE: Force monster to look at camera
|
// 2. STARE: Force monster to look at camera
|
||||||
@@ -307,84 +334,44 @@ export class Monster {
|
|||||||
if (!this.audioCtx || dist > 20) return;
|
if (!this.audioCtx || dist > 20) return;
|
||||||
const t = this.audioCtx.currentTime;
|
const t = this.audioCtx.currentTime;
|
||||||
|
|
||||||
// Light "patter" noise (Short high-frequency tap) - Increased Volume
|
// Optimized: Reduced complexity
|
||||||
const g = this.audioCtx.createGain();
|
const g = this.audioCtx.createGain();
|
||||||
g.gain.setValueAtTime(0.25 * (1 - dist / 20), t); // Increased from 0.08 to 0.25
|
g.gain.setValueAtTime(0.15 * (1 - dist / 20), t);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.01, t + 0.05);
|
||||||
|
|
||||||
const osc = this.audioCtx.createOscillator();
|
const osc = this.audioCtx.createOscillator();
|
||||||
osc.type = 'triangle';
|
osc.type = 'triangle';
|
||||||
osc.frequency.setValueAtTime(400, t);
|
osc.frequency.setValueAtTime(400, t);
|
||||||
osc.frequency.exponentialRampToValueAtTime(1200, t + 0.01);
|
osc.frequency.exponentialRampToValueAtTime(100, t + 0.05);
|
||||||
osc.frequency.exponentialRampToValueAtTime(80, t + 0.03);
|
|
||||||
|
|
||||||
const filter = this.audioCtx.createBiquadFilter();
|
osc.connect(g);
|
||||||
filter.type = 'bandpass';
|
|
||||||
filter.frequency.value = 800;
|
|
||||||
filter.Q.value = 1;
|
|
||||||
|
|
||||||
osc.connect(filter);
|
|
||||||
filter.connect(g);
|
|
||||||
g.connect(this.panner);
|
g.connect(this.panner);
|
||||||
|
|
||||||
osc.start(t);
|
osc.start(t);
|
||||||
osc.stop(t + 0.04);
|
osc.stop(t + 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAudio(dt, dist, monsterPos) {
|
updateAudio(dt, dist, monsterPos) {
|
||||||
if (!this.audioCtx || !this.player || !this.audioStarted) return;
|
if (!this.audioCtx || !this.player || !this.audioStarted) return;
|
||||||
|
|
||||||
// Update Panner Position
|
// Update Panner Position Only (Player updates Listener)
|
||||||
this.panner.positionX.setTargetAtTime(monsterPos.x, this.audioCtx.currentTime, 0.1);
|
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.positionY.setTargetAtTime(monsterPos.y + 0.5, this.audioCtx.currentTime, 0.1);
|
||||||
this.panner.positionZ.setTargetAtTime(monsterPos.z, 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)
|
// Deep Quiet Heavy Breathing (Slower rhythm)
|
||||||
const breathCycle = Math.sin(this.audioCtx.currentTime * 1.2) * 0.5 + 0.5;
|
const breathCycle = Math.sin(this.audioCtx.currentTime * 1.2) * 0.5 + 0.5;
|
||||||
let targetBreath = 0;
|
let targetBreath = 0;
|
||||||
if (dist < 15) {
|
if (dist < 15) {
|
||||||
targetBreath = (1 - dist / 15) * 0.3 * breathCycle;
|
targetBreath = (1 - dist / 15) * 0.3 * breathCycle;
|
||||||
|
// Removed deep breath allocation spam
|
||||||
// Randomly trigger a deep breath sound
|
|
||||||
if (Math.random() < 0.003) {
|
|
||||||
this.playDeepBreath();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.breathGain.gain.setTargetAtTime(targetBreath, this.audioCtx.currentTime, 0.1);
|
this.breathGain.gain.setTargetAtTime(targetBreath, this.audioCtx.currentTime, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
playDeepBreath() {
|
playDeepBreath() {
|
||||||
if (!this.audioCtx) return;
|
// Removed heavy buffer allocation logic
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -409,13 +396,4 @@ 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
347
src/Monster2.js
347
src/Monster2.js
@@ -1,347 +0,0 @@
|
|||||||
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();
|
|
||||||
|
|
||||||
// Listen for player overload pulse
|
|
||||||
window.addEventListener('flashlightOverload', (e) => this.onOverload(e.detail.position));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
290
src/Player.js
290
src/Player.js
@@ -4,10 +4,13 @@ import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockCont
|
|||||||
// but we'll stick to this for now as it's standard examples path.
|
// but we'll stick to this for now as it's standard examples path.
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
constructor(camera, colliders) {
|
constructor(camera, colliders, rechargeStations = [], colliderBoxes = [], collectableFlares = []) {
|
||||||
this.camera = camera;
|
this.camera = camera;
|
||||||
this.camera.rotation.order = 'YXZ'; // Standard FPS rotation order to prevent gimbal lock
|
this.camera.rotation.order = 'YXZ'; // Standard FPS rotation order to prevent gimbal lock
|
||||||
this.colliders = colliders;
|
this.colliders = colliders;
|
||||||
|
this.rechargeStations = rechargeStations; // NEW: Track recharge stations
|
||||||
|
this.colliderBoxes = colliderBoxes; // NEW: Pre-calculated bounding boxes for optimization
|
||||||
|
this.collectableFlares = collectableFlares; // NEW: Track flare pickups
|
||||||
|
|
||||||
// Player stats
|
// Player stats
|
||||||
this.speed = 3.0; // Slower "Horror" walk speed
|
this.speed = 3.0; // Slower "Horror" walk speed
|
||||||
@@ -34,12 +37,20 @@ export class Player {
|
|||||||
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.flares = 3; // NEW: Start with 3 flares
|
||||||
|
this.maxFlares = 5; // NEW: Maximum flares
|
||||||
this.stamina = 100.0;
|
this.stamina = 100.0;
|
||||||
this.isSprinting = false;
|
this.isSprinting = false;
|
||||||
|
this.baseIntensity = 3.0; // NEW: Track desired brightness (defaults to 3.0)
|
||||||
this.baseDrain = 0.5; // Drain per second at base intensity
|
this.baseDrain = 0.5; // Drain per second at base intensity
|
||||||
|
|
||||||
|
// NEW: Hiding State
|
||||||
|
this.isHiding = false;
|
||||||
|
this.hidingSpot = null; // Store reference to locker
|
||||||
|
this.storedPos = new THREE.Vector3(); // Store position before hiding
|
||||||
|
|
||||||
// Animation
|
// Animation
|
||||||
this.headBobTimer = 0;
|
this.bobTime = 0;
|
||||||
this.lastStepTime = 0;
|
this.lastStepTime = 0;
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
@@ -53,9 +64,13 @@ export class Player {
|
|||||||
this.setupFlashlight();
|
this.setupFlashlight();
|
||||||
|
|
||||||
// Survival tools state
|
// Survival tools state
|
||||||
this.lastOverloadTime = 0;
|
|
||||||
this.overloadCooldown = 5000; // 5 seconds
|
this.overloadCooldown = 5000; // 5 seconds
|
||||||
this.flashLight = null; // Temporary point light for overload
|
this.flashLight = null; // Temporary point light for overload
|
||||||
|
|
||||||
|
// NEW: Cache UI elements to avoid per-frame DOM lookup
|
||||||
|
this.uiStamina = document.getElementById('stamina-bar');
|
||||||
|
this.uiBattery = document.getElementById('battery-level');
|
||||||
|
this.uiFlares = document.getElementById('flare-count');
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFlashlight() {
|
setupFlashlight() {
|
||||||
@@ -81,6 +96,7 @@ export class Player {
|
|||||||
bulb.position.z = -0.101; // Tip of body
|
bulb.position.z = -0.101; // Tip of body
|
||||||
this.flashlightGroup.add(bulb);
|
this.flashlightGroup.add(bulb);
|
||||||
this.bulbMesh = bulb; // To toggle emission
|
this.bulbMesh = bulb; // To toggle emission
|
||||||
|
this.bulbMesh.visible = this.flashlightOn; // Sync with initial state
|
||||||
|
|
||||||
// Hand (Simple representation)
|
// Hand (Simple representation)
|
||||||
const handGeo = new THREE.BoxGeometry(0.08, 0.08, 0.15);
|
const handGeo = new THREE.BoxGeometry(0.08, 0.08, 0.15);
|
||||||
@@ -99,10 +115,10 @@ export class Player {
|
|||||||
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
|
// Enable shadows (Performance optimized: reduced from 512)
|
||||||
this.flashlight.castShadow = true;
|
this.flashlight.castShadow = true;
|
||||||
this.flashlight.shadow.mapSize.width = 512;
|
this.flashlight.shadow.mapSize.width = 256;
|
||||||
this.flashlight.shadow.mapSize.height = 512;
|
this.flashlight.shadow.mapSize.height = 256;
|
||||||
this.flashlight.shadow.camera.near = 0.5;
|
this.flashlight.shadow.camera.near = 0.5;
|
||||||
this.flashlight.shadow.camera.far = 50;
|
this.flashlight.shadow.camera.far = 50;
|
||||||
this.flashlight.shadow.bias = -0.001;
|
this.flashlight.shadow.bias = -0.001;
|
||||||
@@ -135,8 +151,9 @@ 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 'KeyR': Overload removed
|
||||||
case 'KeyQ': this.handleFlare(); break; // Throw Flare
|
case 'KeyQ': this.handleFlare(); break; // Throw Flare
|
||||||
|
case 'KeyE': this.handleInteract(); break; // NEW: Interact
|
||||||
case 'ShiftLeft': this.isSprinting = true; break;
|
case 'ShiftLeft': this.isSprinting = true; break;
|
||||||
case 'ShiftRight': this.isSprinting = true; break;
|
case 'ShiftRight': this.isSprinting = true; break;
|
||||||
}
|
}
|
||||||
@@ -215,8 +232,7 @@ export class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
const stamBar = document.getElementById('stamina-bar');
|
if (this.uiStamina) this.uiStamina.style.width = this.stamina + '%';
|
||||||
if (stamBar) stamBar.style.width = this.stamina + '%';
|
|
||||||
|
|
||||||
const accel = currentSpeed * 10.0; // Acceleration to reach max speed against friction (assuming friction 10)
|
const accel = currentSpeed * 10.0; // Acceleration to reach max speed against friction (assuming friction 10)
|
||||||
|
|
||||||
@@ -227,12 +243,12 @@ export class Player {
|
|||||||
this.controls.moveRight(-this.velocity.x * dt);
|
this.controls.moveRight(-this.velocity.x * dt);
|
||||||
this.controls.moveForward(-this.velocity.z * dt);
|
this.controls.moveForward(-this.velocity.z * dt);
|
||||||
|
|
||||||
// Simple Collision: Push back
|
// Simple Collision: Push back (Optimized to use pre-calculated boxes)
|
||||||
const playerPos = this.camera.position;
|
const playerPos = this.camera.position;
|
||||||
const playerRadius = 0.5;
|
const playerRadius = 0.5;
|
||||||
|
|
||||||
for (const collider of this.colliders) {
|
for (let i = 0; i < this.colliderBoxes.length; i++) {
|
||||||
const box = new THREE.Box3().setFromObject(collider);
|
const box = this.colliderBoxes[i];
|
||||||
if (playerPos.x > box.min.x - playerRadius && playerPos.x < box.max.x + playerRadius &&
|
if (playerPos.x > box.min.x - playerRadius && playerPos.x < box.max.x + playerRadius &&
|
||||||
playerPos.z > box.min.z - playerRadius && playerPos.z < box.max.z + playerRadius) {
|
playerPos.z > box.min.z - playerRadius && playerPos.z < box.max.z + playerRadius) {
|
||||||
|
|
||||||
@@ -278,12 +294,11 @@ export class Player {
|
|||||||
this.battery = Math.max(0, this.battery - drain);
|
this.battery = Math.max(0, this.battery - drain);
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
const battEl = document.getElementById('battery-level');
|
if (this.uiBattery) this.uiBattery.textContent = Math.floor(this.battery) + '%';
|
||||||
if (battEl) battEl.textContent = Math.floor(this.battery) + '%';
|
|
||||||
if (this.battery <= 20) {
|
if (this.battery <= 20) {
|
||||||
if (battEl) battEl.style.color = 'red';
|
if (this.uiBattery) this.uiBattery.style.color = 'red';
|
||||||
} else {
|
} else {
|
||||||
if (battEl) battEl.style.color = 'white';
|
if (this.uiBattery) this.uiBattery.style.color = 'white';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Die if empty
|
// Die if empty
|
||||||
@@ -295,31 +310,104 @@ export class Player {
|
|||||||
window.log('Battery depleted!');
|
window.log('Battery depleted!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle manual adjustment
|
||||||
// Handle manual adjustment
|
// Handle manual adjustment
|
||||||
if (this.adjustDim || this.adjustBright) {
|
if (this.adjustDim || this.adjustBright) {
|
||||||
const speed = 2.0 * dt;
|
const speed = 2.0 * dt;
|
||||||
const angleSpeed = 0.5 * dt;
|
const angleSpeed = 0.5 * dt;
|
||||||
|
|
||||||
if (this.adjustDim) {
|
if (this.adjustDim) {
|
||||||
this.flashlight.intensity = Math.max(0, this.flashlight.intensity - speed * 10);
|
this.baseIntensity = Math.max(0, this.baseIntensity - speed * 10);
|
||||||
this.flashlight.angle = Math.max(0.1, this.flashlight.angle - angleSpeed);
|
this.flashlight.angle = Math.max(0.1, this.flashlight.angle - angleSpeed);
|
||||||
}
|
}
|
||||||
if (this.adjustBright) {
|
if (this.adjustBright) {
|
||||||
this.flashlight.intensity = Math.min(15, this.flashlight.intensity + speed * 10);
|
this.baseIntensity = Math.min(15, this.baseIntensity + speed * 10);
|
||||||
this.flashlight.angle = Math.min(Math.PI / 2, this.flashlight.angle + angleSpeed);
|
this.flashlight.angle = Math.min(Math.PI / 2, this.flashlight.angle + angleSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update actual intensity immediately for responsiveness
|
||||||
|
this.flashlight.intensity = this.baseIntensity;
|
||||||
|
|
||||||
// Sync bulb light
|
// Sync bulb light
|
||||||
this.bulbLight.intensity = this.flashlight.intensity * 0.2;
|
this.bulbLight.intensity = this.flashlight.intensity * 0.2;
|
||||||
|
|
||||||
// Log occasionally for feedback
|
// Log occasionally for feedback
|
||||||
if (Math.random() < 0.1) window.log(`Light: Int=${this.flashlight.intensity.toFixed(1)} Ang=${this.flashlight.angle.toFixed(2)}`);
|
if (Math.random() < 0.1) window.log(`Light: Int=${this.baseIntensity.toFixed(1)} Ang=${this.flashlight.angle.toFixed(2)}`);
|
||||||
} else {
|
} else {
|
||||||
// Flicker logic
|
// Flicker logic
|
||||||
const flicker = (Math.random() - 0.5) * 0.5;
|
const flicker = (Math.random() - 0.5) * 0.5;
|
||||||
this.flashlight.intensity += flicker;
|
// Fix: Reset to base + flicker instead of accumulating drift
|
||||||
|
this.flashlight.intensity = this.baseIntensity + flicker;
|
||||||
this.bulbLight.intensity = Math.max(0, this.flashlight.intensity * 0.2);
|
this.bulbLight.intensity = Math.max(0, this.flashlight.intensity * 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Recharge Logic
|
||||||
|
let isRecharging = false;
|
||||||
|
// playerPos is already defined above in the update method
|
||||||
|
for (const station of this.rechargeStations) {
|
||||||
|
const dist = playerPos.distanceTo(station.position);
|
||||||
|
if (dist < 2.5) { // Interaction range
|
||||||
|
isRecharging = true;
|
||||||
|
this.battery = Math.min(100, this.battery + 15 * dt); // Recharge rate
|
||||||
|
|
||||||
|
// Visual feedback on station
|
||||||
|
if (station.indicator) {
|
||||||
|
station.indicator.material.color.setHex(0xffff00); // Yellow while charging
|
||||||
|
station.light.color.setHex(0xffff00);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.random() < 0.05) window.log('FLASHLIGHT_RECHARGING...');
|
||||||
|
break; // Only recharge from one at a time
|
||||||
|
} else {
|
||||||
|
// Reset visual feedback if not charging
|
||||||
|
if (station.indicator) {
|
||||||
|
station.indicator.material.color.setHex(0x00ff00); // Green when ready
|
||||||
|
station.light.color.setHex(0x00ff00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-pickup logic logic removed in favor of manual interact (Key E)
|
||||||
|
|
||||||
|
if (this.debugTimer > 1.0) {
|
||||||
|
this.debugTimer = 0;
|
||||||
|
window.log(`DEBUG: On=${this.flashlightOn} Batt=${this.battery.toFixed(1)} Base=${this.baseIntensity} Int=${this.flashlight.intensity.toFixed(2)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateAudioListener(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAudioListener(dt) {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
|
||||||
|
// Optimize: throttle updates if needed, but per-frame is smoother
|
||||||
|
const pos = this.camera.position;
|
||||||
|
const dir = this.camera.getWorldDirection(new THREE.Vector3());
|
||||||
|
|
||||||
|
const listener = this.ctx.listener;
|
||||||
|
|
||||||
|
// Using setTargetAtTime for smooth transition
|
||||||
|
const time = this.ctx.currentTime;
|
||||||
|
if (listener.positionX) {
|
||||||
|
listener.positionX.setTargetAtTime(pos.x, time, 0.1);
|
||||||
|
listener.positionY.setTargetAtTime(pos.y, time, 0.1);
|
||||||
|
listener.positionZ.setTargetAtTime(pos.z, time, 0.1);
|
||||||
|
} else {
|
||||||
|
// Legacy support
|
||||||
|
listener.setPosition(pos.x, pos.y, pos.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener.forwardX) {
|
||||||
|
listener.forwardX.setTargetAtTime(dir.x, time, 0.1);
|
||||||
|
listener.forwardY.setTargetAtTime(dir.y, time, 0.1);
|
||||||
|
listener.forwardZ.setTargetAtTime(dir.z, time, 0.1);
|
||||||
|
listener.upX.setTargetAtTime(0, time, 0.1);
|
||||||
|
listener.upY.setTargetAtTime(1, time, 0.1);
|
||||||
|
listener.upZ.setTargetAtTime(0, time, 0.1);
|
||||||
|
} else {
|
||||||
|
listener.setOrientation(dir.x, dir.y, dir.z, 0, 1, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playFootstep(isRunning = false) {
|
playFootstep(isRunning = false) {
|
||||||
@@ -362,47 +450,17 @@ export class Player {
|
|||||||
// Ambience removed per user request
|
// Ambience removed per user request
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOverload() {
|
|
||||||
if (!this.controls || this.lockLook) return;
|
|
||||||
if (!this.flashlightOn || this.battery < 25) {
|
|
||||||
window.log('NOT_ENOUGH_ENERGY_FOR_OVERLOAD');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this.lastOverloadTime < this.overloadCooldown) {
|
|
||||||
window.log('OVERLOAD_COOLDOWN_ACTIVE');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.battery -= 25;
|
|
||||||
this.lastOverloadTime = now;
|
|
||||||
window.log('CRITICAL_SIGNAL_BURST_TRIGGERED');
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Visual "blind" effect (could be expanded)
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.flashLight) {
|
|
||||||
this.flashLight.visible = false;
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
// Emit event for monsters to react
|
|
||||||
const event = new CustomEvent('flashlightOverload', {
|
|
||||||
detail: { position: this.camera.position.clone() }
|
|
||||||
});
|
|
||||||
window.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFlare() {
|
handleFlare() {
|
||||||
if (!this.controls || this.lockLook) return;
|
if (!this.controls || this.lockLook) return;
|
||||||
|
|
||||||
|
if (this.flares <= 0) {
|
||||||
|
window.log('OUT_OF_FLARES');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flares--;
|
||||||
|
this.updateFlareUI();
|
||||||
|
|
||||||
// This will be handled in Game.js by listening for an event
|
// This will be handled in Game.js by listening for an event
|
||||||
const event = new CustomEvent('throwFlare', {
|
const event = new CustomEvent('throwFlare', {
|
||||||
detail: {
|
detail: {
|
||||||
@@ -412,4 +470,118 @@ export class Player {
|
|||||||
});
|
});
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFlareUI() {
|
||||||
|
if (this.uiFlares) this.uiFlares.textContent = this.flares;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInteract() {
|
||||||
|
if (!this.controls || !this.camera) return;
|
||||||
|
|
||||||
|
// 1. If Hiding, Exit Hiding
|
||||||
|
if (this.isHiding) {
|
||||||
|
this.exitHiding();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Flare Pickup Logic (Legacy but kept)
|
||||||
|
// Check for flares first? Or prioritize hiding? Let's check distance.
|
||||||
|
const playerPos = this.camera.position;
|
||||||
|
let foundFlare = false;
|
||||||
|
|
||||||
|
for (const pickup of this.collectableFlares) {
|
||||||
|
if (!pickup.active) continue;
|
||||||
|
if (playerPos.distanceTo(pickup.position) < 2.5) {
|
||||||
|
if (this.flares < this.maxFlares) {
|
||||||
|
this.flares++;
|
||||||
|
// this.world.removeFlarePickup(pickup); // Assuming access to world, but here we just hide it
|
||||||
|
pickup.active = false;
|
||||||
|
pickup.group.visible = false;
|
||||||
|
this.updateFlareUI();
|
||||||
|
window.log('FLARE_COLLECTED');
|
||||||
|
foundFlare = true;
|
||||||
|
} else {
|
||||||
|
window.log('INVENTORY_FULL');
|
||||||
|
foundFlare = true; // Handled interaction
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundFlare) return;
|
||||||
|
|
||||||
|
// 3. Check for Hiding Spots (Raycast for Lockers)
|
||||||
|
// We use the camera direction
|
||||||
|
const raycaster = new THREE.Raycaster();
|
||||||
|
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);
|
||||||
|
|
||||||
|
// Filter for objects that are "hideable"
|
||||||
|
// We need to check children of the scene... this is expensive if we check everything.
|
||||||
|
// Better: Check culledObjects from the World if possible, OR
|
||||||
|
// Since we don't have easy access to 'world.culledObjects' here directly without passing it,
|
||||||
|
// we can iterate the player's vicinity or use a simpler distance check if we had the list.
|
||||||
|
// Assuming we don't have the list easily, let's look at what the camera is pointing at.
|
||||||
|
// Actually, 'this.controls.getObject()' is the camera wrapper.
|
||||||
|
// The scene is not directly stored in Player usually, but passed in update?
|
||||||
|
// Wait, Player doesn't store Scene.
|
||||||
|
// FIX: We need to pass the list of hideable objects or the World to the Player.
|
||||||
|
// For now, let's assume we can access `game.world` or passed objects.
|
||||||
|
// BUT, we can just iterate `this.world.culledObjects` if we link it.
|
||||||
|
// In Game.js, `player.world = world` was not set explicitly but player is passed world objects.
|
||||||
|
|
||||||
|
if (this.world && this.world.culledObjects) {
|
||||||
|
const interactables = this.world.culledObjects.filter(o => o.visible && o.userData.isHideable);
|
||||||
|
// Simple distance check first
|
||||||
|
for (const obj of interactables) {
|
||||||
|
if (playerPos.distanceTo(obj.position) < 2.0) {
|
||||||
|
this.enterHiding(obj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enterHiding(obj) {
|
||||||
|
this.isHiding = true;
|
||||||
|
this.hidingSpot = obj;
|
||||||
|
this.storedPos = this.storedPos || new THREE.Vector3(); // Initialize if not exists
|
||||||
|
this.storedPos.copy(this.camera.position);
|
||||||
|
|
||||||
|
// Move Player inside (approximate center of locker)
|
||||||
|
// Locker origin is at bottom center. Height 2.0. Center is y=1.0.
|
||||||
|
// We want eye level ~1.6
|
||||||
|
this.camera.position.set(obj.position.x, 1.6, obj.position.z);
|
||||||
|
|
||||||
|
// Turn off light
|
||||||
|
this.flashlightOn = false;
|
||||||
|
this.flashlight.visible = false;
|
||||||
|
if (this.bulbLight) this.bulbLight.visible = false;
|
||||||
|
|
||||||
|
// UI Feedback
|
||||||
|
window.log("Entered Hiding Spot. Press E to exit.");
|
||||||
|
|
||||||
|
// Vignette Effect (Pseudo via CSS)
|
||||||
|
document.body.style.boxShadow = "inset 0 0 150px 100px #000"; // Darken edges
|
||||||
|
}
|
||||||
|
|
||||||
|
exitHiding() {
|
||||||
|
this.isHiding = false;
|
||||||
|
this.hidingSpot = null;
|
||||||
|
|
||||||
|
// Restore position (slightly offset to avoid getting stuck)
|
||||||
|
// We push them forward relative to where they entered or just restore.
|
||||||
|
// Restoring `storedPos` is safest.
|
||||||
|
this.camera.position.copy(this.storedPos);
|
||||||
|
|
||||||
|
// Remove Vignette
|
||||||
|
document.body.style.boxShadow = "none";
|
||||||
|
|
||||||
|
// Auto-restore flashlight if battery valid
|
||||||
|
if (this.battery > 0) {
|
||||||
|
this.flashlightOn = true;
|
||||||
|
this.flashlight.visible = true;
|
||||||
|
if (this.bulbLight) this.bulbLight.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log("Exited Hiding.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
508
src/World.js
508
src/World.js
@@ -4,11 +4,32 @@ export class World {
|
|||||||
constructor(scene) {
|
constructor(scene) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.colliders = [];
|
this.colliders = [];
|
||||||
this.staticObstacles = []; // Track pillars etc for spawning
|
this.staticObstacles = [];
|
||||||
this.dustParticles = null;
|
this.dustParticles = null;
|
||||||
this.safeZones = []; // [{ position: Vector3, radius: number }]
|
this.safeZones = [];
|
||||||
|
this.rechargeStations = [];
|
||||||
|
this.colliderBoxes = [];
|
||||||
|
|
||||||
|
this.texWood = this.createProceduralTexture('wood');
|
||||||
|
this.texFabric = this.createProceduralTexture('fabric');
|
||||||
|
|
||||||
|
// NEW: Power System variables removed
|
||||||
|
this.ambientLight = null;
|
||||||
|
|
||||||
|
// NEW: Collectable Flares
|
||||||
|
this.collectableFlares = [];
|
||||||
|
|
||||||
|
// NEW: Culling System for Performance
|
||||||
|
this.culledObjects = []; // { mesh: Object3D, active: bool }
|
||||||
|
this.cullDistance = 25; // Hide things beyond 25 units
|
||||||
|
|
||||||
|
// Material Palette (Optimization)
|
||||||
|
this.materials = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
load() {
|
load() {
|
||||||
// Generate Textures
|
// Generate Textures
|
||||||
this.texConcrete = this.createProceduralTexture('concrete');
|
this.texConcrete = this.createProceduralTexture('concrete');
|
||||||
@@ -16,67 +37,95 @@ export class World {
|
|||||||
this.texWood = this.createProceduralTexture('wood');
|
this.texWood = this.createProceduralTexture('wood');
|
||||||
this.texFabric = this.createProceduralTexture('fabric');
|
this.texFabric = this.createProceduralTexture('fabric');
|
||||||
|
|
||||||
// Standard lighting for horror
|
// Initialize Shared Materials
|
||||||
const ambientLight = new THREE.AmbientLight(0x404040, 0.2); // Very dim ambient
|
this.materials.concrete = new THREE.MeshStandardMaterial({ map: this.texConcrete, roughness: 0.9, metalness: 0.1 });
|
||||||
this.scene.add(ambientLight);
|
this.materials.wall = new THREE.MeshStandardMaterial({ map: this.texWall, roughness: 0.95 });
|
||||||
|
this.materials.wood = new THREE.MeshStandardMaterial({ map: this.texWood, roughness: 0.9, color: 0x4a3c31 });
|
||||||
|
this.materials.woodDark = new THREE.MeshStandardMaterial({ map: this.texWood, roughness: 1.0, color: 0x2a1a0a });
|
||||||
|
this.materials.fabric = new THREE.MeshStandardMaterial({ map: this.texFabric, roughness: 1.0, color: 0x4e342e });
|
||||||
|
this.materials.fabricWorn = new THREE.MeshStandardMaterial({ map: this.texFabric, roughness: 1.0, color: 0x3a3028 });
|
||||||
|
this.materials.metal = new THREE.MeshStandardMaterial({ color: 0x4a5a6a, roughness: 0.7, metalness: 0.6 });
|
||||||
|
this.materials.metalRust = new THREE.MeshStandardMaterial({ color: 0x5a4535, roughness: 0.9, metalness: 0.5 });
|
||||||
|
this.materials.black = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 1.0 });
|
||||||
|
this.materials.glass = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.1, metalness: 0.8 });
|
||||||
|
|
||||||
// Floor (Dirty Concrete)
|
// Tiling
|
||||||
const floorGeo = new THREE.PlaneGeometry(60, 60);
|
this.materials.concrete.map.repeat.set(10, 10);
|
||||||
|
this.materials.wall.map.repeat.set(2, 2);
|
||||||
|
|
||||||
|
// Standard lighting for horror
|
||||||
|
this.ambientLight = new THREE.AmbientLight(0x404040, 0.2); // Very dim ambient
|
||||||
|
this.scene.add(this.ambientLight);
|
||||||
|
|
||||||
|
// NEW: Distance Fog to hide culling
|
||||||
|
// Color 0x000000 (Black), Density 0.04 (Fades out by ~25-30 units)
|
||||||
|
this.scene.fog = new THREE.FogExp2(0x000000, 0.05);
|
||||||
|
|
||||||
|
const worldSize = 60;
|
||||||
|
|
||||||
|
// Floor
|
||||||
|
const floorGeo = new THREE.PlaneGeometry(worldSize, worldSize);
|
||||||
const floorMat = new THREE.MeshStandardMaterial({
|
const floorMat = new THREE.MeshStandardMaterial({
|
||||||
map: this.texConcrete,
|
map: this.texConcrete,
|
||||||
roughness: 0.9,
|
roughness: 0.9,
|
||||||
metalness: 0.1
|
metalness: 0.1
|
||||||
});
|
});
|
||||||
|
floorMat.map.repeat.set(10, 10);
|
||||||
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;
|
floor.receiveShadow = true;
|
||||||
this.scene.add(floor);
|
this.scene.add(floor);
|
||||||
|
|
||||||
// Ceiling (Dirty Concrete, Darker)
|
// Ceiling
|
||||||
const ceilingGeo = new THREE.PlaneGeometry(60, 60);
|
const ceilingGeo = new THREE.PlaneGeometry(worldSize, worldSize);
|
||||||
const ceilingMat = new THREE.MeshStandardMaterial({
|
const ceilingMat = new THREE.MeshStandardMaterial({
|
||||||
map: this.texConcrete,
|
map: this.texConcrete,
|
||||||
roughness: 1.0,
|
roughness: 1.0,
|
||||||
metalness: 0
|
metalness: 0
|
||||||
});
|
});
|
||||||
|
ceilingMat.map.repeat.set(10, 10);
|
||||||
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;
|
||||||
ceiling.receiveShadow = true;
|
ceiling.receiveShadow = true;
|
||||||
this.scene.add(ceiling);
|
this.scene.add(ceiling);
|
||||||
|
|
||||||
// Simple walls (Expanded to 60x60 with Moldy Plaster)
|
// Walls
|
||||||
this.createWall(0, 2.5, -30, 60, 5); // Back
|
this.createWall(0, 2.5, -worldSize / 2, worldSize, 5); // North
|
||||||
this.createWall(0, 2.5, 30, 60, 5); // Front
|
this.createWall(0, 2.5, worldSize / 2, worldSize, 5); // South
|
||||||
this.createWall(-30, 2.5, 0, 60, 5, true); // Left
|
this.createWall(-worldSize / 2, 2.5, 0, worldSize, 5, true); // West
|
||||||
this.createWall(30, 2.5, 0, 60, 5, true); // Right
|
this.createWall(worldSize / 2, 2.5, 0, worldSize, 5, true); // East
|
||||||
|
|
||||||
// Add some "horror" pillars (Spread out, Concrete)
|
// Pillars
|
||||||
for (let i = 0; i < 12; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const px = (Math.random() - 0.5) * 50;
|
const x = (Math.random() - 0.5) * (worldSize - 5);
|
||||||
const pz = (Math.random() - 0.5) * 50;
|
const z = (Math.random() - 0.5) * (worldSize - 5);
|
||||||
this.createPillar(px, 2.5, pz);
|
this.createPillar(x, 2.5, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a red object to find
|
// Furniture
|
||||||
const target = new THREE.Mesh(
|
this.createFurniture(30);
|
||||||
new THREE.BoxGeometry(1, 1, 1),
|
|
||||||
new THREE.MeshStandardMaterial({ color: 0x880000 })
|
// Safe Zones
|
||||||
);
|
this.spawnSafeZone(10, 10);
|
||||||
target.position.set(5, 0.5, -5);
|
this.spawnSafeZone(-15, -20);
|
||||||
this.scene.add(target);
|
this.spawnSafeZone(20, -10);
|
||||||
this.colliders.push(target);
|
|
||||||
|
// NEW: Spawn randomized flares
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const x = (Math.random() - 0.5) * 50;
|
||||||
|
const z = (Math.random() - 0.5) * 50;
|
||||||
|
this.spawnFlarePickup(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
this.createFurniture(45);
|
|
||||||
this.createDust();
|
this.createDust();
|
||||||
|
|
||||||
// Spawn Safe Zones (Fixed points for balance)
|
window.log('WORLD_INITIALIZED: Static generation active');
|
||||||
this.spawnSafeZone(12, 12);
|
|
||||||
this.spawnSafeZone(-12, -12);
|
|
||||||
this.spawnSafeZone(15, -15);
|
|
||||||
this.spawnSafeZone(-15, 15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
createProceduralTexture(type) {
|
createProceduralTexture(type) {
|
||||||
const size = 512;
|
const size = 512;
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@@ -148,12 +197,15 @@ export class World {
|
|||||||
|
|
||||||
createWall(x, y, z, width, height, rotate = false) {
|
createWall(x, y, z, width, height, rotate = false) {
|
||||||
const geo = new THREE.BoxGeometry(width, height, 0.5);
|
const geo = new THREE.BoxGeometry(width, height, 0.5);
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
const mat = this.materials.wall;
|
||||||
map: this.texWall,
|
// Tile the texture - handled in init for the base material, but if we need unique tiling per wall size...
|
||||||
roughness: 0.95
|
// We can't easily change repeat per mesh if they share material.
|
||||||
});
|
// Ideal: Use World-Space mapping or just Clone the material if different size.
|
||||||
// Tile the texture
|
// For optimization, we accept fixed tiling or clone.
|
||||||
mat.map.repeat.set(width / 5, height / 5);
|
// Let's clone for walls since there are only 4 of them.
|
||||||
|
const instMat = mat.clone();
|
||||||
|
instMat.map = mat.map.clone(); // Clone texture wrapper to allow different repeat? No, texture data is shared.
|
||||||
|
instMat.map.repeat.set(width / 5, height / 5);
|
||||||
|
|
||||||
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);
|
||||||
@@ -162,14 +214,16 @@ export class World {
|
|||||||
wall.receiveShadow = true;
|
wall.receiveShadow = true;
|
||||||
this.scene.add(wall);
|
this.scene.add(wall);
|
||||||
this.colliders.push(wall);
|
this.colliders.push(wall);
|
||||||
|
|
||||||
|
// Fix: Add to colliderBoxes
|
||||||
|
wall.updateMatrixWorld(true);
|
||||||
|
const bbox = new THREE.Box3().setFromObject(wall);
|
||||||
|
this.colliderBoxes.push(bbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
createPillar(x, y, z) {
|
createPillar(x, y, z) {
|
||||||
const geo = new THREE.BoxGeometry(1, 5, 1);
|
const geo = new THREE.BoxGeometry(1, 5, 1);
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
const mat = this.materials.concrete;
|
||||||
map: this.texConcrete, // Reuse concrete for pillars
|
|
||||||
roughness: 0.9
|
|
||||||
});
|
|
||||||
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.castShadow = true;
|
||||||
@@ -177,6 +231,20 @@ export class World {
|
|||||||
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
|
||||||
|
|
||||||
|
// Fix: Add to colliderBoxes
|
||||||
|
pillar.updateMatrixWorld(true);
|
||||||
|
const bbox = new THREE.Box3().setFromObject(pillar);
|
||||||
|
this.colliderBoxes.push(bbox);
|
||||||
|
|
||||||
|
// Add to culler
|
||||||
|
this.culledObjects.push(pillar);
|
||||||
|
|
||||||
|
// Mount an emergency red floodlight on the pillar
|
||||||
|
// Performance: Only 50% of pillars have active lights
|
||||||
|
if (Math.random() < 0.5) {
|
||||||
|
this.spawnRedFloodlight(x, 3.5, z + 0.51, 0, pillar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createFurniture(count) {
|
createFurniture(count) {
|
||||||
@@ -224,7 +292,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() * 15); // 15 furniture types
|
const type = Math.floor(Math.random() * 16); // 16 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();
|
||||||
@@ -232,6 +300,9 @@ export class World {
|
|||||||
group.rotation.y = rot;
|
group.rotation.y = rot;
|
||||||
this.scene.add(group);
|
this.scene.add(group);
|
||||||
|
|
||||||
|
// Add to culler
|
||||||
|
this.culledObjects.push(group);
|
||||||
|
|
||||||
if (type === 0) this.spawnCouch(group);
|
if (type === 0) this.spawnCouch(group);
|
||||||
else if (type === 1) this.spawnTable(group);
|
else if (type === 1) this.spawnTable(group);
|
||||||
else if (type === 2) this.spawnChair(group);
|
else if (type === 2) this.spawnChair(group);
|
||||||
@@ -247,14 +318,46 @@ export class World {
|
|||||||
else if (type === 12) this.spawnWornArmchair(group);
|
else if (type === 12) this.spawnWornArmchair(group);
|
||||||
else if (type === 13) this.spawnRustyWorkbench(group);
|
else if (type === 13) this.spawnRustyWorkbench(group);
|
||||||
else if (type === 14) this.spawnBrokenMirror(group);
|
else if (type === 14) this.spawnBrokenMirror(group);
|
||||||
|
else if (type === 15) this.spawnLocker(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spawnLocker(group) {
|
||||||
|
this.addColliderBox(group, 0.9, 2.1, 0.9);
|
||||||
|
|
||||||
|
// Critical: Mark as hideable for Player interaction
|
||||||
|
group.userData.isHideable = true;
|
||||||
|
|
||||||
|
// Shared Metal Material
|
||||||
|
const mat = this.materials.metal;
|
||||||
|
|
||||||
|
// Main Body
|
||||||
|
const body = new THREE.Mesh(new THREE.BoxGeometry(0.8, 2.0, 0.8), mat);
|
||||||
|
body.position.y = 1.0;
|
||||||
|
body.castShadow = true; body.receiveShadow = true;
|
||||||
|
group.add(body);
|
||||||
|
|
||||||
|
// Slits / Vents
|
||||||
|
const ventGeo = new THREE.BoxGeometry(0.6, 0.05, 0.05);
|
||||||
|
const ventMat = this.materials.black;
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const vent = new THREE.Mesh(ventGeo, ventMat);
|
||||||
|
vent.position.set(0, 1.6 + (i * 0.1), 0.41);
|
||||||
|
group.add(vent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle
|
||||||
|
const handle = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.15, 0.05), this.materials.metalRust);
|
||||||
|
handle.position.set(0.3, 1.0, 0.42);
|
||||||
|
group.add(handle);
|
||||||
|
}
|
||||||
|
|
||||||
spawnBed(group) {
|
spawnBed(group) {
|
||||||
this.addColliderBox(group, 1.2, 0.6, 2.0);
|
this.addColliderBox(group, 1.2, 0.6, 2.0);
|
||||||
|
|
||||||
const frameMat = new THREE.MeshStandardMaterial({ color: 0x3e2723, roughness: 0.9, metalness: 0.4 });
|
const frameMat = this.materials.woodDark;
|
||||||
const mattressMat = new THREE.MeshStandardMaterial({ map: this.texFabric, color: 0x888877, roughness: 1.0 });
|
const mattressMat = this.materials.fabricWorn;
|
||||||
|
|
||||||
const f1 = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.1, 2.0), frameMat); f1.position.y = 0.3; group.add(f1);
|
const f1 = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.1, 2.0), frameMat); f1.position.y = 0.3; group.add(f1);
|
||||||
const hbTop = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.05, 0.05), frameMat); hbTop.position.set(0, 0.9, -1.0); group.add(hbTop);
|
const hbTop = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.05, 0.05), frameMat); hbTop.position.set(0, 0.9, -1.0); group.add(hbTop);
|
||||||
@@ -267,7 +370,7 @@ export class World {
|
|||||||
spawnBookshelf(group) {
|
spawnBookshelf(group) {
|
||||||
this.addColliderBox(group, 1.1, 2.0, 0.5);
|
this.addColliderBox(group, 1.1, 2.0, 0.5);
|
||||||
|
|
||||||
const mat = new THREE.MeshStandardMaterial({ map: this.texWood, color: 0x4a3c31, roughness: 0.9 });
|
const mat = this.materials.wood;
|
||||||
|
|
||||||
const left = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.0, 0.4), mat); left.position.set(-0.5, 1.0, 0); group.add(left);
|
const left = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.0, 0.4), mat); left.position.set(-0.5, 1.0, 0); group.add(left);
|
||||||
const right = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.0, 0.4), mat); right.position.set(0.5, 1.0, 0); group.add(right);
|
const right = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.0, 0.4), mat); right.position.set(0.5, 1.0, 0); group.add(right);
|
||||||
@@ -277,7 +380,7 @@ export class World {
|
|||||||
if (Math.random() < 0.2) continue;
|
if (Math.random() < 0.2) continue;
|
||||||
const shelf = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.05, 0.38), mat); shelf.position.set(0, y, 0); shelf.rotation.z = (Math.random() - 0.5) * 0.2; group.add(shelf);
|
const shelf = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.05, 0.38), mat); shelf.position.set(0, y, 0); shelf.rotation.z = (Math.random() - 0.5) * 0.2; group.add(shelf);
|
||||||
if (Math.random() > 0.5) {
|
if (Math.random() > 0.5) {
|
||||||
const book = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.25, 0.2), new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff }));
|
const book = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.25, 0.2), this.materials.fabric);
|
||||||
book.position.set((Math.random() - 0.5) * 0.8, y + 0.15, 0); book.rotation.z = (Math.random() - 0.5) * 0.5; group.add(book);
|
book.position.set((Math.random() - 0.5) * 0.8, y + 0.15, 0); book.rotation.z = (Math.random() - 0.5) * 0.5; group.add(book);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,7 +389,7 @@ export class World {
|
|||||||
spawnDrawer(group) {
|
spawnDrawer(group) {
|
||||||
this.addColliderBox(group, 0.9, 1.2, 0.5);
|
this.addColliderBox(group, 0.9, 1.2, 0.5);
|
||||||
|
|
||||||
const mat = new THREE.MeshStandardMaterial({ map: this.texWood, color: 0x3d2b1f, roughness: 0.8 });
|
const mat = this.materials.wood;
|
||||||
|
|
||||||
const body = new THREE.Mesh(new THREE.BoxGeometry(0.9, 1.2, 0.5), mat); body.position.y = 0.6; group.add(body);
|
const body = new THREE.Mesh(new THREE.BoxGeometry(0.9, 1.2, 0.5), mat); body.position.y = 0.6; group.add(body);
|
||||||
|
|
||||||
@@ -298,6 +401,7 @@ export class World {
|
|||||||
group.add(d);
|
group.add(d);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addColliderBox(group, w, h, d) {
|
addColliderBox(group, w, h, d) {
|
||||||
// Create invisible hitbox: slightly smaller than visual to forgive AABB rotation errors
|
// Create invisible hitbox: slightly smaller than visual to forgive AABB rotation errors
|
||||||
const geo = new THREE.BoxGeometry(w * 0.85, h, d * 0.85);
|
const geo = new THREE.BoxGeometry(w * 0.85, h, d * 0.85);
|
||||||
@@ -306,17 +410,18 @@ export class World {
|
|||||||
box.position.y = h / 2;
|
box.position.y = h / 2;
|
||||||
group.add(box);
|
group.add(box);
|
||||||
this.colliders.push(box);
|
this.colliders.push(box);
|
||||||
|
|
||||||
|
// NEW: Pre-calculate bounding box for optimization
|
||||||
|
group.updateMatrixWorld(true); // Critical: Update transform before computing box
|
||||||
|
const bbox = new THREE.Box3().setFromObject(box);
|
||||||
|
this.colliderBoxes.push(bbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnCouch(group) {
|
spawnCouch(group) {
|
||||||
this.addColliderBox(group, 2.2, 1.0, 0.8);
|
this.addColliderBox(group, 2.2, 1.0, 0.8);
|
||||||
|
|
||||||
// Use worn fabric texture
|
// Use worn fabric texture
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
const mat = this.materials.fabricWorn;
|
||||||
map: this.texFabric,
|
|
||||||
roughness: 1.0,
|
|
||||||
color: 0x666666
|
|
||||||
});
|
|
||||||
|
|
||||||
// Base (Sagging)
|
// Base (Sagging)
|
||||||
const base = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.45, 0.8), mat);
|
const base = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.45, 0.8), mat);
|
||||||
@@ -353,11 +458,11 @@ export class World {
|
|||||||
spawnTable(group) {
|
spawnTable(group) {
|
||||||
this.addColliderBox(group, 1.5, 0.9, 1.0);
|
this.addColliderBox(group, 1.5, 0.9, 1.0);
|
||||||
|
|
||||||
const mat = new THREE.MeshStandardMaterial({ map: this.texWood, roughness: 0.8, metalness: 0.1 });
|
const mat = this.materials.wood;
|
||||||
|
|
||||||
// Top
|
// Top
|
||||||
const top = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.1, 1.0), mat);
|
const top = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.1, 1.0), mat);
|
||||||
top.position.y = 0.8;
|
top.position.y = 0.8;
|
||||||
|
top.castShadow = true; top.receiveShadow = true;
|
||||||
group.add(top);
|
group.add(top);
|
||||||
|
|
||||||
// Legs
|
// Legs
|
||||||
@@ -376,7 +481,7 @@ export class World {
|
|||||||
spawnChair(group) {
|
spawnChair(group) {
|
||||||
this.addColliderBox(group, 0.5, 0.8, 0.5); // Simple box for chair
|
this.addColliderBox(group, 0.5, 0.8, 0.5); // Simple box for chair
|
||||||
|
|
||||||
const mat = new THREE.MeshStandardMaterial({ map: this.texWood, roughness: 0.9 });
|
const mat = this.materials.wood;
|
||||||
|
|
||||||
// Seat
|
// Seat
|
||||||
const seat = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.08, 0.5), mat);
|
const seat = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.08, 0.5), mat);
|
||||||
@@ -406,12 +511,12 @@ export class World {
|
|||||||
spawnLamp(group) {
|
spawnLamp(group) {
|
||||||
this.addColliderBox(group, 0.1, 2.0, 0.1);
|
this.addColliderBox(group, 0.1, 2.0, 0.1);
|
||||||
|
|
||||||
const mat = new THREE.MeshStandardMaterial({ color: 0x3e2723, roughness: 0.9, metalness: 0.3 });
|
const mat = this.materials.metalRust;
|
||||||
const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 2.0, 8), mat);
|
const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 2.0, 8), mat);
|
||||||
pole.position.y = 1.0; pole.rotation.z = 0.1;
|
pole.position.y = 1.0; pole.rotation.z = 0.1;
|
||||||
group.add(pole); // No collider
|
group.add(pole); // No collider
|
||||||
|
|
||||||
const shade = new THREE.Mesh(new THREE.ConeGeometry(0.4, 0.5, 16, 1, true), new THREE.MeshStandardMaterial({ color: 0xd7ccc8, side: THREE.DoubleSide, roughness: 1.0 }));
|
const shade = new THREE.Mesh(new THREE.ConeGeometry(0.4, 0.5, 16, 1, true), this.materials.fabric);
|
||||||
shade.position.set(-0.1, 1.9, 0); shade.rotation.z = 0.3;
|
shade.position.set(-0.1, 1.9, 0); shade.rotation.z = 0.3;
|
||||||
group.add(shade);
|
group.add(shade);
|
||||||
}
|
}
|
||||||
@@ -419,22 +524,21 @@ export class World {
|
|||||||
spawnClock(group) {
|
spawnClock(group) {
|
||||||
this.addColliderBox(group, 0.7, 2.2, 0.6);
|
this.addColliderBox(group, 0.7, 2.2, 0.6);
|
||||||
|
|
||||||
const woodColor = 0x2a1a10;
|
const mat = this.materials.woodDark;
|
||||||
const mat = new THREE.MeshStandardMaterial({ color: woodColor, roughness: 0.6 });
|
|
||||||
const base = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.4, 0.6), mat); base.position.y = 0.2; group.add(base);
|
const base = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.4, 0.6), mat); base.position.y = 0.2; group.add(base);
|
||||||
const body = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.4, 0.45), mat); body.position.y = 1.1; group.add(body);
|
const body = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.4, 0.45), mat); body.position.y = 1.1; group.add(body);
|
||||||
const headBox = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.7, 0.5), mat); headBox.position.y = 2.15; group.add(headBox);
|
const headBox = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.7, 0.5), mat); headBox.position.y = 2.15; group.add(headBox);
|
||||||
const top = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.35, 0.2, 4), mat); top.rotation.y = Math.PI / 4; top.position.y = 2.6; group.add(top);
|
const top = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.35, 0.2, 4), mat); top.rotation.y = Math.PI / 4; top.position.y = 2.6; group.add(top);
|
||||||
const face = new THREE.Mesh(new THREE.CircleGeometry(0.25, 32), new THREE.MeshBasicMaterial({ color: 0xeeddcc })); face.position.set(0, 2.15, 0.26); group.add(face);
|
const face = new THREE.Mesh(new THREE.CircleGeometry(0.25, 32), this.materials.fabric); face.position.set(0, 2.15, 0.26); group.add(face);
|
||||||
const glass = new THREE.Mesh(new THREE.PlaneGeometry(0.3, 1.0), new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.1, metalness: 0.8 })); glass.position.set(0, 1.1, 0.23); group.add(glass);
|
const glass = new THREE.Mesh(new THREE.PlaneGeometry(0.3, 1.0), this.materials.glass); glass.position.set(0, 1.1, 0.23); group.add(glass);
|
||||||
const pendulum = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.05, 16), new THREE.MeshStandardMaterial({ color: 0xae8b0c, metalness: 0.8, roughness: 0.3 })); pendulum.rotation.x = Math.PI / 2; pendulum.position.set(0, 0.9, 0.24); group.add(pendulum);
|
const pendulum = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.05, 16), this.materials.metal); pendulum.rotation.x = Math.PI / 2; pendulum.position.set(0, 0.9, 0.24); group.add(pendulum);
|
||||||
const rod = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.8, 0.02), new THREE.MeshStandardMaterial({ color: 0xae8b0c })); rod.position.set(0, 1.3, 0.24); group.add(rod);
|
const rod = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.8, 0.02), this.materials.metal); rod.position.set(0, 1.3, 0.24); group.add(rod);
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnMannequin(group) {
|
spawnMannequin(group) {
|
||||||
this.addColliderBox(group, 0.5, 1.9, 0.4);
|
this.addColliderBox(group, 0.5, 1.9, 0.4);
|
||||||
|
|
||||||
const mat = new THREE.MeshStandardMaterial({ color: 0xd0c0b0, roughness: 0.4 });
|
const mat = this.materials.fabric;
|
||||||
const hips = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.2, 0.25), mat); hips.position.y = 0.95; group.add(hips);
|
const hips = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.2, 0.25), mat); hips.position.y = 0.95; group.add(hips);
|
||||||
const legGeo = new THREE.CylinderGeometry(0.09, 0.07, 0.95, 8);
|
const legGeo = new THREE.CylinderGeometry(0.09, 0.07, 0.95, 8);
|
||||||
const legL = new THREE.Mesh(legGeo, mat); legL.position.set(-0.12, 0.475, 0); group.add(legL);
|
const legL = new THREE.Mesh(legGeo, mat); legL.position.set(-0.12, 0.475, 0); group.add(legL);
|
||||||
@@ -451,23 +555,23 @@ export class World {
|
|||||||
spawnTV(group) {
|
spawnTV(group) {
|
||||||
this.addColliderBox(group, 0.8, 1.5, 0.6);
|
this.addColliderBox(group, 0.8, 1.5, 0.6);
|
||||||
|
|
||||||
const standMat = new THREE.MeshStandardMaterial({ color: 0x4a3c31, roughness: 0.9 });
|
const standMat = this.materials.wood;
|
||||||
const stand = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.6, 0.5), standMat); stand.position.y = 0.3; group.add(stand);
|
const stand = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.6, 0.5), standMat); stand.position.y = 0.3; group.add(stand);
|
||||||
|
|
||||||
const tvMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.5 });
|
const tvMat = this.materials.black;
|
||||||
const tv = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.55, 0.55), tvMat); tv.position.y = 0.875; group.add(tv);
|
const tv = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.55, 0.55), tvMat); tv.position.y = 0.875; group.add(tv);
|
||||||
|
|
||||||
const screenMat = new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.6, roughness: 0.2 });
|
const screenMat = this.materials.glass;
|
||||||
const screen = new THREE.Mesh(new THREE.SphereGeometry(0.35, 32, 32, 0, Math.PI * 2, 0, 0.6), screenMat);
|
const screen = new THREE.Mesh(new THREE.SphereGeometry(0.35, 32, 32, 0, Math.PI * 2, 0, 0.6), screenMat);
|
||||||
screen.position.set(0, 0.9, 0.15); screen.scale.set(0.9, 0.7, 0.5); screen.rotation.x = -Math.PI / 2; group.add(screen);
|
screen.position.set(0, 0.9, 0.15); screen.scale.set(0.9, 0.7, 0.5); screen.rotation.x = -Math.PI / 2; group.add(screen);
|
||||||
|
|
||||||
const sidePanel = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.4, 0.05), new THREE.MeshStandardMaterial({ color: 0x333333 }));
|
const sidePanel = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.4, 0.05), this.materials.black);
|
||||||
sidePanel.position.set(0.25, 0.875, 0.28); group.add(sidePanel);
|
sidePanel.position.set(0.25, 0.875, 0.28); group.add(sidePanel);
|
||||||
const knob1 = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.05), new THREE.MeshStandardMaterial({ color: 0x888888 }));
|
const knob1 = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.05), this.materials.metal);
|
||||||
knob1.rotation.x = Math.PI / 2; knob1.position.set(0.25, 0.95, 0.31); group.add(knob1);
|
knob1.rotation.x = Math.PI / 2; knob1.position.set(0.25, 0.95, 0.31); group.add(knob1);
|
||||||
const knob2 = knob1.clone(); knob2.position.set(0.25, 0.8, 0.31); group.add(knob2);
|
const knob2 = knob1.clone(); knob2.position.set(0.25, 0.8, 0.31); group.add(knob2);
|
||||||
|
|
||||||
const ant = new THREE.Mesh(new THREE.CylinderGeometry(0.005, 0.005, 0.6), new THREE.MeshStandardMaterial({ color: 0xaaaaaa }));
|
const ant = new THREE.Mesh(new THREE.CylinderGeometry(0.005, 0.005, 0.6), this.materials.metal);
|
||||||
ant.position.set(-0.1, 1.3, 0); ant.rotation.z = 0.4; group.add(ant);
|
ant.position.set(-0.1, 1.3, 0); ant.rotation.z = 0.4; group.add(ant);
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -478,11 +582,7 @@ export class World {
|
|||||||
this.addColliderBox(group, 0.8, 0.6, 0.8);
|
this.addColliderBox(group, 0.8, 0.6, 0.8);
|
||||||
|
|
||||||
// Worn, splintered wood
|
// Worn, splintered wood
|
||||||
const woodMat = new THREE.MeshStandardMaterial({
|
const woodMat = this.materials.woodDark;
|
||||||
map: this.texWood,
|
|
||||||
color: 0x2a1a0a, // Dark, aged
|
|
||||||
roughness: 1.0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Main crate body (missing some planks)
|
// Main crate body (missing some planks)
|
||||||
const plankGeo = new THREE.BoxGeometry(0.8, 0.08, 0.08);
|
const plankGeo = new THREE.BoxGeometry(0.8, 0.08, 0.08);
|
||||||
@@ -534,15 +634,8 @@ export class World {
|
|||||||
this.addColliderBox(group, 0.6, 1.0, 0.6);
|
this.addColliderBox(group, 0.6, 1.0, 0.6);
|
||||||
|
|
||||||
// Rusty metal and rotting wood
|
// Rusty metal and rotting wood
|
||||||
const metalMat = new THREE.MeshStandardMaterial({
|
const metalMat = this.materials.metalRust;
|
||||||
color: 0x4a3520,
|
const stainMat = this.materials.black;
|
||||||
roughness: 0.9,
|
|
||||||
metalness: 0.3
|
|
||||||
});
|
|
||||||
const stainMat = new THREE.MeshStandardMaterial({
|
|
||||||
color: 0x1a1a10,
|
|
||||||
roughness: 1.0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Main barrel body (cylinder)
|
// Main barrel body (cylinder)
|
||||||
const bodyGeo = new THREE.CylinderGeometry(0.28, 0.25, 0.9, 12);
|
const bodyGeo = new THREE.CylinderGeometry(0.28, 0.25, 0.9, 12);
|
||||||
@@ -552,11 +645,7 @@ export class World {
|
|||||||
group.add(body);
|
group.add(body);
|
||||||
|
|
||||||
// Metal bands (rusty)
|
// Metal bands (rusty)
|
||||||
const bandMat = new THREE.MeshStandardMaterial({
|
const bandMat = this.materials.metal;
|
||||||
color: 0x3a2a1a,
|
|
||||||
roughness: 0.7,
|
|
||||||
metalness: 0.6
|
|
||||||
});
|
|
||||||
[0.15, 0.5, 0.85].forEach(y => {
|
[0.15, 0.5, 0.85].forEach(y => {
|
||||||
if (Math.random() < 0.15) return; // Missing band
|
if (Math.random() < 0.15) return; // Missing band
|
||||||
const band = new THREE.Mesh(new THREE.TorusGeometry(0.27, 0.02, 8, 16), bandMat);
|
const band = new THREE.Mesh(new THREE.TorusGeometry(0.27, 0.02, 8, 16), bandMat);
|
||||||
@@ -588,16 +677,9 @@ export class World {
|
|||||||
this.addColliderBox(group, 0.9, 1.0, 0.9);
|
this.addColliderBox(group, 0.9, 1.0, 0.9);
|
||||||
|
|
||||||
// Torn, faded fabric
|
// Torn, faded fabric
|
||||||
const fabricMat = new THREE.MeshStandardMaterial({
|
// Torn, faded fabric
|
||||||
map: this.texFabric,
|
const fabricMat = this.materials.fabricWorn;
|
||||||
color: 0x3a3028, // Dirty brown-gray
|
const woodMat = this.materials.woodDark;
|
||||||
roughness: 1.0
|
|
||||||
});
|
|
||||||
const woodMat = new THREE.MeshStandardMaterial({
|
|
||||||
map: this.texWood,
|
|
||||||
color: 0x1f150a,
|
|
||||||
roughness: 0.9
|
|
||||||
});
|
|
||||||
|
|
||||||
// Legs (wooden, one possibly broken)
|
// Legs (wooden, one possibly broken)
|
||||||
const legGeo = new THREE.BoxGeometry(0.08, 0.2, 0.08);
|
const legGeo = new THREE.BoxGeometry(0.08, 0.2, 0.08);
|
||||||
@@ -646,16 +728,8 @@ export class World {
|
|||||||
this.addColliderBox(group, 1.5, 1.0, 0.7);
|
this.addColliderBox(group, 1.5, 1.0, 0.7);
|
||||||
|
|
||||||
// Rusted metal and old wood
|
// Rusted metal and old wood
|
||||||
const metalMat = new THREE.MeshStandardMaterial({
|
const metalMat = this.materials.metalRust;
|
||||||
color: 0x5a4535,
|
const woodMat = this.materials.wood;
|
||||||
roughness: 0.8,
|
|
||||||
metalness: 0.5
|
|
||||||
});
|
|
||||||
const woodMat = new THREE.MeshStandardMaterial({
|
|
||||||
map: this.texWood,
|
|
||||||
color: 0x2a1a0a,
|
|
||||||
roughness: 0.95
|
|
||||||
});
|
|
||||||
|
|
||||||
// Metal frame legs
|
// Metal frame legs
|
||||||
const legGeo = new THREE.BoxGeometry(0.08, 0.8, 0.08);
|
const legGeo = new THREE.BoxGeometry(0.08, 0.8, 0.08);
|
||||||
@@ -705,21 +779,8 @@ export class World {
|
|||||||
this.addColliderBox(group, 0.8, 1.5, 0.15);
|
this.addColliderBox(group, 0.8, 1.5, 0.15);
|
||||||
|
|
||||||
// Ornate but damaged frame
|
// Ornate but damaged frame
|
||||||
const frameMat = new THREE.MeshStandardMaterial({
|
const frameMat = this.materials.wood;
|
||||||
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);
|
const frameT = new THREE.Mesh(new THREE.BoxGeometry(0.85, 0.08, 0.1), frameMat);
|
||||||
frameT.position.set(0, 1.4, 0);
|
frameT.position.set(0, 1.4, 0);
|
||||||
frameT.castShadow = true; frameT.receiveShadow = true;
|
frameT.castShadow = true; frameT.receiveShadow = true;
|
||||||
@@ -739,26 +800,12 @@ export class World {
|
|||||||
group.add(frameR);
|
group.add(frameR);
|
||||||
|
|
||||||
// Mirror surface (dark, tarnished)
|
// Mirror surface (dark, tarnished)
|
||||||
|
const mirrorMat = this.materials.glass;
|
||||||
const mirror = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.25, 0.02), mirrorMat);
|
const mirror = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.25, 0.02), mirrorMat);
|
||||||
mirror.position.set(0, 0.75, 0.04);
|
mirror.position.set(0, 0.75, 0.04);
|
||||||
mirror.receiveShadow = true;
|
mirror.receiveShadow = true;
|
||||||
group.add(mirror);
|
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
|
// Leaning against wall or fallen
|
||||||
if (Math.random() < 0.4) {
|
if (Math.random() < 0.4) {
|
||||||
group.rotation.x = -0.3;
|
group.rotation.x = -0.3;
|
||||||
@@ -770,8 +817,8 @@ export class World {
|
|||||||
|
|
||||||
|
|
||||||
createDust() {
|
createDust() {
|
||||||
// Create 800 dust particles (Reduced)
|
// Create 200 dust particles (Performance optimization: reduced from 800)
|
||||||
const count = 800;
|
const count = 200;
|
||||||
const geometry = new THREE.BufferGeometry();
|
const geometry = new THREE.BufferGeometry();
|
||||||
const positions = new Float32Array(count * 3);
|
const positions = new Float32Array(count * 3);
|
||||||
const colors = new Float32Array(count * 3); // For controlling opacity/brightness per particle
|
const colors = new Float32Array(count * 3); // For controlling opacity/brightness per particle
|
||||||
@@ -833,6 +880,13 @@ export class World {
|
|||||||
light.position.set(x, 4.5, z);
|
light.position.set(x, 4.5, z);
|
||||||
light.target.position.set(x, 0, z);
|
light.target.position.set(x, 0, z);
|
||||||
light.castShadow = true;
|
light.castShadow = true;
|
||||||
|
|
||||||
|
// NEW: Disable shadow auto-update for static lights (Boosts performance)
|
||||||
|
light.shadow.mapSize.width = 256;
|
||||||
|
light.shadow.mapSize.height = 256;
|
||||||
|
light.shadow.autoUpdate = false;
|
||||||
|
light.shadow.needsUpdate = true;
|
||||||
|
|
||||||
this.scene.add(light);
|
this.scene.add(light);
|
||||||
this.scene.add(light.target);
|
this.scene.add(light.target);
|
||||||
|
|
||||||
@@ -851,6 +905,51 @@ export class World {
|
|||||||
baseIntensity: 12.0,
|
baseIntensity: 12.0,
|
||||||
baseGlowIntensity: 4.0
|
baseGlowIntensity: 4.0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 4. Spawn Recharge Station
|
||||||
|
this.spawnRechargeStation(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnRechargeStation(x, z) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.position.set(x, 0, z + 2.5); // Slightly offset from center of safe zone
|
||||||
|
|
||||||
|
// Visual: Industrial Terminal/Pod
|
||||||
|
const baseGeo = new THREE.BoxGeometry(0.6, 1.2, 0.4);
|
||||||
|
const baseMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.8, roughness: 0.2 });
|
||||||
|
const base = new THREE.Mesh(baseGeo, baseMat);
|
||||||
|
base.position.y = 0.6;
|
||||||
|
group.add(base);
|
||||||
|
|
||||||
|
// Screen/Interface
|
||||||
|
const screenGeo = new THREE.PlaneGeometry(0.4, 0.3);
|
||||||
|
const screenMat = new THREE.MeshBasicMaterial({ color: 0x002200 }); // Dark green screen
|
||||||
|
const screen = new THREE.Mesh(screenGeo, screenMat);
|
||||||
|
screen.position.set(0, 0.9, 0.21);
|
||||||
|
group.add(screen);
|
||||||
|
|
||||||
|
// Glowing Indicator
|
||||||
|
const indicatorGeo = new THREE.SphereGeometry(0.05, 8, 8);
|
||||||
|
const indicatorMat = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); // Green for "Ready"
|
||||||
|
const indicator = new THREE.Mesh(indicatorGeo, indicatorMat);
|
||||||
|
indicator.position.set(0, 1.1, 0.21);
|
||||||
|
group.add(indicator);
|
||||||
|
|
||||||
|
const pointLight = new THREE.PointLight(0x00ff00, 1.0, 2);
|
||||||
|
pointLight.position.set(0, 1.1, 0.3);
|
||||||
|
group.add(pointLight);
|
||||||
|
|
||||||
|
this.scene.add(group);
|
||||||
|
|
||||||
|
// Add to colliders so player can't walk through it
|
||||||
|
this.addColliderBox(group, 0.6, 1.2, 0.4);
|
||||||
|
|
||||||
|
// Store for interaction
|
||||||
|
this.rechargeStations.push({
|
||||||
|
position: new THREE.Vector3(x, 0, z + 2.5),
|
||||||
|
indicator: indicator,
|
||||||
|
light: pointLight
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
breakSafeZone(index) {
|
breakSafeZone(index) {
|
||||||
@@ -874,7 +973,64 @@ export class World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spawnRedFloodlight(x, y, z, rotationY) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.position.set(x, y, z);
|
||||||
|
group.rotation.y = rotationY;
|
||||||
|
|
||||||
|
// Visual: Industrial wall bracket and lamp
|
||||||
|
const bracketGeo = new THREE.BoxGeometry(0.1, 0.5, 0.4);
|
||||||
|
const bracketMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
|
||||||
|
const bracket = new THREE.Mesh(bracketGeo, bracketMat);
|
||||||
|
bracket.position.z = 0.2;
|
||||||
|
group.add(bracket);
|
||||||
|
|
||||||
|
const lampGeo = new THREE.CylinderGeometry(0.1, 0.15, 0.2, 12);
|
||||||
|
const lampMat = new THREE.MeshStandardMaterial({ color: 0x660000, emissive: 0x330000 });
|
||||||
|
const lamp = new THREE.Mesh(lampGeo, lampMat);
|
||||||
|
lamp.rotation.x = -Math.PI / 3;
|
||||||
|
lamp.position.set(0, 0, 0.4);
|
||||||
|
group.add(lamp);
|
||||||
|
|
||||||
|
// Actual Light: Red Flood
|
||||||
|
const light = new THREE.SpotLight(0xff0000, 5.0, 30, Math.PI / 3, 0.5, 2);
|
||||||
|
light.position.set(0, 0, 0.4);
|
||||||
|
// Small target helper
|
||||||
|
const target = new THREE.Object3D();
|
||||||
|
target.position.set(0, -10, 10);
|
||||||
|
group.add(target);
|
||||||
|
light.target = target;
|
||||||
|
group.add(light);
|
||||||
|
this.scene.add(group);
|
||||||
|
}
|
||||||
|
updateCulling(player) {
|
||||||
|
if (!player || !player.camera) return;
|
||||||
|
const pPos = player.camera.position;
|
||||||
|
|
||||||
|
for (const obj of this.culledObjects) {
|
||||||
|
const distSq = pPos.distanceToSquared(obj.position);
|
||||||
|
const maxSq = this.cullDistance * this.cullDistance;
|
||||||
|
|
||||||
|
// If visible toggle changes, update it
|
||||||
|
if (distSq > maxSq && obj.visible) {
|
||||||
|
obj.visible = false;
|
||||||
|
} else if (distSq <= maxSq && !obj.visible) {
|
||||||
|
obj.visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Shared static vectors to avoid per-frame allocation in update()
|
||||||
|
static _pPos = new THREE.Vector3();
|
||||||
|
static _toPart = new THREE.Vector3();
|
||||||
|
static _lightPos = new THREE.Vector3();
|
||||||
|
static _lightDir = new THREE.Vector3();
|
||||||
|
static _targetPos = new THREE.Vector3();
|
||||||
|
|
||||||
update(dt, player) {
|
update(dt, player) {
|
||||||
|
// Power update removed
|
||||||
|
this.updateCulling(player); // NEW: Run culling
|
||||||
|
|
||||||
if (!this.dustParticles) return;
|
if (!this.dustParticles) return;
|
||||||
|
|
||||||
const positions = this.dustParticles.geometry.attributes.position.array;
|
const positions = this.dustParticles.geometry.attributes.position.array;
|
||||||
@@ -882,12 +1038,15 @@ export class World {
|
|||||||
const velocities = this.dustParticles.userData.velocities;
|
const velocities = this.dustParticles.userData.velocities;
|
||||||
|
|
||||||
// Flashlight info
|
// Flashlight info
|
||||||
let lightPos, lightDir, lightAngle, lightDist, isLightOn;
|
const lightPos = World._lightPos;
|
||||||
|
const lightDir = World._lightDir;
|
||||||
|
let lightAngle = 0;
|
||||||
|
let lightDist = 0;
|
||||||
|
let isLightOn = false;
|
||||||
|
|
||||||
if (player && player.flashlight) {
|
if (player && player.flashlight) {
|
||||||
// Get world position and direction of FLASHLIGHT
|
// Get world position and direction of FLASHLIGHT
|
||||||
lightPos = new THREE.Vector3();
|
const targetPos = World._targetPos;
|
||||||
lightDir = new THREE.Vector3();
|
|
||||||
const targetPos = new THREE.Vector3();
|
|
||||||
|
|
||||||
player.flashlight.getWorldPosition(lightPos);
|
player.flashlight.getWorldPosition(lightPos);
|
||||||
player.flashlight.target.getWorldPosition(targetPos);
|
player.flashlight.target.getWorldPosition(targetPos);
|
||||||
@@ -900,7 +1059,10 @@ export class World {
|
|||||||
isLightOn = player.flashlightOn;
|
isLightOn = player.flashlightOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pPos = new THREE.Vector3(); // Temp vector
|
const pPos = World._pPos;
|
||||||
|
const toPart = World._toPart;
|
||||||
|
const lightDistSq = lightDist * lightDist;
|
||||||
|
const cosAngleThreshold = Math.cos(lightAngle); // NEW: Pre-calculate threshold
|
||||||
|
|
||||||
for (let i = 0; i < velocities.length; i++) {
|
for (let i = 0; i < velocities.length; i++) {
|
||||||
const v = velocities[i];
|
const v = velocities[i];
|
||||||
@@ -919,20 +1081,22 @@ export class World {
|
|||||||
// 2. Lighting Check (Volumetric Beam)
|
// 2. Lighting Check (Volumetric Beam)
|
||||||
let brightness = 0;
|
let brightness = 0;
|
||||||
if (isLightOn) {
|
if (isLightOn) {
|
||||||
// Check distance
|
// Check distance squared (faster than distance)
|
||||||
pPos.set(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]);
|
pPos.set(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]);
|
||||||
const dist = pPos.distanceTo(lightPos);
|
const distSq = pPos.distanceToSquared(lightPos);
|
||||||
|
|
||||||
if (dist < lightDist) {
|
if (distSq < lightDistSq) {
|
||||||
// Vector from light to particle
|
// Vector from light to particle
|
||||||
const toPart = pPos.sub(lightPos).normalize();
|
toPart.subVectors(pPos, lightPos).normalize();
|
||||||
const angle = toPart.angleTo(lightDir);
|
const dot = toPart.dot(lightDir); // NEW: Use dot product for speed
|
||||||
|
|
||||||
// Strictly inside the cone with soft edges
|
if (dot > cosAngleThreshold) {
|
||||||
if (angle < lightAngle) {
|
const dist = Math.sqrt(distSq);
|
||||||
// "Point of Light" Effect:
|
// "Point of Light" Effect:
|
||||||
// 1. Radial falloff: Brightest in center of beam, fades to edge
|
// 1. Radial falloff: Brightest in center of beam, fades to edge
|
||||||
const radialFactor = 1.0 - (angle / lightAngle);
|
// We can approximate this with the dot product:
|
||||||
|
// dot is 1.0 at center, cosAngleThreshold at edge
|
||||||
|
const radialFactor = (dot - cosAngleThreshold) / (1.0 - cosAngleThreshold);
|
||||||
|
|
||||||
// 2. Distance falloff: Fades with distance
|
// 2. Distance falloff: Fades with distance
|
||||||
const distFactor = 1.0 - (dist / lightDist);
|
const distFactor = 1.0 - (dist / lightDist);
|
||||||
@@ -953,4 +1117,36 @@ export class World {
|
|||||||
this.dustParticles.geometry.attributes.position.needsUpdate = true;
|
this.dustParticles.geometry.attributes.position.needsUpdate = true;
|
||||||
this.dustParticles.geometry.attributes.color.needsUpdate = true;
|
this.dustParticles.geometry.attributes.color.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spawnFlarePickup(x, z) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.position.set(x, 0.1, z);
|
||||||
|
|
||||||
|
// Visual: Red stick
|
||||||
|
const geo = new THREE.CylinderGeometry(0.05, 0.05, 0.4, 8);
|
||||||
|
geo.rotateX(Math.PI / 2); // Lay flat
|
||||||
|
const mat = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0x330000 });
|
||||||
|
const mesh = new THREE.Mesh(geo, mat);
|
||||||
|
group.add(mesh);
|
||||||
|
|
||||||
|
// Visual: Small glow
|
||||||
|
const light = new THREE.PointLight(0xff0000, 1, 2);
|
||||||
|
light.position.y = 0.2;
|
||||||
|
group.add(light);
|
||||||
|
|
||||||
|
this.scene.add(group);
|
||||||
|
|
||||||
|
this.collectableFlares.push({
|
||||||
|
position: group.position,
|
||||||
|
group: group,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFlarePickup(pickup) {
|
||||||
|
pickup.active = false;
|
||||||
|
pickup.group.visible = false;
|
||||||
|
// Optionally remove from scene to save memory, but hiding is faster for now
|
||||||
|
this.scene.remove(pickup.group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ window.log = (msg) => {
|
|||||||
const span = document.createElement('div');
|
const span = document.createElement('div');
|
||||||
span.textContent = `> ${msg}`;
|
span.textContent = `> ${msg}`;
|
||||||
logDiv.appendChild(span);
|
logDiv.appendChild(span);
|
||||||
|
|
||||||
|
// Performance: Cap log size
|
||||||
|
if (logDiv.children.length > 50) {
|
||||||
|
logDiv.removeChild(logDiv.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
logDiv.scrollTop = logDiv.scrollHeight;
|
logDiv.scrollTop = logDiv.scrollHeight;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
66
style.css
66
style.css
@@ -28,7 +28,8 @@ body {
|
|||||||
/* Let clicks pass through to canvas if needed, but start screen needs clicks */
|
/* Let clicks pass through to canvas if needed, but start screen needs clicks */
|
||||||
}
|
}
|
||||||
|
|
||||||
#start-screen {
|
#start-screen,
|
||||||
|
#loading-screen {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -72,3 +73,66 @@ h1 {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
transition: width 0.1s linear;
|
transition: width 0.1s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#heartbeat {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heart-icon {
|
||||||
|
fill: #ff3333;
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#power-warning {
|
||||||
|
display: none;
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
text-shadow: 0 0 10px red;
|
||||||
|
animation: flash 0.5s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flash {
|
||||||
|
from {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#crosshair {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background-color: white;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user