Package weight, payload capacity and per-kg payout (ISS-045)
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:
2026-09-20 12:42:03 +00:00
parent 85b7b07f53
commit 179fad450d
24 changed files with 231 additions and 118 deletions
+7 -8
View File
@@ -9,7 +9,7 @@ import { formatCash, formatDeadlineRelative, formatFuel, formatSimClock } from '
import { TIME_SCALES, scaleToSlider, sliderToScale } from '@/lib/timeScale';
import { useGameAction } from '@/lib/useGameAction';
import { useSimClock } from '@/lib/useSimClock';
import { slotsUsedFor } from './RightPane';
import { cargoWeightFor } from './RightPane';
import type { HoverTarget } from './hoverTypes';
interface LeftPaneProps {
@@ -39,13 +39,12 @@ function planeLocation(p: PlaneState): string {
return p.locationName;
}
// Cargo loadout "n/capacity pkgs" — expired packages still occupy their slot.
// Cargo loadout "X.Xt / Y.Yt" (v0.27.0) — used/max payload in tonnes, one decimal;
// expired packages still occupy their payload.
function loadoutFor(state: StateSnapshot, p: PlaneState): string {
const used = state.packages.filter(
(pkg) => pkg.planeId === p.id && (pkg.status === 'loaded' || pkg.status === 'expired'),
).length;
const capacity = state.catalog.find((e) => e.model === p.model)?.capacity ?? PLANE_TYPES[p.model]?.capacity ?? 0;
return `${used}/${capacity} pkgs`;
const used = cargoWeightFor(state, p.id);
const maxPayloadKg = state.catalog.find((e) => e.model === p.model)?.maxPayloadKg ?? PLANE_TYPES[p.model]?.maxPayloadKg ?? 0;
return `${(used / 1000).toFixed(1)}t / ${(maxPayloadKg / 1000).toFixed(1)}t`;
}
// ISS-001 — an optimistic slider value is cleared exactly when a rendered snapshot CONFIRMS it
@@ -207,7 +206,7 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
// ISS-043 (v0.26.0): a plane with queued legs always shows its block too — the queue is the
// reason it's here even when grounded with nothing on board.
const onboard = state
? state.planes.filter((p) => p.status === 'flying' || slotsUsedFor(state, p.id) > 0 || (p.queue?.length ?? 0) > 0)
? state.planes.filter((p) => p.status === 'flying' || cargoWeightFor(state, p.id) > 0 || (p.queue?.length ?? 0) > 0)
: [];
// ISS-023 (v0.21.0): Relocate — any GROUNDED plane can be repositioned to any airport via the