Add dock economy and mission systems
This commit is contained in:
parent
bb302b0ac7
commit
c8ced60086
210
src/progression/dock-economy.js
Normal file
210
src/progression/dock-economy.js
Normal 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,
|
||||
};
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user