export interface Checkpoint { id: string; center: readonly [number, number, number]; half: readonly [number, number, number]; } export interface RaceForkRoute { id: string; checkpoints: readonly Checkpoint[]; } /** * An alternate-route section (#286.3): after passing mainline checkpoint `afterIndex`, a racer commits * to whichever route's first checkpoint they hit, runs its checkpoints in order, and rejoins at * mainline `afterIndex + 1`. Every completed route contributes exactly one checkpoint of `progress` * regardless of length, and each hit records a split — so route time accounting comes for free. */ export interface RaceFork { id: string; /** Mainline checkpoint index the fork opens after; must be before the finish line. */ afterIndex: number; routes: readonly RaceForkRoute[]; } export interface RaceTrackConfig { checkpoints: readonly Checkpoint[]; laps?: number; forks?: readonly RaceFork[]; } export interface RaceTrack { readonly checkpoints: readonly Checkpoint[]; readonly laps: number; readonly forks: readonly RaceFork[]; } /** * A race track is an ordered ring of checkpoint trigger volumes plus a lap count. The final checkpoint * is the lap/finish line: a racer completes a lap by passing all checkpoints in order and hitting the * last one. `forks` splice alternate route segments between mainline checkpoints. */ export declare function raceTrack(config: RaceTrackConfig): RaceTrack; export interface RacerProgress { racerId: string; lap: number; nextCheckpoint: number; lastCheckpoint: number; progress: number; position: number; finished: boolean; finishTime: number | null; eliminated: boolean; splits: readonly number[]; /** Fork id → route id last taken through it. */ routesTaken: Readonly>; /** Route the racer is currently inside, or `null` on the mainline. */ activeRoute: { forkId: string; routeId: string; } | null; } export type RaceEvent = { type: "checkpoint.hit"; racerId: string; checkpoint: number; lap: number; time: number; /** Set when the hit is a fork-route checkpoint (`checkpoint` is then the fork's `afterIndex`). */ fork?: { forkId: string; routeId: string; index: number; }; } | { type: "fork.taken"; racerId: string; forkId: string; routeId: string; time: number; } | { type: "lap.completed"; racerId: string; lap: number; time: number; } | { type: "position.changed"; racerId: string; position: number; previous: number; } | { type: "race.finished"; ranking: readonly string[]; time: number; }; export type RaceWinCondition = (standings: readonly RacerProgress[], track: RaceTrack) => readonly string[] | null; /** Race ends when `count` racers have crossed the finish; ranking is the current standings order. */ export declare function firstPastPost(count?: number): RaceWinCondition; /** Round-cut (Fall Guys): the first `k` finishers qualify; race resolves the moment `k` cross. */ export declare function topK(k: number): RaceWinCondition; /** Every non-eliminated racer must finish. */ export declare function everyoneFinishes(): RaceWinCondition; /** Destruction-derby last-man-standing: ends when at most one racer is left un-eliminated. */ export declare function lastStanding(): RaceWinCondition; export interface RaceStateConfig { track: RaceTrack; win?: RaceWinCondition; } /** * Race state machine (issue #87). Drive it each tick with `update(now, positions)` — `now` is game time * (`ctx.time`), `positions` maps each racer to a world point tested against the ordered checkpoint * volumes. It emits `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished`, keeps * cumulative split times for PB deltas, resolves a pluggable win condition (first-past-post, round-cut, * derby last-standing), and `resetToCheckpoint` hands back a respawn pose at the racer's last checkpoint. * `removeRacer` drops a racer mid-race and `reset` returns the whole instance to its pre-race state for reuse. */ export declare class RaceState { private readonly track; private readonly win; private readonly racers; private readonly order; private raceFinished; private finalRanking; constructor(config: RaceStateConfig); addRacer(racerId: string, startTime?: number): void; /** Removes a racer's progress/standing and renumbers the remaining field's positions; a no-op for an unknown id. */ removeRacer(racerId: string): void; /** Clears all racer progress and finish state back to construction time; `track` and `win` are untouched, so the same instance can run another race. */ reset(): void; eliminate(racerId: string): void; get finished(): boolean; get ranking(): readonly string[]; progressOf(racerId: string): RacerProgress | null; private snapshot; standings(): readonly RacerProgress[]; private expectedCheckpoint; resetToCheckpoint(racerId: string): { position: [number, number, number]; heading: number; } | null; private compare; update(now: number, positions: Record | Map): readonly RaceEvent[]; } /** * A checkpoint race state machine — laps, forks, live standings, splits, and pluggable win conditions. * * @capability race-track a checkpoint race with laps, standings, splits, and win conditions */ export declare function createRaceState(config: RaceStateConfig): RaceState; /** Wall-clock lap timing: the current/last/best/total split-book every racing HUD reads. */ export interface LapTimerSnapshot { /** Seconds elapsed in the lap now underway. */ currentLap: number; /** Duration of the lap just completed, or `null` before the first lap closes. */ lastLap: number | null; /** Fastest completed lap so far, or `null` before the first lap closes. */ bestLap: number | null; /** Total time across every lap, including the current one. */ total: number; /** Number of laps completed. */ lapCount: number; /** Completed lap durations in order — the split book for a results screen. */ splits: readonly number[]; } /** * Wall-clock lap timer complementing {@link RaceState}: feed it `tick(dt)` each frame and call * `completeLap()` when the race emits a player `lap.completed` event. It accumulates the current lap, * carries best/last, banks splits, and `penalize()` folds a time penalty into the running lap — the * timing bookkeeping every racer reimplemented on top of the position-only race state. */ export interface LapTimer { /** Advance the current lap and total by `dt` seconds. */ tick(dt: number): void; /** Close the current lap: bank its split, update last/best, reset the current lap to 0, and return its time. */ completeLap(): number; /** Add a time penalty (seconds) to the lap currently underway. */ penalize(seconds: number): void; /** Clear all timing back to the start line. */ reset(): void; /** Immutable view of the current timing state. */ snapshot(): LapTimerSnapshot; } /** * Create a {@link LapTimer} starting at lap 0 with no splits, best, or last time recorded. * * @capability lap-timer wall-clock current/last/best lap timing with splits */ export declare function createLapTimer(): LapTimer; /** * The lifecycle every race runs through before, during, and after the clock: `idle` on the grid, * `countdown` on the lights, `racing` once they go green, `finished` at the flag. A pure value driven * by the transition functions below — it carries no track, racer, or lap data, so it composes with * {@link RaceState} (position/laps) and {@link LapTimer} (wall-clock) rather than duplicating them. */ export type RacePhase = "idle" | "countdown" | "racing" | "finished"; /** Immutable lifecycle snapshot: the current {@link RacePhase} plus the two clocks it owns. */ export interface RaceSessionState { /** Where the session sits in the start→countdown→race→finish lifecycle. */ readonly phase: RacePhase; /** Seconds left on the countdown, or 0 outside the `countdown` phase. */ readonly countdown: number; /** Seconds of green-flag racing accumulated so far. */ readonly elapsed: number; } /** Options for {@link startRaceCountdown}. */ export interface RaceCountdownOptions { /** Countdown length in seconds before the race goes green (default 3). A value ≤ 0 starts racing immediately. */ readonly seconds?: number; } /** * The pre-race session on the grid: `idle`, both clocks at zero. Call {@link startRaceCountdown} to * light the lights, or hold here until the field is ready. * * @capability race-session pure idle→countdown→racing→finished clock every racer wraps around its state */ export declare function idleRaceSession(): RaceSessionState; /** * Drop the lights: return a fresh `countdown` session of `seconds` (default 3). A non-positive length * skips straight to `racing` for a standing start with no countdown. */ export declare function startRaceCountdown(options?: RaceCountdownOptions): RaceSessionState; /** * Advance the session by `dt` seconds: bleed the countdown down and flip to `racing` when it reaches * zero, or accumulate `elapsed` while `racing`. `idle` and `finished` are inert. Overshoot past the * countdown is dropped rather than banked into `elapsed`, so the race clock always starts from zero. */ export declare function tickRaceSession(session: RaceSessionState, dt: number): RaceSessionState; /** Cross the flag: move a `racing` session to `finished`, freezing its `elapsed`. A no-op in any other phase. */ export declare function finishRaceSession(session: RaceSessionState): RaceSessionState; /** Whether a racer won or lost, derived from where they placed against the winning-places cutoff. */ export type RaceOutcome = "win" | "lose"; /** A racer's finishing position (`place`, 1-based) in a finish order, and the {@link RaceOutcome} it earns. */ export interface RacePlacement { readonly racerId: string; /** 1-based finishing position: `1` is the winner. */ readonly place: number; readonly outcome: RaceOutcome; } /** Options for {@link racePlacements} / {@link placementOf} / {@link raceOutcomeOf}. */ export interface PlacementOptions { /** How many top places count as a win (default 1). A podium finish would pass `3`. */ readonly winningPlaces?: number; } /** * Turn a finish-order ranking (index 0 = winner, e.g. the `ranking` of a `race.finished` event) into * per-racer {@link RacePlacement}s — the `1st/2nd/3rd` + win/lose every results screen shows. * * @capability race-placements finish order into per-racer place + win/lose for a results screen */ export declare function racePlacements(finishOrder: readonly string[], options?: PlacementOptions): readonly RacePlacement[]; /** * One racer's {@link RacePlacement} within a finish order, or `null` if they never crossed the line. * * @capability race-placements look up one racer's place + win/lose within a finish order */ export declare function placementOf(finishOrder: readonly string[], racerId: string, options?: PlacementOptions): RacePlacement | null; /** * The win/lose verdict for one racer in a finish order — `ranking[0] === player ? "win" : "lose"`, the * check every racing game hand-rolls, generalized to a `winningPlaces` cutoff. A racer absent from the * order counts as a `lose`. */ export declare function raceOutcomeOf(finishOrder: readonly string[], racerId: string, options?: PlacementOptions): RaceOutcome; /** * Per-segment durations from a cumulative split book (`splits[i]` = elapsed time at checkpoint `i`): * `segments[i] = splits[i] − splits[i−1]`, the first measured from `start` (default 0). Turns the * cumulative splits {@link RacerProgress} records into the individual leg times a results screen shows. * * @capability lap-splits turn cumulative split times into per-segment leg times */ export declare function splitSegments(splits: readonly number[], start?: number): number[]; /** * Per-lap durations from a cumulative split book with `gatesPerLap` checkpoints per lap — each lap's time * is its finish-gate split minus the previous lap's finish. Only complete laps are returned. * * @capability lap-splits per-lap durations from a cumulative split book */ export declare function lapDurations(splits: readonly number[], gatesPerLap: number): number[]; /** * Elementwise delta of a cumulative split book against a `reference` book (a personal best or par lap): * positive means behind the reference at that checkpoint. Compared up to the shorter length — the * `+0.3s` / `−1.2s` gap every racing HUD shows against its ghost. * * @capability lap-splits delta of splits against a personal-best or par reference */ export declare function parDelta(splits: readonly number[], reference: readonly number[]): number[];