import { INSYS_LEG_DAYS, WIN_CREDITS, GOODS } from './constants.js'; import { advancePlanet, priceOf, applyTrade, marketView } from './economy.js'; import { otherEnd } from './galaxy.js'; import { rollFor } from './rng.js'; import { pushLog } from './world.js'; export function playerSystem(world) { const loc = world.player.loc; if (loc.kind === 'docked') return world.galaxy.planets[loc.planetId].systemId; if (loc.kind === 'lane') { const f = (world.t - loc.departT) / (loc.arriveT - loc.departT); return f < 0.5 ? loc.fromSys : loc.toSys; // nearest end, for news purposes } return loc.systemId; } export function cargoUsed(player) { return Object.values(player.cargo).reduce((a, b) => a + b, 0); } function fromDescriptor(loc) { if (loc.kind === 'docked') return { type: 'planet', id: loc.planetId }; if (loc.kind === 'at-gate') return { type: 'gate', laneId: loc.laneId }; return { type: 'planet', id: 0 }; } // target: { planetId } to fly to a planet in the current system, // or { laneId } to jump through a lane touching the current system. // A { laneId } command is a COMMITTED flight: intent is stored and the // lane jump auto-fires on reaching the gate (cleared only in startLane). // Movement commands are rejected while in transit, so intent cannot go stale. export function goTo(world, target) { const p = world.player; if (!p.alive || p.won) return false; if (p.loc.kind === 'insys-leg' || p.loc.kind === 'lane') return false; const sys = playerSystem(world); if (target.planetId != null) { const planet = world.galaxy.planets[target.planetId]; if (!planet || planet.systemId !== sys) return false; if (p.loc.kind === 'docked' && p.loc.planetId === target.planetId) return false; p.loc = { kind: 'insys-leg', systemId: sys, from: fromDescriptor(p.loc), to: { type: 'planet', id: target.planetId }, departT: world.t, arriveT: world.t + INSYS_LEG_DAYS, }; world.queue.push(p.loc.arriveT, 'player-arrive', {}); return true; } if (target.laneId != null) { const lane = world.galaxy.lanes[target.laneId]; if (!lane || (lane.a !== sys && lane.b !== sys)) return false; p.intent = { laneId: target.laneId }; if (p.loc.kind === 'at-gate' && p.loc.laneId === target.laneId) { startLane(world); return true; } p.loc = { kind: 'insys-leg', systemId: sys, from: fromDescriptor(p.loc), to: { type: 'gate', laneId: target.laneId }, departT: world.t, arriveT: world.t + INSYS_LEG_DAYS, }; world.queue.push(p.loc.arriveT, 'player-arrive', {}); return true; } return false; } function startLane(world) { const p = world.player; const lane = world.galaxy.lanes[p.intent.laneId]; const fromSys = playerSystem(world); p.loc = { kind: 'lane', laneId: lane.id, fromSys, toSys: otherEnd(lane, fromSys), departT: world.t, arriveT: world.t + lane.days, }; p.intent = null; world.queue.push(p.loc.arriveT, 'player-arrive', {}); pushLog(world, `Jumping to ${world.galaxy.systems[p.loc.toSys].name} (${lane.days}d)`); } export function onPlayerArrive(world) { const p = world.player; const loc = p.loc; if (loc.kind === 'insys-leg' && world.t >= loc.arriveT - 1e-9) { if (loc.to.type === 'planet') { p.loc = { kind: 'docked', planetId: loc.to.id }; recordDockIntel(world, loc.to.id); pushLog(world, `Docked at ${world.galaxy.planets[loc.to.id].name}`); } else { p.loc = { kind: 'at-gate', systemId: loc.systemId, laneId: loc.to.laneId }; if (p.intent && p.intent.laneId === loc.to.laneId) startLane(world); } return; } if (loc.kind === 'lane' && world.t >= loc.arriveT - 1e-9) { const lane = world.galaxy.lanes[loc.laneId]; const trip = ++p.tripCount; // Player hazard odds are deliberately kinder than NPC hazardProbs // (4% vs 6% destroyed, 15% vs 20% cargo-lost per unit hazard). const r = rollFor(`w:${world.seed}:player`, `trip${trip}`); if (r < lane.hazard * 0.04) { p.alive = false; pushLog(world, 'Your ship was destroyed by pirates.'); return; } if (r < lane.hazard * 0.04 + lane.hazard * 0.15 && cargoUsed(p) > 0) { p.cargo = {}; pushLog(world, 'Pirates! You jettisoned your cargo and escaped.'); } p.loc = { kind: 'at-gate', systemId: loc.toSys, laneId: loc.laneId }; pushLog(world, `Arrived in ${world.galaxy.systems[loc.toSys].name}`); } } function recordDockIntel(world, planetId) { const planet = world.galaxy.planets[planetId]; advancePlanet(planet, world.t); const prices = Object.fromEntries(GOODS.map((g) => [g, priceOf(g, planet.stock[g])])); world.player.priceBook[planetId] = { time: world.t, prices }; } export function buyGood(world, good, qty) { const p = world.player; if (p.loc.kind !== 'docked' || !p.alive) return false; const planet = world.galaxy.planets[p.loc.planetId]; advancePlanet(planet, world.t); const price = priceOf(good, planet.stock[good]); qty = Math.min(qty, Math.floor(planet.stock[good]), Math.floor(p.credits / price), p.capacity - cargoUsed(p)); if (qty < 1) return false; applyTrade(planet, world.t, good, -qty); p.credits = Math.round((p.credits - price * qty) * 100) / 100; p.cargo[good] = (p.cargo[good] || 0) + qty; recordDockIntel(world, planet.id); return true; } export function sellGood(world, good, qty) { const p = world.player; if (p.loc.kind !== 'docked' || !p.alive) return false; qty = Math.min(qty, p.cargo[good] || 0); if (qty < 1) return false; const planet = world.galaxy.planets[p.loc.planetId]; const price = applyTrade(planet, world.t, good, qty); p.credits = Math.round((p.credits + price * qty) * 100) / 100; p.cargo[good] -= qty; if (p.cargo[good] === 0) delete p.cargo[good]; recordDockIntel(world, planet.id); if (p.credits >= WIN_CREDITS && !p.won) { p.won = true; pushLog(world, `You amassed ${p.credits} credits. Victory!`); } return true; } export function playerMarket(world) { const p = world.player; if (p.loc.kind !== 'docked') return null; return marketView(world.galaxy.planets[p.loc.planetId], world.t); }