# Small Hours track format (smallhours.track.v2) Small Hours is a top-down pixel racer: toy vehicles race on household surfaces at night (a kitchen table, a bathtub, a workbench). A track is one JSON file. This guide is for people and AI agents writing that file by hand. Everything below the hand-written parts is generated from the game's own data, so it is always current. - Studio (visual editor, checker, export): https://deckraces.duckdns.org/studio/ - JSON Schema: https://deckraces.duckdns.org/studio/track.schema.json ## Workflow 1. Write the JSON (start from the examples at the end). 2. Check it: in the Studio, press **JSON**, paste it in, and read the report (errors must be fixed; warnings are advice). With the game's repo: `node tools/check_track.mjs my_track.json`. 3. Press **Apply** (or **Add as new track**), look at it, adjust by hand if you like. 4. Press **Export for game**. The `.smallhours` file it downloads is what the game loads. ## Coordinates and units - Pixels and seconds. The origin is the map's top-left corner; x goes right, y goes **down**. - Angles are in degrees, clockwise (because y points down), 0 = pointing right. - One screen is 1024×576. Tiles are 32 px. The game's camera usually shows 512×288 world pixels around the car (2× zoom), so a 1024×576 map is "one screen" but still scrolls. - The baseline car (Runabout) tops out at 335 px/s; a lap of 2000–3000 px is 8–11 s. ## Top-level fields | Field | Required | Meaning | |---|---|---| | `format` | yes | Always `"smallhours.track.v2"`. | | `name` | yes | Shown in the game, 1–40 characters. | | `world` | yes | The surface everything sits on; also picks the default vehicle (table below). | | `size` | yes | `[width, height]`, one of: `[1024,576]`, `[2048,1152]`, `[3072,1152]`, `[3072,1728]`, `[4096,1728]`, `[6144,576]`, `[2048,2304]`. | | `route` | yes | `"loop"` (laps) or `"point_to_point"` (start at the first point, finish at the last). | | `laps` | yes | 1–20 for a loop; a point_to_point route is always 1. | | `start` | yes | Where the start/finish line sits along the route, as a share of its length (0 ≤ start < 1). `0.02` = just after point 1. | | `points` | yes | The route, 4–200 points `{"x", "y", "w"}` (below). | | `props` | no | Objects on the table (below). | | `zones` | no | Circular surface patches (below). | | `regions` | no | Raised rectangles: a book on the floor, a tray on the table (below). | | `tile`, `length`, `gates` | no | Worked out by the Studio and the game; leave them out. | ## The route: `points` - The road follows a **Catmull-Rom spline through every point, in order**. Cars drive in point order. - For a loop, **don't repeat the first point at the end**: the curve closes itself. - `w` is the road width at that point (28–150 px; it blends smoothly between points). 60–90 is usual; under 50 is a tight squeeze for four cars; 100+ is a wide sweeper. - Space points 150–400 px apart. More points don't make it smoother, they make it wobblier. - Keep each point at least `w/2` from the map edge. - The road must not touch or cross itself: keep the centre lines of two stretches at least (w₁ + w₂)/2 + 12 px apart. There are no bridges or tunnels for the road itself. - Hairpins are fine. The tightest bend in the game's tracks has a radius of about 0.22 × the road width; under 0.1 × the road folds back on itself and is an error. - Put the start line (`start`) on a straight. ## Worlds | `world` | Name | Default vehicle | |---|---|---| | `breakfast` | Kitchen table | Runabout | | `garden` | Back garden | Runabout | | `bath` | Bathtub | Dinghy | | `sand` | Sandpit | Puffer | | `felt` | Felt table | Runabout | | `rug` | Bedroom rug | Runabout | | `bench` | Workbench | Crumb tank | In the bathtub the "road" is a water channel and the vehicles are boats. ## Props A prop is `{"kind", "x", "y"}` plus optional fields; anything left out uses the kind's default. - `shape` is fixed by the kind (circle, rect or scatter); leave it out. - Circle: `radius` (8–90). Rect: `w` (20–260), `h` (8–140), `angle` (−180 to 180). Scatter (crumbs, a decoration only): `radius` (20–100), `count` (10–120). - `color` `"#rrggbb"`, `height` (0–16 px; 0 = flat, painted on). - `behaviour` and `params`: only for the kinds that have one (table); `params` may set any subset. - `active_laps`: `[from, until]`, the prop appears on lap `from` and is gone after lap `until` (0 = never goes away). `[3, 0]` = from lap 3 on. Leave it out for always. What a prop does in the game depends on its kind: - **solid**: too tall to climb; cars bounce off. Don't block the road; use one as a chicane. - **push**: a loose object cars shove about; it drifts home after a few seconds. - **platform**: drive up onto it and hop off the far edge at speed. - **bump**: a kicker; a short hop when crossed at speed. - **draw**: painted or flat; no physics of its own (glue, ramp and plank act through their behaviour). | `kind` | Shape | Default size | Height | Role | Behaviour: params (range, default) | |---|---|---|---|---|---| | `bowl` | circle | radius 46 | 9 | solid | — | | `orange` | circle | radius 20 | 8 | push | — | | `mug` | circle | radius 24 | 12 | solid | — | | `ball` | circle | radius 16 | 10 | push | — | | `rock` | circle | radius 28 | 8 | solid | — | | `duck` | circle | radius 26 | 12 | push | — | | `drain` | circle | radius 34 | 0 | draw | — | | `book` | rect | 100×68 | 8 | platform | — | | `ruler` | rect | 150×18 | 3 | bump | — | | `block` | rect | 36×36 | 12 | solid | — | | `soap` | rect | 54×34 | 10 | push | — | | `crumbs` | scatter | radius 42, count 44 | — | draw | — | | `glue` | rect | 46×14 | 5 | draw | `glue`: `trig` trigger speed (20–200, 60), `slow` slows to % (10–90, 25), `dry` dries after s (4–30, 14) | | `ramp` | rect | 90×44 | 4 | draw | `ramp`: `trig` min speed (40–250, 110), `air` launch (0.2–1.5, 0.6) | | `plank` | rect | 180×40 | 2 | draw | `bridge` | | `marbles` | circle | radius 20 | 10 | solid | `marbles`: `every` release every s (2–15, 6), `count` marbles (1–8, 4) | | `magnet` | circle | radius 18 | 8 | solid | `magnet`: `reach` reach px (60–300, 160), `pull` pull (50–600, 300) | Behaviours: - `glue` (glue tube): squirts glue when a car passes faster than `trig`; cars in the puddle are capped at `slow` % of top speed; puddles dry after `dry` s. - `ramp`: cars faster than `trig` px/s take off (`air` sets how long); place it across the road. - `bridge` (plank): a way across a `gap` zone. Place it across the gap, along the road. - `marbles` (marble cup): releases `count` marbles every `every` s. - `magnet`: pulls metal vehicles (the tank) within `reach` px with strength `pull`. ## Zones A zone is a circle `{"type", "x", "y", "radius"}` (20–160 px), with optional `active_laps`. Place it on the road to matter. Zones are painted under the road, so on the road they are felt more than seen. | `type` | Name | Effect on the road | |---|---|---| | `slick` | Slick | Very little grip (spilt milk) | | `loose` | Loose | Loose gravel: slower, less grip | | `mud` | Mud | Half top speed, heavy drag | | `oil` | Oil | Almost no grip, weak brakes | | `boost` | Boost | Faster (top speed ×1.35) and quicker to accelerate | | `water` | Water | Boats float; anything else wades very slowly (hovercraft skim it) | | `gap` | Gap (fall) | A hole: cars fall in and respawn at the last gate | Off the road everywhere is slower (60 % top speed). A `gap` across the road needs a `plank` (or a `ramp` before it) or every car falls in. ## Regions A region is a raised rectangle with its own surface: `{"world", "level", "x", "y", "w", "h"}`, at least 200×150 px. `level` is `floor`, `chair`, `table`, `shelf` (higher is taller; its drop shadow grows with the level). Driving off a region's edge onto a lower level is a drop; its edge is a wall from below. ## The checker Errors (must fix): wrong or missing fields, values out of range, points under 20 px apart, a road that touches or crosses itself, a bend that folds back. Warnings (advice): road off the map edge, a bend sharper than the game's tracks, the start on a bend, a solid prop on the middle of the road, a gap with no way over, a very short lap. Limits: 200 points, 150 props, 100 zones, 20 regions. ## Examples A minimal loop (one screen, kitchen table): ```json { "format": "smallhours.track.v2", "name": "First Loop", "world": "breakfast", "size": [1024, 576], "route": "loop", "laps": 3, "start": 0.02, "points": [ {"x": 180, "y": 300, "w": 80}, {"x": 260, "y": 140, "w": 80}, {"x": 512, "y": 100, "w": 80}, {"x": 760, "y": 140, "w": 80}, {"x": 850, "y": 300, "w": 80}, {"x": 760, "y": 450, "w": 80}, {"x": 512, "y": 480, "w": 80}, {"x": 260, "y": 450, "w": 80} ], "props": [{"kind": "bowl", "x": 512, "y": 290}], "zones": [] } ``` A richer one: a narrow wiggle at the top, a glue tube from lap 2, a plank over a gap, mud, a boost on the start straight, and a book, a mug and a ball in the infield: ```json { "format": "smallhours.track.v2", "name": "Glue Gap", "world": "breakfast", "size": [1024, 576], "route": "loop", "laps": 3, "start": 0.02, "points": [ {"x": 160, "y": 300, "w": 84}, {"x": 200, "y": 140, "w": 80}, {"x": 380, "y": 90, "w": 76}, {"x": 560, "y": 130, "w": 56}, {"x": 700, "y": 90, "w": 70}, {"x": 860, "y": 140, "w": 80}, {"x": 900, "y": 300, "w": 84}, {"x": 860, "y": 450, "w": 80}, {"x": 700, "y": 500, "w": 76}, {"x": 520, "y": 470, "w": 60}, {"x": 360, "y": 500, "w": 76}, {"x": 200, "y": 460, "w": 80} ], "props": [ {"kind": "glue", "x": 905, "y": 330, "angle": 90, "params": {"slow": 30}, "active_laps": [2, 0]}, {"kind": "plank", "x": 610, "y": 490, "w": 150, "h": 40, "angle": 0}, {"kind": "book", "x": 520, "y": 300}, {"kind": "ball", "x": 330, "y": 300}, {"kind": "mug", "x": 740, "y": 300}, {"kind": "crumbs", "x": 280, "y": 220, "radius": 30, "count": 24} ], "zones": [ {"type": "gap", "x": 610, "y": 490, "radius": 45}, {"type": "mud", "x": 380, "y": 95, "radius": 40}, {"type": "boost", "x": 160, "y": 300, "radius": 36} ] } ```