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
+
+
+
@@ -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;
}