diff --git a/html/index.html b/html/index.html
index 9e4b17c..9684d57 100644
--- a/html/index.html
+++ b/html/index.html
@@ -1,6 +1,8 @@
+
@@ -368,5 +371,13 @@
}, 100); // 100ms delay to ensure everything is loaded
});
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/html/js/input/touchControls.js b/html/js/input/touchControls.js
new file mode 100644
index 0000000..67a1352
--- /dev/null
+++ b/html/js/input/touchControls.js
@@ -0,0 +1,58 @@
+/**
+ * Touch controls for mobile: an on-screen D-pad and swipe-to-move on the game area.
+ * Both emit the SAME 'player_move' { direction } event the keyboard already uses, so no
+ * game logic changes. The D-pad markup lives in index.html and is shown via CSS on small
+ * screens; swipe is bound to #game-display.
+ */
+(function () {
+ function move(direction) {
+ if (direction && window.socket && window.socket.connected) {
+ window.socket.emit('player_move', { direction: direction });
+ }
+ }
+
+ function initDpad() {
+ var map = { 'dpad-up': 'up', 'dpad-down': 'down', 'dpad-left': 'left', 'dpad-right': 'right' };
+ Object.keys(map).forEach(function (id) {
+ var el = document.getElementById(id);
+ if (!el) return;
+ // touchstart drives touch input; preventDefault cancels the emulated click so a
+ // single tap doesn't move twice. click handles mouse (desktop testing).
+ el.addEventListener('touchstart', function (e) { e.preventDefault(); move(map[id]); }, { passive: false });
+ el.addEventListener('click', function () { move(map[id]); });
+ });
+ }
+
+ function initSwipe() {
+ var area = document.getElementById('game-display');
+ if (!area) return;
+ var sx = 0, sy = 0, tracking = false;
+ var THRESHOLD = 24; // px before a drag counts as a swipe
+ area.addEventListener('touchstart', function (e) {
+ if (e.touches.length !== 1) { tracking = false; return; }
+ sx = e.touches[0].clientX; sy = e.touches[0].clientY; tracking = true;
+ }, { passive: true });
+ area.addEventListener('touchend', function (e) {
+ if (!tracking) return;
+ tracking = false;
+ var t = e.changedTouches[0];
+ var dx = t.clientX - sx, dy = t.clientY - sy;
+ var adx = Math.abs(dx), ady = Math.abs(dy);
+ if (adx < THRESHOLD && ady < THRESHOLD) return; // a tap, not a swipe
+ if (adx > ady) move(dx > 0 ? 'right' : 'left');
+ else move(dy > 0 ? 'down' : 'up');
+ }, { passive: true });
+ }
+
+ function init() {
+ // Mark touch devices so the D-pad also shows on touch tablets / landscape phones
+ // wider than the mobile media-query breakpoint.
+ if ('ontouchstart' in window || (navigator.maxTouchPoints || 0) > 0) {
+ document.body.classList.add('touch');
+ }
+ initDpad();
+ initSwipe();
+ }
+ if (document.readyState !== 'loading') init();
+ else document.addEventListener('DOMContentLoaded', init);
+})();
diff --git a/html/styles/stylesheet.css b/html/styles/stylesheet.css
index 2ec323f..85294b5 100644
--- a/html/styles/stylesheet.css
+++ b/html/styles/stylesheet.css
@@ -86,18 +86,6 @@
}
/* Add debug overlay to see container bounds */
-.rotdis::before {
- content: "GAME AREA";
- position: absolute;
- top: 5px;
- left: 5px;
- color: rgba(255,255,255,0.3);
- font-family: monospace;
- font-size: 12px;
- z-index: 1;
- pointer-events: none;
-}
-
/* Chat messages area */
.chat-container {
grid-area: chat;
@@ -325,3 +313,82 @@ li.debug {
color: #9cf6ff;
}
+
+/* =========================================================================
+ Mobile / touch support
+ ========================================================================= */
+#touch-dpad { display: none; }
+
+@media (max-width: 820px) {
+ /* Stack the desktop 3-column grid into a single column */
+ .container {
+ grid-template-areas:
+ "header"
+ "status"
+ "game"
+ "chat"
+ "input"
+ "footer";
+ grid-template-columns: 1fr;
+ grid-template-rows: auto auto 1fr auto auto auto;
+ height: 100vh;
+ height: 100dvh; /* exclude mobile browser chrome where supported */
+ }
+
+ .banner { font-size: 16px; padding: 4px 6px; }
+ #staticbanner { font-size: 15px !important; }
+ .status {
+ border-right: none;
+ border-bottom: 1px solid #444;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 4px 14px;
+ padding: 6px 8px;
+ font-size: 12px;
+ }
+ .chat-container { min-height: 0; }
+ #messageBox { max-height: 16vh; }
+ .footer { font-size: 9px; padding: 2px 4px; line-height: 1.3; }
+
+ /* Fixed-width modals would overflow tiny screens — make them fit */
+ #payment-ui, #history-panel, #leaderboard-panel {
+ min-width: 0 !important;
+ width: 94vw !important;
+ max-width: 94vw !important;
+ max-height: 88vh !important;
+ overflow-y: auto !important;
+ padding: 14px !important;
+ }
+
+}
+
+/* On-screen D-pad — styling defined unconditionally; shown on small screens (media query
+ above) OR any touch device (body.touch set by touchControls.js for landscape/tablets). */
+@media (max-width: 820px) { #touch-dpad { display: grid; } }
+body.touch #touch-dpad { display: grid; }
+#touch-dpad {
+ grid-template-areas: ". up ." "left . right" ". down .";
+ gap: 8px;
+ position: fixed;
+ right: 12px;
+ bottom: 70px;
+ z-index: 1500;
+ opacity: 0.55;
+ touch-action: none;
+}
+#touch-dpad button {
+ width: 54px; height: 54px;
+ font-size: 22px;
+ background: rgba(0,40,0,0.85);
+ color: #0f0;
+ border: 2px solid #0f0;
+ border-radius: 10px;
+ padding: 0;
+ -webkit-user-select: none; user-select: none;
+ touch-action: manipulation;
+}
+#touch-dpad button:active { background: #0f0; color: #000; }
+#dpad-up { grid-area: up; }
+#dpad-down { grid-area: down; }
+#dpad-left { grid-area: left; }
+#dpad-right { grid-area: right; }