Package weight, payload capacity and per-kg payout (ISS-045)
CI / build + unit + e2e (pull_request) Successful in 8m2s
CI / build + unit + e2e (pull_request) Successful in 8m2s
Packages carry a real weightKg (unit weight x count with +/-30% jitter), plane capacity becomes maxPayloadKg instead of 6 slots, load validation sums cargo weight against the payload, and the payout gains a per-kg part. Loadout display shows tonnage; OpenAPI swaps CatalogEntry.capacity for maxPayloadKg and adds PackageState.weightKg.
This commit is contained in:
+32
-9
@@ -50,6 +50,8 @@ export interface SeedPackage {
|
||||
reward: number;
|
||||
cargoType: string;
|
||||
cargoCount: number;
|
||||
// Package weight in kg (v0.27.0, ISS-045) — unit weight × count with ±30% jitter.
|
||||
weightKg: number;
|
||||
deadlineSimTime: number;
|
||||
status: 'open';
|
||||
planeId: null;
|
||||
@@ -80,10 +82,12 @@ export const PACKAGE_COUNT_MAX = 12; // inclusive
|
||||
// count after expiries, so the job board never runs dry.
|
||||
export const MARKET_TARGET_OPEN_PACKAGES = 10;
|
||||
|
||||
// Reward formula (v0.6.0 — deliberately simple; real revenue/cost modeling is a
|
||||
// later increment): reward = BASE_REWARD + REWARD_PER_KM * great-circle km.
|
||||
// Reward formula (v0.6.0 distance part, deliberately simple; real revenue/cost
|
||||
// modeling is a later increment; v0.27.0 weight part): reward = BASE_REWARD +
|
||||
// REWARD_PER_KM * great-circle km + REWARD_PER_KG * package weight kg.
|
||||
export const BASE_REWARD = 500;
|
||||
export const REWARD_PER_KM = 2.5;
|
||||
export const REWARD_PER_KG = 0.5;
|
||||
|
||||
// Package deadlines: now + a plausible game-time window of 6–72 hours.
|
||||
const DEADLINE_MIN_HOURS = 6;
|
||||
@@ -91,19 +95,33 @@ const DEADLINE_MAX_HOURS = 72;
|
||||
|
||||
const CARGO_TYPES = ['Passengers', 'Mail', 'Perishables', 'Electronics', 'Auto parts'] as const;
|
||||
|
||||
export function rewardForDistanceKm(km: number): number {
|
||||
return Math.round(BASE_REWARD + REWARD_PER_KM * km);
|
||||
// ponytail: balance-tunable unit weights (kg per passenger/pallet). Chosen so every cargo
|
||||
// type at MAX count × max jitter (1.3×) still fits the ATR 72-600's payload alone, and
|
||||
// typical packages land ~400–2500 kg — a normal flight carries roughly 3–6 by weight.
|
||||
export const UNIT_WEIGHT_KG: Record<string, number> = {
|
||||
Passengers: 50, // max 50 × 68 × 1.3 = 4420 < 4800
|
||||
Mail: 60, // max 60 × 50 × 1.3 = 3900
|
||||
Perishables: 70, // max 70 × 50 × 1.3 = 4550
|
||||
Electronics: 40, // max 40 × 50 × 1.3 = 2600
|
||||
'Auto parts': 72, // max 72 × 50 × 1.3 = 4680
|
||||
};
|
||||
|
||||
export function rewardFor(km: number, weightKg: number): number {
|
||||
return Math.round(BASE_REWARD + REWARD_PER_KM * km + REWARD_PER_KG * weightKg);
|
||||
}
|
||||
|
||||
// Shared package generator, used by BOTH the first-run seed and the runtime
|
||||
// market spawn (v0.7.0) so spawned packages match seeded ones exactly: random
|
||||
// distinct airports from the dataset, distance-based reward, cargo flavor,
|
||||
// deadline now + 6–72h game time (or now + deadlineOffsetMs when given — used by
|
||||
// the e2e test hook to force a near-term expiry). The RNG call order per package
|
||||
// is fixed: origin, dest, cargoType, cargoCount, [deadline window].
|
||||
// distinct airports from the dataset, distance+weight-based reward, cargo flavor,
|
||||
// weight = unit weight × count with ±30% jitter (v0.27.0), deadline now + 6–72h
|
||||
// game time (or now + deadlineOffsetMs when given — used by the e2e test hook to
|
||||
// force a near-term expiry). The RNG call order per package is fixed: origin,
|
||||
// dest, cargoType, cargoCount, weight jitter, [deadline window].
|
||||
// v0.8.0: originIcao/destIcao optionally FORCE an endpoint (spawn coverage rule +
|
||||
// the e2e create-package hook); a forced endpoint consumes no RNG draws, so
|
||||
// callers that omit both get byte-identical behavior to before.
|
||||
// v0.27.0: weightKg optionally FORCES the package weight (e2e hook) — it skips the
|
||||
// jitter draw, same pattern as forced endpoints.
|
||||
export function generatePackage(input: {
|
||||
id: string;
|
||||
airports: SeedAirport[];
|
||||
@@ -112,6 +130,7 @@ export function generatePackage(input: {
|
||||
deadlineOffsetMs?: number;
|
||||
originIcao?: string;
|
||||
destIcao?: string;
|
||||
weightKg?: number;
|
||||
}): SeedPackage {
|
||||
const { airports, rng, nowMs } = input;
|
||||
if (airports.length < 2) throw new Error('package generation needs at least two airports');
|
||||
@@ -143,6 +162,9 @@ export function generatePackage(input: {
|
||||
// Passengers up to the ATR 72's ~68 seats; other cargo in pallets.
|
||||
const cargoCount =
|
||||
cargoType === 'Passengers' ? 1 + Math.floor(rng() * 68) : 1 + Math.floor(rng() * 50);
|
||||
// Weight (v0.27.0): unit weight × count with ±30% jitter, integer kg — drawn AFTER
|
||||
// cargoCount so the fixed RNG order above holds; a forced weightKg skips the draw.
|
||||
const weightKg = input.weightKg ?? Math.round(UNIT_WEIGHT_KG[cargoType] * cargoCount * (0.7 + 0.6 * rng()));
|
||||
const deadlineMs =
|
||||
input.deadlineOffsetMs ?? (DEADLINE_MIN_HOURS + rng() * (DEADLINE_MAX_HOURS - DEADLINE_MIN_HOURS)) * 3_600_000;
|
||||
|
||||
@@ -150,9 +172,10 @@ export function generatePackage(input: {
|
||||
id: input.id,
|
||||
originIcao: origin.icao,
|
||||
destIcao: dest.icao,
|
||||
reward: rewardForDistanceKm(km),
|
||||
reward: rewardFor(km, weightKg),
|
||||
cargoType,
|
||||
cargoCount,
|
||||
weightKg,
|
||||
deadlineSimTime: Math.round(nowMs + deadlineMs),
|
||||
status: 'open',
|
||||
planeId: null,
|
||||
|
||||
Reference in New Issue
Block a user