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