Add dock economy and mission systems

This commit is contained in:
Admin 2026-09-21 00:00:23 +00:00
parent bb302b0ac7
commit c8ced60086

View File

@ -0,0 +1,210 @@
import { createSeededRandom } from '../world/seeded-random.js';
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
export const TRADE_GOODS = {
provisions: { id: 'provisions', name: 'Provisions', basePrice: 34 },
ore: { id: 'ore', name: 'Ore', basePrice: 52 },
fuel: { id: 'fuel', name: 'Aether Fuel', basePrice: 46 },
machinery: { id: 'machinery', name: 'Machinery', basePrice: 82 },
};
const TYPE_MODIFIERS = {
capital: { provisions: 0.96, ore: 1.02, fuel: 1.0, machinery: 0.94 },
industrial: { provisions: 1.08, ore: 0.72, fuel: 0.96, machinery: 0.8 },
fuel: { provisions: 1.08, ore: 1.04, fuel: 0.7, machinery: 1.06 },
military: { provisions: 1.12, ore: 1.1, fuel: 1.04, machinery: 1.14 },
trade: { provisions: 0.94, ore: 1.0, fuel: 1.0, machinery: 0.96 },
};
const resourceToGood = (resource) => resource === 'food'
? 'provisions'
: resource === 'ore'
? 'ore'
: resource === 'fuel'
? 'fuel'
: resource;
export const tradeCargoUnits = (progression = {}) => {
const cargo = progression.tradeCargo || {};
const goods = Object.values(cargo).reduce((sum, qty) => sum + Math.max(0, Number(qty) || 0), 0);
const missionUnits = progression.activeMission?.type === 'delivery'
? Math.max(0, progression.activeMission.cargoUnits || 0)
: 0;
return goods + missionUnits;
};
export const marketAtPort = (world, port) => {
if (!port) return [];
const typeMods = TYPE_MODIFIERS[port.type] || TYPE_MODIFIERS.trade;
const exports = new Set((port.exports || []).map(resourceToGood));
const imports = new Set((port.imports || []).map(resourceToGood));
return Object.values(TRADE_GOODS).map((good) => {
const random = createSeededRandom(`${world?.seed || 'world'}:${port.id}:${good.id}:market`);
const jitter = 0.93 + random() * 0.14;
let modifier = typeMods[good.id] || 1;
if (exports.has(good.id)) modifier *= 0.76;
if (imports.has(good.id)) modifier *= 1.2;
modifier *= 0.96 + (1 - (port.wealth || 0.5)) * 0.08;
const localPrice = Math.max(8, Math.round(good.basePrice * modifier * jitter));
return {
goodId: good.id,
name: good.name,
buyPrice: localPrice,
sellPrice: Math.max(5, Math.round(localPrice * 0.86)),
exported: exports.has(good.id),
imported: imports.has(good.id),
};
});
};
export const buyTradeGood = (progression, marketItem, stats, quantity = 1) => {
const qty = Math.max(1, Math.floor(quantity));
const price = Math.max(0, marketItem?.buyPrice || 0) * qty;
if (!marketItem?.goodId || price <= 0) return { ...progression, traded: false, reason: 'invalid-good' };
if ((progression.credits || 0) < price) return { ...progression, traded: false, reason: 'insufficient-credits' };
if (tradeCargoUnits(progression) + qty > (stats?.cargoCapacity || 0)) {
return { ...progression, traded: false, reason: 'cargo-full' };
}
const tradeCargo = { ...(progression.tradeCargo || {}) };
tradeCargo[marketItem.goodId] = (tradeCargo[marketItem.goodId] || 0) + qty;
return {
...progression,
tradeCargo,
credits: progression.credits - price,
traded: true,
tradeAction: { type: 'buy', goodId: marketItem.goodId, quantity: qty, price },
};
};
export const sellTradeGood = (progression, marketItem, quantity = 1) => {
const qty = Math.max(1, Math.floor(quantity));
const held = Math.max(0, progression.tradeCargo?.[marketItem?.goodId] || 0);
if (!marketItem?.goodId || held < qty) return { ...progression, traded: false, reason: 'not-held' };
const tradeCargo = { ...(progression.tradeCargo || {}) };
tradeCargo[marketItem.goodId] = held - qty;
if (tradeCargo[marketItem.goodId] <= 0) delete tradeCargo[marketItem.goodId];
const price = Math.max(0, marketItem.sellPrice || 0) * qty;
return {
...progression,
tradeCargo,
credits: (progression.credits || 0) + price,
traded: true,
tradeAction: { type: 'sell', goodId: marketItem.goodId, quantity: qty, price },
};
};
const routeDistance = (route) => Math.hypot(
(route?.to?.x || 0) - (route?.from?.x || 0),
(route?.to?.y || 0) - (route?.from?.y || 0),
);
const connectedRoutes = (world, portId) => (world?.tradeRoutes || [])
.filter((route) => route.fromPortId === portId || route.toPortId === portId);
const otherPortId = (route, portId) => route.fromPortId === portId
? route.toPortId
: route.fromPortId;
export const missionOffersAtPort = (world, port, defeated = 0) => {
if (!port) return [];
const routes = connectedRoutes(world, port.id)
.sort((a, b) => b.value - a.value || routeDistance(b) - routeDistance(a));
const deliveryRoute = routes[0] || null;
const targetPort = deliveryRoute
? world.ports.find(({ id }) => id === otherPortId(deliveryRoute, port.id))
: null;
const random = createSeededRandom(`${world?.seed || 'world'}:${port.id}:missions`);
const offers = [];
if (deliveryRoute && targetPort) {
const cargoUnits = 1 + Math.floor(random() * 2);
const distance = routeDistance(deliveryRoute);
const reward = Math.round(120 + distance * 0.11 + deliveryRoute.value * 160 + cargoUnits * 30);
offers.push({
id: `delivery:${port.id}:${targetPort.id}`,
type: 'delivery',
title: `Courier Run → ${targetPort.name}`,
description: `Carry ${cargoUnits} sealed cargo unit${cargoUnits === 1 ? '' : 's'} to ${targetPort.name}.`,
originPortId: port.id,
originPortName: port.name,
targetPortId: targetPort.id,
targetPortName: targetPort.name,
cargoUnits,
reward,
});
}
const killTarget = 2 + Math.floor(random() * 3);
offers.push({
id: `bounty:${port.id}:${killTarget}`,
type: 'bounty',
title: `Pirate Bounty ×${killTarget}`,
description: `Destroy ${killTarget} pirate airship${killTarget === 1 ? '' : 's'} and claim the bounty at any dock.`,
originPortId: port.id,
originPortName: port.name,
targetKills: killTarget,
reward: 150 + killTarget * 90,
baselineKills: defeated,
});
return offers;
};
export const acceptMission = (progression, mission, stats, defeated = 0) => {
if (!mission) return { ...progression, missionAccepted: false, reason: 'invalid-mission' };
if (progression.activeMission) return { ...progression, missionAccepted: false, reason: 'mission-active' };
if (mission.type === 'delivery' && tradeCargoUnits(progression) + (mission.cargoUnits || 0) > (stats?.cargoCapacity || 0)) {
return { ...progression, missionAccepted: false, reason: 'cargo-full' };
}
return {
...progression,
activeMission: {
...mission,
baselineKills: mission.type === 'bounty' ? defeated : mission.baselineKills,
},
missionAccepted: true,
};
};
export const missionStatus = (progression, { portId = null, defeated = 0 } = {}) => {
const mission = progression?.activeMission || null;
if (!mission) return null;
if (mission.type === 'delivery') {
const ready = portId === mission.targetPortId;
return {
...mission,
progressText: ready ? `Arrived at ${mission.targetPortName}` : `Deliver to ${mission.targetPortName}`,
ready,
};
}
const kills = Math.max(0, defeated - (mission.baselineKills || 0));
return {
...mission,
kills,
progressText: `${Math.min(kills, mission.targetKills)}/${mission.targetKills} pirates destroyed`,
ready: kills >= mission.targetKills,
};
};
export const completeMission = (progression, context = {}) => {
const status = missionStatus(progression, context);
if (!status) return { ...progression, missionCompleted: false, reason: 'no-mission' };
if (!status.ready) return { ...progression, missionCompleted: false, reason: 'not-complete' };
return {
...progression,
credits: (progression.credits || 0) + Math.max(0, status.reward || 0),
activeMission: null,
missionCompleted: true,
lastCompletedMission: status,
};
};