airship-phaser/tests/progression.test.mjs

163 lines
6.0 KiB
JavaScript

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');
});