import topHudSrc from './assets/controller/top-hud.js'; import playerHealthSrc from './assets/controller/player-health-fresh-sheet.png'; import enemyHealthSrc from './assets/controller/enemy-health.js'; import cannonRoundelSrc from './assets/controller/cannon-roundel.js'; import helmFrameSrc from './assets/controller/helm-frame.js'; import helmKnobSrc from './assets/controller/helm-knob.js'; import { airshipLog } from './diagnostics.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.controller-active { overflow: hidden; } #controller-hud { position: absolute; inset: 0; z-index: 1; overflow: hidden; pointer-events: none; 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(47, 22, 2, .95); } .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%; height: auto; aspect-ratio: 960 / 246; overflow: visible; } .steam-top-value { position: absolute; top: 63%; transform: translate(-50%, -50%); display: grid; place-items: center; min-width: 0; height: 15%; padding: 0 2px; text-align: center; white-space: nowrap; overflow: hidden; color: #f7dfa1; font-size: clamp(9px, 2.45vw, 12px); font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; letter-spacing: .015em; line-height: 1; text-shadow: 0 1px 1px rgba(0, 0, 0, .95), 0 0 3px rgba(42, 19, 2, .78); } .steam-speed { left: 9.3%; width: 7%; } .steam-heading { left: 21.2%; width: 12%; } .steam-wave { left: 50%; top: 67%; width: 8%; } .steam-hostiles { left: 65.4%; width: 7%; } .steam-kills { left: 78.2%; width: 7%; } .steam-range { left: 91.1%; width: 12%; } .steam-player-card, .steam-enemy-card { top: clamp(80px, 10.3vh, 90px); aspect-ratio: 360 / 130; width: min(31vw, 120px); overflow: visible; } .steam-health-art { position: absolute; inset: 0; z-index: 0; background-repeat: no-repeat; background-size: 400% 300%; background-position: 0 0; background-color: transparent; image-rendering: auto; pointer-events: none; } .steam-player-card { left: 1%; } .steam-player-card.steam-hit { animation: steam-player-hit 220ms ease-out; } @keyframes steam-player-hit { 0% { filter: brightness(1); transform: translateX(0); } 25% { filter: brightness(1.9); transform: translateX(-4px); } 55% { filter: brightness(1.35); transform: translateX(4px); } 100% { filter: brightness(1); transform: translateX(0); } } .steam-enemy-card { right: 1%; } .steam-card-name, .steam-card-value { position: absolute; z-index: 2; transform: translate(-50%, -50%); white-space: nowrap; text-align: center; pointer-events: none; } .steam-card-name { top: 28%; color: #f6d58e; font-size: clamp(8px, 2.2vw, 11px); letter-spacing: .06em; } .steam-player-card .steam-card-name { left: 61%; } .steam-enemy-card .steam-card-name { left: 39%; } .steam-card-value { top: 67%; color: #fff0bd; font-size: clamp(8px, 2.4vw, 12px); } .steam-player-card .steam-card-value { left: 61%; } .steam-enemy-card .steam-card-value { left: 39%; } .steam-radar-wrap { left: 1%; bottom: max(8px, env(safe-area-inset-bottom)); width: min(39vw, 174px); aspect-ratio: 1; } .steam-helm-frame { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; pointer-events: none; z-index: 1; } .steam-piece > img.steam-helm-knob { position: absolute; left: 50%; top: 46%; width: 16%; height: auto; object-fit: contain; transform: translate(-50%, -50%); pointer-events: none; z-index: 3; will-change: transform; } .steam-helm-label { position: absolute; left: 50%; bottom: 5%; transform: translateX(-50%); width: 55%; text-align: center; color: #ffe5a0; font-size: clamp(8px, 2.5vw, 12px); letter-spacing: .12em; pointer-events: none; } #steam-radar-contacts { position: absolute; left: 17%; top: 14%; width: 66%; height: 69%; border-radius: 50%; overflow: hidden; z-index: 2; } .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-cannon-port, .steam-cannon-starboard { right: 3%; width: min(22vw, 104px); aspect-ratio: 1; overflow: visible; } .steam-cannon-port { bottom: max(116px, calc(env(safe-area-inset-bottom) + 112px)); } .steam-cannon-starboard { bottom: max(8px, env(safe-area-inset-bottom)); } .steam-cannon-port > img, .steam-cannon-starboard > img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; clip-path: circle(49% at 50% 50%); } .steam-reload-track { position: absolute; left: 12%; right: 12%; bottom: -7%; height: 5px; border: 1px solid rgba(232, 184, 91, .42); border-radius: 999px; background: rgba(8, 25, 31, .72); overflow: hidden; pointer-events: none; } .steam-reload-fill { display: block; width: 100%; height: 100%; transform: scaleX(1); transform-origin: left center; background: rgba(255, 224, 140, .92); transition: transform 60ms linear; } .steam-action-label { position: absolute; left: 50%; bottom: 7%; transform: translateX(-50%); min-width: 48%; padding: 2px 7px; border: 1px solid rgba(232, 184, 91, .55); border-radius: 999px; background: rgba(8, 25, 31, .82); text-align: center; color: #ffe5a0; font-size: clamp(8px, 2.3vw, 11px); line-height: 1.1; letter-spacing: .1em; white-space: nowrap; box-shadow: 0 1px 4px rgba(0,0,0,.5); } .steam-status { display: none; position: absolute; left: 50%; top: 27%; 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.controller-active #drag-stick { left: 1%; bottom: max(8px, env(safe-area-inset-bottom)); top: auto; width: min(39vw, 174px); height: auto; aspect-ratio: 1; border: 0; border-radius: 50%; background: transparent; box-shadow: none; backdrop-filter: none; -webkit-backdrop-filter: none; z-index: 3; } #mobile-controls.controller-active #drag-stick::before, #mobile-controls.controller-active #drag-stick::after, #mobile-controls.controller-active #drag-thumb { opacity: 0; } #mobile-controls.controller-active .mobile-control { min-width: 0; height: auto; 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.controller-active .mobile-control.active, #mobile-controls.controller-active .mobile-control:active { background: rgba(255, 210, 112, .08); transform: none; } #mobile-controls.controller-active .mobile-control.port, #mobile-controls.controller-active .mobile-control.starboard { left: auto; right: 3%; width: min(22vw, 104px); aspect-ratio: 1; border-radius: 50%; } #mobile-controls.controller-active .mobile-control.port { top: auto; bottom: max(116px, calc(env(safe-area-inset-bottom) + 112px)); } #mobile-controls.controller-active .mobile-control.starboard { top: auto; bottom: max(8px, 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: 14%; height: auto; aspect-ratio: 1; border-radius: 50%; } @media (max-height: 700px) and (max-width: 699px) { .steam-player-card, .steam-enemy-card { top: 76px; } .steam-radar-wrap { width: 34vw; bottom: max(6px, env(safe-area-inset-bottom)); } #mobile-controls.controller-active #drag-stick { width: 34vw; bottom: max(6px, env(safe-area-inset-bottom)); } .steam-cannon-port, .steam-cannon-starboard, #mobile-controls.controller-active .mobile-control.port, #mobile-controls.controller-active .mobile-control.starboard { width: min(20vw, 90px); } .steam-cannon-port, #mobile-controls.controller-active .mobile-control.port { bottom: max(106px, calc(env(safe-area-inset-bottom) + 102px)); } } @media (min-width: 700px) { :root { --steam-wide-helm-size: clamp(118px, 28vh, 180px); --steam-wide-cannon-size: clamp(72px, 18vh, 100px); --steam-wide-edge-right: max(18px, env(safe-area-inset-right)); --steam-wide-edge-left: max(18px, env(safe-area-inset-left)); --steam-wide-bottom: max(12px, env(safe-area-inset-bottom)); } .steam-top { left: 50%; width: min(64vw, 560px); height: auto; aspect-ratio: 960 / 246; transform: translateX(-50%); } .steam-top-value { font-size: clamp(9px, 1.15vw, 13px); } .steam-player-card, .steam-enemy-card { top: clamp(70px, 20vh, 104px); } .steam-player-card { left: max(12px, env(safe-area-inset-left)); width: min(18vw, 180px); } .steam-enemy-card { right: max(12px, env(safe-area-inset-right)); width: min(18vw, 180px); } .steam-card-name { font-size: clamp(8px, 1.15vw, 12px); } .steam-card-value { font-size: clamp(8px, 1.25vw, 12px); } .steam-radar-wrap, #mobile-controls.controller-active #drag-stick { left: var(--steam-wide-edge-left); bottom: var(--steam-wide-bottom); width: var(--steam-wide-helm-size); } .steam-cannon-port, .steam-cannon-starboard, #mobile-controls.controller-active .mobile-control.port, #mobile-controls.controller-active .mobile-control.starboard { top: auto; bottom: var(--steam-wide-bottom); width: var(--steam-wide-cannon-size); } .steam-cannon-starboard, #mobile-controls.controller-active .mobile-control.starboard { right: var(--steam-wide-edge-right); } .steam-cannon-port, #mobile-controls.controller-active .mobile-control.port { right: calc(var(--steam-wide-edge-right) + var(--steam-wide-cannon-size) + 14px); } #mobile-controls.controller-active .mobile-control.restart { right: max(12px, env(safe-area-inset-right)); top: clamp(118px, 30vh, 168px); width: clamp(52px, 8vw, 76px); } .steam-status { top: clamp(92px, 24vh, 144px); max-width: min(44%, 430px); font-size: clamp(10px, 1.25vw, 14px); } } @media (min-width: 1100px) { .steam-top { width: min(52vw, 560px); } .steam-player-card { width: min(28vw, 360px); } .steam-enemy-card { width: min(25vw, 330px); } } `; document.head.appendChild(style); root.classList.add('controller-active'); const hud = document.createElement('div'); hud.id = 'controller-hud'; root.prepend(hud); 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; image.addEventListener('error', () => { airshipLog('hud:image-error', { component: className, srcType: src?.slice?.(0, 32) || 'unknown', }, 'error'); }); piece.appendChild(image); if (inner) piece.insertAdjacentHTML('beforeend', inner); hud.appendChild(piece); return piece; }; addPiece('steam-top', topHudSrc, ` 0 000° 1 0 0 -- `); const addHealthPiece = (className, src, inner = '') => { const piece = document.createElement('div'); piece.className = `steam-piece ${className}`; const art = document.createElement('span'); art.className = 'steam-health-art'; art.style.backgroundImage = `url("${src}")`; piece.appendChild(art); const probe = new Image(); probe.addEventListener('load', () => airshipLog('hud:health-sheet-loaded', { component: className })); probe.addEventListener('error', () => airshipLog('hud:health-sheet-error', { component: className }, 'error')); probe.src = src; if (inner) piece.insertAdjacentHTML('beforeend', inner); hud.appendChild(piece); return piece; }; const playerHealthPiece = addHealthPiece('steam-player-card', playerHealthSrc, ` SPARROW 100/100 `); const enemyHealthPiece = addHealthPiece('steam-enemy-card', enemyHealthSrc, ` PIRATE CLEAR `); const radarPiece = document.createElement('div'); radarPiece.className = 'steam-piece steam-radar-wrap'; const helmFrame = document.createElement('img'); helmFrame.className = 'steam-helm-frame'; helmFrame.src = helmFrameSrc; helmFrame.alt = ''; helmFrame.draggable = false; helmFrame.addEventListener('load', () => airshipLog('hud:helm-frame-loaded')); helmFrame.addEventListener('error', () => airshipLog('hud:helm-frame-error', {}, 'error')); radarPiece.appendChild(helmFrame); const helmKnob = document.createElement('img'); helmKnob.className = 'steam-helm-knob'; helmKnob.src = helmKnobSrc; helmKnob.alt = ''; helmKnob.draggable = false; helmKnob.addEventListener('load', () => airshipLog('hud:helm-knob-loaded')); helmKnob.addEventListener('error', () => airshipLog('hud:helm-knob-error', {}, 'error')); radarPiece.appendChild(helmKnob); radarPiece.insertAdjacentHTML('beforeend', `