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:
+10
-10
@@ -20,7 +20,7 @@ import { HUB_ICAO } from './seed';
|
||||
// GameActionError carrying the HTTP status:
|
||||
// 404 — unknown entity id
|
||||
// 409 — valid ids but the current state forbids the action (wrong package/plane
|
||||
// status, plane not at origin, no free slot, insufficient fuel/funds, last plane)
|
||||
// status, plane not at origin, no payload room, insufficient fuel/funds, last plane)
|
||||
// 400 — bad input that isn't an existence problem (unknown airport/model)
|
||||
|
||||
export class GameActionError extends Error {
|
||||
@@ -43,8 +43,9 @@ async function airportByIcao(icao: string): Promise<{ icao: string; lat: number;
|
||||
// --- Load (POST /api/packages/:id/load {planeId}) ---
|
||||
|
||||
// open → loaded, bound to the plane. The plane must be GROUNDED at the package's
|
||||
// origin with a free slot (slots = packages 'loaded' or 'expired' on it — expired
|
||||
// cargo keeps occupying its space until delivered/abandoned).
|
||||
// origin with enough payload room (v0.27.0: used kg = sum of weightKg over packages
|
||||
// 'loaded' or 'expired' on it — expired cargo keeps occupying its space until
|
||||
// delivered/abandoned).
|
||||
export async function loadPackage(inst: GameDb, packageId: string, planeId: string): Promise<void> {
|
||||
const [pkgRows, planeRows] = await Promise.all([
|
||||
inst.db.select().from(schema.packages).where(eq(schema.packages.id, packageId)),
|
||||
@@ -69,13 +70,12 @@ export async function loadPackage(inst: GameDb, packageId: string, planeId: stri
|
||||
|
||||
const type = PLANE_TYPES[plane.model];
|
||||
if (!type) throw new Error(`unknown plane model ${plane.model}`); // → 500, cannot happen for seeded data
|
||||
const occupied = (
|
||||
await inst.db.select().from(schema.packages).where(
|
||||
and(inArray(schema.packages.status, ['loaded', 'expired']), eq(schema.packages.planeId, plane.id)),
|
||||
)
|
||||
).length;
|
||||
if (occupied >= type.capacity) {
|
||||
throw new GameActionError(409, `no free slots: the ${type.name} carries ${occupied}/${type.capacity} packages`);
|
||||
const occupied = await inst.db.select({ weightKg: schema.packages.weightKg }).from(schema.packages).where(
|
||||
and(inArray(schema.packages.status, ['loaded', 'expired']), eq(schema.packages.planeId, plane.id)),
|
||||
);
|
||||
const usedKg = occupied.reduce((sum, r) => sum + (r.weightKg ?? 0), 0);
|
||||
if (usedKg + pkg.weightKg > type.maxPayloadKg) {
|
||||
throw new GameActionError(409, `no payload room: the ${type.name} carries ${Math.round(usedKg)}/${type.maxPayloadKg} kg`);
|
||||
}
|
||||
|
||||
await inst.db.update(schema.packages)
|
||||
|
||||
Reference in New Issue
Block a user