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
+32
View File
@@ -0,0 +1,32 @@
// Abandon penalty (v0.12.0) — dropping a loaded package costs a fraction of its
// payout that grows with time pressure: 15% right after creation, up to 60% at
// the deadline. Pure function shared by the server action (lib/game/actions.ts)
// and the client-side preview in components/RightPane.tsx so both always agree.
export const ABANDON_PENALTY_MIN = 0.15; // fraction of the payout, at creation time
export const ABANDON_PENALTY_SPAN = 0.45; // added across the package's lifetime → max 60% at the deadline
// The penalty fraction for a given elapsedFraction ∈ [0, 1] (clamped).
export function abandonPenaltyPct(elapsedFraction: number): number {
const f = Math.min(1, Math.max(0, elapsedFraction));
return ABANDON_PENALTY_MIN + ABANDON_PENALTY_SPAN * f;
}
// How much of the package's lifetime has elapsed at clockMs (clamped to [0, 1]).
export function elapsedFractionFor(createdSimTime: number, deadlineSimTime: number, clockMs: number): number {
const span = deadlineSimTime - createdSimTime;
if (span <= 0) return 1; // degenerate — treat as fully elapsed (max penalty)
return Math.min(1, Math.max(0, (clockMs - createdSimTime) / span));
}
// The exact € penalty for abandoning `pkg` at game time clockMs. An already-
// EXPIRED package abandons for free; cash is floored at 0 by the caller
// (account.cash = max(0, cash − penalty)). Rounded to cents like all money math.
export function abandonPenalty(
pkg: { status: string; reward: number; createdSimTime: number; deadlineSimTime: number },
clockMs: number,
): number {
if (pkg.status === 'expired') return 0;
const pct = abandonPenaltyPct(elapsedFractionFor(pkg.createdSimTime, pkg.deadlineSimTime, clockMs));
return Math.round(pkg.reward * pct * 100) / 100;
}