feat: v0.12.0 — contracts → packages: load/fly/abandon gameplay (ATR 72-600 capacity 6, manual flight to any airport, time-scaled abandon penalty 15–60%), one-time DB migration preserving wallet/clock; new endpoints /api/packages/:id/load|abandon + /api/planes/:planeId/fly

This commit is contained in:
2026-09-18 11:02:56 +00:00
parent 0208673da7
commit ab2e9fa54a
49 changed files with 2424 additions and 1409 deletions
+15 -7
View File
@@ -1,7 +1,7 @@
'use client';
import { useCallback, useEffect, useRef, useState, type ChangeEvent } from 'react';
import type { ContractState, PlaneState, StateSnapshot } from '@/lib/game/state';
import type { PlaneState, StateSnapshot } from '@/lib/game/state';
import { PLANE_TYPES } from '@/lib/game/catalog';
import { formatCash, formatFuel, formatSimClock } from '@/lib/format';
import { TIME_SCALES, scaleToSlider, sliderToScale } from '@/lib/timeScale';
@@ -19,14 +19,21 @@ interface LeftPaneProps {
const SCALE_SLIDER_STEP = 1 / (TIME_SCALES.length - 1); // one doubling per keyboard-arrow step
const SCALE_COMMIT_DEBOUNCE_MS = 250;
function planeLocation(p: PlaneState, contracts: ContractState[]): string {
if (p.status === 'flying' && p.contractId) {
const c = contracts.find((c) => c.id === p.contractId);
if (c) return `${p.locationName} → ${c.destName}`;
}
// Flying planes show their current leg from the snapshot's dest fields (v0.12.0).
function planeLocation(p: PlaneState): string {
if (p.status === 'flying' && p.destName) return `${p.locationName} → ${p.destName}`;
return p.locationName;
}
// Cargo loadout "n/capacity pkgs" — expired packages still occupy their slot.
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`;
}
export default function LeftPane({ state, loading, error, onRefresh }: LeftPaneProps) {
const [busy, setBusy] = useState(false);
@@ -156,7 +163,8 @@ export default function LeftPane({ state, loading, error, onRefresh }: LeftPaneP
</div>
<div className="plane-sub">
<span data-testid={`plane-fuel-${p.id}`}>{formatFuel(p.fuel, p.maxFuel)}</span>
<span className="plane-loc">{planeLocation(p, state.contracts)}</span>
<span data-testid={`plane-loadout-${p.id}`}>{loadoutFor(state, p)}</span>
<span className="plane-loc">{planeLocation(p)}</span>
</div>
</li>
))}