/** * DOOM 3D - First Person Player Controller & Physics * Smooth movement, Pointer Lock look, step-climbing physics, stats, inventory and cheat codes. */ class DoomPlayer { constructor(engine, sound) { this.engine = engine; this.sound = sound; this.camera = engine.camera; // Position & Physics this.position = new THREE.Vector3(0, 2.0, 0); this.velocity = new THREE.Vector3(); this.euler = new THREE.Euler(0, 0, 0, 'YXZ'); this.isGrounded = false; this.height = 1.8; this.radius = 0.5; this.stepHeight = 0.95; // Speeds this.walkSpeed = 11.0; this.runSpeed = 19.0; this.jumpForce = 12.0; this.gravity = 26.0; this.mouseSensitivity = 0.0022; // Controls Input State this.keys = { forward: false, backward: false, left: false, right: false, sprint: false, jump: false, interact: false, shoot: false }; this.isPointerLocked = false; // Player Stats this.maxHealth = 100; this.health = 100; this.maxArmor = 100; this.armor = 0; this.isDead = false; this.godMode = false; this.noclip = false; // Ammo Reserves this.ammo = { bullets: 50, shells: 0, rockets: 0, cells: 0 }; this.maxAmmo = { bullets: 200, shells: 50, rockets: 50, cells: 300 }; // Keys & Inventory this.keysInventory = { blue: false, red: false, yellow: false }; // Head Bobbing this.bobTimer = 0; this.bobOffset = new THREE.Vector2(); // Slime Damage Timer this.slimeTimer = 0; // Cheat Input Buffer this.cheatBuffer = ''; this.setupEventListeners(); } setupEventListeners() { // Keyboard down window.addEventListener('keydown', (e) => { this.onKeyDown(e); }); // Keyboard up window.addEventListener('keyup', (e) => { this.onKeyUp(e); }); // Mouse move (Pointer Lock) document.addEventListener('mousemove', (e) => { if (!this.isPointerLocked || this.isDead) return; const movementX = e.movementX || e.mozMovementX || e.webkitMovementX || 0; const movementY = e.movementY || e.mozMovementY || e.webkitMovementY || 0; this.euler.y -= movementX * this.mouseSensitivity; this.euler.x -= movementY * this.mouseSensitivity; // Clamp pitch look angle to [-89 deg, +89 deg] const maxPitch = Math.PI / 2 - 0.02; this.euler.x = Math.max(-maxPitch, Math.min(maxPitch, this.euler.x)); this.camera.quaternion.setFromEuler(this.euler); }); // Pointer lock state changes document.addEventListener('pointerlockchange', () => { this.isPointerLocked = (document.pointerLockElement === this.engine.renderer.domElement); }); // Canvas click request pointer lock this.engine.renderer.domElement.addEventListener('click', () => { if (!this.isPointerLocked && !this.isDead) { this.requestPointerLock(); } }); // Mouse click to shoot window.addEventListener('mousedown', (e) => { if (e.button === 0 && this.isPointerLocked && !this.isDead) { this.keys.shoot = true; if (window.game && window.game.weapons) { window.game.weapons.triggerAttack(); } } }); window.addEventListener('mouseup', (e) => { if (e.button === 0) { this.keys.shoot = false; } }); } requestPointerLock() { try { this.engine.renderer.domElement.requestPointerLock(); if (this.sound) this.sound.resume(); } catch (e) { console.warn('Pointer lock request error:', e); } } exitPointerLock() { try { document.exitPointerLock(); } catch (e) {} } onKeyDown(e) { const code = e.code; if (code === 'KeyW' || code === 'ArrowUp') this.keys.forward = true; if (code === 'KeyS' || code === 'ArrowDown') this.keys.backward = true; if (code === 'KeyA' || code === 'ArrowLeft') this.keys.left = true; if (code === 'KeyD' || code === 'ArrowRight') this.keys.right = true; if (code === 'ShiftLeft' || code === 'ShiftRight') this.keys.sprint = true; if (code === 'Space') { this.keys.jump = true; e.preventDefault(); } if (code === 'KeyE' || code === 'Space') { this.keys.interact = true; this.checkInteractions(); } // Weapon hotkeys (1-7) if (window.game && window.game.weapons) { if (code === 'Digit1') window.game.weapons.selectWeapon(0); // Fists if (code === 'Digit2') window.game.weapons.selectWeapon(1); // Pistol if (code === 'Digit3') window.game.weapons.selectWeapon(2); // Shotgun if (code === 'Digit4') window.game.weapons.selectWeapon(3); // Chaingun if (code === 'Digit5') window.game.weapons.selectWeapon(4); // Rocket Launcher if (code === 'Digit6') window.game.weapons.selectWeapon(5); // Plasma Gun if (code === 'Digit7') window.game.weapons.selectWeapon(6); // BFG 9000 } // Classic DOOM Cheats this.checkCheatCode(e.key); } onKeyUp(e) { const code = e.code; if (code === 'KeyW' || code === 'ArrowUp') this.keys.forward = false; if (code === 'KeyS' || code === 'ArrowDown') this.keys.backward = false; if (code === 'KeyA' || code === 'ArrowLeft') this.keys.left = false; if (code === 'KeyD' || code === 'ArrowRight') this.keys.right = false; if (code === 'ShiftLeft' || code === 'ShiftRight') this.keys.sprint = false; if (code === 'Space') this.keys.jump = false; if (code === 'KeyE') this.keys.interact = false; } checkCheatCode(char) { if (!char || char.length > 1) return; this.cheatBuffer = (this.cheatBuffer + char.toLowerCase()).slice(-8); // IDDQD (God Mode) if (this.cheatBuffer.endsWith('iddqd')) { this.godMode = !this.godMode; this.health = 100; if (window.game && window.game.hud) { window.game.hud.showNotification(this.godMode ? 'DEGNAMELESS INVULNERABILITY ON' : 'DEGNAMELESS INVULNERABILITY OFF'); } if (this.sound) this.sound.playItemPickup('weapon'); } // IDKFA (All Weapons, Full Ammo & Keys) else if (this.cheatBuffer.endsWith('idkfa')) { this.ammo.bullets = this.maxAmmo.bullets; this.ammo.shells = this.maxAmmo.shells; this.ammo.rockets = this.maxAmmo.rockets; this.ammo.cells = this.maxAmmo.cells; this.armor = 200; this.keysInventory.blue = true; this.keysInventory.red = true; this.keysInventory.yellow = true; if (window.game && window.game.weapons) { window.game.weapons.unlocked = [true, true, true, true, true, true, true]; } if (window.game && window.game.hud) { window.game.hud.showNotification('VERY HAPPY AMMO ADDED'); } if (this.sound) this.sound.playItemPickup('weapon'); } // IDCLIP (Noclip) else if (this.cheatBuffer.endsWith('idclip')) { this.noclip = !this.noclip; if (window.game && window.game.hud) { window.game.hud.showNotification(this.noclip ? 'NOCLIP ON' : 'NOCLIP OFF'); } } } checkInteractions() { if (!window.game || !window.game.map) return; window.game.map.interactWithDoors(this.position, this.keysInventory); } // ---------------------------------------------------- // DAMAGE & COMBAT LOGIC // ---------------------------------------------------- takeDamage(amount, sourcePos = null) { if (this.isDead || this.godMode) return; // Armor absorbs 1/3 to 1/2 of damage let dmgToHealth = amount; if (this.armor > 0) { const absorb = Math.min(this.armor, Math.floor(amount * 0.5)); this.armor -= absorb; dmgToHealth = amount - absorb; } this.health = Math.max(0, this.health - dmgToHealth); if (this.sound) { this.sound.playPlayerPain(); } if (window.game && window.game.hud) { window.game.hud.triggerDamageFlash(); } if (this.health <= 0) { this.die(); } } heal(amount, isMega = false) { if (this.isDead) return false; const cap = isMega ? 200 : this.maxHealth; if (this.health >= cap) return false; this.health = Math.min(cap, this.health + amount); return true; } addArmor(amount, isMega = false) { if (this.isDead) return false; const cap = isMega ? 200 : this.maxArmor; if (this.armor >= cap) return false; this.armor = Math.min(cap, this.armor + amount); return true; } addAmmo(type, count) { if (!this.ammo[type]) return false; if (this.ammo[type] >= this.maxAmmo[type]) return false; this.ammo[type] = Math.min(this.maxAmmo[type], this.ammo[type] + count); return true; } die() { this.isDead = true; this.health = 0; if (window.game && window.game.hud) { window.game.hud.showGameOver(); } } respawn(spawnPos) { this.isDead = false; this.health = 100; this.armor = 0; this.ammo.bullets = 50; this.ammo.shells = 0; this.ammo.rockets = 0; this.ammo.cells = 0; this.position.copy(spawnPos || new THREE.Vector3(0, 2, 0)); this.velocity.set(0, 0, 0); this.euler.set(0, 0, 0, 'YXZ'); this.camera.quaternion.setFromEuler(this.euler); } // ---------------------------------------------------- // PHYSICS & COLLISION UPDATE // ---------------------------------------------------- update(delta) { if (this.isDead) { // Death cam sinking to floor if (this.camera.position.y > this.position.y - 1.2) { this.camera.position.y -= delta * 3; } return; } // 1. Calculate horizontal movement direction relative to camera yaw const moveDir = new THREE.Vector3(); if (this.keys.forward) moveDir.z -= 1; if (this.keys.backward) moveDir.z += 1; if (this.keys.left) moveDir.x -= 1; if (this.keys.right) moveDir.x += 1; if (moveDir.lengthSq() > 0) { moveDir.normalize(); // Rotate move direction by camera's horizontal yaw (euler.y) moveDir.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.euler.y); } // Speed const curSpeed = this.keys.sprint ? this.runSpeed : this.walkSpeed; const targetVelX = moveDir.x * curSpeed; const targetVelZ = moveDir.z * curSpeed; // Smooth acceleration/damping this.velocity.x += (targetVelX - this.velocity.x) * Math.min(1, delta * 14); this.velocity.z += (targetVelZ - this.velocity.z) * Math.min(1, delta * 14); // 2. Jump & Gravity if (!this.noclip) { if (this.keys.jump && this.isGrounded) { this.velocity.y = this.jumpForce; this.isGrounded = false; } this.velocity.y -= this.gravity * delta; } else { this.velocity.y = 0; if (this.keys.jump) this.position.y += curSpeed * delta; if (this.keys.sprint) this.position.y -= curSpeed * delta; } // 3. Collision Resolution against Level Geometry if (!this.noclip && window.game && window.game.map) { this.resolveCollisions(delta); } else { this.position.addScaledVector(this.velocity, delta); } // 4. Update Camera position with Head Bobbing const isMoving = (this.keys.forward || this.keys.backward || this.keys.left || this.keys.right) && this.isGrounded; if (isMoving) { const bobSpeed = this.keys.sprint ? 16 : 11; this.bobTimer += delta * bobSpeed; this.bobOffset.x = Math.cos(this.bobTimer * 0.5) * 0.05; this.bobOffset.y = Math.sin(this.bobTimer) * 0.08; } else { this.bobOffset.x *= 0.85; this.bobOffset.y *= 0.85; } this.camera.position.set( this.position.x + this.bobOffset.x, this.position.y + this.height + this.bobOffset.y, this.position.z ); // 5. Slime Pit Hazard Damage if (window.game && window.game.map) { if (window.game.map.isInSlimePit(this.position)) { this.slimeTimer += delta; if (this.slimeTimer >= 0.6) { this.slimeTimer = 0; this.takeDamage(6); if (this.sound) this.sound.playSlimeHurt(); } } else { this.slimeTimer = 0; } } } resolveCollisions(delta) { const colliders = window.game.map.getColliders(); // Horizontal move step X this.position.x += this.velocity.x * delta; this.collideHorizontal(colliders, 'x'); // Horizontal move step Z this.position.z += this.velocity.z * delta; this.collideHorizontal(colliders, 'z'); // Vertical move step Y this.position.y += this.velocity.y * delta; this.collideVertical(colliders); } collideHorizontal(colliders, axis) { const pMinX = this.position.x - this.radius; const pMaxX = this.position.x + this.radius; const pMinZ = this.position.z - this.radius; const pMaxZ = this.position.z + this.radius; const pMinY = this.position.y; const pMaxY = this.position.y + this.height; for (let i = 0; i < colliders.length; i++) { const box = colliders[i]; // Check AABB overlap if (pMaxX > box.min.x && pMinX < box.max.x && pMaxZ > box.min.z && pMinZ < box.max.z && pMaxY > box.min.y && pMinY < box.max.y) { // Check if step-climbing is possible (stairs, curbs, low ledges) const stepUp = box.max.y - pMinY; if (stepUp > 0 && stepUp <= this.stepHeight && (this.isGrounded || this.velocity.y <= 1.0)) { this.position.y = box.max.y + 0.02; this.isGrounded = true; this.velocity.y = 0; continue; } // Push back on axis if (axis === 'x') { if (this.velocity.x > 0) { this.position.x = box.min.x - this.radius; } else if (this.velocity.x < 0) { this.position.x = box.max.x + this.radius; } this.velocity.x = 0; } else if (axis === 'z') { if (this.velocity.z > 0) { this.position.z = box.min.z - this.radius; } else if (this.velocity.z < 0) { this.position.z = box.max.z + this.radius; } this.velocity.z = 0; } } } } collideVertical(colliders) { const pMinX = this.position.x - this.radius; const pMaxX = this.position.x + this.radius; const pMinZ = this.position.z - this.radius; const pMaxZ = this.position.z + this.radius; const pMinY = this.position.y; const pMaxY = this.position.y + this.height; let onGround = false; for (let i = 0; i < colliders.length; i++) { const box = colliders[i]; if (pMaxX > box.min.x && pMinX < box.max.x && pMaxZ > box.min.z && pMinZ < box.max.z && pMaxY > box.min.y && pMinY < box.max.y) { if (this.velocity.y < 0) { // Falling onto platform / floor this.position.y = box.max.y; this.velocity.y = 0; onGround = true; } else if (this.velocity.y > 0) { // Hitting ceiling this.position.y = box.min.y - this.height; this.velocity.y = 0; } } } this.isGrounded = onGround; } } window.DoomPlayer = DoomPlayer;