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, } 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 destroys the projectile, applies 6 damage, and leaves gameplay active', () => { 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, true); assert.equal(shot.active, false); 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); });