import { STARTING_REPUTATION } from './seed'; // Aircraft catalog — in-code constants for now (no DB table yet). One type is // enough for the MVP; add more entries to PLANE_TYPES as they are designed. // ponytail: circular import with seed.ts is safe — neither module touches the other's // bindings at top level, only inside function bodies. export interface PlaneType { id: string; name: string; // Fuel capacity, liters. maxFuel: number; // Fuel burn per km of flight at a FULL tank, liters/km (cruise) — the weighted burn // model (v0.16.0) calibrates its rate constant to this value at F = maxFuel. burnPerKm: number; // Operating empty weight in kg (v0.16.0) — feeds the weight-proportional burn model. emptyWeightKg: number; // Cruise speed, km/h — used by the sim tick to advance planes along routes. speedKmh: number; // Market price in € (v0.8.0): buying costs `price`, selling pays out // SELL_FRACTION × price. price: number; // Max payload in kg (v0.27.0, ISS-045 — replaces the v0.12.0 slot count): total // package weight that can be loaded on one plane. ATR 72-600-class figure, tuned // so every cargo type at max count × 1.3 jitter still fits alone (see UNIT_WEIGHT_KG). maxPayloadKg: number; } export const PLANE_TYPES: Record = { atr72: { id: 'atr72', name: 'ATR 72-600', maxFuel: 6300, burnPerKm: 1.75, // full-tank rate: ~893 L/h ≈ 714 kg/h at 510 km/h cruise (Jet-A1 ≈ 0.8 kg/L); the weighted model burns less as fuel drops off emptyWeightKg: 9_300, // ATR 72-600 operating empty weight speedKmh: 510, // €200k — with the starting cash of €20k and typical hop rewards (~€1–3k), // a second ATR takes sustained play to afford, as intended for the MVP sink. price: 200_000, // ponytail: balance-tunable payload (kg) — every cargo type at max count × 1.3 jitter // must stay below this so any single package fits alone; typical loads carry ~3–6 packages. maxPayloadKg: 4_800, }, // Progression ladder above the ATR starter (v0.28.0, ISS-047): bigger tanks, faster airframes, // higher prices — everything downstream is data-driven via PLANE_TYPES[model]. Values are // balance-tunable; keep maxPayloadKg ≥ every cargo type at max count × 1.3 jitter so any // single package still fits alone (same rule as the ATR above). crj900: { id: 'crj900', name: 'CRJ-900', maxFuel: 8_000, burnPerKm: 2.1, emptyWeightKg: 17_500, speedKmh: 830, price: 450_000, maxPayloadKg: 6_500, }, e175: { id: 'e175', name: 'Embraer E175', maxFuel: 9_500, burnPerKm: 2.0, emptyWeightKg: 23_000, speedKmh: 840, price: 900_000, maxPayloadKg: 9_000, }, a320: { id: 'a320', name: 'Airbus A320-200', maxFuel: 12_000, burnPerKm: 2.2, emptyWeightKg: 41_000, speedKmh: 845, price: 1_800_000, maxPayloadKg: 18_000, }, a330: { id: 'a330', name: 'Airbus A330-200', maxFuel: 20_000, burnPerKm: 2.16, emptyWeightKg: 95_000, speedKmh: 910, price: 4_500_000, maxPayloadKg: 48_000, }, b777: { id: 'b777', name: 'Boeing 777-300ER', maxFuel: 30_000, burnPerKm: 2.5, emptyWeightKg: 130_000, speedKmh: 920, price: 9_000_000, maxPayloadKg: 80_000, }, }; // v0.54.0 (ISS-119): fastest cruise speed in the catalog — a generated job must be finishable by // the best available aircraft, so seed's expiry floor references this, not the starter's 510 km/h // (that would wreck long-job economics). Data-driven: adding a faster plane tightens the floor. export const FASTEST_CATALOG_SPEED_KMH = Math.max(...Object.values(PLANE_TYPES).map((t) => t.speedKmh)); // The plane the player starts with. export const STARTER_MODEL = 'atr72'; // Jet-A1 fuel price in €/liter (v0.8.0): top-up cost = liters × this — applied to the automatic // dispatch top-ups since ISS-082 (manual refuel removed). export const FUEL_PRICE_PER_L = 0.8; // Fraction of the catalog price paid out when selling a plane (v0.8.0). export const SELL_FRACTION = 0.5; // Shared rep-discount shape (v0.38.0, ISS-072): linear in reputation above the starting value, // capped — exactly 0 at/below STARTING_REPUTATION so default-rep behavior is unchanged. export function repRate(reputation: number, perPoint: number, cap: number): number { return Math.min(cap, perPoint * Math.max(0, reputation - STARTING_REPUTATION)); } // ponytail: balance-tunable — plane purchase discount per rep point above the starting value. export const PLANE_DISCOUNT_PER_REP = 0.01; // ponytail: balance-tunable — max plane purchase discount (reached at rep ≥ 50). export const MAX_PLANE_DISCOUNT = 0.4; // ponytail: balance-tunable — refuel price discount per rep point above the starting value. export const FUEL_DISCOUNT_PER_REP = 0.005; // ponytail: balance-tunable — max refuel price discount (reached at rep ≥ 50). export const MAX_FUEL_DISCOUNT = 0.2; // --- Maintenance (v0.39.0, ISS-069) --- // ponytail: balance-tunable — health lost per km of landed legs (~130 mid-range EDDF↔LEMD hops to break from full; 1/10× slower since v0.43.0, ISS-085). export const WEAR_PER_KM = 0.0005; // ponytail: balance-tunable — flat repair cost in € (per-type if balance demands it). export const REPAIR_COST = 1_500; // ponytail: balance-tunable — how long a repair grounds the plane, sim ms (3 game days). export const REPAIR_DURATION_MS = 3 * 86_400_000; // Snapshot shape of one catalog entry in GET /api/state (v0.10.0): `model` is the // id used by buy/dispatch, `label` the display name — clients compute all market // previews (buy price, sell payout) from these + the economy constants. export interface CatalogEntry { model: string; label: string; price: number; maxFuel: number; burnPerKm: number; // Weighted burn model inputs (v0.16.0) — clients can mirror the closed forms in // lib/sim/movement.ts with these + FUEL_DENSITY_KG_PER_L. emptyWeightKg: number; speedKmh: number; // Max payload in kg (v0.27.0) — total package weight that can be loaded on one plane. maxPayloadKg: number; } // The catalog as a flat array, one entry per plane type — the single source of // truth for both the sim (PLANE_TYPES) and the client snapshot. export function catalogEntries(): CatalogEntry[] { return Object.values(PLANE_TYPES).map((t) => ({ model: t.id, label: t.name, price: t.price, maxFuel: t.maxFuel, burnPerKm: t.burnPerKm, emptyWeightKg: t.emptyWeightKg, speedKmh: t.speedKmh, maxPayloadKg: t.maxPayloadKg, })); }