Queue consumed on dispatch; engine chain hardened; queued planes unsellable (ISS-088)
CI / build + unit + e2e (pull_request) Failing after 12m59s

This commit is contained in:
2026-09-21 22:36:01 +00:00
parent 76f5b6f8ce
commit 479b993c09
10 changed files with 407 additions and 27 deletions
+35 -24
View File
@@ -246,32 +246,43 @@ export async function deliverPackages(inst: GameDb, clockMs: number): Promise<nu
let topUpFuel: number | null = null; // post-top-up fuel for the chained departure, when a top-up happened
if (queue.length > 0 && newHealth > 0) {
const nextDest = queue[0];
const next = byIcao.get(nextDest); // unknown ICAO in the queue → cannot chain
if (next) {
// Wind-adjusted requirement for the NEXT leg — it departs from `to` (v0.36.0).
const wNext = weatherFor(to.icao);
const nextRequiredL = requiredFuelFor(
type.burnPerKm * windBurnFactor(wNext.windKt, wNext.windDirDeg, initialBearingDeg(to.lat, to.lon, next.lat, next.lon)),
type.emptyWeightKg,
type.maxFuel,
haversineKm(to.lat, to.lon, next.lat, next.lon),
);
if (nextRequiredL <= landedFuel) {
queue.shift();
chainedTo = nextDest;
} else {
// ISS-082: auto top-up — same target/price terms as flyPlane's dispatch refuel.
const target = Math.min(type.maxFuel, Math.max(landedFuel, nextRequiredL * 1.5));
const liters = Math.max(0, target - landedFuel);
// Fresh account read (same pattern as the delivery awards above) so a top-up in this tick sees current cash.
const fresh = (await inst.db.select().from(schema.account))[0];
if (!fresh) throw new Error('account row missing');
const cost = Math.round(liters * FUEL_PRICE_PER_L * (1 - repRate(fresh.reputation, FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT)) * 100) / 100;
if (cost === 0 || fresh.cash >= cost) {
// ponytail: defensive guard for legacy rows — a queued leg to the airport we just landed at is a
// zero-distance no-op hop; drop it and ground instead of flying. Normal play can't produce this
// since v0.46.0 (flyPlane consumes queue[0] on dispatch, every chain shifts its own leg).
if (nextDest === to.icao) {
queue.shift();
} else {
const next = byIcao.get(nextDest); // unknown ICAO in the queue → cannot chain
if (next) {
// Wind-adjusted requirement for the NEXT leg — it departs from `to` (v0.36.0).
const wNext = weatherFor(to.icao);
const nextRequiredL = requiredFuelFor(
type.burnPerKm * windBurnFactor(wNext.windKt, wNext.windDirDeg, initialBearingDeg(to.lat, to.lon, next.lat, next.lon)),
type.emptyWeightKg,
type.maxFuel,
haversineKm(to.lat, to.lon, next.lat, next.lon),
);
// v0.46.0 backstop: a leg that doesn't fit even a full tank is unflyable — ground with the queue
// intact, mirroring flyPlane's 409 without throwing in the engine.
if (nextRequiredL > type.maxFuel) {
// no chain — the grounded update below keeps the queue for a later dispatch
} else if (nextRequiredL <= landedFuel) {
queue.shift();
chainedTo = nextDest;
topUpFuel = target;
if (cost > 0) await inst.db.update(schema.account).set({ cash: Math.round((fresh.cash - cost) * 100) / 100 });
} else {
// ISS-082: auto top-up — same target/price terms as flyPlane's dispatch refuel.
const target = Math.min(type.maxFuel, Math.max(landedFuel, nextRequiredL * 1.5));
const liters = Math.max(0, target - landedFuel);
// Fresh account read (same pattern as the delivery awards above) so a top-up in this tick sees current cash.
const fresh = (await inst.db.select().from(schema.account))[0];
if (!fresh) throw new Error('account row missing');
const cost = Math.round(liters * FUEL_PRICE_PER_L * (1 - repRate(fresh.reputation, FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT)) * 100) / 100;
if (cost === 0 || fresh.cash >= cost) {
queue.shift();
chainedTo = nextDest;
topUpFuel = target;
if (cost > 0) await inst.db.update(schema.account).set({ cash: Math.round((fresh.cash - cost) * 100) / 100 });
}
}
}
}