feat(ui): mobile-friendly game layout for portrait and landscape phones

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.
This commit is contained in:
Tronax 2026-08-16 19:18:11 +02:00
parent 4c14425a6d
commit 914af42210
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
4 changed files with 178 additions and 17 deletions

View file

@ -43,24 +43,26 @@ const badgeColor = computed(() => {
<div v-else class="game-layout"> <div v-else class="game-layout">
<Hud /> <Hud />
<MPBar /> <MPBar />
<div class="canvas-wrap"> <div class="game-main">
<div class="canvas-box" :style="frameStyle"> <div class="canvas-wrap">
<GameCanvas /> <div class="canvas-box" :style="frameStyle">
<div <GameCanvas />
v-if="badgeText" <div
class="board-badge" v-if="badgeText"
:style="{ background: badgeColor }" class="board-badge"
> :style="{ background: badgeColor }"
{{ badgeText }} >
{{ badgeText }}
</div>
<TowerPanel />
<ObstaclePanel />
<PiPCanvas />
<PauseOverlay />
<EndOverlay />
</div> </div>
<TowerPanel />
<ObstaclePanel />
<PiPCanvas />
<PauseOverlay />
<EndOverlay />
</div> </div>
<TowerShop />
</div> </div>
<TowerShop />
</div> </div>
<AuthModal /> <AuthModal />
@ -85,6 +87,13 @@ const badgeColor = computed(() => {
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
.game-main {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.canvas-wrap { .canvas-wrap {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@ -92,6 +101,18 @@ const badgeColor = computed(() => {
align-items: center; align-items: center;
justify-content: 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 { .canvas-box {
position: relative; position: relative;
border-radius: 10px; border-radius: 10px;

View file

@ -21,9 +21,11 @@ function fit(): void {
canvas.style.width = `${w}px` canvas.style.width = `${w}px`
canvas.style.height = `${h}px` canvas.style.height = `${h}px`
engine.resizeCanvas(w, h) 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 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 } { function toGame(e: MouseEvent): { x: number; y: number } {

View file

@ -219,4 +219,73 @@ function exitToMenu(): void {
color: #fff; color: #fff;
min-width: 60px; 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;
}
}
</style> </style>

View file

@ -95,4 +95,73 @@ function pick(kind: typeof TOWER_ORDER[number]): void {
border-radius: 4px; border-radius: 4px;
padding: 0 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;
}
}
</style> </style>