feat(mobile): viewport, responsive layout, and touch controls

The layout was already partly responsive (max-width:100vw, min(480px,90vw)
modals, canvas max-width:100%, and the renderer centres a fixed viewport on the
player) but had no viewport meta, so none of it engaged on phones.

- Add the viewport (+charset) meta — unlocks the existing responsive rules.
- Mobile media query (<=820px): stack the 3-column grid into one column
  (header/status/game/chat/input/footer), shrink fonts, and cap the fixed-width
  play/history/leaderboard modals to 94vw so they fit instead of overflowing.
- On-screen D-pad + swipe-to-move (new touchControls.js), both emitting the
  same player_move {direction} event the keyboard uses — no game-logic change.
  Shown on small screens or any touch device (body.touch).
- Remove the leftover "GAME AREA" debug overlay.
This commit is contained in:
jwinterm
2026-05-31 16:13:21 -04:00
parent 9a546a1b6c
commit bfca9b340f
3 changed files with 148 additions and 12 deletions
+11
View File
@@ -1,6 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Wownerogue - A Wownero Roguelike</title>
<link rel="stylesheet" type="text/css" href="styles/stylesheet.css">
<link rel="icon" type="image/x-icon" href="favicon.ico" id="favicon">
@@ -81,6 +83,7 @@
<script src="js/network/socketHandlers.js"></script>
<script src="js/network/smirkAuth.js"></script>
<script src="js/input/inputHandler.js"></script>
<script src="js/input/touchControls.js"></script>
</head>
<body>
<div class="container">
@@ -368,5 +371,13 @@
}, 100); // 100ms delay to ensure everything is loaded
});
</script>
<!-- On-screen movement D-pad (shown on touch/mobile via CSS) -->
<div id="touch-dpad" aria-hidden="true">
<button id="dpad-up" type="button" aria-label="Move up">▲</button>
<button id="dpad-left" type="button" aria-label="Move left">◀</button>
<button id="dpad-right" type="button" aria-label="Move right">▶</button>
<button id="dpad-down" type="button" aria-label="Move down">▼</button>
</div>
</body>
</html>
+58
View File
@@ -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);
})();
+79 -12
View File
@@ -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; }