176 lines
6.6 KiB
TypeScript
176 lines
6.6 KiB
TypeScript
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<string, PlaneType> = {
|
||
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,
|
||
},
|
||
};
|
||
|
||
// 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 (~13 mid-range EDDF↔LEMD hops to break from full).
|
||
export const WEAR_PER_KM = 0.005;
|
||
|
||
// ponytail: balance-tunable — burn multiplier for a broken plane (health ≤ 0): it still flies, just thirsty.
|
||
export const BROKEN_BURN_MULTIPLIER = 1.5;
|
||
|
||
// Broken-state burn factor for one plane's health — the third multiplicative term alongside wind at every
|
||
// per-plane burn site (flyPlane dispatch top-up + backstop, engine landed fuel + auto-chain, live display).
|
||
export function conditionBurnMultiplier(health: number): number {
|
||
return health <= 0 ? BROKEN_BURN_MULTIPLIER : 1;
|
||
}
|
||
|
||
// 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,
|
||
}));
|
||
}
|