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,/);
});