From 1c879345c00826974b92149b6ad5133b1e15e37c Mon Sep 17 00:00:00 2001 From: Admin Date: Fri, 18 Sep 2026 22:33:25 +0000 Subject: [PATCH] Match player and enemy health HUD styling --- src/mobile-hud.js | 59 ++++++++++++++++++++++++++++++++++++------- tests/wiring.test.mjs | 19 ++++++++------ 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/mobile-hud.js b/src/mobile-hud.js index c804f77..3aeda4d 100644 --- a/src/mobile-hud.js +++ b/src/mobile-hud.js @@ -1,5 +1,4 @@ import topHudSrc from './assets/controller/top-hud.js'; -import playerHealthSrc from './assets/controller/player-health.js'; import enemyHealthSrc from './assets/controller/enemy-health.js'; import cannonRoundelSrc from './assets/controller/cannon-roundel.js'; import helmFrameSrc from './assets/controller/helm-frame.js'; @@ -79,16 +78,52 @@ if (!isDesktopPointer.matches) { top: clamp(80px, 10.3vh, 90px); aspect-ratio: 360 / 130; width: min(31vw, 120px); - background-repeat: no-repeat; - background-position: center; - background-color: transparent; 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%; - background-size: 400% 300%; + } + + .steam-player-card .steam-health-art { + transform: scaleX(-1); + } + + .steam-player-badge { + position: absolute; + left: 16.5%; + top: 51%; + width: 27%; + aspect-ratio: 1; + transform: translate(-50%, -50%); + z-index: 1; + display: grid; + place-items: center; + border: 1px solid rgba(235, 190, 92, .92); + border-radius: 50%; + background: + radial-gradient(circle at 42% 35%, rgba(31, 128, 126, .96) 0 34%, rgba(7, 61, 67, .98) 65% 100%); + box-shadow: + inset 0 0 0 2px rgba(94, 47, 9, .72), + 0 1px 3px rgba(0, 0, 0, .5); + color: #f1c86c; + font-family: Georgia, 'Times New Roman', serif; + font-size: clamp(11px, 4vw, 18px); + line-height: 1; + text-shadow: 0 1px 2px #2b1303; + pointer-events: none; } .steam-player-card.steam-hit { @@ -104,7 +139,6 @@ if (!isDesktopPointer.matches) { .steam-enemy-card { right: 1%; - background-size: 400% 300%; } .steam-card-name, @@ -553,7 +587,11 @@ if (!isDesktopPointer.matches) { const addHealthPiece = (className, src, inner = '') => { const piece = document.createElement('div'); piece.className = `steam-piece ${className}`; - piece.style.backgroundImage = `url("${src}")`; + + 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 })); @@ -565,7 +603,8 @@ if (!isDesktopPointer.matches) { return piece; }; - const playerHealthPiece = addHealthPiece('steam-player-card', playerHealthSrc, ` + const playerHealthPiece = addHealthPiece('steam-player-card', enemyHealthSrc, ` + SPARROW 100/100 `); @@ -655,7 +694,9 @@ if (!isDesktopPointer.matches) { const setHealthFrame = (piece, frame) => { const col = frame % 4; const row = Math.floor(frame / 4); - piece.style.backgroundPosition = `${(col / 3) * 100}% ${(row / 2) * 100}%`; + const art = piece.querySelector('.steam-health-art'); + if (!art) return; + art.style.backgroundPosition = `${(col / 3) * 100}% ${(row / 2) * 100}%`; }; const updateRadar = (contacts = []) => { diff --git a/tests/wiring.test.mjs b/tests/wiring.test.mjs index 394485e..ac30e31 100644 --- a/tests/wiring.test.mjs +++ b/tests/wiring.test.mjs @@ -125,16 +125,18 @@ test('portrait health sprites sit tightly beneath the top HUD', async () => { assert.match(source, /@media \(min-width: 700px\)[\s\S]*?\.steam-player-card,[\s\S]*?\.steam-enemy-card \{[\s\S]*?top: clamp\(70px, 20vh, 104px\)/); }); -test('mobile HUD uses the player sprite sheet for health states', async () => { +test('player and enemy health cards share one visual family', async () => { const source = await read('src/mobile-hud.js'); - assert.match(source, /import playerHealthSrc from '.\/assets\/controller\/player-health\.js'/); assert.match(source, /import enemyHealthSrc from '.\/assets\/controller\/enemy-health\.js'/); - assert.match(source, /addHealthPiece\('steam-player-card', playerHealthSrc/); - assert.match(source, /\.steam-player-card \{[\s\S]*?background-size: 400% 300%/); + assert.match(source, /addHealthPiece\('steam-player-card', enemyHealthSrc/); + assert.match(source, /addHealthPiece\('steam-enemy-card', enemyHealthSrc/); + assert.match(source, /\.steam-player-card \.steam-health-art \{[\s\S]*?transform: scaleX\(-1\)/); + assert.match(source, /class="steam-player-badge"/); + assert.match(source, /\.steam-health-art \{[\s\S]*?background-size: 400% 300%/); assert.match(source, /setHealthFrame\(\s*playerHealthPiece,\s*healthFrame\(playerHull, playerMaxHull\)/); + assert.doesNotMatch(source, /playerHealthSrc/); assert.doesNotMatch(source, /player-health-hq-frame\.js/); - assert.doesNotMatch(source, /steam-player-health-fill/); }); test('uploaded top HUD uses its new aspect ratio', async () => { @@ -243,12 +245,13 @@ test('player overlap resolves an explicitly tagged pirate projectile before queu }); -test('player health uses the same 4x3 sprite frame addressing as enemy health', async () => { +test('health frame positioning is applied to the shared art layer', async () => { const source = await read('src/mobile-hud.js'); assert.match(source, /const healthFrame = \(current, max\) =>/); assert.match(source, /const setHealthFrame = \(piece, frame\) =>/); + assert.match(source, /piece\.querySelector\('\.steam-health-art'\)/); + assert.match(source, /art\.style\.backgroundPosition/); assert.match(source, /setHealthFrame\(\s*playerHealthPiece,/); - assert.match(source, /background-size: 400% 300%/); - assert.doesNotMatch(source, /setPlayerHealthLevel/); + assert.match(source, /setHealthFrame\(\s*enemyHealthPiece,/); });