import * as THREE from 'three'; import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls.js'; export class Player { constructor(camera, colliders) { this.camera = camera; this.colliders = colliders; // Player stats this.speed = 5.0; this.height = 1.7; // Eyes height // Init controls this.controls = new PointerLockControls(camera, document.body); // Movement state this.moveForward = false; this.moveBackward = false; this.moveLeft = false; this.moveRight = false; this.velocity = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.flashlightOn = true; // Started as ON this.setupInput(); this.setupFlashlight(); } setupFlashlight() { this.flashlight = new THREE.SpotLight(0xffffff, 10); this.flashlight.angle = Math.PI / 6; this.flashlight.penumbra = 0.3; this.flashlight.decay = 2; this.flashlight.distance = 15; this.camera.add(this.flashlight); this.flashlight.position.set(0, 0, 0); this.flashlight.target.position.set(0, 0, -1); this.camera.add(this.flashlight.target); } setupInput() { const onKeyDown = (event) => { switch (event.code) { case 'KeyW': this.moveForward = true; break; case 'KeyA': this.moveLeft = true; break; case 'KeyS': this.moveBackward = true; break; case 'KeyD': this.moveRight = true; break; case 'KeyF': this.toggleFlashlight(); break; } }; const onKeyUp = (event) => { switch (event.code) { case 'KeyW': this.moveForward = false; break; case 'KeyA': this.moveLeft = false; break; case 'KeyS': this.moveBackward = false; break; case 'KeyD': this.moveRight = false; break; } }; document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp); } toggleFlashlight() { if (!this.controls.isLocked) return; // Only toggle when game is active this.flashlightOn = !this.flashlightOn; if (this.flashlight) { this.flashlight.visible = this.flashlightOn; } } lockControls() { this.controls.lock(); } getObject() { return this.controls.getObject(); } update(dt) { if (!this.controls.isLocked) return; // Friction-like dampening // Simple direct velocity for now this.velocity.x = 0; this.velocity.z = 0; this.direction.z = Number(this.moveForward) - Number(this.moveBackward); this.direction.x = Number(this.moveRight) - Number(this.moveLeft); this.direction.normalize(); // Ensure consistent speed in all directions if (this.moveForward || this.moveBackward) this.velocity.z -= this.direction.z * this.speed * dt; if (this.moveLeft || this.moveRight) this.velocity.x -= this.direction.x * this.speed * dt; // Apply movement this.controls.moveRight(-this.velocity.x); this.controls.moveForward(-this.velocity.z); // Simple Collision: Push back const playerPos = this.controls.getObject().position; const playerRadius = 0.5; // approximated radius for (const collider of this.colliders) { // Assume colliders are BoxGeometry meshes aligned with axes for now const box = new THREE.Box3().setFromObject(collider); // Check if player is inside the box (expanded by radius) // We only check X/Z for walls 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) { // Very simple resolution: determine closest edge and push out // This is a naive implementation but works for static orthogonal walls const dx1 = Math.abs(playerPos.x - (box.min.x - playerRadius)); const dx2 = Math.abs(playerPos.x - (box.max.x + playerRadius)); const dz1 = Math.abs(playerPos.z - (box.min.z - playerRadius)); const dz2 = Math.abs(playerPos.z - (box.max.z + playerRadius)); const minOverlap = Math.min(dx1, dx2, dz1, dz2); if (minOverlap === dx1) playerPos.x = box.min.x - playerRadius; else if (minOverlap === dx2) playerPos.x = box.max.x + playerRadius; else if (minOverlap === dz1) playerPos.z = box.min.z - playerRadius; else if (minOverlap === dz2) playerPos.z = box.max.z + playerRadius; } } // Keep player on ground playerPos.y = this.height; // Flashlight flicker effect (subtle) if (this.flashlight && this.flashlightOn) { this.flashlight.intensity = 10 + Math.random() * 0.5; } } }