Map HUD to game state and animate player hits
This commit is contained in:
parent
6b7cf5cdaf
commit
e0508c0d43
@ -49,6 +49,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.playerBroadsideReloadMs = PLAYER_BROADSIDE_RELOAD_MS;
|
this.playerBroadsideReloadMs = PLAYER_BROADSIDE_RELOAD_MS;
|
||||||
this.helmVector = { x: 0, y: 0 };
|
this.helmVector = { x: 0, y: 0 };
|
||||||
this.pendingPlayerHits = [];
|
this.pendingPlayerHits = [];
|
||||||
|
this.playerHitSequence = 0;
|
||||||
this.lastDiagnosticsAt = -Infinity;
|
this.lastDiagnosticsAt = -Infinity;
|
||||||
this.wave = 1;
|
this.wave = 1;
|
||||||
this.defeated = 0;
|
this.defeated = 0;
|
||||||
@ -78,6 +79,8 @@ class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
this.player = this.physics.add.sprite(1200, 1550, 'airships', 0);
|
this.player = this.physics.add.sprite(1200, 1550, 'airships', 0);
|
||||||
this.player.setDepth(6).setCollideWorldBounds(true).setDisplaySize(74, 74);
|
this.player.setDepth(6).setCollideWorldBounds(true).setDisplaySize(74, 74);
|
||||||
|
this.playerBaseScaleX = this.player.scaleX;
|
||||||
|
this.playerBaseScaleY = this.player.scaleY;
|
||||||
this.player.body.setAllowGravity(false);
|
this.player.body.setAllowGravity(false);
|
||||||
this.player.body.setSize(42, 56, true);
|
this.player.body.setSize(42, 56, true);
|
||||||
this.player.play('player-idle');
|
this.player.play('player-idle');
|
||||||
@ -367,12 +370,14 @@ class GameScene extends Phaser.Scene {
|
|||||||
if (this.gameState !== 'playing') {
|
if (this.gameState !== 'playing') {
|
||||||
if (Phaser.Input.Keyboard.JustDown(this.keys.restart)) this.scene.restart();
|
if (Phaser.Input.Keyboard.JustDown(this.keys.restart)) this.scene.restart();
|
||||||
this.drawHud();
|
this.drawHud();
|
||||||
|
this.publishHudState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processPlayerHits();
|
this.processPlayerHits();
|
||||||
if (this.gameState !== 'playing') {
|
if (this.gameState !== 'playing') {
|
||||||
this.drawHud();
|
this.drawHud();
|
||||||
|
this.publishHudState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,6 +401,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.drawWeaponArcs();
|
this.drawWeaponArcs();
|
||||||
this.drawHud();
|
this.drawHud();
|
||||||
this.checkGameState();
|
this.checkGameState();
|
||||||
|
this.publishHudState();
|
||||||
|
|
||||||
if (hadQueuedPlayerHit) {
|
if (hadQueuedPlayerHit) {
|
||||||
airshipLog('combat:post-hit-frame-complete', {
|
airshipLog('combat:post-hit-frame-complete', {
|
||||||
@ -630,7 +636,8 @@ class GameScene extends Phaser.Scene {
|
|||||||
for (const impact of hits) {
|
for (const impact of hits) {
|
||||||
this.spawnExplosion(impact.x, impact.y, 0.32);
|
this.spawnExplosion(impact.x, impact.y, 0.32);
|
||||||
}
|
}
|
||||||
this.flashShip(this.player, 0xffcccc);
|
this.playerHitSequence += 1;
|
||||||
|
this.playPlayerHitReaction(totalDamage);
|
||||||
airshipLog('combat:player-hit-fx', {
|
airshipLog('combat:player-hit-fx', {
|
||||||
impacts: hits.length,
|
impacts: hits.length,
|
||||||
scale: 0.32,
|
scale: 0.32,
|
||||||
@ -678,6 +685,81 @@ class GameScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playPlayerHitReaction(damage = PIRATE_SHOT_DAMAGE) {
|
||||||
|
if (!this.player?.active) return;
|
||||||
|
|
||||||
|
this.tweens.killTweensOf(this.player);
|
||||||
|
this.player.setScale(this.playerBaseScaleX, this.playerBaseScaleY);
|
||||||
|
this.player.setTint(0xffd7d7);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: this.player,
|
||||||
|
scaleX: this.playerBaseScaleX * 1.12,
|
||||||
|
scaleY: this.playerBaseScaleY * 1.12,
|
||||||
|
alpha: 0.62,
|
||||||
|
duration: 70,
|
||||||
|
yoyo: true,
|
||||||
|
ease: 'Quad.easeOut',
|
||||||
|
onComplete: () => {
|
||||||
|
if (!this.player?.active) return;
|
||||||
|
this.player.setScale(this.playerBaseScaleX, this.playerBaseScaleY);
|
||||||
|
this.player.setAlpha(1);
|
||||||
|
this.player.clearTint();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cameras.main.shake(120, clamp(0.0035 + damage * 0.00055, 0.004, 0.012));
|
||||||
|
}
|
||||||
|
|
||||||
|
buildHudState() {
|
||||||
|
const player = this.player;
|
||||||
|
if (!player) return null;
|
||||||
|
|
||||||
|
const activePirates = (this.pirates?.getChildren?.() || []).filter((pirate) => pirate?.active);
|
||||||
|
const { pirate: nearest, range } = this.nearestPirate();
|
||||||
|
const now = this.time?.now ?? 0;
|
||||||
|
const reloadReady = (side) => clamp(
|
||||||
|
(now - (this.lastPlayerShot?.[side] ?? -Infinity)) / this.playerBroadsideReloadMs,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
gameState: this.gameState,
|
||||||
|
speed: this.playerSpeed,
|
||||||
|
heading: normalizeDegrees(player.angle),
|
||||||
|
wave: this.wave,
|
||||||
|
hostiles: activePirates.length,
|
||||||
|
kills: this.defeated,
|
||||||
|
player: {
|
||||||
|
hull: this.playerHull,
|
||||||
|
maxHull: PLAYER_MAX_HULL,
|
||||||
|
hitSequence: this.playerHitSequence,
|
||||||
|
},
|
||||||
|
target: nearest ? {
|
||||||
|
hull: nearest.getData('hull') || 0,
|
||||||
|
maxHull: nearest.getData('maxHull') || PIRATE_FALLBACK_MAX_HULL,
|
||||||
|
name: nearest.getData('classLabel') || 'PIRATE',
|
||||||
|
range,
|
||||||
|
} : null,
|
||||||
|
reload: {
|
||||||
|
port: reloadReady('port'),
|
||||||
|
starboard: reloadReady('starboard'),
|
||||||
|
},
|
||||||
|
radar: activePirates.slice(0, 10).map((pirate) => ({
|
||||||
|
dx: (pirate.x - player.x) / 900,
|
||||||
|
dy: (pirate.y - player.y) / 900,
|
||||||
|
})),
|
||||||
|
status: this.status?.text || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
publishHudState() {
|
||||||
|
const state = this.buildHudState();
|
||||||
|
if (!state) return;
|
||||||
|
window.dispatchEvent(new CustomEvent('airship:hud-state', { detail: state }));
|
||||||
|
}
|
||||||
|
|
||||||
updateProjectiles() {
|
updateProjectiles() {
|
||||||
for (const group of [this.playerShots, this.pirateShots]) {
|
for (const group of [this.playerShots, this.pirateShots]) {
|
||||||
for (const shot of group.getChildren()) {
|
for (const shot of group.getChildren()) {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import Phaser from 'phaser';
|
|
||||||
import topHudSrc from './assets/controller/top-hud.js';
|
import topHudSrc from './assets/controller/top-hud.js';
|
||||||
import playerHealthSrc from './assets/controller/player-health.js';
|
import playerHealthSrc from './assets/controller/player-health.js';
|
||||||
import enemyHealthSrc from './assets/controller/enemy-health.js';
|
import enemyHealthSrc from './assets/controller/enemy-health.js';
|
||||||
@ -88,7 +87,18 @@ if (!isDesktopPointer.matches) {
|
|||||||
.steam-player-card {
|
.steam-player-card {
|
||||||
left: -1%;
|
left: -1%;
|
||||||
width: 53%;
|
width: 53%;
|
||||||
background-size: 100% 100%;
|
background-size: 400% 300%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
.steam-enemy-card {
|
||||||
@ -97,33 +107,6 @@ if (!isDesktopPointer.matches) {
|
|||||||
background-size: 400% 300%;
|
background-size: 400% 300%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.steam-player-health-tint,
|
|
||||||
.steam-player-health-cover {
|
|
||||||
position: absolute;
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steam-player-health-tint {
|
|
||||||
left: 39.5%;
|
|
||||||
top: 56.5%;
|
|
||||||
width: 49%;
|
|
||||||
height: 19%;
|
|
||||||
transform-origin: left center;
|
|
||||||
background: transparent;
|
|
||||||
mix-blend-mode: color;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steam-player-health-cover {
|
|
||||||
right: 11.5%;
|
|
||||||
top: 56.5%;
|
|
||||||
width: 0;
|
|
||||||
height: 19%;
|
|
||||||
background: #111416;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steam-card-name,
|
.steam-card-name,
|
||||||
.steam-card-value {
|
.steam-card-value {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -582,8 +565,6 @@ if (!isDesktopPointer.matches) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const playerHealthPiece = addHealthPiece('steam-player-card', playerHealthSrc, `
|
const playerHealthPiece = addHealthPiece('steam-player-card', playerHealthSrc, `
|
||||||
<span class="steam-player-health-tint" data-steam="player-health-tint"></span>
|
|
||||||
<span class="steam-player-health-cover" data-steam="player-health-cover"></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>
|
||||||
`);
|
`);
|
||||||
@ -663,31 +644,6 @@ if (!isDesktopPointer.matches) {
|
|||||||
return dot;
|
return dot;
|
||||||
});
|
});
|
||||||
|
|
||||||
const oldHudKeys = [
|
|
||||||
'hudGraphics',
|
|
||||||
'brandText',
|
|
||||||
'speedText',
|
|
||||||
'headingText',
|
|
||||||
'waveText',
|
|
||||||
'combatText',
|
|
||||||
'playerHullLabel',
|
|
||||||
'enemyHullLabel',
|
|
||||||
'controlsHint',
|
|
||||||
'status',
|
|
||||||
];
|
|
||||||
|
|
||||||
const getScene = () => {
|
|
||||||
for (const game of Phaser.GAMES || []) {
|
|
||||||
const scene = game?.scene?.getScene?.('game');
|
|
||||||
if (scene?.sys?.isActive?.()) return scene;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideLegacyHud = (scene) => {
|
|
||||||
for (const key of oldHudKeys) scene[key]?.setVisible?.(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
||||||
|
|
||||||
const healthFrame = (current, max) => {
|
const healthFrame = (current, max) => {
|
||||||
@ -701,18 +657,16 @@ if (!isDesktopPointer.matches) {
|
|||||||
piece.style.backgroundPosition = `${(col / 3) * 100}% ${(row / 2) * 100}%`;
|
piece.style.backgroundPosition = `${(col / 3) * 100}% ${(row / 2) * 100}%`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRadar = (scene) => {
|
const updateRadar = (contacts = []) => {
|
||||||
const pirates = scene.pirates?.getChildren?.().filter((pirate) => pirate?.active) || [];
|
|
||||||
|
|
||||||
radarDots.forEach((dot, index) => {
|
radarDots.forEach((dot, index) => {
|
||||||
const pirate = pirates[index];
|
const contact = contacts[index];
|
||||||
if (!pirate) {
|
if (!contact) {
|
||||||
dot.hidden = true;
|
dot.hidden = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dx = (pirate.x - scene.player.x) / 900;
|
let dx = Number(contact.dx) || 0;
|
||||||
let dy = (pirate.y - scene.player.y) / 900;
|
let dy = Number(contact.dy) || 0;
|
||||||
const distance = Math.hypot(dx, dy);
|
const distance = Math.hypot(dx, dy);
|
||||||
if (distance > 1) {
|
if (distance > 1) {
|
||||||
dx /= distance;
|
dx /= distance;
|
||||||
@ -725,63 +679,58 @@ if (!isDesktopPointer.matches) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const update = () => {
|
let lastPlayerHitSequence = 0;
|
||||||
const scene = getScene();
|
|
||||||
|
|
||||||
if (scene?.player) {
|
const pulsePlayerHit = (sequence) => {
|
||||||
hideLegacyHud(scene);
|
if (!Number.isFinite(sequence) || sequence <= lastPlayerHitSequence) return;
|
||||||
|
lastPlayerHitSequence = sequence;
|
||||||
|
playerHealthPiece.classList.remove('steam-hit');
|
||||||
|
void playerHealthPiece.offsetWidth;
|
||||||
|
playerHealthPiece.classList.add('steam-hit');
|
||||||
|
};
|
||||||
|
|
||||||
const heading = ((scene.player.angle % 360) + 360) % 360;
|
const renderHudState = (state) => {
|
||||||
const activePirates = scene.pirates?.countActive?.(true) || 0;
|
if (!state?.player) return;
|
||||||
const nearest = scene.nearestPirate?.() || { pirate: null, range: Infinity };
|
|
||||||
const pirate = nearest.pirate;
|
|
||||||
const range = nearest.range;
|
|
||||||
const playerHull = Math.max(0, scene.playerHull ?? 100);
|
|
||||||
const enemyHull = pirate ? Math.max(0, pirate.getData?.('hull') ?? 0) : 0;
|
|
||||||
const enemyMaxHull = pirate ? Math.max(1, pirate.getData?.('maxHull') ?? 120) : 120;
|
|
||||||
const enemyName = pirate ? (pirate.getData?.('classLabel') || 'PIRATE') : 'PIRATE';
|
|
||||||
|
|
||||||
fields.speed.textContent = `${Math.abs(scene.playerSpeed || 0).toFixed(0)}${scene.playerSpeed < 0 ? 'R' : ''}`;
|
const playerHull = Math.max(0, state.player.hull ?? 100);
|
||||||
fields.heading.textContent = `${heading.toFixed(0).padStart(3, '0')}°`;
|
const playerMaxHull = Math.max(1, state.player.maxHull ?? 100);
|
||||||
fields.wave.textContent = String(scene.wave || 1);
|
const target = state.target || null;
|
||||||
fields.hostiles.textContent = String(activePirates);
|
|
||||||
fields.kills.textContent = String(scene.defeated || 0);
|
|
||||||
fields.range.textContent = Number.isFinite(range) ? `${Math.round(range)}m` : '--';
|
|
||||||
fields['player-hull'].textContent = `${playerHull}/100`;
|
|
||||||
fields['enemy-name'].textContent = enemyName;
|
|
||||||
fields['enemy-hull'].textContent = pirate ? `${enemyHull}/${enemyMaxHull}` : 'CLEAR';
|
|
||||||
const playerPct = clamp01(playerHull / 100);
|
|
||||||
fields['player-health-cover'].style.width = `${(1 - playerPct) * 49}%`;
|
|
||||||
fields['player-health-tint'].style.transform = `scaleX(${playerPct})`;
|
|
||||||
fields['player-health-tint'].style.background = playerPct > 0.66
|
|
||||||
? 'transparent'
|
|
||||||
: playerPct > 0.33
|
|
||||||
? 'rgba(255, 210, 0, .58)'
|
|
||||||
: 'rgba(255, 45, 18, .72)';
|
|
||||||
setHealthFrame(enemyHealthPiece, pirate ? healthFrame(enemyHull, enemyMaxHull) : 11);
|
|
||||||
|
|
||||||
const reloadMs = Math.max(1, scene.playerBroadsideReloadMs || 850);
|
fields.speed.textContent = `${Math.abs(state.speed || 0).toFixed(0)}${state.speed < 0 ? 'R' : ''}`;
|
||||||
const now = scene.time?.now ?? 0;
|
fields.heading.textContent = `${(state.heading || 0).toFixed(0).padStart(3, '0')}°`;
|
||||||
const portReady = clamp01((now - (scene.lastPlayerShot?.port ?? -Infinity)) / reloadMs);
|
fields.wave.textContent = String(state.wave || 1);
|
||||||
const starboardReady = clamp01((now - (scene.lastPlayerShot?.starboard ?? -Infinity)) / reloadMs);
|
fields.hostiles.textContent = String(state.hostiles || 0);
|
||||||
fields['port-reload'].style.transform = `scaleX(${portReady})`;
|
fields.kills.textContent = String(state.kills || 0);
|
||||||
fields['starboard-reload'].style.transform = `scaleX(${starboardReady})`;
|
fields.range.textContent = target && Number.isFinite(target.range) ? `${Math.round(target.range)}m` : '--';
|
||||||
|
fields['player-hull'].textContent = `${playerHull}/${playerMaxHull}`;
|
||||||
|
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';
|
||||||
|
|
||||||
const statusText = scene.status?.text?.trim?.() || '';
|
setHealthFrame(playerHealthPiece, healthFrame(playerHull, playerMaxHull));
|
||||||
|
setHealthFrame(
|
||||||
|
enemyHealthPiece,
|
||||||
|
target ? healthFrame(target.hull || 0, target.maxHull || 120) : 11,
|
||||||
|
);
|
||||||
|
|
||||||
|
fields['port-reload'].style.transform = `scaleX(${clamp01(state.reload?.port ?? 1)})`;
|
||||||
|
fields['starboard-reload'].style.transform = `scaleX(${clamp01(state.reload?.starboard ?? 1)})`;
|
||||||
|
|
||||||
|
const statusText = String(state.status || '').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);
|
pulsePlayerHit(state.player.hitSequence ?? 0);
|
||||||
}
|
updateRadar(state.radar || []);
|
||||||
|
|
||||||
requestAnimationFrame(update);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.addEventListener('airship:hud-state', (event) => {
|
||||||
|
renderHudState(event.detail);
|
||||||
|
});
|
||||||
|
|
||||||
airshipLog('hud:mounted', {
|
airshipLog('hud:mounted', {
|
||||||
mobile: true,
|
mobile: true,
|
||||||
pieces: hud.querySelectorAll('.steam-piece').length,
|
pieces: hud.querySelectorAll('.steam-piece').length,
|
||||||
});
|
});
|
||||||
|
|
||||||
requestAnimationFrame(update);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,7 +98,9 @@ test('player hit FX runs after queued hit processing, outside the overlap callba
|
|||||||
|
|
||||||
assert.match(source, /this\.pendingPlayerHits\.push\(\{[\s\S]*?x: shot\.x,[\s\S]*?y: shot\.y/);
|
assert.match(source, /this\.pendingPlayerHits\.push\(\{[\s\S]*?x: shot\.x,[\s\S]*?y: shot\.y/);
|
||||||
assert.match(source, /if \(appliedHits > 0\) \{[\s\S]*?for \(const impact of hits\) \{[\s\S]*?this\.spawnExplosion\(impact\.x, impact\.y, 0\.32\)/);
|
assert.match(source, /if \(appliedHits > 0\) \{[\s\S]*?for \(const impact of hits\) \{[\s\S]*?this\.spawnExplosion\(impact\.x, impact\.y, 0\.32\)/);
|
||||||
assert.match(source, /this\.flashShip\(this\.player, 0xffcccc\)/);
|
assert.match(source, /this\.playPlayerHitReaction\(totalDamage\)/);
|
||||||
|
assert.match(source, /this\.cameras\.main\.shake\(120/);
|
||||||
|
assert.match(source, /this\.tweens\.add\(\{/);
|
||||||
assert.match(source, /combat:player-hit-fx-error/);
|
assert.match(source, /combat:player-hit-fx-error/);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -134,10 +136,11 @@ test('mobile HUD uses uploaded player and enemy health sprite sheets', async ()
|
|||||||
assert.match(source, /addHealthPiece\('steam-enemy-card', enemyHealthSrc/);
|
assert.match(source, /addHealthPiece\('steam-enemy-card', enemyHealthSrc/);
|
||||||
assert.match(source, /const healthFrame = \(current, max\) =>/);
|
assert.match(source, /const healthFrame = \(current, max\) =>/);
|
||||||
assert.match(source, /const setHealthFrame = \(piece, frame\) =>/);
|
assert.match(source, /const setHealthFrame = \(piece, frame\) =>/);
|
||||||
assert.match(source, /player-health-cover/);
|
assert.match(source, /\.steam-player-card \{[\s\S]*?background-size: 400% 300%/);
|
||||||
assert.match(source, /player-health-tint/);
|
assert.match(source, /setHealthFrame\(playerHealthPiece, healthFrame\(playerHull, playerMaxHull\)\)/);
|
||||||
assert.match(source, /const enemyMaxHull = pirate \? Math\.max\(1, pirate\.getData\?\.\('maxHull'\) \?\? 120\) : 120/);
|
assert.match(source, /setHealthFrame\([\s\S]*?enemyHealthPiece,[\s\S]*?target \? healthFrame\(target\.hull \|\| 0, target\.maxHull \|\| 120\) : 11/);
|
||||||
assert.match(source, /setHealthFrame\(enemyHealthPiece, pirate \? healthFrame\(enemyHull, enemyMaxHull\) : 11\)/);
|
assert.doesNotMatch(source, /player-health-cover/);
|
||||||
|
assert.doesNotMatch(source, /player-health-tint/);
|
||||||
assert.doesNotMatch(source, /steam-bar-fill/);
|
assert.doesNotMatch(source, /steam-bar-fill/);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -181,3 +184,33 @@ test('mobile cannon HUD shows independent reload progress', async () => {
|
|||||||
assert.match(source, /scene\.lastPlayerShot\?\.port/);
|
assert.match(source, /scene\.lastPlayerShot\?\.port/);
|
||||||
assert.match(source, /scene\.lastPlayerShot\?\.starboard/);
|
assert.match(source, /scene\.lastPlayerShot\?\.starboard/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('mobile HUD is driven by authoritative game state events', async () => {
|
||||||
|
const [game, hud] = await Promise.all([
|
||||||
|
read('src/game-v4.js'),
|
||||||
|
read('src/mobile-hud.js'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.match(game, /CustomEvent\('airship:hud-state', \{ detail: state \}\)/);
|
||||||
|
assert.match(game, /hostiles: activePirates\.length/);
|
||||||
|
assert.match(game, /hitSequence: this\.playerHitSequence/);
|
||||||
|
assert.match(hud, /addEventListener\('airship:hud-state'/);
|
||||||
|
assert.match(hud, /renderHudState\(event\.detail\)/);
|
||||||
|
assert.doesNotMatch(hud, /Phaser\.GAMES/);
|
||||||
|
assert.doesNotMatch(hud, /getScene\(\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('processed player hits drive sprite and HUD hit reactions', async () => {
|
||||||
|
const [game, hud] = await Promise.all([
|
||||||
|
read('src/game-v4.js'),
|
||||||
|
read('src/mobile-hud.js'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.match(game, /this\.playerHitSequence \+= 1/);
|
||||||
|
assert.match(game, /playPlayerHitReaction\(damage = PIRATE_SHOT_DAMAGE\)/);
|
||||||
|
assert.match(game, /scaleX: this\.playerBaseScaleX \* 1\.12/);
|
||||||
|
assert.match(game, /this\.cameras\.main\.shake\(120/);
|
||||||
|
assert.match(hud, /steam-player-card\.steam-hit/);
|
||||||
|
assert.match(hud, /pulsePlayerHit\(state\.player\.hitSequence \?\? 0\)/);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user