Sharpen player health HUD
This commit is contained in:
parent
7395b5b125
commit
4ee9aa036b
1
src/assets/controller/player-health-hq-frame.js
Normal file
1
src/assets/controller/player-health-hq-frame.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,7 +1,5 @@
|
|||||||
import topHudSrc from './assets/controller/top-hud.js';
|
import topHudSrc from './assets/controller/top-hud.js';
|
||||||
import playerHealthFullSrc from './assets/controller/player-health-frame-v2-00.js';
|
import playerHealthFrameSrc from './assets/controller/player-health-hq-frame.js';
|
||||||
import playerHealthMidSrc from './assets/controller/player-health-frame-v2-02.js';
|
|
||||||
import playerHealthLowSrc from './assets/controller/player-health-frame-v2-03.js';
|
|
||||||
import enemyHealthSrc from './assets/controller/enemy-health.js';
|
import enemyHealthSrc from './assets/controller/enemy-health.js';
|
||||||
import cannonRoundelSrc from './assets/controller/cannon-roundel.js';
|
import cannonRoundelSrc from './assets/controller/cannon-roundel.js';
|
||||||
import helmFrameSrc from './assets/controller/helm-frame.js';
|
import helmFrameSrc from './assets/controller/helm-frame.js';
|
||||||
@ -93,6 +91,22 @@ if (!isDesktopPointer.matches) {
|
|||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.steam-player-health-fill {
|
||||||
|
position: absolute;
|
||||||
|
left: 41.2%;
|
||||||
|
top: 60.2%;
|
||||||
|
width: 45.4%;
|
||||||
|
height: 16.4%;
|
||||||
|
border-radius: 999px;
|
||||||
|
transform: scaleX(1);
|
||||||
|
transform-origin: left center;
|
||||||
|
background: linear-gradient(180deg, #62ff39 0%, #10d326 100%);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(255,255,255,.6), 0 0 4px rgba(68,255,68,.28);
|
||||||
|
transition: transform 150ms ease-out, background 120ms linear;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.steam-player-card.steam-hit {
|
.steam-player-card.steam-hit {
|
||||||
animation: steam-player-hit 220ms ease-out;
|
animation: steam-player-hit 220ms ease-out;
|
||||||
}
|
}
|
||||||
@ -112,6 +126,7 @@ if (!isDesktopPointer.matches) {
|
|||||||
.steam-card-name,
|
.steam-card-name,
|
||||||
.steam-card-value {
|
.steam-card-value {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -566,7 +581,8 @@ if (!isDesktopPointer.matches) {
|
|||||||
return piece;
|
return piece;
|
||||||
};
|
};
|
||||||
|
|
||||||
const playerHealthPiece = addHealthPiece('steam-player-card', playerHealthFullSrc, `
|
const playerHealthPiece = addHealthPiece('steam-player-card', playerHealthFrameSrc, `
|
||||||
|
<span class="steam-player-health-fill" data-steam="player-health-fill"></span>
|
||||||
<span class="steam-card-name">SPARROW</span>
|
<span class="steam-card-name">SPARROW</span>
|
||||||
<span class="steam-card-value" data-steam="player-hull">100/100</span>
|
<span class="steam-card-value" data-steam="player-hull">100/100</span>
|
||||||
`);
|
`);
|
||||||
@ -659,15 +675,15 @@ if (!isDesktopPointer.matches) {
|
|||||||
piece.style.backgroundPosition = `${(col / 3) * 100}% ${(row / 2) * 100}%`;
|
piece.style.backgroundPosition = `${(col / 3) * 100}% ${(row / 2) * 100}%`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPlayerHealthImage = (current, max) => {
|
const setPlayerHealthLevel = (current, max) => {
|
||||||
const pct = max > 0 ? clamp01(current / max) : 0;
|
const pct = max > 0 ? clamp01(current / max) : 0;
|
||||||
const src = pct > 0.66
|
const fill = fields['player-health-fill'];
|
||||||
? playerHealthFullSrc
|
fill.style.transform = `scaleX(${pct})`;
|
||||||
|
fill.style.background = pct > 0.66
|
||||||
|
? 'linear-gradient(180deg, #62ff39 0%, #10d326 100%)'
|
||||||
: pct > 0.33
|
: pct > 0.33
|
||||||
? playerHealthMidSrc
|
? 'linear-gradient(180deg, #fff23f 0%, #e6c20d 100%)'
|
||||||
: playerHealthLowSrc;
|
: 'linear-gradient(180deg, #ff6b2f 0%, #e72418 100%)';
|
||||||
playerHealthPiece.style.backgroundImage = `url("${src}")`;
|
|
||||||
playerHealthPiece.style.backgroundPosition = 'center';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRadar = (contacts = []) => {
|
const updateRadar = (contacts = []) => {
|
||||||
@ -719,7 +735,7 @@ if (!isDesktopPointer.matches) {
|
|||||||
fields['enemy-name'].textContent = target?.name || 'PIRATE';
|
fields['enemy-name'].textContent = target?.name || 'PIRATE';
|
||||||
fields['enemy-hull'].textContent = target ? `${Math.max(0, target.hull || 0)}/${Math.max(1, target.maxHull || 120)}` : 'CLEAR';
|
fields['enemy-hull'].textContent = target ? `${Math.max(0, target.hull || 0)}/${Math.max(1, target.maxHull || 120)}` : 'CLEAR';
|
||||||
|
|
||||||
setPlayerHealthImage(playerHull, playerMaxHull);
|
setPlayerHealthLevel(playerHull, playerMaxHull);
|
||||||
setHealthFrame(
|
setHealthFrame(
|
||||||
enemyHealthPiece,
|
enemyHealthPiece,
|
||||||
target ? healthFrame(target.hull || 0, target.maxHull || 120) : 11,
|
target ? healthFrame(target.hull || 0, target.maxHull || 120) : 11,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import cannonRoundel from '../src/assets/controller/cannon-roundel.js';
|
|||||||
import helmFrame from '../src/assets/controller/helm-frame.js';
|
import helmFrame from '../src/assets/controller/helm-frame.js';
|
||||||
import helmKnob from '../src/assets/controller/helm-knob.js';
|
import helmKnob from '../src/assets/controller/helm-knob.js';
|
||||||
import playerHealth from '../src/assets/controller/player-health.js';
|
import playerHealth from '../src/assets/controller/player-health.js';
|
||||||
|
import playerHealthHqFrame from '../src/assets/controller/player-health-hq-frame.js';
|
||||||
import enemyHealth from '../src/assets/controller/enemy-health.js';
|
import enemyHealth from '../src/assets/controller/enemy-health.js';
|
||||||
|
|
||||||
const assets = {
|
const assets = {
|
||||||
@ -106,3 +107,11 @@ test('enemyHealth health sheet stays high resolution', () => {
|
|||||||
assert.ok(width / 4 >= 300, 'enemyHealth frame width must stay >= 300px');
|
assert.ok(width / 4 >= 300, 'enemyHealth frame width must stay >= 300px');
|
||||||
assert.ok(height / 3 >= 100, 'enemyHealth frame height must stay >= 100px');
|
assert.ok(height / 3 >= 100, 'enemyHealth frame height must stay >= 100px');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('high-resolution player health frame is complete and retina-ready', () => {
|
||||||
|
decodeWebP('playerHealthHqFrame', playerHealthHqFrame);
|
||||||
|
const { width, height } = webpDimensions('playerHealthHqFrame', playerHealthHqFrame);
|
||||||
|
assert.ok(width >= 600, `playerHealthHqFrame width must stay >= 600px, got ${width}px`);
|
||||||
|
assert.ok(height >= 216, `playerHealthHqFrame height must stay >= 216px, got ${height}px`);
|
||||||
|
});
|
||||||
|
|||||||
@ -65,8 +65,8 @@ test('wide-screen HUD caps portrait-scaled artwork instead of stretching with vi
|
|||||||
const source = await read('src/mobile-hud.js');
|
const source = await read('src/mobile-hud.js');
|
||||||
|
|
||||||
assert.match(source, /\.steam-top \{[\s\S]*?width: min\(64vw, 560px\)/);
|
assert.match(source, /\.steam-top \{[\s\S]*?width: min\(64vw, 560px\)/);
|
||||||
assert.match(source, /\.steam-player-card \{[\s\S]*?width: min\(34vw, 360px\)/);
|
assert.match(source, /@media \(min-width: 700px\)[\s\S]*?\.steam-player-card \{[\s\S]*?width: min\(18vw, 180px\)/);
|
||||||
assert.match(source, /\.steam-enemy-card \{[\s\S]*?width: min\(30vw, 330px\)/);
|
assert.match(source, /@media \(min-width: 700px\)[\s\S]*?\.steam-enemy-card \{[\s\S]*?width: min\(18vw, 180px\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('non-lethal player hit processing does not mutate the player object or game state', async () => {
|
test('non-lethal player hit processing does not mutate the player object or game state', async () => {
|
||||||
@ -125,24 +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\)/);
|
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 uploaded player and enemy health sprite sheets', async () => {
|
test('mobile HUD uses a high-resolution player frame with continuous health fill', async () => {
|
||||||
const source = await read('src/mobile-hud.js');
|
const source = await read('src/mobile-hud.js');
|
||||||
|
|
||||||
assert.match(source, /import playerHealthSrc from '.\/assets\/controller\/player-health\.js'/);
|
assert.match(source, /import playerHealthFrameSrc from '.\/assets\/controller\/player-health-hq-frame\.js'/);
|
||||||
assert.match(source, /import enemyHealthSrc from '.\/assets\/controller\/enemy-health\.js'/);
|
assert.match(source, /import enemyHealthSrc from '.\/assets\/controller\/enemy-health\.js'/);
|
||||||
assert.doesNotMatch(source, /import leftStatusSrc/);
|
assert.match(source, /addHealthPiece\('steam-player-card', playerHealthFrameSrc/);
|
||||||
assert.doesNotMatch(source, /import rightControlsSrc/);
|
assert.match(source, /data-steam="player-health-fill"/);
|
||||||
assert.match(source, /addHealthPiece\('steam-player-card', playerHealthSrc/);
|
assert.match(source, /const setPlayerHealthLevel = \(current, max\) =>/);
|
||||||
assert.match(source, /addHealthPiece\('steam-enemy-card', enemyHealthSrc/);
|
assert.match(source, /fill\.style\.transform = \`scaleX\(\$\{pct\}\)\`/);
|
||||||
assert.match(source, /const healthFrame = \(current, max\) =>/);
|
assert.match(source, /setPlayerHealthLevel\(playerHull, playerMaxHull\)/);
|
||||||
assert.match(source, /const setHealthFrame = \(piece, frame\) =>/);
|
assert.match(source, /\.steam-player-health-fill \{[\s\S]*?width: 45\.4%/);
|
||||||
assert.match(source, /\.steam-player-card \{[\s\S]*?background-size: 100% 100%/);
|
assert.doesNotMatch(source, /playerHealthFullSrc|playerHealthMidSrc|playerHealthLowSrc/);
|
||||||
assert.match(source, /width: min\(31vw, 120px\)/);
|
|
||||||
assert.match(source, /setPlayerHealthImage\(playerHull, playerMaxHull\)/);
|
|
||||||
assert.match(source, /setHealthFrame\([\s\S]*?enemyHealthPiece,[\s\S]*?target \? healthFrame\(target\.hull \|\| 0, target\.maxHull \|\| 120\) : 11/);
|
|
||||||
assert.doesNotMatch(source, /player-health-cover/);
|
|
||||||
assert.doesNotMatch(source, /player-health-tint/);
|
|
||||||
assert.doesNotMatch(source, /steam-bar-fill/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uploaded top HUD uses its new aspect ratio', async () => {
|
test('uploaded top HUD uses its new aspect ratio', async () => {
|
||||||
@ -251,12 +245,12 @@ test('player overlap resolves an explicitly tagged pirate projectile before queu
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('player health uses discrete full-resolution frame images instead of sprite-sheet magnification', async () => {
|
test('player health frame stays static while CSS fill tracks exact hull percentage', async () => {
|
||||||
const source = await read('src/mobile-hud.js');
|
const source = await read('src/mobile-hud.js');
|
||||||
|
|
||||||
assert.match(source, /player-health-frame-v2-00\.js/);
|
assert.match(source, /player-health-hq-frame\.js/);
|
||||||
assert.match(source, /player-health-frame-v2-02\.js/);
|
assert.match(source, /const setPlayerHealthLevel = \(current, max\) =>/);
|
||||||
assert.match(source, /player-health-frame-v2-03\.js/);
|
assert.match(source, /fill\.style\.transform = \`scaleX\(\$\{pct\}\)\`/);
|
||||||
assert.match(source, /const setPlayerHealthImage = \(current, max\)/);
|
assert.doesNotMatch(source, /setPlayerHealthImage/);
|
||||||
assert.doesNotMatch(source, /setHealthFrame\(playerHealthPiece/);
|
assert.doesNotMatch(source, /player-health-frame-v2-/);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user