# Make a kart race in 15 minutes

Start from the Kart Starter, then add a rival, a start line, a sunny sky, roadside plants and a race HUD with a
minimap. Every step is something you ask your AI, with the tool call it makes, or a command you type. Nothing is
modelled or painted by hand.

Steps marked **(wave 120+)** use pieces that are not in a release yet; the rest work today.

| Step | What you get | Minutes |
| --- | --- | --- |
| 1 | Your own copy of the Kart Starter | 1 |
| 2 | The forgiving arcade kart **(wave 120+)** | 1 |
| 3 | A chequered start line **(wave 120+)** | 1 |
| 4 | A rival kart that drives the road | 2 |
| 5 | A sunny midday sky **(wave 120+)** | 1 |
| 6 | Ground and roadside plants **(wave 120+)** | 3 |
| 7 | A minimap image of the track **(wave 120+)** | 1 |
| 8 | The race HUD: place, lap, timer, countdown, minimap **(wave 120+)** | 4 |
| 9 | Play it | 1 |

## 1. Copy the Kart Starter

The Kart Starter comes with the Windows Launcher (Samples). Copy its folder and give the copy a name, for
example `my-race`. On Linux, or from the engine source, copy `samples/kart-starter`. Then tell your AI to open
it:

> Open the project in `my-race`, and open its scene `scenes/main.scene.json`.

The AI calls `project.open {path}` and then `scene.open {scene: "scenes/main.scene.json"}`. Open the scene
every time a new AI session starts: the tools that change a scene need one open.

## 2. The arcade kart (wave 120+)

> Make the kart the arcade kart.

`scene.patch` replaces the vehicle's preset:

```json
{ "op": "replace", "path": "/entities/@kart/components/2/preset", "value": "arcade-kart" }
```

`arcade-kart` is `kart` with the four assists on: a soft landing, a respawn on the road, glancing wall hits that
keep their speed, and a calmer kart on the grass. The sample's own steering and drift settings stay. VEHICLES.md
lists each assist's fields.

## 3. A chequered start line (wave 120+)

> Paint a chequered start line where the lap starts.

The line belongs to the road file, `scenes/tracks/loop.road.json`. Add a `markings` block beside `surface`:

```json
"markings": { "start": { "atS": 0, "rows": 2 } },
```

`atS` is metres along the road (0 is where laps count), `rows` 1 to 8, and `colors` takes two `#rrggbb` if
black and off-white are not what you want. The line is paint only: the kart drives over it as over the road.
The road already has an asphalt finish (`surface.finish`); try `"preset": "worn-asphalt"` for an older track.

No tool writes a road file yet, so the AI edits it as text. Check it with `argus validate
my-race/scenes/main.scene.json`.

## 4. A rival kart

> Copy the kart as a rival that drives the track by itself, starting beside me on the left.

The AI copies the kart with all its parts (`entity.copy {ids: ["kart"]}`, then `entity.paste {clip}`, which
names the copy `kart-copy`) and changes the copy with one `scene.patch`:

```json
[
  { "op": "replace", "path": "/entities/@kart-copy/name", "value": "rival" },
  { "op": "replace", "path": "/entities/@kart-copy/tags", "value": ["rival"] },
  { "op": "replace", "path": "/entities/@kart-copy/transform/position", "value": [57.5, 0.05, 8] },
  { "op": "replace", "path": "/entities/@kart/transform/position", "value": [62.5, 0.05, 8] },
  { "op": "add", "path": "/entities/@kart-copy/components/2/input", "value": "script" },
  { "op": "replace", "path": "/entities/@kart-copy/components/3",
    "value": { "type": "script", "module": "road-driver", "props": { "road": "track", "cornerGrip": 0.8, "startOn": "race-go" } } }
]
```

`road-driver` is a built-in behaviour: it steers along the road, takes each bend at the speed its grip allows and
backs off a wall when it is stuck. `startOn` holds it on the grid until the countdown's GO (step 8).
`input: "script"` gives the kart to it instead of your keys. `road.sample`
gives places on the road if you want a different grid: `[0, -3]` is 3 m left of the line.

## 5. A sunny midday sky (wave 120+)

> Give the scene the sunny-midday look.

The look is a recipe (LOOK-PRESETS.md). The AI copies its sky and grade into the scene with `scene.patch`:

```json
[
  { "op": "replace", "path": "/meta/environment", "value": { "ambient": 0.4,
    "sky": { "kind": "atmosphere", "preset": "clear-noon", "intensity": 1, "rotation": 3.1416, "sunFromSky": true,
             "atmosphere": { "preset": "stylised", "haze": 0.6 }, "clouds": { "preset": "fair" } } } },
  { "op": "add", "path": "/meta/render", "value": { "tonemap": "aces", "look": { "preset": "bright-day" } } }
]
```

`sunFromSky` makes the scene's `sun` light follow the sky. Clouds draw in HD only.

## 6. Ground and roadside plants (wave 120+)

Plants grow on a terrain, and the Kart Starter stands on a flat box. Swap it:

> Make a 256 m flat terrain centred under the track, remove the old ground, then plant the roadside-verge
> preset over the track.

1. `terrain.create {path: "terrain/track.terrain.json", extentM: [256, 256], entity: true, layers: [{name:
   "grass", material: "scenes/materials/grass.material.json", uvScaleM: 6}]}`. Without a layer the ground is
   bare. A terrain covers x and z from 0 to its size from where it stands, so move it to `[-128, 0, -128]` to
   centre it, and remove `ground`, in one `scene.patch`.
2. `foliage.preset {preset: "roadside-verge", area: [-100, -100, 100, 100]}`. It keeps shrubs 2 m off the road
   edge and trees from 8 m, and answers with how many plants and triangles it placed. Run it again with
   another `seed` for another layout; `revert` with its `changeId` takes it back. `meadow`, `pine-forest` and
   `broadleaf-wood` are the other presets (FOLIAGE-PRESETS.md).

Trees have colliders: a kart that leaves the road hits them.

## 7. A minimap image of the track (wave 120+)

```sh
argus minimap-bake my-race/scenes/main.scene.json
```

It writes `ui/images/minimap.png`, the road from above in white on transparent, and `ui/images/minimap.json`
with the ground area it shows: `{ minX, minZ, maxX, maxZ, width, height }`. Bake again after you change the road.

## 8. The race HUD (wave 120+)

> Replace the HUD with the race kit: my place, the lap out of 3, the race timer, a 3-2-1-GO countdown, a final
> lap banner and the minimap with both karts.

The AI writes two files with `behaviour.write` and attaches a third to the track.

`ui/screens/hud.tsx`, a screen named `hud`, so it takes the place of the stock HUD. It imports the bounds from
`minimap.json` rather than copying its numbers, so baking again after a road change needs no edit:

```tsx
import { defineScreen, h, t } from "@argus/ui";
import { Minimap, RaceBanner, RacePosition, RaceTimer, StartCountdown } from "@argus/ui/components";
import BOUNDS from "../images/minimap.json";

export default defineScreen({
  name: "hud",
  clickThrough: true,
  render: () => (
    <container class="race-hud">
      <container class="race-hud__top">
        <RacePosition id="pos" place={null} of={2} size="lg" />
        <container class="race-hud__lap">
          <text class="race-hud__label">{t("race.lap")}</text>
          <text id="lap-value" class="race-hud__value" data={{ literal: true }}>1 / 3</text>
        </container>
        <RaceTimer id="timer" time={null} last={null} best={null} delta={null} />
      </container>
      <container class="race-hud__middle">
        <StartCountdown id="countdown" step={3} />
        <RaceBanner id="banner" kind="final-lap" />
      </container>
      <container class="race-hud__bottom">
        <Minimap id="minimap" bounds={BOUNDS} src="ui/images/minimap.png"
          markers={[{ id: "rival", x: 57.5, z: 8, player: 2 }, { id: "you", x: 62.5, z: 8, player: 1, you: true }]} />
      </container>
    </container>
  ),
});
```

`ui/styles/race-hud.css` places the pieces; the pieces bring their own looks. `tabular-nums` keeps the lap
digits from shifting as they change **(wave 120+)**:

```css
.race-hud { width: 100%; height: 100%; flex-direction: column; justify-content: space-between; padding: 16ui; }
.race-hud__top { flex-direction: row; justify-content: space-between; align-items: flex-start; }
.race-hud__lap { flex-direction: column; align-items: center; gap: 2ui; background-color: var(--scrim); padding: 6ui 14ui; border-radius: 6ui; }
.race-hud__label { font-size: var(--text-sm); color: var(--muted); }
.race-hud__value { font-size: var(--text-xl); font-weight: 700; color: var(--on-surface-strong); font-variant-numeric: tabular-nums; }
.race-hud__middle { flex-direction: column; align-items: center; align-self: stretch; gap: 12ui; }
.race-hud__bottom { flex-direction: row; justify-content: space-between; align-items: flex-end; }
```

`scenes/behaviors/race.ts`, attached to `track` with props `{ road: "track", laps: 3 }`. The engine counts laps
and places (`road.progress`); this only writes them into the HUD. It finds the karts by their tags (`player`,
and the `rival` the patch in step 4 gave the copy), not by id, and takes the same imported bounds:

```ts
import type { Behavior, UiProp } from "@argus/api"
import { banner, minimap, ordinal, raceClock, startCountdown } from "@argus/gameplay"
import type { MinimapRun, RaceBannerRun, StartCountdownRun } from "@argus/gameplay"
// What argus minimap-bake wrote beside the image: baking again reloads this behaviour.
import BOUNDS from "../../ui/images/minimap.json"

interface State { time: number; racing: boolean; shown: Record<string, string> }
interface Props { road?: string; laps?: number }

// The kit's controllers count their own time, so they sit beside ctx.state, not in it.
const runs = new Map<string, { countdown: StartCountdownRun; banner: RaceBannerRun; map: MinimapRun }>()

export default {
  init(ctx) {
    ctx.state.time = 0
    ctx.state.racing = false
    ctx.state.shown = {}
    const run = { countdown: startCountdown(ctx), banner: banner(ctx), map: minimap(ctx, { bounds: BOUNDS }) }
    runs.set(ctx.entity.id, run)
    run.countdown.start()
    ctx.events.on("race-go", () => { ctx.state.racing = true })
  },
  update(ctx, dt) {
    const s = ctx.state
    const run = runs.get(ctx.entity.id)
    const road = ctx.world.road(ctx.props.road ?? "track")
    if (run === undefined || road === null) return
    const laps = ctx.props.laps ?? 3
    run.countdown.update(dt)
    const player = ctx.world.query({ tag: "player" })[0]
    const rival = ctx.world.query({ tag: "rival" })[0]
    if (player === undefined || rival === undefined) return
    const you = road.progress(player)
    road.progress(rival)
    if (you === null) return
    run.map.place("you", player.worldPosition)
    run.map.place("rival", rival.worldPosition)
    if (s.racing && you.lap < laps) s.time += dt
    // Write a node only when its text changed.
    const set = (id: string, field: UiProp, value: string) => {
      if (s.shown[id + field] === value) return
      s.shown[id + field] = value
      ctx.ui.set(id, field, value)
    }
    set("timer-time", "text", raceClock(s.time))
    set("pos-value", "text", ordinal(you.place))
    set("pos", "data-place", String(you.place))
    const lap = Math.min(laps, you.lap + 1)
    set("lap-value", "text", `${lap} / ${laps}`)
    if (lap === laps && s.shown.finalLap === undefined) {
      s.shown.finalLap = "1"
      run.banner.show("final-lap")
    }
  },
} satisfies Behavior<State, Props>
```

The kit's words go in the scene's `meta.strings`, with one `scene.patch`:

```json
{ "race.lap": "Lap", "ui.race.startGo": "GO!", "ui.race.finalLap": "FINAL LAP",
  "ui.race.wrongWay": "WRONG WAY", "ui.race.newRecord": "NEW RECORD" }
```

Places read 1st and 2nd in English. For other languages, put an `ordinal` message in
`ui/locales/<tag>.json` (UI.md, Ordinals).

## 9. Play it

```sh
argus validate my-race/scenes/main.scene.json
argus run my-race
```

Or ask your AI to `critic.check`, then `play.start` and `play.capture` in the editor. Count down, race the rival
for three laps, and watch the minimap.

## Where next

- The kart's feel: Kart Starter README, "Change it in 10 minutes".
- More HUD pieces (boost meter, item slot, results and podium screens): UI.md, "The race HUD kit", and the
  `race-hud` recipe.
- Other looks and plant presets: LOOK-PRESETS.md, FOLIAGE-PRESETS.md.

