From f1ada7d17ff839eaa32f9a652950d0349cd3f870 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:24:48 +0000 Subject: [PATCH 01/11] Initial plan From df90b0603f33c1d47eaf4310ff476bd3d93d851f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:28:51 +0000 Subject: [PATCH 02/11] Add mobile touch controls and responsive canvas Co-authored-by: Linkatplug <36280686+Linkatplug@users.noreply.github.com> --- index.html | 192 ++++++++++++++++++++++++++- js/Game.js | 5 +- js/systems/MovementSystem.js | 20 ++- js/utils/TouchControls.js | 247 +++++++++++++++++++++++++++++++++++ 4 files changed, 456 insertions(+), 8 deletions(-) create mode 100644 js/utils/TouchControls.js diff --git a/index.html b/index.html index ac5f832..613ba74 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Space InZader - Roguelite Space Shooter @@ -1357,6 +1533,19 @@

🎮 CONTRÔLES

🔫 Tir automatique sur l'ennemi le plus proche

✨ Collectez les orbes verts pour gagner XP

+ + +
+
+
+
+
+ FIRE +
+
+ ⏸ +
+
@@ -1367,6 +1556,7 @@

🎮 CONTRÔLES

+ diff --git a/js/Game.js b/js/Game.js index befe657..b7bbcfb 100644 --- a/js/Game.js +++ b/js/Game.js @@ -76,6 +76,9 @@ class Game { this.audioManager = new AudioManager(); this.scoreManager = new ScoreManager(); + // Touch controls for mobile + this.touchControls = typeof TouchControls !== 'undefined' ? new TouchControls(this.canvas) : null; + // Debug system this.debugOverlay = null; @@ -87,7 +90,7 @@ class Game { // Game systems this.systems = { - movement: new MovementSystem(this.world, this.canvas), + movement: new MovementSystem(this.world, this.canvas, this.touchControls), particle: new ParticleSystem(this.world), collision: new CollisionSystem(this.world, this.gameState, this.audioManager, null), combat: new CombatSystem(this.world, this.gameState, this.audioManager), diff --git a/js/systems/MovementSystem.js b/js/systems/MovementSystem.js index a779397..3c6f3cc 100644 --- a/js/systems/MovementSystem.js +++ b/js/systems/MovementSystem.js @@ -4,10 +4,11 @@ */ class MovementSystem { - constructor(world, canvas) { + constructor(world, canvas, touchControls = null) { this.world = world; this.canvas = canvas; this.keys = {}; + this.touchControls = touchControls; this.setupInputHandlers(); } @@ -95,11 +96,18 @@ class MovementSystem { let dx = 0; let dy = 0; - // WASD or ZQSD movement - if (this.keys['w'] || this.keys['z']) dy -= 1; - if (this.keys['s']) dy += 1; - if (this.keys['a'] || this.keys['q']) dx -= 1; - if (this.keys['d']) dx += 1; + // Check for touch input first (mobile) + if (this.touchControls && this.touchControls.isEnabled()) { + const touchDir = this.touchControls.getDirection(); + dx = touchDir.x; + dy = touchDir.y; + } else { + // WASD or ZQSD movement (keyboard) + if (this.keys['w'] || this.keys['z']) dy -= 1; + if (this.keys['s']) dy += 1; + if (this.keys['a'] || this.keys['q']) dx -= 1; + if (this.keys['d']) dx += 1; + } // Check if player is actively moving const hasInput = (dx !== 0 || dy !== 0); diff --git a/js/utils/TouchControls.js b/js/utils/TouchControls.js new file mode 100644 index 0000000..c50d3d6 --- /dev/null +++ b/js/utils/TouchControls.js @@ -0,0 +1,247 @@ +/** + * @file TouchControls.js + * @description Handles touch input for mobile devices + */ + +class TouchControls { + constructor(canvas) { + this.canvas = canvas; + this.enabled = false; + this.joystickActive = false; + this.joystickCenter = { x: 0, y: 0 }; + this.joystickPosition = { x: 0, y: 0 }; + this.joystickDirection = { x: 0, y: 0 }; + this.fireButtonPressed = false; + + // Touch identifiers + this.joystickTouchId = null; + this.fireButtonTouchId = null; + + // Elements + this.touchControls = document.getElementById('touchControls'); + this.joystick = document.getElementById('touchJoystick'); + this.joystickInner = document.getElementById('touchJoystickInner'); + this.fireButton = document.getElementById('touchFireButton'); + this.pauseButton = document.getElementById('touchPauseButton'); + + // Auto-detect mobile + this.isMobile = this.detectMobile(); + + if (this.isMobile) { + this.enable(); + } + + this.setupTouchHandlers(); + this.setupCanvasResize(); + } + + detectMobile() { + // Check if device is mobile or tablet + const userAgent = navigator.userAgent.toLowerCase(); + const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); + const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + const isSmallScreen = window.innerWidth <= 768; + + return (isMobileUA || isTouchDevice) && isSmallScreen; + } + + setupCanvasResize() { + // Make canvas responsive + const resizeCanvas = () => { + if (this.isMobile) { + const container = document.getElementById('gameContainer'); + const canvas = this.canvas; + const menuCanvas = document.getElementById('menuStarfield'); + + // Set canvas to fill viewport while maintaining aspect ratio + const aspectRatio = 16 / 9; // Original 1280x720 + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + const windowAspect = windowWidth / windowHeight; + + let newWidth, newHeight; + + if (windowAspect > aspectRatio) { + // Window is wider than canvas aspect ratio + newHeight = windowHeight; + newWidth = newHeight * aspectRatio; + } else { + // Window is taller than canvas aspect ratio + newWidth = windowWidth; + newHeight = newWidth / aspectRatio; + } + + // Apply size to container + container.style.width = `${newWidth}px`; + container.style.height = `${newHeight}px`; + + // Canvas maintains internal resolution but scales visually + canvas.style.width = `${newWidth}px`; + canvas.style.height = `${newHeight}px`; + + if (menuCanvas) { + menuCanvas.style.width = `${newWidth}px`; + menuCanvas.style.height = `${newHeight}px`; + } + } + }; + + window.addEventListener('resize', resizeCanvas); + window.addEventListener('orientationchange', () => { + setTimeout(resizeCanvas, 100); + }); + + // Initial resize + resizeCanvas(); + } + + setupTouchHandlers() { + if (!this.joystick || !this.fireButton) return; + + // Joystick touch handlers + this.joystick.addEventListener('touchstart', (e) => { + e.preventDefault(); + if (this.joystickTouchId === null) { + const touch = e.changedTouches[0]; + this.joystickTouchId = touch.identifier; + this.handleJoystickStart(touch); + } + }, { passive: false }); + + this.joystick.addEventListener('touchmove', (e) => { + e.preventDefault(); + for (let touch of e.changedTouches) { + if (touch.identifier === this.joystickTouchId) { + this.handleJoystickMove(touch); + break; + } + } + }, { passive: false }); + + this.joystick.addEventListener('touchend', (e) => { + e.preventDefault(); + for (let touch of e.changedTouches) { + if (touch.identifier === this.joystickTouchId) { + this.handleJoystickEnd(); + break; + } + } + }, { passive: false }); + + // Fire button touch handlers + this.fireButton.addEventListener('touchstart', (e) => { + e.preventDefault(); + if (this.fireButtonTouchId === null) { + const touch = e.changedTouches[0]; + this.fireButtonTouchId = touch.identifier; + this.fireButtonPressed = true; + } + }, { passive: false }); + + this.fireButton.addEventListener('touchend', (e) => { + e.preventDefault(); + for (let touch of e.changedTouches) { + if (touch.identifier === this.fireButtonTouchId) { + this.fireButtonPressed = false; + this.fireButtonTouchId = null; + break; + } + } + }, { passive: false }); + + // Pause button handler + this.pauseButton.addEventListener('touchstart', (e) => { + e.preventDefault(); + // Trigger ESC key event for pause + const escEvent = new KeyboardEvent('keydown', { + key: 'Escape', + code: 'Escape', + keyCode: 27 + }); + window.dispatchEvent(escEvent); + }, { passive: false }); + } + + handleJoystickStart(touch) { + this.joystickActive = true; + const rect = this.joystick.getBoundingClientRect(); + this.joystickCenter = { + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2 + }; + this.handleJoystickMove(touch); + } + + handleJoystickMove(touch) { + if (!this.joystickActive) return; + + const rect = this.joystick.getBoundingClientRect(); + const maxDistance = rect.width / 2 - 30; // Account for inner circle size + + // Calculate relative position + const dx = touch.clientX - this.joystickCenter.x; + const dy = touch.clientY - this.joystickCenter.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Clamp to max distance + const clampedDistance = Math.min(distance, maxDistance); + const angle = Math.atan2(dy, dx); + + // Update joystick position + this.joystickPosition.x = Math.cos(angle) * clampedDistance; + this.joystickPosition.y = Math.sin(angle) * clampedDistance; + + // Calculate normalized direction + if (distance > 5) { // Dead zone + this.joystickDirection.x = dx / distance; + this.joystickDirection.y = dy / distance; + } else { + this.joystickDirection.x = 0; + this.joystickDirection.y = 0; + } + + // Update visual position + this.joystickInner.style.transform = `translate(calc(-50% + ${this.joystickPosition.x}px), calc(-50% + ${this.joystickPosition.y}px))`; + } + + handleJoystickEnd() { + this.joystickActive = false; + this.joystickTouchId = null; + this.joystickDirection.x = 0; + this.joystickDirection.y = 0; + this.joystickPosition.x = 0; + this.joystickPosition.y = 0; + + // Reset visual position + this.joystickInner.style.transform = 'translate(-50%, -50%)'; + } + + enable() { + this.enabled = true; + if (this.touchControls) { + this.touchControls.classList.add('active'); + } + } + + disable() { + this.enabled = false; + if (this.touchControls) { + this.touchControls.classList.remove('active'); + } + } + + getDirection() { + return { + x: this.joystickDirection.x, + y: this.joystickDirection.y + }; + } + + isFirePressed() { + return this.fireButtonPressed; + } + + isEnabled() { + return this.enabled; + } +} From 8f53d915852f5c3ca07b10051800694d682f3b28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:31:08 +0000 Subject: [PATCH 03/11] Improve mobile detection and simplify touch controls Co-authored-by: Linkatplug <36280686+Linkatplug@users.noreply.github.com> --- index.html | 23 +---------------------- js/utils/TouchControls.js | 3 ++- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/index.html b/index.html index 613ba74..11e0760 100644 --- a/index.html +++ b/index.html @@ -1154,28 +1154,7 @@ } .touch-fire-button { - position: absolute; - bottom: 80px; - right: 80px; - width: 120px; - height: 120px; - background: rgba(255, 0, 0, 0.3); - border: 3px solid rgba(255, 0, 0, 0.5); - border-radius: 50%; - pointer-events: all; - touch-action: none; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - font-weight: bold; - color: #ff0000; - text-shadow: 0 0 10px #ff0000; - } - - .touch-fire-button:active { - background: rgba(255, 0, 0, 0.5); - border-color: #ff0000; + display: none; /* Hidden - game has auto-fire */ } .touch-pause-button { diff --git a/js/utils/TouchControls.js b/js/utils/TouchControls.js index c50d3d6..058b293 100644 --- a/js/utils/TouchControls.js +++ b/js/utils/TouchControls.js @@ -42,7 +42,8 @@ class TouchControls { const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isSmallScreen = window.innerWidth <= 768; - return (isMobileUA || isTouchDevice) && isSmallScreen; + // Enable on small screens regardless of device type (for mobile browsers) + return isSmallScreen || (isMobileUA || isTouchDevice); } setupCanvasResize() { From 73e44d2fefb40d4fe04869bd7e8474aab3771a59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:32:29 +0000 Subject: [PATCH 04/11] Add dynamic resize detection for touch controls Co-authored-by: Linkatplug <36280686+Linkatplug@users.noreply.github.com> --- js/utils/TouchControls.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/js/utils/TouchControls.js b/js/utils/TouchControls.js index 058b293..4c3a72a 100644 --- a/js/utils/TouchControls.js +++ b/js/utils/TouchControls.js @@ -49,7 +49,18 @@ class TouchControls { setupCanvasResize() { // Make canvas responsive const resizeCanvas = () => { - if (this.isMobile) { + // Re-detect if we should be in mobile mode based on screen size + const shouldBeMobile = window.innerWidth <= 768; + + if (shouldBeMobile !== this.enabled) { + if (shouldBeMobile) { + this.enable(); + } else { + this.disable(); + } + } + + if (shouldBeMobile) { const container = document.getElementById('gameContainer'); const canvas = this.canvas; const menuCanvas = document.getElementById('menuStarfield'); @@ -84,6 +95,21 @@ class TouchControls { menuCanvas.style.width = `${newWidth}px`; menuCanvas.style.height = `${newHeight}px`; } + } else { + // Reset to default desktop size + const container = document.getElementById('gameContainer'); + const canvas = this.canvas; + const menuCanvas = document.getElementById('menuStarfield'); + + container.style.width = ''; + container.style.height = ''; + canvas.style.width = ''; + canvas.style.height = ''; + + if (menuCanvas) { + menuCanvas.style.width = ''; + menuCanvas.style.height = ''; + } } }; From 44451ae694c78e20b5adb95e78ea4863e3ad2568 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:34:27 +0000 Subject: [PATCH 05/11] Refactor magic numbers to constants in TouchControls Co-authored-by: Linkatplug <36280686+Linkatplug@users.noreply.github.com> --- js/utils/TouchControls.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/js/utils/TouchControls.js b/js/utils/TouchControls.js index 4c3a72a..3b35cbe 100644 --- a/js/utils/TouchControls.js +++ b/js/utils/TouchControls.js @@ -13,6 +13,10 @@ class TouchControls { this.joystickDirection = { x: 0, y: 0 }; this.fireButtonPressed = false; + // Joystick configuration constants + this.JOYSTICK_INNER_RADIUS = 30; // Half of inner circle size (60px / 2) + this.JOYSTICK_DEAD_ZONE = 5; // Minimum distance to register movement + // Touch identifiers this.joystickTouchId = null; this.fireButtonTouchId = null; @@ -203,7 +207,7 @@ class TouchControls { if (!this.joystickActive) return; const rect = this.joystick.getBoundingClientRect(); - const maxDistance = rect.width / 2 - 30; // Account for inner circle size + const maxDistance = rect.width / 2 - this.JOYSTICK_INNER_RADIUS; // Calculate relative position const dx = touch.clientX - this.joystickCenter.x; @@ -218,8 +222,8 @@ class TouchControls { this.joystickPosition.x = Math.cos(angle) * clampedDistance; this.joystickPosition.y = Math.sin(angle) * clampedDistance; - // Calculate normalized direction - if (distance > 5) { // Dead zone + // Calculate normalized direction with dead zone + if (distance > this.JOYSTICK_DEAD_ZONE) { this.joystickDirection.x = dx / distance; this.joystickDirection.y = dy / distance; } else { From 5f82f22f5fa2c0df4c48869953516b4c970cf0c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:27:25 +0000 Subject: [PATCH 06/11] Fix landscape 16:9 touch controls and fullscreen support Co-authored-by: Linkatplug <36280686+Linkatplug@users.noreply.github.com> --- index.html | 66 +++++++++++++++++++++++++++++++++++++++ js/utils/TouchControls.js | 56 +++++++++++++++++---------------- 2 files changed, 95 insertions(+), 27 deletions(-) diff --git a/index.html b/index.html index 11e0760..dd1bdad 100644 --- a/index.html +++ b/index.html @@ -1266,6 +1266,72 @@ right: 30px; } } + + /* Landscape Mode Optimization for Mobile */ + @media (orientation: landscape) and (max-height: 768px) { + body { + overflow: hidden; + position: fixed; + width: 100%; + height: 100%; + } + + #gameContainer { + width: 100% !important; + height: 100% !important; + box-shadow: none; + } + + #gameCanvas { + width: 100% !important; + height: 100% !important; + border: none; + } + + .touch-joystick { + bottom: 20px; + left: 20px; + width: 120px; + height: 120px; + } + + .touch-joystick-inner { + width: 48px; + height: 48px; + } + + .touch-pause-button { + top: 10px; + right: 10px; + width: 48px; + height: 48px; + } + + .ship-selection { + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin: 10px; + max-width: 95%; + max-height: 80vh; + overflow-y: auto; + } + + .ship-card { + padding: 10px; + font-size: 12px; + } + + .title { + font-size: 24px; + margin-bottom: 15px; + } + + .button { + padding: 8px 20px; + font-size: 12px; + margin: 5px; + } + } diff --git a/js/utils/TouchControls.js b/js/utils/TouchControls.js index 3b35cbe..aa4fec7 100644 --- a/js/utils/TouchControls.js +++ b/js/utils/TouchControls.js @@ -45,16 +45,32 @@ class TouchControls { const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isSmallScreen = window.innerWidth <= 768; + const smallerDimension = Math.min(window.innerWidth, window.innerHeight); - // Enable on small screens regardless of device type (for mobile browsers) - return isSmallScreen || (isMobileUA || isTouchDevice); + // Enable on small screens OR mobile devices (both portrait and landscape) + // In landscape, smaller dimension (height) determines if it's a mobile device + return isSmallScreen || isMobileUA || isTouchDevice || smallerDimension <= 768; } setupCanvasResize() { // Make canvas responsive const resizeCanvas = () => { - // Re-detect if we should be in mobile mode based on screen size - const shouldBeMobile = window.innerWidth <= 768; + // Improved mobile detection for both portrait and landscape + // Check screen dimensions to determine if we're on a mobile device + const smallerDimension = Math.min(window.innerWidth, window.innerHeight); + const largerDimension = Math.max(window.innerWidth, window.innerHeight); + const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + const userAgent = navigator.userAgent.toLowerCase(); + const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); + + // Enable on mobile-sized screens: + // - Smaller dimension <= 768px AND larger dimension < 1024px (mobile in any orientation) + // - OR mobile UA with reasonable dimensions + // - OR touch device with clearly mobile dimensions + const shouldBeMobile = + (smallerDimension <= 768 && largerDimension < 1024) || // Mobile size check + (isMobileUA && largerDimension <= 1366) || // Mobile UA with reasonable size + (isTouchDevice && smallerDimension <= 600); // Small touch device if (shouldBeMobile !== this.enabled) { if (shouldBeMobile) { @@ -69,35 +85,21 @@ class TouchControls { const canvas = this.canvas; const menuCanvas = document.getElementById('menuStarfield'); - // Set canvas to fill viewport while maintaining aspect ratio - const aspectRatio = 16 / 9; // Original 1280x720 + // Make canvas fill entire viewport for full screen experience const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; - const windowAspect = windowWidth / windowHeight; - - let newWidth, newHeight; - - if (windowAspect > aspectRatio) { - // Window is wider than canvas aspect ratio - newHeight = windowHeight; - newWidth = newHeight * aspectRatio; - } else { - // Window is taller than canvas aspect ratio - newWidth = windowWidth; - newHeight = newWidth / aspectRatio; - } - // Apply size to container - container.style.width = `${newWidth}px`; - container.style.height = `${newHeight}px`; + // Apply full viewport size to container + container.style.width = `${windowWidth}px`; + container.style.height = `${windowHeight}px`; - // Canvas maintains internal resolution but scales visually - canvas.style.width = `${newWidth}px`; - canvas.style.height = `${newHeight}px`; + // Canvas scales to fill container + canvas.style.width = `${windowWidth}px`; + canvas.style.height = `${windowHeight}px`; if (menuCanvas) { - menuCanvas.style.width = `${newWidth}px`; - menuCanvas.style.height = `${newHeight}px`; + menuCanvas.style.width = `${windowWidth}px`; + menuCanvas.style.height = `${windowHeight}px`; } } else { // Reset to default desktop size From b69b3d541d901e21010873571e5d931af79a4a33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:31:27 +0000 Subject: [PATCH 07/11] Refactor magic numbers to named constants in mobile detection Co-authored-by: Linkatplug <36280686+Linkatplug@users.noreply.github.com> --- js/utils/TouchControls.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/js/utils/TouchControls.js b/js/utils/TouchControls.js index aa4fec7..ab9d19e 100644 --- a/js/utils/TouchControls.js +++ b/js/utils/TouchControls.js @@ -17,6 +17,12 @@ class TouchControls { this.JOYSTICK_INNER_RADIUS = 30; // Half of inner circle size (60px / 2) this.JOYSTICK_DEAD_ZONE = 5; // Minimum distance to register movement + // Mobile detection thresholds + this.MOBILE_SMALLER_DIMENSION_THRESHOLD = 768; // Max height/width for mobile + this.MOBILE_LARGER_DIMENSION_THRESHOLD = 1024; // Max other dimension for mobile + this.TABLET_DIMENSION_THRESHOLD = 1366; // Max dimension for tablets + this.SMALL_TOUCH_DEVICE_THRESHOLD = 600; // Clearly mobile touch devices + // Touch identifiers this.joystickTouchId = null; this.fireButtonTouchId = null; @@ -44,12 +50,11 @@ class TouchControls { const userAgent = navigator.userAgent.toLowerCase(); const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; - const isSmallScreen = window.innerWidth <= 768; const smallerDimension = Math.min(window.innerWidth, window.innerHeight); // Enable on small screens OR mobile devices (both portrait and landscape) // In landscape, smaller dimension (height) determines if it's a mobile device - return isSmallScreen || isMobileUA || isTouchDevice || smallerDimension <= 768; + return smallerDimension <= this.MOBILE_SMALLER_DIMENSION_THRESHOLD || isMobileUA || isTouchDevice; } setupCanvasResize() { @@ -64,13 +69,14 @@ class TouchControls { const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); // Enable on mobile-sized screens: - // - Smaller dimension <= 768px AND larger dimension < 1024px (mobile in any orientation) + // - Smaller dimension <= threshold AND larger dimension < threshold (mobile in any orientation) // - OR mobile UA with reasonable dimensions // - OR touch device with clearly mobile dimensions const shouldBeMobile = - (smallerDimension <= 768 && largerDimension < 1024) || // Mobile size check - (isMobileUA && largerDimension <= 1366) || // Mobile UA with reasonable size - (isTouchDevice && smallerDimension <= 600); // Small touch device + (smallerDimension <= this.MOBILE_SMALLER_DIMENSION_THRESHOLD && + largerDimension < this.MOBILE_LARGER_DIMENSION_THRESHOLD) || + (isMobileUA && largerDimension <= this.TABLET_DIMENSION_THRESHOLD) || + (isTouchDevice && smallerDimension <= this.SMALL_TOUCH_DEVICE_THRESHOLD); if (shouldBeMobile !== this.enabled) { if (shouldBeMobile) { From be8773582ac8de16b68d3d002990ada06c468f93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:11:51 +0000 Subject: [PATCH 08/11] Implement full-screen joystick and fullscreen button Co-authored-by: Linkatplug <36280686+Linkatplug@users.noreply.github.com> --- index.html | 96 ++++++++++++---------- js/utils/TouchControls.js | 167 +++++++++++++++++++++++++++++--------- 2 files changed, 179 insertions(+), 84 deletions(-) diff --git a/index.html b/index.html index dd1bdad..87a32b6 100644 --- a/index.html +++ b/index.html @@ -1128,16 +1128,17 @@ } .touch-joystick { - position: absolute; - bottom: 80px; - left: 80px; + position: fixed; width: 150px; height: 150px; - background: rgba(0, 255, 255, 0.1); - border: 3px solid rgba(0, 255, 255, 0.3); + background: rgba(0, 255, 255, 0.15); + border: 3px solid rgba(0, 255, 255, 0.4); border-radius: 50%; - pointer-events: all; + pointer-events: none; touch-action: none; + transition: opacity 0.2s; + opacity: 0; + z-index: 100; } .touch-joystick-inner { @@ -1146,11 +1147,11 @@ left: 50%; width: 60px; height: 60px; - background: rgba(0, 255, 255, 0.5); + background: rgba(0, 255, 255, 0.6); border: 2px solid #00ffff; border-radius: 50%; transform: translate(-50%, -50%); - transition: all 0.1s; + transition: none; } .touch-fire-button { @@ -1174,12 +1175,37 @@ font-size: 12px; font-weight: bold; color: #00ffff; + z-index: 1000; } .touch-pause-button:active { background: rgba(0, 255, 255, 0.5); } + .touch-fullscreen-button { + position: absolute; + top: 90px; + right: 20px; + width: 60px; + height: 60px; + background: rgba(0, 255, 255, 0.3); + border: 2px solid rgba(0, 255, 255, 0.5); + border-radius: 10px; + pointer-events: all; + touch-action: none; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + font-weight: bold; + color: #00ffff; + z-index: 1000; + } + + .touch-fullscreen-button:active { + background: rgba(0, 255, 255, 0.5); + } + /* Mobile Responsive Styles */ @media (max-width: 768px) { body { @@ -1223,30 +1249,20 @@ padding: 15px; } - .touch-joystick { - bottom: 40px; - left: 40px; - width: 130px; - height: 130px; - } - - .touch-joystick-inner { + .touch-pause-button { + top: 10px; + right: 10px; width: 50px; height: 50px; + font-size: 11px; } - .touch-fire-button { - bottom: 40px; - right: 40px; - width: 100px; - height: 100px; - } - - .touch-pause-button { - top: 10px; + .touch-fullscreen-button { + top: 70px; right: 10px; width: 50px; height: 50px; + font-size: 18px; } } @@ -1255,16 +1271,6 @@ .ship-selection { grid-template-columns: 1fr; } - - .touch-joystick { - bottom: 30px; - left: 30px; - } - - .touch-fire-button { - bottom: 30px; - right: 30px; - } } /* Landscape Mode Optimization for Mobile */ @@ -1288,23 +1294,20 @@ border: none; } - .touch-joystick { - bottom: 20px; - left: 20px; - width: 120px; - height: 120px; - } - - .touch-joystick-inner { + .touch-pause-button { + top: 10px; + right: 10px; width: 48px; height: 48px; + font-size: 11px; } - .touch-pause-button { - top: 10px; + .touch-fullscreen-button { + top: 65px; right: 10px; width: 48px; height: 48px; + font-size: 16px; } .ship-selection { @@ -1590,6 +1593,9 @@

🎮 CONTRÔLES

⏸
+
+ â›¶ +
diff --git a/js/utils/TouchControls.js b/js/utils/TouchControls.js index ab9d19e..b04afb6 100644 --- a/js/utils/TouchControls.js +++ b/js/utils/TouchControls.js @@ -33,6 +33,7 @@ class TouchControls { this.joystickInner = document.getElementById('touchJoystickInner'); this.fireButton = document.getElementById('touchFireButton'); this.pauseButton = document.getElementById('touchPauseButton'); + this.fullscreenButton = document.getElementById('touchFullscreenButton'); // Auto-detect mobile this.isMobile = this.detectMobile(); @@ -42,6 +43,7 @@ class TouchControls { } this.setupTouchHandlers(); + this.setupFullscreenHandler(); this.setupCanvasResize(); } @@ -135,19 +137,33 @@ class TouchControls { } setupTouchHandlers() { - if (!this.joystick || !this.fireButton) return; + if (!this.canvas) return; - // Joystick touch handlers - this.joystick.addEventListener('touchstart', (e) => { + // Use the entire game container as joystick area + const gameContainer = document.getElementById('gameContainer'); + if (!gameContainer) return; + + // Full-screen joystick handlers on the game container + gameContainer.addEventListener('touchstart', (e) => { + // Check if touch is on pause button or other UI elements + const target = e.target; + if (target.closest('.touch-pause-button') || + target.closest('.touch-fullscreen-button') || + target.closest('.menu-screen') || + target.closest('.level-up-screen') || + target.closest('.game-over-screen')) { + return; // Let UI elements handle their own touch + } + e.preventDefault(); - if (this.joystickTouchId === null) { + if (this.joystickTouchId === null && e.changedTouches.length > 0) { const touch = e.changedTouches[0]; this.joystickTouchId = touch.identifier; this.handleJoystickStart(touch); } }, { passive: false }); - this.joystick.addEventListener('touchmove', (e) => { + gameContainer.addEventListener('touchmove', (e) => { e.preventDefault(); for (let touch of e.changedTouches) { if (touch.identifier === this.joystickTouchId) { @@ -157,7 +173,7 @@ class TouchControls { } }, { passive: false }); - this.joystick.addEventListener('touchend', (e) => { + gameContainer.addEventListener('touchend', (e) => { e.preventDefault(); for (let touch of e.changedTouches) { if (touch.identifier === this.joystickTouchId) { @@ -167,57 +183,121 @@ class TouchControls { } }, { passive: false }); - // Fire button touch handlers - this.fireButton.addEventListener('touchstart', (e) => { - e.preventDefault(); - if (this.fireButtonTouchId === null) { - const touch = e.changedTouches[0]; - this.fireButtonTouchId = touch.identifier; - this.fireButtonPressed = true; - } - }, { passive: false }); - - this.fireButton.addEventListener('touchend', (e) => { - e.preventDefault(); + gameContainer.addEventListener('touchcancel', (e) => { for (let touch of e.changedTouches) { - if (touch.identifier === this.fireButtonTouchId) { - this.fireButtonPressed = false; - this.fireButtonTouchId = null; + if (touch.identifier === this.joystickTouchId) { + this.handleJoystickEnd(); break; } } }, { passive: false }); // Pause button handler - this.pauseButton.addEventListener('touchstart', (e) => { + if (this.pauseButton) { + this.pauseButton.addEventListener('touchstart', (e) => { + e.preventDefault(); + e.stopPropagation(); + // Trigger ESC key event for pause + const escEvent = new KeyboardEvent('keydown', { + key: 'Escape', + code: 'Escape', + keyCode: 27 + }); + window.dispatchEvent(escEvent); + }, { passive: false }); + } + } + + setupFullscreenHandler() { + if (!this.fullscreenButton) return; + + this.fullscreenButton.addEventListener('touchstart', (e) => { e.preventDefault(); - // Trigger ESC key event for pause - const escEvent = new KeyboardEvent('keydown', { - key: 'Escape', - code: 'Escape', - keyCode: 27 - }); - window.dispatchEvent(escEvent); + e.stopPropagation(); + this.toggleFullscreen(); }, { passive: false }); + + this.fullscreenButton.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.toggleFullscreen(); + }); + + // Update button icon when fullscreen state changes + document.addEventListener('fullscreenchange', () => this.updateFullscreenButton()); + document.addEventListener('webkitfullscreenchange', () => this.updateFullscreenButton()); + document.addEventListener('mozfullscreenchange', () => this.updateFullscreenButton()); + document.addEventListener('MSFullscreenChange', () => this.updateFullscreenButton()); + } + + toggleFullscreen() { + const elem = document.documentElement; + + if (!document.fullscreenElement && + !document.webkitFullscreenElement && + !document.mozFullScreenElement && + !document.msFullscreenElement) { + // Enter fullscreen + if (elem.requestFullscreen) { + elem.requestFullscreen(); + } else if (elem.webkitRequestFullscreen) { + elem.webkitRequestFullscreen(); + } else if (elem.mozRequestFullScreen) { + elem.mozRequestFullScreen(); + } else if (elem.msRequestFullscreen) { + elem.msRequestFullscreen(); + } + } else { + // Exit fullscreen + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } + } + + updateFullscreenButton() { + if (!this.fullscreenButton) return; + + const isFullscreen = document.fullscreenElement || + document.webkitFullscreenElement || + document.mozFullScreenElement || + document.msFullscreenElement; + + // Update button icon: ⛶ for enter fullscreen, ⊗ for exit fullscreen + this.fullscreenButton.textContent = isFullscreen ? '⊗' : '⛶'; } handleJoystickStart(touch) { this.joystickActive = true; - const rect = this.joystick.getBoundingClientRect(); + // Set joystick center at the touch position this.joystickCenter = { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2 + x: touch.clientX, + y: touch.clientY }; + + // Position the joystick visual at the touch point + if (this.joystick) { + this.joystick.style.left = `${touch.clientX}px`; + this.joystick.style.top = `${touch.clientY}px`; + this.joystick.style.transform = 'translate(-50%, -50%)'; + this.joystick.style.opacity = '1'; + } + this.handleJoystickMove(touch); } handleJoystickMove(touch) { if (!this.joystickActive) return; - const rect = this.joystick.getBoundingClientRect(); - const maxDistance = rect.width / 2 - this.JOYSTICK_INNER_RADIUS; + const maxDistance = 75; // Maximum distance from center for full input - // Calculate relative position + // Calculate relative position from joystick center const dx = touch.clientX - this.joystickCenter.x; const dy = touch.clientY - this.joystickCenter.y; const distance = Math.sqrt(dx * dx + dy * dy); @@ -226,7 +306,7 @@ class TouchControls { const clampedDistance = Math.min(distance, maxDistance); const angle = Math.atan2(dy, dx); - // Update joystick position + // Update joystick position for visual feedback this.joystickPosition.x = Math.cos(angle) * clampedDistance; this.joystickPosition.y = Math.sin(angle) * clampedDistance; @@ -239,8 +319,10 @@ class TouchControls { this.joystickDirection.y = 0; } - // Update visual position - this.joystickInner.style.transform = `translate(calc(-50% + ${this.joystickPosition.x}px), calc(-50% + ${this.joystickPosition.y}px))`; + // Update visual position of inner joystick + if (this.joystickInner) { + this.joystickInner.style.transform = `translate(calc(-50% + ${this.joystickPosition.x}px), calc(-50% + ${this.joystickPosition.y}px))`; + } } handleJoystickEnd() { @@ -251,8 +333,15 @@ class TouchControls { this.joystickPosition.x = 0; this.joystickPosition.y = 0; + // Fade out the joystick visual + if (this.joystick) { + this.joystick.style.opacity = '0'; + } + // Reset visual position - this.joystickInner.style.transform = 'translate(-50%, -50%)'; + if (this.joystickInner) { + this.joystickInner.style.transform = 'translate(-50%, -50%)'; + } } enable() { From bafcf471c7ef8467c4de7b5df81dde4f5ebf4a49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:13:34 +0000 Subject: [PATCH 09/11] Address code review feedback - fix event casing, add constant, improve comments Co-authored-by: Linkatplug <36280686+Linkatplug@users.noreply.github.com> --- js/utils/TouchControls.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/js/utils/TouchControls.js b/js/utils/TouchControls.js index b04afb6..8ddfa5c 100644 --- a/js/utils/TouchControls.js +++ b/js/utils/TouchControls.js @@ -16,6 +16,7 @@ class TouchControls { // Joystick configuration constants this.JOYSTICK_INNER_RADIUS = 30; // Half of inner circle size (60px / 2) this.JOYSTICK_DEAD_ZONE = 5; // Minimum distance to register movement + this.JOYSTICK_MAX_DISTANCE = 75; // Maximum distance from center for full input // Mobile detection thresholds this.MOBILE_SMALLER_DIMENSION_THRESHOLD = 768; // Max height/width for mobile @@ -227,7 +228,7 @@ class TouchControls { document.addEventListener('fullscreenchange', () => this.updateFullscreenButton()); document.addEventListener('webkitfullscreenchange', () => this.updateFullscreenButton()); document.addEventListener('mozfullscreenchange', () => this.updateFullscreenButton()); - document.addEventListener('MSFullscreenChange', () => this.updateFullscreenButton()); + document.addEventListener('msfullscreenchange', () => this.updateFullscreenButton()); } toggleFullscreen() { @@ -269,7 +270,9 @@ class TouchControls { document.mozFullScreenElement || document.msFullscreenElement; - // Update button icon: ⛶ for enter fullscreen, ⊗ for exit fullscreen + // Update button icon: + // ⛶ (U+26F6) = "Square Four Corners" - indicates expand to fullscreen + // ⊗ (U+2297) = "Circled Times" - indicates close/exit fullscreen this.fullscreenButton.textContent = isFullscreen ? '⊗' : '⛶'; } @@ -295,7 +298,7 @@ class TouchControls { handleJoystickMove(touch) { if (!this.joystickActive) return; - const maxDistance = 75; // Maximum distance from center for full input + const maxDistance = this.JOYSTICK_MAX_DISTANCE; // Calculate relative position from joystick center const dx = touch.clientX - this.joystickCenter.x; From 733c78047405a32206f66fe8f0bbda4d5a57b6ad Mon Sep 17 00:00:00 2001 From: LinkAtPlug <36280686+Linkatplug@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:25:48 +0100 Subject: [PATCH 10/11] Fix mobile menu taps blocked by touch joystick handler --- js/utils/TouchControls.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/js/utils/TouchControls.js b/js/utils/TouchControls.js index 8ddfa5c..0ec0579 100644 --- a/js/utils/TouchControls.js +++ b/js/utils/TouchControls.js @@ -150,9 +150,21 @@ class TouchControls { const target = e.target; if (target.closest('.touch-pause-button') || target.closest('.touch-fullscreen-button') || + target.closest('#mainMenu') || + target.closest('#pauseMenu') || + target.closest('#commandsScreen') || + target.closest('#optionsScreen') || + target.closest('#scoreboardScreen') || + target.closest('#creditsScreen') || target.closest('.menu-screen') || target.closest('.level-up-screen') || - target.closest('.game-over-screen')) { + target.closest('.game-over-screen') || + target.closest('.meta-screen') || + target.closest('button') || + target.closest('a') || + target.closest('input') || + target.closest('select') || + target.closest('textarea')) { return; // Let UI elements handle their own touch } From f25de3e1d18d1ea751e26f3f73adc372b4349961 Mon Sep 17 00:00:00 2001 From: LinkAtPlug <36280686+Linkatplug@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:36:36 +0100 Subject: [PATCH 11/11] Fix remaining mobile tap blocking and hide Android stats bar --- js/utils/TouchControls.js | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/js/utils/TouchControls.js b/js/utils/TouchControls.js index 8ddfa5c..d3bbbde 100644 --- a/js/utils/TouchControls.js +++ b/js/utils/TouchControls.js @@ -38,6 +38,7 @@ class TouchControls { // Auto-detect mobile this.isMobile = this.detectMobile(); + this.isAndroidDevice = /android/i.test(navigator.userAgent); if (this.isMobile) { this.enable(); @@ -46,6 +47,22 @@ class TouchControls { this.setupTouchHandlers(); this.setupFullscreenHandler(); this.setupCanvasResize(); + this.applyAndroidUIAdjustments(); + } + + applyAndroidUIAdjustments() { + if (!this.isAndroidDevice) return; + + const statsDisplay = document.getElementById('statsDisplay'); + const statsOverlayPanel = document.getElementById('statsOverlayPanel'); + + if (statsDisplay) { + statsDisplay.style.display = 'none'; + } + + if (statsOverlayPanel) { + statsOverlayPanel.style.display = 'none'; + } } detectMobile() { @@ -150,9 +167,21 @@ class TouchControls { const target = e.target; if (target.closest('.touch-pause-button') || target.closest('.touch-fullscreen-button') || + target.closest('#mainMenu') || + target.closest('#pauseMenu') || + target.closest('#commandsScreen') || + target.closest('#optionsScreen') || + target.closest('#scoreboardScreen') || + target.closest('#creditsScreen') || target.closest('.menu-screen') || target.closest('.level-up-screen') || - target.closest('.game-over-screen')) { + target.closest('.game-over-screen') || + target.closest('.meta-screen') || + target.closest('button') || + target.closest('a') || + target.closest('input') || + target.closest('select') || + target.closest('textarea')) { return; // Let UI elements handle their own touch } @@ -165,9 +194,11 @@ class TouchControls { }, { passive: false }); gameContainer.addEventListener('touchmove', (e) => { - e.preventDefault(); + if (this.joystickTouchId === null) return; + for (let touch of e.changedTouches) { if (touch.identifier === this.joystickTouchId) { + e.preventDefault(); this.handleJoystickMove(touch); break; } @@ -175,9 +206,11 @@ class TouchControls { }, { passive: false }); gameContainer.addEventListener('touchend', (e) => { - e.preventDefault(); + if (this.joystickTouchId === null) return; + for (let touch of e.changedTouches) { if (touch.identifier === this.joystickTouchId) { + e.preventDefault(); this.handleJoystickEnd(); break; }