feat: v0.9.2 — power-of-two time scale: slider steps through the 1/2/4/8/16/32/64 ladder, POST /api/sim clamp widened to [1,64]

This commit is contained in:
2026-09-17 19:15:15 +00:00
parent 62f9929b51
commit 03653fc50e
12 changed files with 80 additions and 51 deletions
+6 -6
View File
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef, useState, type ChangeEvent } from 'reac
import type { ContractState, PlaneState, StateSnapshot } from '@/lib/game/state';
import { PLANE_TYPES } from '@/lib/game/catalog';
import { formatCash, formatFuel, formatSimClock } from '@/lib/format';
import { scaleToSlider, sliderToScale } from '@/lib/timeScale';
import { TIME_SCALES, scaleToSlider, sliderToScale } from '@/lib/timeScale';
interface LeftPaneProps {
state: StateSnapshot | null;
@@ -14,9 +14,9 @@ interface LeftPaneProps {
onRefresh: () => void;
}
// Log-scale slider (v0.9.1): position t ∈ [0,1] → scale = round(60^t), so equal distances
// are equal multiplicative steps. 60 discrete positions keep keyboard arrows usable (~7% per step).
const SCALE_SLIDER_STEP = 1 / 59;
// Power-of-two slider (v0.9.2): position t ∈ [0,1] → nearest step of the ladder
// 1×/2×/4×/8×/16×/32×/64× — each of the 7 positions doubles the speed.
const SCALE_SLIDER_STEP = 1 / (TIME_SCALES.length - 1); // one doubling per keyboard-arrow step
const SCALE_COMMIT_DEBOUNCE_MS = 250;
function planeLocation(p: PlaneState, contracts: ContractState[]): string {
@@ -34,7 +34,7 @@ export default function LeftPane({ state, loading, error, onRefresh }: LeftPaneP
// controls without waiting for the next poll tick. On failure keep the stale UI — the
// running poll retries on its own and surfaces the error state. A commit that lands while
// another POST is in flight is held in a single slot and flushed right after, so the last
// user intent always reaches the server (the server's [1,60] clamp stays as backstop).
// user intent always reaches the server (the server's [1,64] clamp stays as backstop).
const queuedRef = useRef<{ timeScale?: number; paused?: boolean } | null>(null);
const applySimControls = useCallback(
@@ -73,7 +73,7 @@ export default function LeftPane({ state, loading, error, onRefresh }: LeftPaneP
// Slider commit (v0.9.1): the displayed value follows the drag optimistically via
// pendingScale; POST /api/sim fires after a 250 ms debounce, or immediately when the
// user releases the slider (browsers fire a native 'change' on release). The server's
// [1,60] clamp stays as the backstop.
// [1,64] clamp stays as the backstop.
const [pendingScale, setPendingScaleState] = useState<number | null>(null);
const pendingRef = useRef<number | null>(null);
const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);