Maintenance: distance wear, broken-state burn ×1.5, repair action (ISS-069, v0.39.0)
CI / build + unit + e2e (pull_request) Successful in 9m34s
CI / build + unit + e2e (pull_request) Successful in 9m34s
This commit is contained in:
+41
-3
@@ -9,12 +9,15 @@ import { requiredFuelFor, windBurnFactor } from '@/lib/sim/movement';
|
||||
import { runAction } from '@/lib/sim/engine';
|
||||
import { getStateSnapshot, parseIcaoQueue } from './state';
|
||||
import {
|
||||
conditionBurnMultiplier,
|
||||
FUEL_DISCOUNT_PER_REP,
|
||||
FUEL_PRICE_PER_L,
|
||||
MAX_FUEL_DISCOUNT,
|
||||
MAX_PLANE_DISCOUNT,
|
||||
PLANE_DISCOUNT_PER_REP,
|
||||
PLANE_TYPES,
|
||||
REPAIR_COST,
|
||||
REPAIR_DURATION_MS,
|
||||
repRate,
|
||||
SELL_FRACTION,
|
||||
} from './catalog';
|
||||
@@ -118,8 +121,9 @@ export async function flyPlane(inst: GameDb, planeId: string, destIcao: string):
|
||||
const distanceKm = haversineKm(origin.lat, origin.lon, destFeature.properties.lat, destFeature.properties.lon);
|
||||
// Weighted burn model (v0.16.0): the leg needs enough fuel to cover it arriving empty; wind-
|
||||
// adjusted (v0.36.0) by departure weather vs route heading — every call site uses this same factor.
|
||||
// v0.39.0 (ISS-069): a broken plane (health ≤ 0) burns BROKEN_BURN_MULTIPLIER × more per km.
|
||||
const w = weatherFor(plane.locationIcao);
|
||||
const burnPerKm = type.burnPerKm * windBurnFactor(w.windKt, w.windDirDeg, initialBearingDeg(origin.lat, origin.lon, destFeature.properties.lat, destFeature.properties.lon));
|
||||
const burnPerKm = type.burnPerKm * windBurnFactor(w.windKt, w.windDirDeg, initialBearingDeg(origin.lat, origin.lon, destFeature.properties.lat, destFeature.properties.lon)) * conditionBurnMultiplier(plane.health);
|
||||
const requiredL = requiredFuelFor(burnPerKm, type.emptyWeightKg, type.maxFuel, distanceKm);
|
||||
if (requiredL > plane.fuel) {
|
||||
throw new GameActionError(409, `insufficient fuel: the leg needs ${Math.round(requiredL)} L but the plane has ${Math.round(plane.fuel)} L`);
|
||||
@@ -284,6 +288,7 @@ export async function buyPlane(inst: GameDb, model: string): Promise<{ planeId:
|
||||
lat: home.lat,
|
||||
lon: home.lon,
|
||||
departureSimTime: null,
|
||||
health: 100, // new planes arrive at full health (v0.39.0, ISS-069)
|
||||
});
|
||||
await inst.db.update(schema.account).set({ cash: Math.round((account.cash - cost) * 100) / 100 });
|
||||
await persist(inst);
|
||||
@@ -367,10 +372,11 @@ export async function refuelPlane(inst: GameDb, planeId: string, opts: RefuelOpt
|
||||
const origin = await airportByIcao(plane.locationIcao);
|
||||
const dest = await airportByIcao(pkg.destIcao);
|
||||
const distanceKm = haversineKm(origin.lat, origin.lon, dest.lat, dest.lon);
|
||||
// Same wind-adjusted requirement flyPlane checks (v0.36.0) — departure weather vs route heading.
|
||||
// Same wind-adjusted requirement flyPlane checks (v0.36.0) — departure weather vs route heading;
|
||||
// v0.39.0 (ISS-069): broken planes burn BROKEN_BURN_MULTIPLIER × more, same factor as flyPlane.
|
||||
const w = weatherFor(plane.locationIcao);
|
||||
const requiredL = requiredFuelFor(
|
||||
type.burnPerKm * windBurnFactor(w.windKt, w.windDirDeg, initialBearingDeg(origin.lat, origin.lon, dest.lat, dest.lon)),
|
||||
type.burnPerKm * windBurnFactor(w.windKt, w.windDirDeg, initialBearingDeg(origin.lat, origin.lon, dest.lat, dest.lon)) * conditionBurnMultiplier(plane.health),
|
||||
type.emptyWeightKg,
|
||||
type.maxFuel,
|
||||
distanceKm,
|
||||
@@ -404,6 +410,38 @@ export async function refuelPlane(inst: GameDb, planeId: string, opts: RefuelOpt
|
||||
return { cost };
|
||||
}
|
||||
|
||||
// --- Repair (POST /api/planes/:planeId/repair) — v0.39.0 (ISS-069) ---
|
||||
|
||||
// Restore a worn-out plane to full health for the flat REPAIR_COST; it is grounded in 'repairing'
|
||||
// status until sim.clockMs + REPAIR_DURATION_MS, when the engine's tick flips it back to
|
||||
// 'available'. Only GROUNDED planes can be repaired (409 otherwise); an already-full-health
|
||||
// repair is a free no-op (mirrors refuel's). 404 unknown plane; 409 not grounded / insufficient funds.
|
||||
export async function repairPlane(inst: GameDb, planeId: string): Promise<{ cost: number }> {
|
||||
const rows = await inst.db.select().from(schema.planes).where(eq(schema.planes.id, planeId));
|
||||
const plane = rows[0];
|
||||
if (!plane) throw new GameActionError(404, `plane ${planeId} not found`);
|
||||
if (plane.status !== 'available') {
|
||||
throw new GameActionError(409, `plane is ${plane.status}, only grounded planes can be repaired`);
|
||||
}
|
||||
// Free no-op when the plane is already at full health — nothing to repair.
|
||||
if (plane.health >= 100) return { cost: 0 };
|
||||
|
||||
const sim = (await inst.db.select().from(schema.simState))[0];
|
||||
if (!sim) throw new Error('sim_state row missing');
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
if (!account) throw new Error('account row missing');
|
||||
if (account.cash < REPAIR_COST) {
|
||||
throw new GameActionError(409, `insufficient funds: repair costs ${REPAIR_COST}, you have ${Math.round(account.cash)}`);
|
||||
}
|
||||
|
||||
await inst.db.update(schema.planes)
|
||||
.set({ health: 100, status: 'repairing', repairUntilSimTime: sim.clockMs + REPAIR_DURATION_MS })
|
||||
.where(eq(schema.planes.id, planeId));
|
||||
await inst.db.update(schema.account).set({ cash: Math.round((account.cash - REPAIR_COST) * 100) / 100 });
|
||||
await persist(inst);
|
||||
return { cost: REPAIR_COST };
|
||||
}
|
||||
|
||||
// --- Route helper (response convention, v0.8.0) ---
|
||||
|
||||
// Run an action under the sim lock — tick FIRST so validation/mutation see current
|
||||
|
||||
Reference in New Issue
Block a user