From 33708515a0af92f1dad5ae1a85692a27f49ef47a Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 17 Sep 2026 22:34:34 +0000 Subject: [PATCH] Wire steampunk mobile HUD overlay --- src/mobile-hud.js | 292 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 src/mobile-hud.js diff --git a/src/mobile-hud.js b/src/mobile-hud.js new file mode 100644 index 0000000..bf2d333 --- /dev/null +++ b/src/mobile-hud.js @@ -0,0 +1,292 @@ +import Phaser from 'phaser'; +import hudFrameSrc from './assets/steampunk-hud-mobile-v2.js'; + +const isDesktopPointer = window.matchMedia('(hover: hover) and (pointer: fine)'); + +if (!isDesktopPointer.matches) { + const root = document.querySelector('#mobile-controls'); + + if (root) { + const style = document.createElement('style'); + style.textContent = ` + #mobile-controls.steampunk-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 { + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + color: #f7e3b0; + font-family: Georgia, 'Times New Roman', serif; + font-weight: 700; + text-shadow: 0 1px 2px #000, 0 0 5px rgba(64, 30, 4, .9); + } + + .steam-stat { + position: absolute; + transform: translate(-50%, -50%); + min-width: 42px; + 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); + 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-hull-name, + .steam-hull-value { + position: absolute; + transform: translate(-50%, -50%); + white-space: nowrap; + } + + .steam-hull-name { + top: 14.25%; + color: #f2d394; + font-size: clamp(8px, 2.3vw, 12px); + letter-spacing: .04em; + } + + .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-value { left: 38.2%; } + .steam-enemy-value { left: 76.5%; } + + .steam-hull-fill { + position: absolute; + top: 16.55%; + height: 1.22%; + min-height: 5px; + transform-origin: left center; + 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); + } + + .steam-player-fill { left: 14.2%; width: 27.5%; } + .steam-enemy-fill { left: 57.1%; width: 27.4%; } + + .steam-status { + display: none; + position: absolute; + left: 50%; + top: 22%; + transform: translateX(-50%); + max-width: 72%; + padding: 7px 12px; + border: 1px solid rgba(201, 146, 60, .7); + border-radius: 8px; + background: rgba(8, 20, 24, .82); + color: #fff0c3; + font-size: clamp(10px, 3vw, 14px); + text-align: center; + white-space: pre-line; + box-shadow: 0 4px 18px rgba(0,0,0,.35); + } + + .steam-status.visible { display: block; } + + #mobile-controls.steampunk-active #drag-stick { + left: 1%; + top: 70.5%; + bottom: auto; + width: 48%; + height: 28.5%; + border: 0; + border-radius: 45%; + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + z-index: 3; + } + + #mobile-controls.steampunk-active #drag-stick::before, + #mobile-controls.steampunk-active #drag-stick::after, + #mobile-controls.steampunk-active #drag-thumb { + opacity: 0; + } + + #mobile-controls.steampunk-active .mobile-control { + min-width: 0; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + color: transparent; + font-size: 0; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + transform: none; + z-index: 3; + } + + #mobile-controls.steampunk-active .mobile-control.active, + #mobile-controls.steampunk-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.steampunk-active .mobile-control.starboard { + left: 57%; + top: 86.3%; + right: auto; + bottom: auto; + width: 42%; + height: 10.5%; + } + + #mobile-controls.steampunk-active .mobile-control.restart { + left: 80.5%; + top: 18.2%; + right: auto; + bottom: auto; + width: 18%; + height: 13.5%; + border-radius: 50%; + } + `; + document.head.appendChild(style); + + root.classList.add('steampunk-active'); + + const frame = document.createElement('img'); + frame.id = 'steampunk-hud-frame'; + frame.src = hudFrameSrc; + frame.alt = ''; + frame.draggable = false; + root.prepend(frame); + + 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 fields = Object.fromEntries( + [...liveHud.querySelectorAll('[data-steam]')].map((el) => [el.dataset.steam, el]), + ); + + const oldHudKeys = [ + 'hudGraphics', + 'brandText', + 'speedText', + 'headingText', + 'waveText', + 'combatText', + 'playerHullLabel', + 'enemyHullLabel', + 'controlsHint', + 'status', + ]; + + const getScene = () => { + for (const game of Phaser.GAMES || []) { + const scene = game?.scene?.getScene?.('game'); + if (scene?.sys?.isActive?.()) return scene; + } + return null; + }; + + const hideLegacyHud = (scene) => { + for (const key of oldHudKeys) scene[key]?.setVisible?.(false); + }; + + const clamp01 = (value) => Math.max(0, Math.min(1, value)); + + const update = () => { + const scene = getScene(); + if (scene?.player) { + hideLegacyHud(scene); + + const heading = ((scene.player.angle % 360) + 360) % 360; + const activePirates = scene.pirates?.countActive?.(true) || 0; + const nearest = scene.nearestPirate?.() || { pirate: null, range: Infinity }; + const pirate = nearest.pirate; + const range = nearest.range; + 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.heading.textContent = `${heading.toFixed(0).padStart(3, '0')}°`; + fields.wave.textContent = String(scene.wave || 1); + fields.hostiles.textContent = String(activePirates); + fields.kills.textContent = String(scene.defeated || 0); + fields.range.textContent = Number.isFinite(range) ? `${Math.round(range)}m` : '--'; + fields['player-hull'].textContent = `${playerHull}/100`; + fields['enemy-hull'].textContent = pirate ? `${enemyHull}/120` : 'CLEAR'; + fields['player-fill'].style.transform = `scaleX(${clamp01(playerHull / 100)})`; + fields['enemy-fill'].style.transform = `scaleX(${pirate ? clamp01(enemyHull / 120) : 0})`; + + const statusText = scene.status?.text?.trim?.() || ''; + fields.status.textContent = statusText; + fields.status.classList.toggle('visible', Boolean(statusText)); + } + + requestAnimationFrame(update); + }; + + requestAnimationFrame(update); + } +}