# Racing module

> defineRaceGame builds a complete 3D arcade car race from one config, with a track, the player's car, AI rivals, a chase camera, a HUD, touch and keyboard controls, and a countdown and finish screen.

`defineRaceGame(config)` makes a full-screen arcade race on a closed track: the player's car against up to six AI rivals, for a number of laps. Use it for any car racing game: circuits, ovals, karts, night races, desert rallies. The starter game every new repo ships with, **Sunset Sprint**, is this module.

```tsx
import { defineRaceGame, TRACK_PRESETS } from '@/engine/modules/racing';
import type { RaceConfig } from '@/engine/modules/racing';
```

Source: `templates/crescent-game/src/engine/modules/racing/`.

## The smallest complete game

This is Sunset Sprint, the starter game in `templates/crescent-game/src/game/index.tsx` (the file there also sets a `badge`):

```tsx
// src/game/index.tsx
import { defineRaceGame } from '@/engine/modules/racing';

export default defineRaceGame({
  id: 'sunset-sprint',
  title: 'Sunset Sprint',
  description: 'Three laps around a coastal circuit as the sun goes down.',
  laps: 3,
  track: { preset: 'circuit', width: 12, seed: 11 },
  car: { color: '#7C5CFF', accent: '#2EE6D6', maxSpeedKmh: 160, acceleration: 1.1, handling: 1.1 },
  opponents: { count: 3, speed: 0.8 },
  theme: { timeOfDay: 'sunset', scenery: 'trees', fog: true },
});
```

Only `id`, `title` and `description` are required. Everything else has a default.

## What the player gets

- A 3D track built from the preset or your points: road, edge lines, a dashed centre line, alternating barriers and scenery placed by `track.seed`.
- A "3, 2, 1, GO!" countdown, during which nobody moves.
- The player's car, starting last on the grid, and AI rivals that follow the racing line, lift for corners and brake rather than drive through the player.
- Laps that count only when the car has passed three checkpoints (at a quarter, half and three quarters of the lap). Reversing over the line clears them, so the line cannot be farmed.
- A HUD: lap (`LAP 1/3`), position, race timer, a speed gauge, a mute button and an exit button.
- A finish card with the position, the total time, the best lap, **Race again** and **Back to games**.
- Engine sounds that follow the speed, tyre squeal, braking, bumps, a whoosh when a rival passes, and a music track.
- Lighting, sky and fog from `theme.timeOfDay`, with car lights at night.

Controls are the engine's `TouchControls`: steer left and right on one side, gas and brake on the other. In a desktop browser the same actions are on the keyboard: A / D or the left and right arrows steer, W or the up arrow is the gas, S, the down arrow or Space brake. See [Input](https://docs.moonspell.app/engine/input.md).

## Config reference

Values outside a range are clamped. Speeds and ratios are multipliers unless stated.

### Top level

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `id` | `string` | required | URL-safe, kebab-case id. |
| `title` | `string` | required | The game's name, shown in the HUD. |
| `description` | `string` | required | One sentence for the game's card. |
| `badge` | `string` | a race car emoji | Card metadata; the race never shows it. |
| `laps` | `number` | `3` | Laps to finish, 1 to 10. |
| `track` | object | see below | The track shape and size. |
| `car` | object | see below | The player's car. |
| `controls` | object | see below | The control layout. |
| `opponents` | object | see below | The AI rivals. |
| `theme` | object | see below | Colours, scenery, light and post-processing. |

### `track`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `preset` | `'oval' \| 'figure-eight' \| 'hairpin' \| 'circuit' \| 'speedway'` | `'circuit'` | A built-in shape. |
| `points` | `[number, number][]` | none | Your own closed loop as `[x, z]` in metres. Needs 4 or more points, otherwise the preset is used. Overrides `preset`. |
| `width` | `number` | `11` | Road width in metres, 8 to 16. |
| `seed` | `number` | `7` | Seed for scenery placement. |

The presets, with their approximate size:

| Preset | Size | Character |
| --- | --- | --- |
| `oval` | 140 x 84 m | fast and simple |
| `speedway` | 240 x 120 m | very fast, long straights |
| `figure-eight` | about 160 m across | crosses itself in the middle |
| `hairpin` | about 225 x 55 m | two long straights, tight ends |
| `circuit` | about 205 x 120 m | varied corners, the default |

`TRACK_PRESETS` exports the point lists, so you can start from one and edit it.

### `car`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `color` | CSS colour | `'#7C5CFF'` | Body colour. |
| `accent` | CSS colour | `'#2EE6D6'` | Cabin colour. Also the HUD accent, the **Race again** button and the game's `accent`. |
| `maxSpeedKmh` | `number` | `150` | Top speed in km/h, 80 to 220. |
| `acceleration` | `number` | `1` | Engine force, 0.5 (sluggish) to 2 (rocket). |
| `handling` | `number` | `1` | Steering response, 0.5 (boat) to 2 (go-kart). Higher also means less understeer. |
| `steeringAssist` | `number` | `0.12` | 0 to 1. Gently steers toward the racing line while the player is not steering. 0 is none, 0.5 and up is very easy. |

### `controls`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `layout` | `'classic' \| 'mirrored'` | `'classic'` | `classic` puts steering on the left and pedals on the right; `mirrored` swaps them. |

### `opponents`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `count` | `number` | `3` | AI cars, 0 to 6. |
| `colors` | `string[]` | six bright colours | Body colours, cycled when shorter than `count`. |
| `speed` | `number` | `0.62` | Pace as a fraction of the player's top speed, 0.4 to 1.1. 0.62 is beatable, 0.75 a fair fight, 0.9 hard. Each rival varies by a few percent so the pack spreads out. |

### `theme`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `timeOfDay` | `'day' \| 'sunset' \| 'night'` | `'day'` | Picks the lighting and the default sky, ground, road and line colours. Night turns on car lights. |
| `sky` | CSS colour | per time of day | Sky and fog colour. |
| `ground` | CSS colour | per time of day | Ground colour. |
| `road` | CSS colour | per time of day | Road colour. |
| `line` | CSS colour | per time of day | Edge line colour. |
| `barrier` | `[string, string]` | `['#E63946', '#F8F9FA']` | Alternating barrier colours. |
| `scenery` | `'trees' \| 'rocks' \| 'cacti' \| 'pillars' \| 'none'` | `'trees'` | What stands around the track. |
| `sceneryColor` | CSS colour | `'#2F7D4A'` (`'#1F4D3A'` at night) | Scenery colour. |
| `fog` | `boolean` | `true` | Distance fog in the sky colour. |
| `postfx` | `'glow' \| 'cinematic' \| 'retro' \| 'off'` | `'off'` | A post-processing look. `glow` makes lights and neon bloom (night and arcade races), `cinematic` is moodier, `retro` punchier. |

## A custom track

```tsx
import { defineRaceGame } from '@/engine/modules/racing';

export default defineRaceGame({
  id: 'square-loop',
  title: 'Square Loop',
  description: 'A rounded square at night with neon barriers.',
  track: { points: [[-70, -70], [0, -85], [70, -70], [85, 0], [70, 70], [0, 85], [-70, 70], [-85, 0]], width: 12 },
  theme: { timeOfDay: 'night', scenery: 'pillars', barrier: ['#FF2BD6', '#1B1E3A'], postfx: 'glow' },
  controls: { layout: 'mirrored' },
});
```

The loop closes itself and a spline smooths the corners. Shapes that drive well:

- 6 to 20 points, roughly 150 to 400 m across.
- Consecutive points 15 to 60 m apart.
- No turn sharper than about 100 degrees.
- The loop does not cross itself unless you want a crossing.

## How the car feels

An arcade bicycle model. Leaving the road slows the car. Barriers are a hard wall: the car scrubs speed and slides along them, and never crosses. Cars collide: the player is pushed out of a rival and loses speed, more for a head-on hit.

## What you can customise

| What | How |
| --- | --- |
| Look | `theme` colours, `scenery`, `timeOfDay`, `postfx`, `car.color` and `accent`, `opponents.colors`. |
| Difficulty | `opponents.count` and `speed`, `laps`, `car.steeringAssist`, `track.width`. |
| Feel | `car.maxSpeedKmh`, `acceleration`, `handling`. |
| Sounds | The module plays fixed files: `assets/audio/engine-idle.wav`, `engine-high.wav`, `squeal.wav`, `accelerate.m4a`, `brake.m4a`, `bump.m4a`, `passby.m4a` and `music-synthwave.m4a`. Replace a file to change its sound. There is no `sounds` field. |
| Car model | The cars are a procedural low-poly model; there is no field for a `.glb`. |
| HUD, rules, anything else | Edit the game's own copy of the module in `src/engine/modules/racing/` (`RaceGame.tsx` is the screen, `RaceHud.tsx` the HUD, `raceState.ts` the lap rules, `vehicle.ts` the physics). Each game repo carries its own engine copy. Or write your own race from the engine pieces: `TouchControls`, `Environment`, `RaceAudio` and the [path utilities](https://docs.moonspell.app/engine/utilities.md) are exported from `@/engine`. |

## Backend

The racing module uses no backend: no leaderboard, no rooms, no saves. To put a race online, add the calls yourself in your copy of `RaceGame.tsx`:

- Best times: `useLeaderboard` with `order: 'asc'`, submitting milliseconds, one board per track. Online, the backend's `BOARDS` table decides the order; a family such as `"lap-*": { order: "asc", min: 5_000, max: 3_600_000 }` covers every track.
- Live races: `useRoom`, sending the car's pose and drawing the other cars from the room.
- Unlocked cars and tracks: [saves and progression](https://docs.moonspell.app/engine/saves.md).

See [Networking](https://docs.moonspell.app/engine/net.md) and the [backend overview](https://docs.moonspell.app/backend/overview.md).

## Dev and testing

In development builds (`__DEV__`) the HUD shows a telemetry line (`testID` `hud-debug`: inputs, speed, position on the track, collisions, sound counters, touches) and an **AUTO** button (`btn-autopilot`) that drives the car by itself. Production builds hide both.

Other `testID`s: `btn-exit`, `btn-mute`, `hud-lap`, `hud-position`, `hud-countdown`, `hud-speed`, `hud-finished`, `btn-restart`, `btn-finish-exit`, and `game-<id>` on the root view.

## Gotchas

- A custom `points` list with fewer than 4 points is ignored without a warning; the preset is used.
- Clamping is silent: `laps: 20` plays 10 laps.
- `opponents.speed` above 0.9 makes rivals very hard to beat; 1.1 is unfair on purpose.
- The `badge` is card metadata. Keep it out of the game's own UI.
- **Race again** remounts the 3D canvas, so anything you add to the scene restarts with it.
