From 974284c624b1ef258563447edbdec546280acc1e Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 17 Sep 2026 22:55:48 +0000 Subject: [PATCH] Use component steampunk controller HUD --- src/mobile-hud.js | 486 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 363 insertions(+), 123 deletions(-) diff --git a/src/mobile-hud.js b/src/mobile-hud.js index bf2d333..a5cf132 100644 --- a/src/mobile-hud.js +++ b/src/mobile-hud.js @@ -1,5 +1,10 @@ import Phaser from 'phaser'; -import hudFrameSrc from './assets/steampunk-hud-mobile-v2.js'; +import topHudSrc from './assets/controller/top-hud.js'; +import leftStatusSrc from './assets/controller/left-status.js'; +import rightControlsSrc from './assets/controller/right-controls.js'; +import radarPanelSrc from './assets/controller/radar-panel.js'; +import cannonActionSrc from './assets/controller/cannon-action.js'; +import helmActionSrc from './assets/controller/helm-action.js'; const isDesktopPointer = window.matchMedia('(hover: hover) and (pointer: fine)'); @@ -9,106 +14,236 @@ if (!isDesktopPointer.matches) { if (root) { const style = document.createElement('style'); style.textContent = ` - #mobile-controls.steampunk-active { + #mobile-controls.controller-active { overflow: hidden; } - #steampunk-hud-frame { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - object-fit: fill; - pointer-events: none; - user-select: none; - -webkit-user-select: none; - z-index: 0; - } - - #steampunk-live-hud { + #controller-hud { position: absolute; inset: 0; z-index: 1; + overflow: hidden; pointer-events: none; - color: #f7e3b0; + user-select: none; + -webkit-user-select: none; + color: #f8e8b7; font-family: Georgia, 'Times New Roman', serif; font-weight: 700; - text-shadow: 0 1px 2px #000, 0 0 5px rgba(64, 30, 4, .9); + text-shadow: 0 1px 2px #000, 0 0 5px rgba(47, 22, 2, .95); } - .steam-stat { + .steam-piece { position: absolute; + pointer-events: none; + } + + .steam-piece > img { + display: block; + width: 100%; + height: auto; + pointer-events: none; + user-select: none; + -webkit-user-select: none; + } + + .steam-top { + left: 0; + top: 0; + width: 100%; + } + + .steam-top-value { + position: absolute; + top: 68%; transform: translate(-50%, -50%); - min-width: 42px; + min-width: 32px; text-align: center; white-space: nowrap; - } - - .steam-stat .label { - display: block; - margin-bottom: 1px; - color: #d6b878; - font-size: clamp(7px, 1.9vw, 10px); - letter-spacing: .08em; - } - - .steam-stat .value { - display: block; - font-size: clamp(12px, 3.5vw, 18px); + color: #ffe6a1; + font-size: clamp(9px, 2.7vw, 13px); line-height: 1; } - .steam-speed { left: 9.5%; top: 8.5%; } - .steam-heading { left: 20.5%; top: 8.5%; } - .steam-wave { left: 50%; top: 9.4%; } - .steam-hostiles { left: 65.6%; top: 8.5%; } - .steam-kills { left: 78.6%; top: 8.5%; } - .steam-range { left: 91.2%; top: 8.5%; } + .steam-speed { left: 9.3%; } + .steam-heading { left: 21.2%; } + .steam-wave { left: 50%; top: 72%; } + .steam-hostiles { left: 65.4%; } + .steam-kills { left: 78.2%; } + .steam-range { left: 91.1%; } - .steam-hull-name, - .steam-hull-value { + .steam-player-card { + left: -1%; + top: clamp(88px, 12.4vh, 108px); + width: 53%; + } + + .steam-enemy-card { + right: -1%; + top: clamp(88px, 12.4vh, 108px); + width: 46%; + } + + .steam-card-name, + .steam-card-value { position: absolute; transform: translate(-50%, -50%); white-space: nowrap; + text-align: center; } - .steam-hull-name { - top: 14.25%; - color: #f2d394; - font-size: clamp(8px, 2.3vw, 12px); - letter-spacing: .04em; + .steam-card-name { + color: #f6d58e; + font-size: clamp(8px, 2.2vw, 11px); + letter-spacing: .06em; } - .steam-player-name { left: 25%; } - .steam-enemy-name { left: 72%; } - - .steam-hull-value { - top: 17.15%; - font-size: clamp(8px, 2.35vw, 12px); + .steam-player-card .steam-card-name { + left: 58%; + top: 31%; } - .steam-player-value { left: 38.2%; } - .steam-enemy-value { left: 76.5%; } + .steam-enemy-card .steam-card-name { + left: 51%; + top: 12%; + } - .steam-hull-fill { + .steam-card-value { + color: #fff0bd; + font-size: clamp(8px, 2.4vw, 12px); + } + + .steam-player-card .steam-card-value { + left: 61%; + top: 67%; + } + + .steam-enemy-card .steam-card-value { + left: 38%; + top: 32%; + } + + .steam-bar { position: absolute; - top: 16.55%; - height: 1.22%; - min-height: 5px; - transform-origin: left center; + overflow: hidden; border-radius: 999px; - background: linear-gradient(180deg, #ff684f 0%, #c11f23 55%, #6f1016 100%); - box-shadow: inset 0 1px 0 rgba(255,255,255,.28), 0 0 4px rgba(228,64,47,.55); + pointer-events: none; } - .steam-player-fill { left: 14.2%; width: 27.5%; } - .steam-enemy-fill { left: 57.1%; width: 27.4%; } + .steam-player-card .steam-bar { + left: 32.3%; + top: 56.2%; + width: 57%; + height: 20%; + } + + .steam-enemy-card .steam-bar { + left: 7.5%; + top: 24.8%; + width: 60.5%; + height: 16%; + } + + .steam-bar-fill { + width: 100%; + height: 100%; + transform-origin: left center; + border-radius: inherit; + background: linear-gradient(180deg, #ffb445 0%, #d43e2f 42%, #821b1c 100%); + box-shadow: inset 0 1px 0 rgba(255,255,255,.35), 0 0 5px rgba(219,63,40,.45); + opacity: .9; + } + + .steam-radar-wrap { + left: 1%; + bottom: clamp(88px, 11.5vh, 106px); + width: min(39vw, 174px); + } + + #steam-radar-contacts { + position: absolute; + left: 17%; + top: 14%; + width: 66%; + height: 69%; + border-radius: 50%; + overflow: hidden; + } + + .steam-radar-player, + .steam-radar-dot { + position: absolute; + width: 6px; + height: 6px; + margin: -3px 0 0 -3px; + border-radius: 50%; + box-shadow: 0 0 6px currentColor; + } + + .steam-radar-player { + left: 50%; + top: 50%; + color: #ffe08c; + background: currentColor; + } + + .steam-radar-dot { + color: #ff5c42; + background: currentColor; + } + + .steam-helm-wrap { + left: -1%; + bottom: max(2px, env(safe-area-inset-bottom)); + width: 48%; + } + + .steam-cannon-port, + .steam-cannon-starboard { + right: -1%; + width: 45%; + } + + .steam-cannon-port { + bottom: max(clamp(88px, 11.8vh, 108px), calc(env(safe-area-inset-bottom) + 86px)); + } + + .steam-cannon-starboard { + bottom: max(2px, env(safe-area-inset-bottom)); + } + + .steam-cannon-starboard > img { + transform: scaleX(-1); + } + + .steam-action-label { + position: absolute; + left: 62%; + top: 43%; + transform: translate(-50%, -50%); + width: 58%; + text-align: center; + color: #ffe5a0; + font-size: clamp(10px, 3vw, 14px); + letter-spacing: .08em; + } + + .steam-action-sub { + position: absolute; + left: 62%; + top: 62%; + transform: translate(-50%, -50%); + width: 58%; + text-align: center; + color: #ceb47b; + font-size: clamp(6px, 1.65vw, 8px); + letter-spacing: .06em; + } .steam-status { display: none; position: absolute; left: 50%; - top: 22%; + top: 27%; transform: translateX(-50%); max-width: 72%; padding: 7px 12px; @@ -122,16 +257,19 @@ if (!isDesktopPointer.matches) { box-shadow: 0 4px 18px rgba(0,0,0,.35); } - .steam-status.visible { display: block; } + .steam-status.visible { + display: block; + } - #mobile-controls.steampunk-active #drag-stick { - left: 1%; - top: 70.5%; - bottom: auto; + #mobile-controls.controller-active #drag-stick { + left: 0; + bottom: max(2px, env(safe-area-inset-bottom)); + top: auto; width: 48%; - height: 28.5%; + height: auto; + aspect-ratio: 378 / 192; border: 0; - border-radius: 45%; + border-radius: 34%; background: transparent; box-shadow: none; backdrop-filter: none; @@ -139,14 +277,15 @@ if (!isDesktopPointer.matches) { z-index: 3; } - #mobile-controls.steampunk-active #drag-stick::before, - #mobile-controls.steampunk-active #drag-stick::after, - #mobile-controls.steampunk-active #drag-thumb { + #mobile-controls.controller-active #drag-stick::before, + #mobile-controls.controller-active #drag-stick::after, + #mobile-controls.controller-active #drag-thumb { opacity: 0; } - #mobile-controls.steampunk-active .mobile-control { + #mobile-controls.controller-active .mobile-control { min-width: 0; + height: auto; padding: 0; border: 0; border-radius: 0; @@ -160,74 +299,148 @@ if (!isDesktopPointer.matches) { z-index: 3; } - #mobile-controls.steampunk-active .mobile-control.active, - #mobile-controls.steampunk-active .mobile-control:active { + #mobile-controls.controller-active .mobile-control.active, + #mobile-controls.controller-active .mobile-control:active { background: rgba(255, 210, 112, .08); transform: none; } - #mobile-controls.steampunk-active .mobile-control.port { - left: 57%; - top: 75.2%; - right: auto; - bottom: auto; - width: 42%; - height: 10.5%; + #mobile-controls.controller-active .mobile-control.port, + #mobile-controls.controller-active .mobile-control.starboard { + left: auto; + right: 0; + width: 45%; + aspect-ratio: 359 / 201; } - #mobile-controls.steampunk-active .mobile-control.starboard { - left: 57%; - top: 86.3%; - right: auto; - bottom: auto; - width: 42%; - height: 10.5%; + #mobile-controls.controller-active .mobile-control.port { + top: auto; + bottom: max(clamp(88px, 11.8vh, 108px), calc(env(safe-area-inset-bottom) + 86px)); } - #mobile-controls.steampunk-active .mobile-control.restart { - left: 80.5%; - top: 18.2%; - right: auto; + #mobile-controls.controller-active .mobile-control.starboard { + top: auto; + bottom: max(2px, env(safe-area-inset-bottom)); + } + + #mobile-controls.controller-active .mobile-control.restart { + left: auto; + right: 1%; + top: clamp(147px, 20.5vh, 179px); bottom: auto; - width: 18%; - height: 13.5%; + width: 14%; + height: auto; + aspect-ratio: 1; border-radius: 50%; } + + @media (max-height: 700px) { + .steam-player-card, + .steam-enemy-card { + top: 76px; + } + + .steam-radar-wrap { + width: 34vw; + bottom: 78px; + } + + .steam-helm-wrap, + #mobile-controls.controller-active #drag-stick { + width: 43%; + } + + .steam-cannon-port, + .steam-cannon-starboard, + #mobile-controls.controller-active .mobile-control.port, + #mobile-controls.controller-active .mobile-control.starboard { + width: 41%; + } + } `; document.head.appendChild(style); - root.classList.add('steampunk-active'); + root.classList.add('controller-active'); - const frame = document.createElement('img'); - frame.id = 'steampunk-hud-frame'; - frame.src = hudFrameSrc; - frame.alt = ''; - frame.draggable = false; - root.prepend(frame); + const hud = document.createElement('div'); + hud.id = 'controller-hud'; + root.prepend(hud); - const liveHud = document.createElement('div'); - liveHud.id = 'steampunk-live-hud'; - liveHud.innerHTML = ` -
SPEED0
-
HEADING000°
-
WAVE1
-
HOSTILES0
-
KILLS0
-
RANGE--
-
SPARROW
-
PIRATE
-
-
-
100/100
-
--
-
- `; - root.appendChild(liveHud); + const addPiece = (className, src, inner = '') => { + const piece = document.createElement('div'); + piece.className = `steam-piece ${className}`; + + const image = document.createElement('img'); + image.src = src; + image.alt = ''; + image.draggable = false; + piece.appendChild(image); + + if (inner) piece.insertAdjacentHTML('beforeend', inner); + hud.appendChild(piece); + return piece; + }; + + addPiece('steam-top', topHudSrc, ` + 0 + 000° + 1 + 0 + 0 + -- + `); + + addPiece('steam-player-card', leftStatusSrc, ` + SPARROW +
+ 100/100 + `); + + addPiece('steam-enemy-card', rightControlsSrc, ` + PIRATE +
+ CLEAR + `); + + const radarPiece = addPiece('steam-radar-wrap', radarPanelSrc, ` +
+ +
+ `); + + addPiece('steam-helm-wrap', helmActionSrc, ` + HELM + DRAG TO STEER + `); + + addPiece('steam-cannon-port', cannonActionSrc, ` + PORT FIRE + BROADSIDE + `); + + addPiece('steam-cannon-starboard', cannonActionSrc, ` + STBD FIRE + BROADSIDE + `); + + const status = document.createElement('div'); + status.className = 'steam-status'; + status.dataset.steam = 'status'; + hud.appendChild(status); const fields = Object.fromEntries( - [...liveHud.querySelectorAll('[data-steam]')].map((el) => [el.dataset.steam, el]), + [...hud.querySelectorAll('[data-steam]')].map((el) => [el.dataset.steam, el]), ); + const radarContacts = radarPiece.querySelector('#steam-radar-contacts'); + const radarDots = Array.from({ length: 10 }, () => { + const dot = document.createElement('span'); + dot.className = 'steam-radar-dot'; + dot.hidden = true; + radarContacts.appendChild(dot); + return dot; + }); + const oldHudKeys = [ 'hudGraphics', 'brandText', @@ -255,8 +468,33 @@ if (!isDesktopPointer.matches) { const clamp01 = (value) => Math.max(0, Math.min(1, value)); + const updateRadar = (scene) => { + const pirates = scene.pirates?.getChildren?.().filter((pirate) => pirate?.active) || []; + + radarDots.forEach((dot, index) => { + const pirate = pirates[index]; + if (!pirate) { + dot.hidden = true; + return; + } + + let dx = (pirate.x - scene.player.x) / 900; + let dy = (pirate.y - scene.player.y) / 900; + const distance = Math.hypot(dx, dy); + if (distance > 1) { + dx /= distance; + dy /= distance; + } + + dot.style.left = `${50 + dx * 36}%`; + dot.style.top = `${50 + dy * 36}%`; + dot.hidden = false; + }); + }; + const update = () => { const scene = getScene(); + if (scene?.player) { hideLegacyHud(scene); @@ -268,7 +506,7 @@ if (!isDesktopPointer.matches) { const playerHull = Math.max(0, scene.playerHull ?? 100); const enemyHull = pirate ? Math.max(0, pirate.getData?.('hull') ?? 0) : 0; - fields.speed.textContent = `${Math.abs(scene.playerSpeed || 0).toFixed(0)}${scene.playerSpeed < 0 ? ' R' : ''}`; + fields.speed.textContent = `${Math.abs(scene.playerSpeed || 0).toFixed(0)}${scene.playerSpeed < 0 ? 'R' : ''}`; fields.heading.textContent = `${heading.toFixed(0).padStart(3, '0')}°`; fields.wave.textContent = String(scene.wave || 1); fields.hostiles.textContent = String(activePirates); @@ -282,6 +520,8 @@ if (!isDesktopPointer.matches) { const statusText = scene.status?.text?.trim?.() || ''; fields.status.textContent = statusText; fields.status.classList.toggle('visible', Boolean(statusText)); + + updateRadar(scene); } requestAnimationFrame(update);