airship-phaser/tests/game-logic.test.mjs

243 lines
6.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
DEFAULT_PIRATE_SHOT_DAMAGE,
PLAYER_HIT_COOLDOWN_MS,
PLAYER_MAX_HULL,
normalizePlayerDamage,
processQueuedPlayerHits,
reducePlayerHitQueue,
reducePlayerProjectileHits,
reducePlayerVolleyHits,
} from '../src/game-logic.js';
test('first pirate hit removes exactly the default damage and does not end the game', () => {
const result = reducePlayerHitQueue({
hull: PLAYER_MAX_HULL,
lastHitAt: -Infinity,
now: 1000,
damages: [DEFAULT_PIRATE_SHOT_DAMAGE],
});
assert.equal(result.hull, 94);
assert.equal(result.appliedHits, 1);
assert.equal(result.ignoredHits, 0);
assert.equal(result.totalDamage, 6);
assert.equal(result.gameOver, false);
});
test('actual queued first player hit applies 6 damage without mutating projectile lifecycle', () => {
const shot = {
active: true,
destroyed: false,
destroy() {
this.destroyed = true;
this.active = false;
},
};
const firstFrame = processQueuedPlayerHits({
hull: PLAYER_MAX_HULL,
lastHitAt: -Infinity,
now: 1000,
hits: [{ shot, damage: DEFAULT_PIRATE_SHOT_DAMAGE }],
});
assert.equal(shot.destroyed, false);
assert.equal(shot.active, true);
assert.equal(firstFrame.hull, 94);
assert.equal(firstFrame.appliedHits, 1);
assert.equal(firstFrame.ignoredHits, 0);
assert.equal(firstFrame.gameOver, false);
const followingFrame = processQueuedPlayerHits({
hull: firstFrame.hull,
lastHitAt: firstFrame.lastHitAt,
now: 1016,
hits: [],
});
assert.equal(followingFrame.hull, 94);
assert.equal(followingFrame.gameOver, false);
assert.equal(followingFrame.appliedHits, 0);
});
test('three cannonballs arriving in one broadside only apply one hull hit', () => {
const result = reducePlayerHitQueue({
hull: 100,
lastHitAt: -Infinity,
now: 2000,
damages: [6, 6, 6],
});
assert.equal(result.hull, 94);
assert.equal(result.appliedHits, 1);
assert.equal(result.ignoredHits, 2);
});
test('hit inside cooldown is ignored and exact cooldown boundary is accepted', () => {
const blocked = reducePlayerHitQueue({
hull: 94,
lastHitAt: 1000,
now: 1000 + PLAYER_HIT_COOLDOWN_MS - 1,
damages: [6],
});
assert.equal(blocked.hull, 94);
assert.equal(blocked.appliedHits, 0);
assert.equal(blocked.ignoredHits, 1);
const accepted = reducePlayerHitQueue({
hull: 94,
lastHitAt: 1000,
now: 1000 + PLAYER_HIT_COOLDOWN_MS,
damages: [6],
});
assert.equal(accepted.hull, 88);
assert.equal(accepted.appliedHits, 1);
});
test('damage normalization handles invalid and extreme values safely', () => {
assert.equal(normalizePlayerDamage(undefined), 6);
assert.equal(normalizePlayerDamage('not-a-number'), 6);
assert.equal(normalizePlayerDamage(999), 25);
assert.equal(normalizePlayerDamage(-12), 1);
assert.equal(normalizePlayerDamage('8'), 8);
});
test('invalid hull state recovers from max hull before applying damage', () => {
const result = reducePlayerHitQueue({
hull: Number.NaN,
lastHitAt: -Infinity,
now: 3000,
damages: [6],
});
assert.equal(result.hull, 94);
assert.equal(result.gameOver, false);
});
test('lethal hit clamps hull at zero and reports game over', () => {
const result = reducePlayerHitQueue({
hull: 5,
lastHitAt: -Infinity,
now: 4000,
damages: [6],
});
assert.equal(result.hull, 0);
assert.equal(result.totalDamage, 6);
assert.equal(result.gameOver, true);
});
test('empty hit queue is a no-op', () => {
const result = reducePlayerHitQueue({
hull: 72,
lastHitAt: 1234,
now: 5000,
damages: [],
});
assert.equal(result.hull, 72);
assert.equal(result.lastHitAt, 1234);
assert.equal(result.appliedHits, 0);
assert.equal(result.ignoredHits, 0);
assert.equal(result.gameOver, false);
});
test('one pirate broadside damages once, and the next broadside damages again', () => {
const firstVolley = reducePlayerVolleyHits({
hull: 100,
hits: [
{ volleyId: 101, damage: 6 },
{ volleyId: 101, damage: 6 },
{ volleyId: 101, damage: 6 },
],
});
assert.equal(firstVolley.hull, 94);
assert.equal(firstVolley.totalDamage, 6);
assert.equal(firstVolley.acceptedHits.length, 1);
assert.equal(firstVolley.ignoredHits.length, 2);
const secondVolley = reducePlayerVolleyHits({
hull: firstVolley.hull,
seenVolleyIds: firstVolley.seenVolleyIds,
hits: [
{ volleyId: 102, damage: 6 },
{ volleyId: 102, damage: 6 },
],
});
assert.equal(secondVolley.hull, 88);
assert.equal(secondVolley.totalDamage, 6);
assert.equal(secondVolley.acceptedHits.length, 1);
assert.equal(secondVolley.ignoredHits.length, 1);
const repeatedSecondVolley = reducePlayerVolleyHits({
hull: secondVolley.hull,
seenVolleyIds: secondVolley.seenVolleyIds,
hits: [{ volleyId: 102, damage: 6 }],
});
assert.equal(repeatedSecondVolley.hull, 88);
assert.equal(repeatedSecondVolley.totalDamage, 0);
assert.equal(repeatedSecondVolley.acceptedHits.length, 0);
assert.equal(repeatedSecondVolley.ignoredHits.length, 1);
});
test('different pirate volleys can both damage even when processed together', () => {
const result = reducePlayerVolleyHits({
hull: 100,
hits: [
{ volleyId: 201, damage: 6 },
{ volleyId: 202, damage: 6 },
],
});
assert.equal(result.hull, 88);
assert.equal(result.totalDamage, 12);
assert.equal(result.acceptedHits.length, 2);
assert.equal(result.ignoredHits.length, 0);
});
test('every new pirate projectile can damage once with no cross-shot suppression', () => {
const first = reducePlayerProjectileHits({
hull: 100,
hits: [{ damage: 6 }],
});
assert.equal(first.hull, 94);
assert.equal(first.totalDamage, 6);
assert.equal(first.appliedHits, 1);
assert.equal(first.gameOver, false);
const second = reducePlayerProjectileHits({
hull: first.hull,
hits: [{ damage: 6 }],
});
assert.equal(second.hull, 88);
assert.equal(second.totalDamage, 6);
assert.equal(second.appliedHits, 1);
assert.equal(second.gameOver, false);
});
test('multiple distinct pirate projectiles in the same frame all apply damage', () => {
const result = reducePlayerProjectileHits({
hull: 100,
hits: [
{ damage: 6 },
{ damage: 6 },
{ damage: 6 },
],
});
assert.equal(result.hull, 82);
assert.equal(result.totalDamage, 18);
assert.equal(result.appliedHits, 3);
});