feat(race-day): start heats with an EFRA/IFMAR count-down and flying start
Race Day Start now runs a count-down (Go, 30 s - 5 min, default per event) with calls every minute, 30 s and 10 s, grid call and 9..3 count for grid starts, hold/resume/+1 min/to 30 s/to 10 s/Go now, and starts the heat by itself when it runs out. The transponder check runs during the count-down, replacing the separate warm-up. Adds a flying start mode (horn + clock is running, own clock per car) as the qualifying default, and the latest personal clock start rules from EFRA 9.3 / IFMAR 4.2.3 for flying and staggered starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { RACE_AUDIO_EVENT_DEFINITIONS, createRaceAudioEngine, normalizeRaceAudio
|
||||
|
||||
const manifest = {
|
||||
events: { transponder_pass: { effect: "effects/transponder_pass.wav" }, start_tone: { effect: "effects/start_beep.wav" } },
|
||||
voice_events: { sv: { drivers_ready: "Redo", start_sequence: "Start på signalen", track_call: "Stopp på banan" }, en: { drivers_ready: "Ready" } },
|
||||
voice_events: { sv: { drivers_ready: "Redo", start_sequence: "Start på signalen", track_call: "Stopp på banan", clock_running: "Klockan går." }, en: { drivers_ready: "Ready" } },
|
||||
};
|
||||
|
||||
test("race audio settings normalize volume, delay, language and switches", () => {
|
||||
@@ -87,6 +87,34 @@ test("staggered start mode calls out each car in grid order, 3s apart, after the
|
||||
]);
|
||||
});
|
||||
|
||||
function createRecordingEngine(calls, settings = {}) {
|
||||
return createRaceAudioEngine({
|
||||
fetchImpl: async () => ({ ok: true, json: async () => manifest }),
|
||||
audioFactory: (src) => ({ play: () => { calls.push(["effect", src.split("/").pop()]); return Promise.resolve(); }, volume: 1 }),
|
||||
speak: async (text) => { calls.push(["voice", text]); return true; },
|
||||
wait: async (ms) => calls.push(["wait", ms]),
|
||||
getSettings: () => normalizeRaceAudioSettings({ startCountdownSeconds: 0, startLights: false, minStartDelay: 2, maxStartDelay: 2, randomStartDelay: false, ...settings }),
|
||||
});
|
||||
}
|
||||
|
||||
test("a flying start has no start procedure: horn, then 'clock is running' (EFRA 9.2b)", async () => {
|
||||
const calls = [];
|
||||
const engine = createRecordingEngine(calls, { startLights: true });
|
||||
const states = [];
|
||||
engine.subscribeStartLights((state) => states.push(state));
|
||||
await engine.runStartSequence({ id: "q1", type: "qualification", startMode: "flying" }, { onTone: () => calls.push(["timing-start"]), skipCountdown: true });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.deepEqual(states, ["IDLE", "GRID", "GO"], "no lights and no random delay");
|
||||
assert.deepEqual(calls, [["effect", "start_beep.wav"], ["timing-start"], ["voice", "Klockan går."]]);
|
||||
});
|
||||
|
||||
test("after the prologue a grid start skips its own count-down and intro calls", async () => {
|
||||
const calls = [];
|
||||
const engine = createRecordingEngine(calls, { startCountdownSeconds: 30 });
|
||||
await engine.runStartSequence({ id: "f1", type: "final", startMode: "position" }, { onTone: () => calls.push(["timing-start"]), skipCountdown: true, skipIntro: true });
|
||||
assert.deepEqual(calls, [["wait", 2000], ["effect", "start_beep.wav"], ["timing-start"]], "only the 0-5 s delay and the horn are left");
|
||||
});
|
||||
|
||||
test("a mass-start session never calls out individual cars, even if a grid order is passed", async () => {
|
||||
const calls = [];
|
||||
const engine = createRaceAudioEngine({
|
||||
@@ -124,7 +152,7 @@ test("cancelling a pending start prevents the official start callback", async ()
|
||||
test("event catalog covers every supported race-audio event", () => {
|
||||
const keys = new Set(RACE_AUDIO_EVENT_DEFINITIONS.map((item) => item.key));
|
||||
[
|
||||
"grid_open", "grid_closed", "race_1_minute", "race_30_seconds", "race_10_seconds", "drivers_ready", "start_sequence", "start_tone", "false_start", "restart_race", "staggered_release", "warmup_start", "warmup_1_minute", "warmup_end",
|
||||
"grid_open", "grid_closed", "race_1_minute", "race_30_seconds", "race_10_seconds", "drivers_ready", "start_sequence", "start_tone", "false_start", "restart_race", "staggered_release", "prologue_start", "prologue_minutes", "prologue_grid", "prologue_count", "prologue_hold", "prologue_resumed", "prologue_cancelled", "clock_running",
|
||||
"transponder_pass", "lap_completed", "personal_best", "fastest_lap", "track_record", "training_stable_pace", "new_leader", "position_gain", "position_loss", "final_lap", "one_minute_remaining", "thirty_seconds_remaining", "ten_seconds_remaining", "time_expired", "race_finished",
|
||||
"track_call", "track_clear", "yellow_flag", "red_flag", "race_paused", "race_resumed", "marshal_warning", "marshal_required", "penalty", "stop_and_go", "drive_through", "disqualified",
|
||||
"pit_lane_open", "pit_lane_closed", "entering_pits", "leaving_pits", "pit_speed_warning",
|
||||
|
||||
Reference in New Issue
Block a user