Slow sim to 1× on landing and resume last selected speed (ISS-057, v0.30.0)
CI / build + unit + e2e (pull_request) Successful in 8m57s
CI / build + unit + e2e (pull_request) Successful in 8m57s
This commit is contained in:
+24
-15
@@ -75,8 +75,7 @@ export async function tickOnce(inst: GameDb): Promise<schema.SimStateRow> {
|
||||
let landedThisTick = 0;
|
||||
if (advanceMs > 0) {
|
||||
await expirePackages(inst, clockMs);
|
||||
// ISS-041: remember the landings so the tick can pause when this was the last
|
||||
// plane in flight.
|
||||
// ISS-057: remember the landings so the tick can slow down when a plane is left on ground.
|
||||
landedThisTick = await deliverPackages(inst, clockMs);
|
||||
await spawnOpenPackages(inst, clockMs);
|
||||
// AFTER delivery on purpose: a plane that just landed gets work spawned
|
||||
@@ -84,18 +83,21 @@ export async function tickOnce(inst: GameDb): Promise<schema.SimStateRow> {
|
||||
await spawnCoveragePackages(inst, clockMs);
|
||||
}
|
||||
|
||||
const sets: Partial<Pick<schema.SimStateRow, 'clockMs' | 'lastRealTickMs' | 'timeScale' | 'paused'>> = {
|
||||
const sets: Partial<Pick<schema.SimStateRow, 'clockMs' | 'lastRealTickMs' | 'timeScale'>> = {
|
||||
clockMs,
|
||||
lastRealTickMs: nowMs,
|
||||
};
|
||||
// Pause on landing (ISS-041): when ≥1 plane landed this tick and NO plane remains in flight,
|
||||
// pause the sim WITHOUT touching timeScale — speed is preserved for the next dispatch.
|
||||
// Arrival-triggered by design: a grounded fleet at high scale is left alone (there is no
|
||||
// auto-resume except starting a flight; see flyPlane). A paused tick never processes landings
|
||||
// (advanceMs === 0), so the trigger can only fire on an advancing tick.
|
||||
// Slow on landing (ISS-057): when ≥1 plane landed this tick and ANY plane is grounded
|
||||
// ('available') afterwards, drop the sim to MIN_TIME_SCALE — a just-landed plane counts, so
|
||||
// any real landing slows. A chained plane stays 'flying' via deliverPackages' auto-chain
|
||||
// branch, so a pure-chain tick with nothing grounded does NOT slow. Pause is manual-only now;
|
||||
// flyPlane restores lastSelectedTimeScale when the last grounded plane takes off. Arrival-
|
||||
// triggered by design: a grounded fleet at high scale is left alone (no idle-enforcement), and
|
||||
// a paused tick never processes landings (advanceMs === 0), so the trigger only fires on an
|
||||
// advancing tick — a manual speed change while planes sit grounded survives until the next landing.
|
||||
if (landedThisTick > 0) {
|
||||
const flying = await inst.db.select().from(schema.planes).where(eq(schema.planes.status, 'flying'));
|
||||
if (flying.length === 0) sets.paused = true;
|
||||
const available = await inst.db.select().from(schema.planes).where(eq(schema.planes.status, 'available'));
|
||||
if (available.length >= 1) sets.timeScale = MIN_TIME_SCALE;
|
||||
}
|
||||
|
||||
// Persist lastRealTickMs even when nothing advanced — it is the tick anchor.
|
||||
@@ -132,8 +134,8 @@ export async function expirePackages(inst: GameDb, clockMs: number): Promise<num
|
||||
// its remaining fuel (a short tank keeps the queue intact for refuel + re-dispatch).
|
||||
// Idempotent — a finalized plane either leaves the 'flying' state or starts a NEW
|
||||
// leg whose own arrival is in the future, so it can never be awarded twice.
|
||||
// Returns planes landed. NOTE: runs BEFORE tickOnce's pause check on purpose —
|
||||
// a chained plane counts as still flying there, so chaining never pauses early.
|
||||
// Returns planes landed. NOTE: runs BEFORE tickOnce's slow-down check on purpose —
|
||||
// a chained plane is already 'flying' again here, so a pure-chain landing never slows the sim.
|
||||
export async function deliverPackages(inst: GameDb, clockMs: number): Promise<number> {
|
||||
const [planeRows, packageRows] = await Promise.all([
|
||||
inst.db.select().from(schema.planes),
|
||||
@@ -320,13 +322,20 @@ export function runAction<T>(fn: (inst: GameDb) => Promise<T>): Promise<T> {
|
||||
|
||||
// Tick first so the returned state reflects current time, then apply the new
|
||||
// controls. timeScale is clamped to [1, 1000]; validation of raw input happens in
|
||||
// the route handler (400 on non-number / non-boolean).
|
||||
// the route handler (400 on non-number / non-boolean). ISS-057: posting a timeScale
|
||||
// ALSO records it as lastSelectedTimeScale — the resume target flyPlane restores when
|
||||
// the last grounded plane takes off. A {paused}-only post never touches it, so the
|
||||
// last manual intent wins without an extra flag.
|
||||
export function applyTimeControls(input: SimControlsInput): Promise<schema.SimStateRow> {
|
||||
return withLock(async () => {
|
||||
const inst = await getGameDb();
|
||||
await tickOnce(inst);
|
||||
const sets: Partial<Pick<schema.SimStateRow, 'timeScale' | 'paused'>> = {};
|
||||
if (input.timeScale !== undefined) sets.timeScale = clampTimeScale(input.timeScale);
|
||||
const sets: Partial<Pick<schema.SimStateRow, 'timeScale' | 'lastSelectedTimeScale' | 'paused'>> = {};
|
||||
if (input.timeScale !== undefined) {
|
||||
const scale = clampTimeScale(input.timeScale);
|
||||
sets.timeScale = scale;
|
||||
sets.lastSelectedTimeScale = scale;
|
||||
}
|
||||
if (input.paused !== undefined) sets.paused = input.paused;
|
||||
if (Object.keys(sets).length > 0) await inst.db.update(schema.simState).set(sets);
|
||||
await persist(inst);
|
||||
|
||||
Reference in New Issue
Block a user