Maintenance: distance wear, broken-state burn ×1.5, repair action (ISS-069, v0.39.0)
CI / build + unit + e2e (pull_request) Successful in 9m34s

This commit is contained in:
2026-09-21 12:06:45 +00:00
parent 929452ca33
commit 336ed4ee7c
28 changed files with 868 additions and 41 deletions
+11
View File
@@ -1,5 +1,16 @@
# Changelog
## v0.39.0 — 2026-09-21 12:02
- **Maintenance (ISS-069).** Planes wear out and can be repaired: distance-based health, a broken state that burns more fuel, and a repair action that grounds the plane for 3 sim days. New balance constants in `lib/game/catalog.ts` (all ponytail-tunable): `WEAR_PER_KM = 0.005`, `BROKEN_BURN_MULTIPLIER = 1.5`, `REPAIR_COST = 1_500` (flat), `REPAIR_DURATION_MS = 3 × 86_400_000`, plus the pure helper `conditionBurnMultiplier(health)` — exactly 1 above zero health, ×1.5 at/below it
- **Schema.** Two new `planes` columns in all four places: Drizzle schema (`lib/db/schema.ts`), raw DDL + guarded `ALTER TABLE … ADD COLUMN` migrations in `migrateColumns()` (pre-existing rows backfill to full health / NULL gate, mirroring the weight_kg/special precedent), and the seed insert path — starter plane in `buildSeed` (`lib/game/seed.ts`) and every `buyPlane` delivery start at `health: 100`. New plane status `'repairing'` added to the three pinned unions (schema enum, `PlaneState`, OpenAPI) plus `mapData.ts`'s feature props
- **Wear.** At landing only — `deliverPackages` (`lib/sim/engine.ts`) drops health by `WEAR_PER_KM × distanceKm` per landed leg (clamped at 0), in both the grounded and auto-chain write branches. A broken plane is one with `health ≤ 0`: it still flies, but burns ×1.5 more fuel — applied as a third factor alongside wind at ALL FIVE burn sites that hold the plane row: `flyPlane` dispatch feasibility + `refuelPlane` forFlight requirement (`lib/game/actions.ts`), engine landed-fuel + auto-chain next-leg check (`lib/sim/engine.ts`), and the live in-flight gauge in `livePlane()` (`lib/game/state.ts`). The engine's landed fuel uses the PRE-landing health (frozen at departure, like fuel) so it stays bit-consistent with the UI gauge; the chained leg's feasibility uses the post-wear value
- **Repair.** New action `repairPlane` (`lib/game/actions.ts`) + POST `/api/planes/{planeId}/repair` (no body): 404 unknown plane, 409 not grounded ("only grounded planes can be repaired"), free no-op at full health (mirrors refuel), else charges the flat `REPAIR_COST`, restores health to 100 and grounds the plane in `'repairing'` with a gate at `clockMs + REPAIR_DURATION_MS`. The engine's new `finishRepairs` pass (same shape as `expirePackages`) runs inside every advancing tick — even when nothing landed — flipping gated planes back to `'available'` and clearing the gate. While repairing, the existing `status !== 'available'` guards in load/fly/refuel/sell/abandon block everything automatically
- **Snapshot & OpenAPI.** `PlaneState` gains `health: number` + `repairUntilSimTime: number | null`; the repair endpoint is documented (200 → snapshot, 404/409). Pins updated in lockstep: `lib/openapi.test.ts` (fourteen endpoints, nine action-endpoint loop with errorCodes `['404','409']`, PlaneState key list) and `e2e/api.spec.ts`
- **UI.** The fleet rows (`components/LeftPane.tsx`) show an amber health bar (new `.health-bar`, modeled on `.fuel-bar`) + a `{n}%` readout next to the fuel text; grounded planes get a red "broken" pill at zero health and an amber "in repair" pill while the gate runs (`.status-broken` / `.status-repairing`). A Repair button appears for any grounded plane below full health — no cost preview, flat cost is server-side only
- **Test hook.** POST `/api/test/set-health` (`FLIGHT_TEST_HOOKS=1` only): `{ planeId, health?, repairUntilMs? }` forces a plane's health (finite in [0,100]) and/or sets its repair gate to `clock + repairUntilMs` — lets e2e force the broken state and shorten an active 3-day gate without flying thousands of km
- **Tests.** New unit tests: wear accumulation per landed leg + clamping at 0 across multiple legs; broken-state ×1.5 landed fuel vs a healthy control on the identical leg; `conditionBurnMultiplier` pins; `flyPlane` feasibility using the adjusted requirement (same tank flyable at full health, 409 when broken); `repairPlane` (exact cost/status/gate/health, free no-op, 409s for flying/repairing/insufficient funds, load/fly/refuel/sell all blocked while repairing) and `finishRepairs` (gate boundary + flip-back through a full `tickOnce` with nothing landed). E2E: new `e2e/maintenance.spec.ts` — the visible flow per the issue (force 42 → UI shows it; force 0 → broken pill + short full-tank leg lands with exactly the ×1.5-adjusted fuel from the real lib helpers; click Repair → exact cash drop + "in repair" pill; shorten the gate via the hook → back to service at health 100), restoring a clean starter on exit
- **E2E drift mitigation.** Wear accumulates across the whole CI run (one shared DB, workers:1) — the self-heal helpers in `e2e/right-pane.spec.ts` (`flyHomeViaApi`), `e2e/two-planes.spec.ts` (`resetFleet`) and `e2e/actions.spec.ts` (`flyHomeViaApi`) now restore full health + clear any active repair gate first, so the farthest-flyable pickers and the FAR round-trip feasibility pin never see a worn or broken starter
## v0.38.0 — 2026-09-21 10:47
- **Reputation economy (ISS-072).** Reputation is no longer a dead stat — it drives the economy through three simple tunable formulas, all built on one new pure helper `repRate(reputation, perPoint, cap)` in `lib/game/catalog.ts` (linear above the starting value of 10, capped; exactly 0 at/below start so default-rep behavior is byte-identical):
- **Cheaper plane purchases** — `buyPlane` (`lib/game/actions.ts`) charges `price × (1 − repRate(rep, PLANE_DISCOUNT_PER_REP = 0.01, MAX_PLANE_DISCOUNT = 0.4))`, rounded to 2dp; the effective cost drives both the funds check and the deduction (up to 40% off at rep ≥ 50)