/** * DOOM 3D - Mobile Touch Controls * Virtual thumbstick for movement, swipe aiming, and action buttons. */ class DoomMobileControls { constructor(player) { this.player = player; this.container = document.getElementById('mobile-controls'); this.joystickBase = document.getElementById('joystick-base'); this.joystickKnob = document.getElementById('joystick-knob'); this.lookZone = document.getElementById('mobile-look-zone'); this.touchMoveId = null; this.touchLookId = null; this.joystickCenter = { x: 0, y: 0 }; this.maxRadius = 50; this.lastLookX = 0; this.lastLookY = 0; this.init(); } init() { const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); if (isTouch && this.container) { this.container.classList.remove('hidden'); } this.setupJoystick(); this.setupLookZone(); this.setupActionButtons(); } setupJoystick() { if (!this.joystickBase || !this.joystickKnob) return; const onTouchStart = (e) => { for (let i = 0; i < e.changedTouches.length; i++) { const touch = e.changedTouches[i]; if (this.touchMoveId === null) { this.touchMoveId = touch.identifier; const rect = this.joystickBase.getBoundingClientRect(); this.joystickCenter.x = rect.left + rect.width / 2; this.joystickCenter.y = rect.top + rect.height / 2; this.handleJoystickMove(touch.clientX, touch.clientY); } } }; const onTouchMove = (e) => { for (let i = 0; i < e.changedTouches.length; i++) { const touch = e.changedTouches[i]; if (touch.identifier === this.touchMoveId) { this.handleJoystickMove(touch.clientX, touch.clientY); } } }; const onTouchEnd = (e) => { for (let i = 0; i < e.changedTouches.length; i++) { const touch = e.changedTouches[i]; if (touch.identifier === this.touchMoveId) { this.touchMoveId = null; this.joystickKnob.style.transform = 'translate(-50%, -50%)'; this.player.keys.forward = false; this.player.keys.backward = false; this.player.keys.left = false; this.player.keys.right = false; } } }; this.joystickBase.addEventListener('touchstart', onTouchStart, { passive: false }); window.addEventListener('touchmove', onTouchMove, { passive: false }); window.addEventListener('touchend', onTouchEnd, { passive: false }); window.addEventListener('touchcancel', onTouchEnd, { passive: false }); } handleJoystickMove(clientX, clientY) { const dx = clientX - this.joystickCenter.x; const dy = clientY - this.joystickCenter.y; const dist = Math.hypot(dx, dy); const clampedDist = Math.min(dist, this.maxRadius); const angle = Math.atan2(dy, dx); const knobX = Math.cos(angle) * clampedDist; const knobY = Math.sin(angle) * clampedDist; this.joystickKnob.style.transform = `translate(calc(-50% + ${knobX}px), calc(-50% + ${knobY}px))`; // Normalize inputs (-1 to 1) const normX = knobX / this.maxRadius; const normY = knobY / this.maxRadius; const threshold = 0.25; this.player.keys.forward = normY < -threshold; this.player.keys.backward = normY > threshold; this.player.keys.left = normX < -threshold; this.player.keys.right = normX > threshold; } setupLookZone() { if (!this.lookZone) return; this.lookZone.addEventListener('touchstart', (e) => { for (let i = 0; i < e.changedTouches.length; i++) { const touch = e.changedTouches[i]; if (this.touchLookId === null) { this.touchLookId = touch.identifier; this.lastLookX = touch.clientX; this.lastLookY = touch.clientY; } } }, { passive: false }); window.addEventListener('touchmove', (e) => { for (let i = 0; i < e.changedTouches.length; i++) { const touch = e.changedTouches[i]; if (touch.identifier === this.touchLookId) { const dx = touch.clientX - this.lastLookX; const dy = touch.clientY - this.lastLookY; this.lastLookX = touch.clientX; this.lastLookY = touch.clientY; // Apply rotation to player camera const sensitivity = this.player.mouseSensitivity * 1.5; this.player.euler.y -= dx * sensitivity; this.player.euler.x -= dy * sensitivity; const maxPitch = Math.PI / 2 - 0.02; this.player.euler.x = Math.max(-maxPitch, Math.min(maxPitch, this.player.euler.x)); this.player.camera.quaternion.setFromEuler(this.player.euler); } } }, { passive: false }); const endLook = (e) => { for (let i = 0; i < e.changedTouches.length; i++) { const touch = e.changedTouches[i]; if (touch.identifier === this.touchLookId) { this.touchLookId = null; } } }; window.addEventListener('touchend', endLook, { passive: false }); window.addEventListener('touchcancel', endLook, { passive: false }); } setupActionButtons() { const btnFire = document.getElementById('btn-mobile-fire'); const btnJump = document.getElementById('btn-mobile-jump'); const btnUse = document.getElementById('btn-mobile-use'); const btnNext = document.getElementById('btn-mobile-next'); const btnPrev = document.getElementById('btn-mobile-prev'); if (btnFire) { btnFire.addEventListener('touchstart', (e) => { e.preventDefault(); this.player.keys.shoot = true; if (window.game && window.game.weapons) { window.game.weapons.triggerAttack(); } }); btnFire.addEventListener('touchend', (e) => { e.preventDefault(); this.player.keys.shoot = false; }); } if (btnJump) { btnJump.addEventListener('touchstart', (e) => { e.preventDefault(); this.player.keys.jump = true; }); btnJump.addEventListener('touchend', (e) => { e.preventDefault(); this.player.keys.jump = false; }); } if (btnUse) { btnUse.addEventListener('touchstart', (e) => { e.preventDefault(); this.player.checkInteractions(); }); } if (btnNext) { btnNext.addEventListener('touchstart', (e) => { e.preventDefault(); if (window.game && window.game.weapons) { window.game.weapons.nextWeapon(); } }); } if (btnPrev) { btnPrev.addEventListener('touchstart', (e) => { e.preventDefault(); if (window.game && window.game.weapons) { window.game.weapons.prevWeapon(); } }); } } } window.DoomMobileControls = DoomMobileControls;