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, resolveTaggedProjectile, } 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); }); test('projectile resolver is independent of Phaser overlap argument order', () => { const player = { getData() { return undefined; }, }; const pirateShot = { getData(key) { return key === 'projectileOwner' ? 'pirate' : undefined; }, }; assert.equal(resolveTaggedProjectile(pirateShot, player, 'pirate'), pirateShot); assert.equal(resolveTaggedProjectile(player, pirateShot, 'pirate'), pirateShot); }); test('projectile resolver never mistakes the player for an untagged shot', () => { const player = { getData() { return undefined; }, }; const untagged = { getData() { return undefined; }, }; assert.equal(resolveTaggedProjectile(player, untagged, 'pirate'), null); });