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
+13 -11
View File
@@ -23,23 +23,25 @@ interface RightPaneProps {
const planeName = (p: PlaneState): string => PLANE_TYPES[p.model]?.name ?? p.model;
// Packages occupying a plane's slots — 'loaded' cargo plus 'expired' cargo, which
// keeps its slot until it is abandoned or the plane lands at its destination.
export function slotsUsedFor(state: StateSnapshot, planeId: string): number {
return state.packages.filter(
(p) => p.planeId === planeId && (p.status === 'loaded' || p.status === 'expired'),
).length;
// Cargo weight (kg) occupying a plane's payload — 'loaded' cargo plus 'expired'
// cargo, which keeps its space until it is abandoned or the plane lands at its
// destination.
export function cargoWeightFor(state: StateSnapshot, planeId: string): number {
return state.packages
.filter((p) => p.planeId === planeId && (p.status === 'loaded' || p.status === 'expired'))
.reduce((sum, p) => sum + p.weightKg, 0);
}
// Planes that can load `pkg` right now: GROUNDED at the package's origin with a
// free slot (capacity from the snapshot catalog). The Load button is enabled iff
// this list is non-empty; exactly one entry loads directly, several show a picker.
// Planes that can load `pkg` right now: GROUNDED at the package's origin with enough
// payload room for it (maxPayloadKg from the snapshot catalog). The Load button is
// enabled iff this list is non-empty; exactly one entry loads directly, several show a picker.
export function loadEligiblePlanes(state: StateSnapshot, pkg: PackageState): PlaneState[] {
return state.planes.filter((p) => {
if (p.status !== 'available') return false;
if (p.locationIcao !== pkg.originIcao) return false;
const capacity = state.catalog.find((e) => e.model === p.model)?.capacity ?? 0;
return slotsUsedFor(state, p.id) < capacity;
const maxPayloadKg = state.catalog.find((e) => e.model === p.model)?.maxPayloadKg ?? 0;
// Mirrors the server check in lib/game/actions.ts loadPackage.
return cargoWeightFor(state, p.id) + pkg.weightKg <= maxPayloadKg;
});
}