147 lines
5.3 KiB
JavaScript
147 lines
5.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { generateWorld } from '../src/world/world-generator.js';
|
|
import { createStarterProgression, deriveShipStats } from '../src/progression/ship-loadout.js';
|
|
import {
|
|
acceptMission,
|
|
bestTradeDestination,
|
|
buyTradeGood,
|
|
completeMission,
|
|
marketAtPort,
|
|
missionOffersAtPort,
|
|
missionStatus,
|
|
payForHullRepair,
|
|
recordRouteTradeEvent,
|
|
sellTradeGood,
|
|
tradeCargoUnits,
|
|
} from '../src/progression/dock-economy.js';
|
|
|
|
test('port markets are deterministic and expose buy/sell prices', () => {
|
|
const world = generateWorld({ seed: 'DOCK-MARKET' });
|
|
const port = world.ports[0];
|
|
const a = marketAtPort(world, port);
|
|
const b = marketAtPort(world, port);
|
|
assert.deepEqual(a, b);
|
|
assert.ok(a.length >= 4);
|
|
assert.ok(a.every((item) => item.buyPrice > item.sellPrice));
|
|
});
|
|
|
|
test('trade cargo respects capacity and selling returns coin', () => {
|
|
const world = generateWorld({ seed: 'DOCK-TRADE' });
|
|
const market = marketAtPort(world, world.ports[0]);
|
|
const item = market[0];
|
|
const starter = { ...createStarterProgression(), credits: 2000 };
|
|
const stats = deriveShipStats(starter.loadout);
|
|
const bought = buyTradeGood(starter, item, stats);
|
|
assert.equal(bought.traded, true);
|
|
assert.equal(tradeCargoUnits(bought), 1);
|
|
assert.ok(bought.credits < starter.credits);
|
|
|
|
const sold = sellTradeGood(bought, item);
|
|
assert.equal(sold.traded, true);
|
|
assert.equal(tradeCargoUnits(sold), 0);
|
|
assert.ok(sold.credits > bought.credits);
|
|
});
|
|
|
|
test('delivery missions follow generated trade routes and complete at destination', () => {
|
|
const world = generateWorld({ seed: 'DOCK-MISSIONS' });
|
|
const origin = world.ports[0];
|
|
const offers = missionOffersAtPort(world, origin, 0);
|
|
const delivery = offers.find(({ type }) => type === 'delivery');
|
|
assert.ok(delivery);
|
|
assert.ok(world.tradeRoutes.some((route) =>
|
|
(route.fromPortId === origin.id && route.toPortId === delivery.targetPortId)
|
|
|| (route.toPortId === origin.id && route.fromPortId === delivery.targetPortId)
|
|
));
|
|
|
|
const starter = createStarterProgression();
|
|
const stats = deriveShipStats(starter.loadout);
|
|
const accepted = acceptMission(starter, delivery, stats, 0);
|
|
assert.equal(accepted.missionAccepted, true);
|
|
assert.equal(missionStatus(accepted, { portId: origin.id, defeated: 0 }).ready, false);
|
|
|
|
const completed = completeMission(accepted, {
|
|
portId: delivery.targetPortId,
|
|
defeated: 0,
|
|
});
|
|
assert.equal(completed.missionCompleted, true);
|
|
assert.equal(completed.activeMission, null);
|
|
assert.equal(completed.credits, starter.credits + delivery.reward);
|
|
});
|
|
|
|
test('bounty missions advance from actual pirate kill count', () => {
|
|
const world = generateWorld({ seed: 'DOCK-BOUNTY' });
|
|
const offers = missionOffersAtPort(world, world.ports[0], 5);
|
|
const bounty = offers.find(({ type }) => type === 'bounty');
|
|
const starter = createStarterProgression();
|
|
const stats = deriveShipStats(starter.loadout);
|
|
const accepted = acceptMission(starter, bounty, stats, 5);
|
|
const pending = missionStatus(accepted, {
|
|
portId: world.ports[0].id,
|
|
defeated: 5 + bounty.targetKills - 1,
|
|
});
|
|
assert.equal(pending.ready, false);
|
|
const ready = missionStatus(accepted, {
|
|
portId: world.ports[0].id,
|
|
defeated: 5 + bounty.targetKills,
|
|
});
|
|
assert.equal(ready.ready, true);
|
|
});
|
|
|
|
|
|
test('mission boards rotate after completed contracts', () => {
|
|
const world = generateWorld({ seed: 'DOCK-ROTATE' });
|
|
const port = world.ports[0];
|
|
const first = missionOffersAtPort(world, port, 0, 0);
|
|
const next = missionOffersAtPort(world, port, 0, 1);
|
|
assert.notDeepEqual(first.map(({ id }) => id), next.map(({ id }) => id));
|
|
});
|
|
|
|
test('merchant losses create shortages and player piracy adds wanted level', () => {
|
|
const world = generateWorld({ seed: 'DOCK-SHORTAGE' });
|
|
const route = world.tradeRoutes[0];
|
|
const destinationPortId = route.toPortId;
|
|
const starter = createStarterProgression();
|
|
const afterLoss = recordRouteTradeEvent(
|
|
starter,
|
|
world,
|
|
route,
|
|
destinationPortId,
|
|
{ kind: 'loss', playerCaused: true },
|
|
);
|
|
assert.equal(afterLoss.economy.wanted, 1);
|
|
assert.ok(afterLoss.economy.reputation < 0);
|
|
assert.ok(Object.values(afterLoss.economy.shortages).some((value) => value > 0));
|
|
|
|
const afterDelivery = recordRouteTradeEvent(
|
|
afterLoss,
|
|
world,
|
|
route,
|
|
destinationPortId,
|
|
{ kind: 'delivery' },
|
|
);
|
|
const before = Math.max(...Object.values(afterLoss.economy.shortages));
|
|
const after = Math.max(...Object.values(afterDelivery.economy.shortages));
|
|
assert.ok(after < before);
|
|
});
|
|
|
|
test('trade hints identify a best destination and real margin', () => {
|
|
const world = generateWorld({ seed: 'DOCK-HINTS' });
|
|
const port = world.ports[0];
|
|
const item = marketAtPort(world, port)[0];
|
|
const opportunity = bestTradeDestination(world, port, item.goodId);
|
|
assert.ok(opportunity);
|
|
assert.ok(Number.isFinite(opportunity.margin));
|
|
assert.notEqual(opportunity.portId, port.id);
|
|
});
|
|
|
|
test('dock repair restores hull when the player can afford it', () => {
|
|
const world = generateWorld({ seed: 'DOCK-REPAIR' });
|
|
const starter = { ...createStarterProgression(), credits: 1000 };
|
|
const result = payForHullRepair(starter, 70, 100, world.ports[0]);
|
|
assert.equal(result.repaired, true);
|
|
assert.equal(result.hull, 100);
|
|
assert.ok(result.progression.credits < starter.credits);
|
|
});
|