import test from 'node:test'; import assert from 'node:assert/strict'; import { bestAffordableMarketUpgrade, buyAndInstallModule, collectSalvage, createStarterProgression, deriveShipStats, describeInventoryEntry, installInventoryModule, installModule, marketForPort, salvageForPirate, scrapInventoryModule, upgradePathForSlot, } from '../src/progression/ship-loadout.js'; test('starter loadout preserves current ship combat and handling values', () => { const progression = createStarterProgression(); const stats = deriveShipStats(progression.loadout); assert.equal(stats.maxHull, 100); assert.equal(stats.maxSpeed, 180); assert.equal(stats.turnRate, 76); assert.equal(stats.reloadMs, 850); assert.equal(stats.cannonDamage, 16); assert.equal(stats.cargoCapacity, 4); }); test('engine upgrades advance one tier at a time', () => { const starter = { ...createStarterProgression(), credits: 10000 }; const mk1 = buyAndInstallModule(starter, 'tuned-propellers'); assert.equal(mk1.purchased, true); assert.equal(mk1.loadout.engine, 'tuned-propellers'); const skip = buyAndInstallModule(starter, 'storm-turbines'); assert.equal(skip.purchased, false); assert.equal(skip.reason, 'tier-locked'); const mk2 = buyAndInstallModule(mk1, 'corsair-overdrive'); assert.equal(mk2.purchased, true); assert.equal(mk2.loadout.engine, 'corsair-overdrive'); }); test('upgrade path exposes current and next tier with price and effect', () => { const starter = createStarterProgression(); const stock = marketForPort({ type: 'trade' }); const path = upgradePathForSlot(starter.loadout, 'engine', stock); assert.equal(path.current.tierLabel, 'Stock'); assert.equal(path.next.tierLabel, 'Mk I'); assert.equal(path.next.price, 800); assert.match(path.next.effectSummary, /speed/); assert.equal(path.next.stocked, true); }); test('installed modules derive meaningful ship stat changes', () => { const progression = installModule(createStarterProgression(), 'corsair-overdrive'); const stats = deriveShipStats(progression.loadout); assert.equal(stats.maxSpeed, 218); assert.equal(stats.acceleration, 112); assert.equal(stats.turnRate, 74); }); test('port purchase deducts credits and installs the module', () => { const starter = createStarterProgression(); const result = buyAndInstallModule(starter, 'tuned-propellers'); assert.equal(result.purchased, false); const funded = { ...starter, credits: 1000 }; const fundedResult = buyAndInstallModule(funded, 'tuned-propellers'); assert.equal(fundedResult.purchased, true); assert.equal(fundedResult.credits, funded.credits - 800); assert.equal(fundedResult.loadout.engine, 'tuned-propellers'); }); test('pirate salvage includes deterministic field repair and collectible module condition', () => { const salvageA = salvageForPirate({ seed: 'WORLD', classKey: 'captain', wave: 4, sequence: 2 }); const salvageB = salvageForPirate({ seed: 'WORLD', classKey: 'captain', wave: 4, sequence: 2 }); assert.deepEqual(salvageA, salvageB); assert.ok(salvageA.repair > 0); const starter = createStarterProgression(); const collected = collectSalvage(starter, salvageA); assert.ok(collected.credits >= starter.credits); assert.ok(collected.scrap >= starter.scrap); if (salvageA.moduleId) { const details = describeInventoryEntry(collected.inventory[0]); assert.ok(details); assert.ok(details.conditionPercent >= 28); assert.ok(details.scrapValue >= 1); } }); test('recovered modules can be converted into scrap', () => { const starter = createStarterProgression(); const collected = collectSalvage(starter, { moduleId: 'reinforced-rudder', condition: 0.5, credits: 0, scrap: 0, repair: 6, }); const result = scrapInventoryModule(collected, 0); assert.equal(result.scrapped, true); assert.equal(result.inventory.length, 0); assert.ok(result.scrap > collected.scrap); }); test('market selects only a valid next-tier affordable upgrade', () => { const starter = createStarterProgression(); const upgrade = bestAffordableMarketUpgrade(starter, { type: 'military' }); assert.ok(upgrade); assert.ok(upgrade.price <= starter.credits); if (Number.isInteger(upgrade.tier)) assert.equal(upgrade.tier, 1); }); test('rudder is stocked at every port type so its upgrade path remains actionable', () => { const portTypes = ['industrial', 'military', 'fuel', 'trade']; for (const type of portTypes) { const stock = marketForPort({ type }); assert.ok(stock.some(({ id }) => id === 'reinforced-rudder')); assert.ok(stock.some(({ id }) => id === 'balanced-rudder')); } }); test('rudder tier improves handling', () => { const starter = { ...createStarterProgression(), credits: 650 }; const result = buyAndInstallModule(starter, 'reinforced-rudder'); const stats = deriveShipStats(result.loadout); assert.equal(result.purchased, true); assert.equal(result.credits, 250); assert.equal(stats.turnRate, 88); }); test('salvaged module condition scales installed effectiveness', () => { const starter = createStarterProgression(); const salvaged = { ...starter, inventory: [{ moduleId: 'reinforced-rudder', condition: 0.5, source: 'salvage' }], }; const installed = installInventoryModule(salvaged, 0); const stats = deriveShipStats(installed.loadout, installed.installedConditions); assert.equal(installed.installedCondition, 0.5); assert.ok(stats.turnRate > 76); assert.ok(stats.turnRate < 88); }); test('installing a selected salvage entry removes that exact inventory item', () => { const starter = createStarterProgression(); const progression = { ...starter, inventory: [ { moduleId: 'reinforced-rudder', condition: 0.4, source: 'salvage' }, { moduleId: 'tuned-propellers', condition: 0.9, source: 'salvage' }, ], }; const installed = installInventoryModule(progression, 1); assert.equal(installed.loadout.engine, 'tuned-propellers'); assert.equal(installed.inventory.length, 1); assert.equal(installed.inventory[0].moduleId, 'reinforced-rudder'); });