Use component steampunk controller HUD

This commit is contained in:
Admin 2026-09-17 22:55:48 +00:00
parent 820abc86e8
commit 974284c624

View File

@ -1,5 +1,10 @@
import Phaser from 'phaser'; 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)'); const isDesktopPointer = window.matchMedia('(hover: hover) and (pointer: fine)');
@ -9,106 +14,236 @@ if (!isDesktopPointer.matches) {
if (root) { if (root) {
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = ` style.textContent = `
#mobile-controls.steampunk-active { #mobile-controls.controller-active {
overflow: hidden; overflow: hidden;
} }
#steampunk-hud-frame { #controller-hud {
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; position: absolute;
inset: 0; inset: 0;
z-index: 1; z-index: 1;
overflow: hidden;
pointer-events: none; pointer-events: none;
color: #f7e3b0; user-select: none;
-webkit-user-select: none;
color: #f8e8b7;
font-family: Georgia, 'Times New Roman', serif; font-family: Georgia, 'Times New Roman', serif;
font-weight: 700; 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; 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%); transform: translate(-50%, -50%);
min-width: 42px; min-width: 32px;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
} color: #ffe6a1;
font-size: clamp(9px, 2.7vw, 13px);
.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; line-height: 1;
} }
.steam-speed { left: 9.5%; top: 8.5%; } .steam-speed { left: 9.3%; }
.steam-heading { left: 20.5%; top: 8.5%; } .steam-heading { left: 21.2%; }
.steam-wave { left: 50%; top: 9.4%; } .steam-wave { left: 50%; top: 72%; }
.steam-hostiles { left: 65.6%; top: 8.5%; } .steam-hostiles { left: 65.4%; }
.steam-kills { left: 78.6%; top: 8.5%; } .steam-kills { left: 78.2%; }
.steam-range { left: 91.2%; top: 8.5%; } .steam-range { left: 91.1%; }
.steam-hull-name, .steam-player-card {
.steam-hull-value { 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; position: absolute;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
white-space: nowrap; white-space: nowrap;
text-align: center;
} }
.steam-hull-name { .steam-card-name {
top: 14.25%; color: #f6d58e;
color: #f2d394; font-size: clamp(8px, 2.2vw, 11px);
font-size: clamp(8px, 2.3vw, 12px); letter-spacing: .06em;
letter-spacing: .04em;
} }
.steam-player-name { left: 25%; } .steam-player-card .steam-card-name {
.steam-enemy-name { left: 72%; } left: 58%;
top: 31%;
.steam-hull-value {
top: 17.15%;
font-size: clamp(8px, 2.35vw, 12px);
} }
.steam-player-value { left: 38.2%; } .steam-enemy-card .steam-card-name {
.steam-enemy-value { left: 76.5%; } 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; position: absolute;
top: 16.55%; overflow: hidden;
height: 1.22%;
min-height: 5px;
transform-origin: left center;
border-radius: 999px; border-radius: 999px;
background: linear-gradient(180deg, #ff684f 0%, #c11f23 55%, #6f1016 100%); pointer-events: none;
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-player-card .steam-bar {
.steam-enemy-fill { left: 57.1%; width: 27.4%; } 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 { .steam-status {
display: none; display: none;
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 22%; top: 27%;
transform: translateX(-50%); transform: translateX(-50%);
max-width: 72%; max-width: 72%;
padding: 7px 12px; padding: 7px 12px;
@ -122,16 +257,19 @@ if (!isDesktopPointer.matches) {
box-shadow: 0 4px 18px rgba(0,0,0,.35); 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 { #mobile-controls.controller-active #drag-stick {
left: 1%; left: 0;
top: 70.5%; bottom: max(2px, env(safe-area-inset-bottom));
bottom: auto; top: auto;
width: 48%; width: 48%;
height: 28.5%; height: auto;
aspect-ratio: 378 / 192;
border: 0; border: 0;
border-radius: 45%; border-radius: 34%;
background: transparent; background: transparent;
box-shadow: none; box-shadow: none;
backdrop-filter: none; backdrop-filter: none;
@ -139,14 +277,15 @@ if (!isDesktopPointer.matches) {
z-index: 3; z-index: 3;
} }
#mobile-controls.steampunk-active #drag-stick::before, #mobile-controls.controller-active #drag-stick::before,
#mobile-controls.steampunk-active #drag-stick::after, #mobile-controls.controller-active #drag-stick::after,
#mobile-controls.steampunk-active #drag-thumb { #mobile-controls.controller-active #drag-thumb {
opacity: 0; opacity: 0;
} }
#mobile-controls.steampunk-active .mobile-control { #mobile-controls.controller-active .mobile-control {
min-width: 0; min-width: 0;
height: auto;
padding: 0; padding: 0;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
@ -160,74 +299,148 @@ if (!isDesktopPointer.matches) {
z-index: 3; z-index: 3;
} }
#mobile-controls.steampunk-active .mobile-control.active, #mobile-controls.controller-active .mobile-control.active,
#mobile-controls.steampunk-active .mobile-control:active { #mobile-controls.controller-active .mobile-control:active {
background: rgba(255, 210, 112, .08); background: rgba(255, 210, 112, .08);
transform: none; transform: none;
} }
#mobile-controls.steampunk-active .mobile-control.port { #mobile-controls.controller-active .mobile-control.port,
left: 57%; #mobile-controls.controller-active .mobile-control.starboard {
top: 75.2%; left: auto;
right: auto; right: 0;
bottom: auto; width: 45%;
width: 42%; aspect-ratio: 359 / 201;
height: 10.5%;
} }
#mobile-controls.steampunk-active .mobile-control.starboard { #mobile-controls.controller-active .mobile-control.port {
left: 57%; top: auto;
top: 86.3%; bottom: max(clamp(88px, 11.8vh, 108px), calc(env(safe-area-inset-bottom) + 86px));
right: auto;
bottom: auto;
width: 42%;
height: 10.5%;
} }
#mobile-controls.steampunk-active .mobile-control.restart { #mobile-controls.controller-active .mobile-control.starboard {
left: 80.5%; top: auto;
top: 18.2%; bottom: max(2px, env(safe-area-inset-bottom));
right: auto; }
#mobile-controls.controller-active .mobile-control.restart {
left: auto;
right: 1%;
top: clamp(147px, 20.5vh, 179px);
bottom: auto; bottom: auto;
width: 18%; width: 14%;
height: 13.5%; height: auto;
aspect-ratio: 1;
border-radius: 50%; 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); document.head.appendChild(style);
root.classList.add('steampunk-active'); root.classList.add('controller-active');
const frame = document.createElement('img'); const hud = document.createElement('div');
frame.id = 'steampunk-hud-frame'; hud.id = 'controller-hud';
frame.src = hudFrameSrc; root.prepend(hud);
frame.alt = '';
frame.draggable = false;
root.prepend(frame);
const liveHud = document.createElement('div'); const addPiece = (className, src, inner = '') => {
liveHud.id = 'steampunk-live-hud'; const piece = document.createElement('div');
liveHud.innerHTML = ` piece.className = `steam-piece ${className}`;
<div class="steam-stat steam-speed"><span class="label">SPEED</span><span class="value" data-steam="speed">0</span></div>
<div class="steam-stat steam-heading"><span class="label">HEADING</span><span class="value" data-steam="heading">000°</span></div> const image = document.createElement('img');
<div class="steam-stat steam-wave"><span class="label">WAVE</span><span class="value" data-steam="wave">1</span></div> image.src = src;
<div class="steam-stat steam-hostiles"><span class="label">HOSTILES</span><span class="value" data-steam="hostiles">0</span></div> image.alt = '';
<div class="steam-stat steam-kills"><span class="label">KILLS</span><span class="value" data-steam="kills">0</span></div> image.draggable = false;
<div class="steam-stat steam-range"><span class="label">RANGE</span><span class="value" data-steam="range">--</span></div> piece.appendChild(image);
<div class="steam-hull-name steam-player-name">SPARROW</div>
<div class="steam-hull-name steam-enemy-name">PIRATE</div> if (inner) piece.insertAdjacentHTML('beforeend', inner);
<div class="steam-hull-fill steam-player-fill" data-steam="player-fill"></div> hud.appendChild(piece);
<div class="steam-hull-fill steam-enemy-fill" data-steam="enemy-fill"></div> return piece;
<div class="steam-hull-value steam-player-value" data-steam="player-hull">100/100</div> };
<div class="steam-hull-value steam-enemy-value" data-steam="enemy-hull">--</div>
<div class="steam-status" data-steam="status"></div> addPiece('steam-top', topHudSrc, `
`; <span class="steam-top-value steam-speed" data-steam="speed">0</span>
root.appendChild(liveHud); <span class="steam-top-value steam-heading" data-steam="heading">000°</span>
<span class="steam-top-value steam-wave" data-steam="wave">1</span>
<span class="steam-top-value steam-hostiles" data-steam="hostiles">0</span>
<span class="steam-top-value steam-kills" data-steam="kills">0</span>
<span class="steam-top-value steam-range" data-steam="range">--</span>
`);
addPiece('steam-player-card', leftStatusSrc, `
<span class="steam-card-name">SPARROW</span>
<div class="steam-bar"><div class="steam-bar-fill" data-steam="player-fill"></div></div>
<span class="steam-card-value" data-steam="player-hull">100/100</span>
`);
addPiece('steam-enemy-card', rightControlsSrc, `
<span class="steam-card-name">PIRATE</span>
<div class="steam-bar"><div class="steam-bar-fill" data-steam="enemy-fill"></div></div>
<span class="steam-card-value" data-steam="enemy-hull">CLEAR</span>
`);
const radarPiece = addPiece('steam-radar-wrap', radarPanelSrc, `
<div id="steam-radar-contacts">
<span class="steam-radar-player"></span>
</div>
`);
addPiece('steam-helm-wrap', helmActionSrc, `
<span class="steam-action-label">HELM</span>
<span class="steam-action-sub">DRAG TO STEER</span>
`);
addPiece('steam-cannon-port', cannonActionSrc, `
<span class="steam-action-label">PORT FIRE</span>
<span class="steam-action-sub">BROADSIDE</span>
`);
addPiece('steam-cannon-starboard', cannonActionSrc, `
<span class="steam-action-label">STBD FIRE</span>
<span class="steam-action-sub">BROADSIDE</span>
`);
const status = document.createElement('div');
status.className = 'steam-status';
status.dataset.steam = 'status';
hud.appendChild(status);
const fields = Object.fromEntries( 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 = [ const oldHudKeys = [
'hudGraphics', 'hudGraphics',
'brandText', 'brandText',
@ -255,8 +468,33 @@ if (!isDesktopPointer.matches) {
const clamp01 = (value) => Math.max(0, Math.min(1, value)); 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 update = () => {
const scene = getScene(); const scene = getScene();
if (scene?.player) { if (scene?.player) {
hideLegacyHud(scene); hideLegacyHud(scene);
@ -268,7 +506,7 @@ if (!isDesktopPointer.matches) {
const playerHull = Math.max(0, scene.playerHull ?? 100); const playerHull = Math.max(0, scene.playerHull ?? 100);
const enemyHull = pirate ? Math.max(0, pirate.getData?.('hull') ?? 0) : 0; 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.heading.textContent = `${heading.toFixed(0).padStart(3, '0')}°`;
fields.wave.textContent = String(scene.wave || 1); fields.wave.textContent = String(scene.wave || 1);
fields.hostiles.textContent = String(activePirates); fields.hostiles.textContent = String(activePirates);
@ -282,6 +520,8 @@ if (!isDesktopPointer.matches) {
const statusText = scene.status?.text?.trim?.() || ''; const statusText = scene.status?.text?.trim?.() || '';
fields.status.textContent = statusText; fields.status.textContent = statusText;
fields.status.classList.toggle('visible', Boolean(statusText)); fields.status.classList.toggle('visible', Boolean(statusText));
updateRadar(scene);
} }
requestAnimationFrame(update); requestAnimationFrame(update);