From 914af422106632f21900c284be30ed35f36a3039 Mon Sep 17 00:00:00 2001 From: Tronax Date: Sun, 16 Aug 2026 19:18:11 +0200 Subject: [PATCH] feat(ui): mobile-friendly game layout for portrait and landscape phones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The game layout stacked HUD, board, and tower shop vertically with desktop-sized chrome, leaving a tiny unusable board on phones (about 250x136px in landscape). Touch input and responsive canvas scaling already existed — only the surrounding layout blocked mobile play. Landscape phones / short viewports (max-height: 560px): - New .game-main wrapper switches to a row layout: the board keeps the full remaining width and the tower shop becomes a vertical column on the right edge with one compact row per tower (icon + name + cost) - HUD compacts to a single row (no 78px wave section, no preview chips, smaller stats and buttons); layout padding and gaps shrink - Board grows from ~250x136 to ~487x268 on a 740x360 viewport Portrait phones (max-width: 560px): - Tower shop renders five compact icon tiles in a single row (69px each, no horizontal overflow) - HUD wraps into compact rows; difficulty chip and preview hidden All screens: - Overlay panels (tower info, obstacle, pause, end screen) may now scale down to 0.65 via --ui-scale so they no longer cover a small board (previously clamped to a minimum of 1) Verified in a real browser: landscape 740x360 (board 487x268, shop column right), portrait 390x844 (board 366x201, shop row fits, no overflow), desktop 1280x800 unchanged (column layout, board 1020x561). Touch flow tested end-to-end: tap shop card -> tap board builds a tower (gold 260->210), tap tower opens the scaled info panel with upgrade/sell. --- src/App.vue | 51 ++++++++++++++++++-------- src/components/GameCanvas.vue | 6 ++- src/components/Hud.vue | 69 +++++++++++++++++++++++++++++++++++ src/components/TowerShop.vue | 69 +++++++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 17 deletions(-) diff --git a/src/App.vue b/src/App.vue index fbcf918..6106aad 100644 --- a/src/App.vue +++ b/src/App.vue @@ -43,24 +43,26 @@ const badgeColor = computed(() => {
-
-
- -
- {{ badgeText }} +
+
+
+ +
+ {{ badgeText }} +
+ + + + +
- - - - -
+
-
@@ -85,6 +87,13 @@ const badgeColor = computed(() => { flex-direction: column; gap: 8px; } +.game-main { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + gap: 8px; +} .canvas-wrap { flex: 1; min-height: 0; @@ -92,6 +101,18 @@ const badgeColor = computed(() => { align-items: center; justify-content: center; } +/* Phones in landscape / very short viewports: the board keeps the full + width and the tower shop becomes a vertical column on the right edge. */ +@media (max-height: 560px) { + .game-layout { + padding: 6px 8px 8px; + gap: 5px; + } + .game-main { + flex-direction: row; + gap: 6px; + } +} .canvas-box { position: relative; border-radius: 10px; diff --git a/src/components/GameCanvas.vue b/src/components/GameCanvas.vue index 81837b5..c95a30f 100644 --- a/src/components/GameCanvas.vue +++ b/src/components/GameCanvas.vue @@ -21,9 +21,11 @@ function fit(): void { canvas.style.width = `${w}px` canvas.style.height = `${h}px` engine.resizeCanvas(w, h) - // overlay panels (tower info, obstacle, end screen) scale with the field + // overlay panels (tower info, obstacle, end screen) scale with the field; + // on small screens (phone) the scale drops below 1 so panels stay readable + // relative to the board instead of covering it completely const box = canvas.parentElement - if (box) box.style.setProperty('--ui-scale', String(Math.min(1.75, Math.max(1, scale)))) + if (box) box.style.setProperty('--ui-scale', String(Math.min(1.75, Math.max(0.65, scale)))) } function toGame(e: MouseEvent): { x: number; y: number } { diff --git a/src/components/Hud.vue b/src/components/Hud.vue index ec5decd..9f5cd54 100644 --- a/src/components/Hud.vue +++ b/src/components/Hud.vue @@ -219,4 +219,73 @@ function exitToMenu(): void { color: #fff; min-width: 60px; } + +/* Phones in landscape / short viewports: compact one-row HUD */ +@media (max-height: 560px) { + .hud { + padding: 4px 8px; + gap: 8px; + } + .hud-stats { + gap: 5px; + } + .stat { + padding: 3px 7px; + font-size: 12px; + } + .hud-wave { + min-width: 120px; + min-height: 0; + gap: 1px; + } + .wave-btn { + font-size: 12.5px; + padding: 6px 10px; + } + .countdown { + font-size: 10px; + } + .preview { + display: none; + } + .hud-controls { + gap: 4px; + } + .icon { + min-width: 34px; + padding: 4px 7px; + font-size: 13px; + } + .icon.confirm { + min-width: 48px; + } +} + +/* Phones in portrait: keep the HUD to two compact rows */ +@media (max-width: 560px) { + .hud { + padding: 5px 8px; + gap: 6px; + } + .stat { + padding: 3px 7px; + font-size: 12px; + } + .stat.diff { + display: none; + } + .hud-wave { + min-height: 0; + order: 3; + min-width: 100%; + } + .preview { + display: none; + } + .icon { + min-width: 36px; + padding: 5px 7px; + font-size: 13px; + } +} diff --git a/src/components/TowerShop.vue b/src/components/TowerShop.vue index 4a6ed2d..0813e32 100644 --- a/src/components/TowerShop.vue +++ b/src/components/TowerShop.vue @@ -95,4 +95,73 @@ function pick(kind: typeof TOWER_ORDER[number]): void { border-radius: 4px; padding: 0 4px; } + +/* Phones in landscape / short viewports: vertical column next to the board, + one compact row per tower (icon + name + cost) */ +@media (max-height: 560px) { + .shop { + flex-direction: column; + flex-wrap: nowrap; + gap: 5px; + } + .shop-card { + flex-direction: row; + align-items: center; + justify-content: flex-start; + min-width: 0; + padding: 6px 9px; + gap: 7px; + border-width: 1px; + border-radius: 9px; + } + .shop-card:hover { + transform: none; + } + .icon { + font-size: 18px; + } + .name { + font-size: 12px; + } + .cost { + font-size: 11.5px; + margin-left: auto; + } + .stats, + .hotkey { + display: none; + } +} + +/* Phones in portrait: five compact icon tiles in a single row */ +@media (max-width: 560px) { + .shop { + flex-wrap: nowrap; + gap: 5px; + } + .shop-card { + flex: 1; + min-width: 0; + padding: 6px 2px; + gap: 1px; + border-width: 1px; + border-radius: 9px; + } + .shop-card:hover { + transform: none; + } + .icon { + font-size: 20px; + } + .name { + font-size: 9.5px; + } + .cost { + font-size: 10.5px; + } + .stats, + .hotkey { + display: none; + } +}