# Splash Game DSL Guide The complete `game.*` API for games running in Makepad Arcade (`apps/arcade`; also `examples/gamemaker`, which shares the same engine and is being retired). A game is ONE splash script — `game.splash` — evaluated live: statements run top to bottom, build the world, then drive it from `game.on_tick`. Every clean edit hot-reloads the running world instantly; a broken edit never replaces it (the last working world keeps running and the error waits in `./tools/ag errors`). This file is loaded into the game-making agent's system prompt by the app (like `splash.md` is for UI work). Keep it in sync with the engine: adding a verb = a match arm in `examples/gamemaker/src/game_view.rs` `game_dispatch` + a row here. ## Key rules - Declare `fn` helpers at the top; `let` bindings may interleave with build statements (`let player = game.mover(...)` after terrain is fine). - Property syntax is `name: value` (colon); calls take `({...})` object args. - **Hex colors containing the letter `e` need the `#x` prefix**: `#x2ecc71`, `#x1e1e2e`. Colors without `e` (like `#ff4444`) work with plain `#`. - The file re-runs from the top on each edit — don't accumulate state across edits; rebuild it. `game.time()` restarts at 0 on every reload. - Closures capture top-level `let` variables — that's how `on_tick` remembers entity ids. **Captured variables are MUTABLE and persist across ticks**: lap counters, cooldowns, game phases are just `let score = 0` at the top and `score = score + 1` inside `on_tick`. - Math: `sin cos sqrt atan2(y,x) abs min max floor round sign clamp`, `%`, `lerp(a, b, t)` (scalars and vectors), constants `PI` and `TAU`, and vector methods `v.length()`, `v.normalized()`, `a.dot(b)`, `a.cross(b)` — steering AI is `(target - me).normalized() * speed`. - Arrays: `let xs = []`, then **`xs.push(v)`** (a METHOD — there is no free `push(xs, v)` function; calling one is an error that stops the game), `xs.len()`, `xs[i]`, and `for x in xs { }`. `game.find("tag")` and `game.overlap_sphere(pos, r)` hand you arrays to walk the same way. - **Use the stock library.** ~4,700 CC0 models ship with Arcade — houses, vehicles, trees, rocks, furniture, dungeon and road tiles, rigged characters. A scene built from bare `game.box` primitives looks cheap; the same scene with real models looks like a game. See **The stock library**. Sound is synthesized; the only file a game itself owns is `game.splash`. - ALWAYS check `./tools/ag errors` after editing. Empty = live. Error = the player still sees the OLD world. ## The shape of a game ```splash let SPEED = 6.0 let JUMP = 11.0 game.sky({}) game.terrain({size: 160, cells: 257, smooth: true, water: 3.5, seed: 7, freq: 0.014, offset: 0.5, amp: 30, step: 0.5, min: 0.5, max: 26, bands: [{h: 3.6, color: #xd9c780}, {h: 13.0, color: #x5cad4c}, {h: 999.0, color: #xf0f5ff}], plaza: {r: 26, ramp: 14, h: 7}}) let player = game.mover({pos: vec3(-4, 9, 8), size: vec3(0.8, 1.6, 0.8), color: #x4a7fd6, tag: "player"}) game.part(player, {pos: vec3(-0.18, 0.55, -0.38), size: vec3(0.14, 0.14, 0.06), color: #x11131a}) game.part(player, {pos: vec3(0.18, 0.55, -0.38), size: vec3(0.14, 0.14, 0.06), color: #x11131a}) game.label(player, "You") game.camera({third_person: player, height: 1.6, boom: 10, pitch: -0.35}) game.on_tick(|dt, input| { game.walk(player, input.move_x * SPEED, input.move_z * SPEED) if input.jump_pressed && game.on_floor(player) { game.jump(player, JUMP) game.sfx("jump") } if game.pos(player).y < -12 { game.set_pos(player, vec3(-4, 9, 8)) } }) ``` ## Spawning (returns an entity id) | call | meaning | |---|---| | `game.box({pos, size, color, tag, sensor, collide, body, glow, shape, rot_y, density, friction, restitution})` | a solid. `sensor: true` = no collision, reports touches (goals, pickups), drawn translucent. `collide: false` = opaque DECORATION — looks solid, no physics (rotated road slabs!). `rot_y: 0.6` turns the visual (collision stays the axis box). `body: "kinematic"` = script-moved platform (set its vel; movers standing on it are carried). `body: "rigid"` = REAL physics (box3d): stacks, tumbles, rotates, bounces — crates, balls, dominoes. Rigids collide with statics/kinematics/each other, NOT with movers; `density`/`friction`/`restitution` tune the material; `shape:"sphere"` rigids roll (collider radius = half width). Rigid state is shared-tier (networked); `push` gives a real impulse. `glow: 2` = emissive | | `game.mover({pos, size, color, tag, gravity, turn_rate, shape})` | a character: gravity + collides with the world. `gravity: 0` floats. Movers **auto-face where they walk** (front = -z); `turn_rate` rad/s (default 7) | | `game.spawn({pos, vel, size, color, tag, life, hits, gravity, glow, shape})` | a projectile: auto-removed after `life` seconds; `hits: true` reports everything it touches through `on_touch` (creatures AND walls) | | `game.part(owner, {pos, size, color, glow, rot_x/rot_y/rot_z, shape})` → part id | a visual-only shape welded to an entity IN ITS FRAME (turns and scales with it; front = -z): eyes, arms, ears, horns, hats, wheels. No collision; dies with its owner | | `game.terrain({...})` | the whole landscape in ONE call — see Terrain below | | `game.label(id, "Bob")` | floating outlined nametag above an entity, camera-facing. `""` removes. Extra labels: `game.label(id, "HELP!", {height: 2.4, color, size})` → label id, update via `game.label_text(lid, "...")` | `shape:` on any of the above picks the visual: `"box"` (default), `"sphere"` (alias `"ball"`), `"cylinder"`, `"cone"`, `"wedge"` (alias `"ramp"`). Collision is always the `size` box — shape is looks only. Round eyes (`shape: "sphere"`), cone horns, cylinder tree trunks, wedge ramps: use them — creatures made only of boxes look stiff. Rendering is instanced per shape, so mixing shapes is free. ## Terrain ```splash game.terrain({size: 160, cells: 257, smooth: true, water: 3.5, seed: 7, freq: 0.014, offset: 0.5, amp: 30, step: 0.5, min: 0.5, max: 26, plaza: {r: 26, ramp: 14, h: 7}, bands: [{h: 3.6, color: sand}, {h: 13, color: grass}, {h: 17.5, color: dirt}, {h: 21, color: stone}, {h: 999, color: snow}]}) ``` - **`smooth: true` always for outdoor worlds** — one connected rolling-hills mesh, walkable slopes, collision by ground height. Without it: stepped columns. - Engine noise (`seed/freq/offset/amp/step/min/max/plaza`) costs NO script budget — up to `cells: 384`. `step` = terrace size (0 = smooth), `plaza` flattens a disc at the origin, the `max` clamp carves plateau peaks. - `bands` paints by height — snow above stone is what makes distant hills read as MOUNTAINS. Or pass `heights:` (flat row-major `z * cells + x` array) and `colors:` for hand-built ground; or `color:` auto-shades one color. - `water: 3.5` adds a translucent lake sheet (a sensor tagged "water"). - `game.ground_y(x, z)` → height there; `game.ground_peak()` → vec3 of the highest point. Place spawns, trees, and the goal ON the terrain with these. ## Driving the game `game.on_tick(|dt, input| ...)` runs 60×/second, fixed step. `input` fields: `left right up down jump shoot grab reset back` (held), `jump_pressed shoot_pressed grab_pressed reset_pressed back_pressed` (this tick only), `axis_x axis_z` (raw −1..1), **`move_x move_z` — the axes rotated to match the camera. ALWAYS walk with these** (raw axes only for `side: true` 2D games), and `look_dx look_dy` — the camera-look delta this tick (0 unless the kid is mouse-orbiting or on the gamepad's RIGHT stick; chase cams use it to yield to the kid's hand). Keyboard (WASD/arrows, Space, F shoot, G grab, R reset — kids get stuck upside-down, give them a reset! — C back) and gamepad (left stick/dpad move, RIGHT stick rotates the camera like the mouse, A jump, X shoot, B grab, Y reset) both feed everything automatically — never write your own camera-from-stick code. **When you add an ability, always give it a gamepad path too**: bind it to one of the named actions above (`jump shoot grab back reset` all have pad buttons) instead of inventing keyboard-only triggers, so the kid on a controller is never locked out of something you built. | call | meaning | |---|---| | `game.walk(id, vx, vz)` | set horizontal velocity (vertical untouched) | | `game.jump(id, v)` | set upward velocity (check `game.on_floor(id)`) | | `game.on_floor(id)` | standing on something? | | `game.pos(id)` / `game.vel(id)` | vec3 position / velocity | | `game.set_pos(id, v)` | teleport (zeroes velocity) | | `game.set_vel(id, v)` | set full velocity | | `game.face(id, yaw)` / `game.yaw(id)` | override / read facing. The override is STICKY (walking doesn't revert it); `game.face(id)` with no yaw hands facing back to auto-face | | `game.find("tag")` | array of ids with that tag | | `game.tag(id)` / `game.distance(a, b)` | tag / distance — `a`/`b` may each be an entity id OR a vec3 point (checkpoints are positions) | | `game.remove(id)` | despawn (parts and labels go with it) | | `game.attach(id, owner, offset)` / `game.detach(id)` | seat-mount (vehicles, carrying) — rider faces with the owner | | `game.attach(id, owner, {pos, mode: "ride", spin: 2})` | latch ON someone (headcrab): pinned each frame, model spins | | `game.speed_mult(id, 0.5)` | scale an entity's walk speed engine-side (debuffs) until changed | | `game.push(id, v)` | ADD to velocity (a shunt, a gust) — `set_vel` overwrites, `push` nudges. Movers pass through each other: to bump someone, detect overlap (`hits`/`on_touch`/`overlap_sphere`) and `push` them. On a `body:"rigid"` entity this is a true mass-scaled impulse (same Δv feel; wakes the body) | | `game.raycast(from, dir, max)` | → nil or `{hit, pos, normal, dist}`. Hits terrain (`hit` = -1), walls, creatures, decor. THE sense for wall-avoiding AI, brake-for-the-car-ahead, line of sight, aimed guns. It also hits the caster — cast from just outside your own body, or skip a hit whose id is you | | `game.overlap_sphere(pos, r)` | → array of entity ids near a point | | `game.ground_normal(x, z)` | → terrain surface normal (align cars to slopes) | | `game.save("best_lap", 42.3)` / `game.load("best_lap", 999)` | persist numbers/strings across edits, reloads AND app restarts — high scores live here. Second load arg = default | | `game.every(secs, \|\| ...)` → timer id / `game.cancel(id)` | repeating timer (game.after also returns a cancellable id now) | | `game.after(secs, \|\| ...)` → timer id | run once, later; `game.cancel(id)` aborts it | | `game.on_touch(\|a, b\| ...)` | a sensor overlapped a mover, or a `hits` projectile touched something. Fires EVERY overlapping tick — latch or remove | | `game.rand()` / `game.rand_range(a, b)` | random, seeded per eval — replays stay repeatable (never bring your own RNG) | | `game.held("left")` / `game.pressed("jump")` / `game.axis("left","right")` | input outside on_tick | | `game.log("msg")` / `game.time()` | debug line into .agent/game.log / seconds since reload | | `game.api()` | dump every verb + its option keys into .agent/game.log — self-lint when an option "did nothing" | ## The look | call | meaning | |---|---| | `game.sky({})` | daylight gradient sky + distance fog. Call it for every outdoor game | | `game.set_color(id, c)` / `game.glow(id_or_part, e)` | restyle / emissive energy (eyes 3–4; ramp it with AI state) | | `game.scale(id, s)` | ease the whole model's scale (giants 1.9, sleep-curl via vec3(1, 0.6, 1)) | | `game.move_part(part, {pos, rot_x/y/z, size, rate})` | ease a part toward a pose (arm reach: `{rot_x: -1.5}`); `rate` defaults 9/s | | `game.beam(a, b, {size: 0.12, color, glow})` | a stretched cable/laser between two points — re-issue it every tick while it exists (grapple ropes, tethers) | | `game.camera({third_person: id, height: 1.6, boom: 10, pitch: -0.35, fov: 70})` | THE camera for 3D exploring: drag looks, wheel zooms, slides in when hills block the view. Tag pure-decoration entities `"scenery"` so they don't pull the camera in. Also: `{follow: id, distance: 16}` orbit, `{side: true}` 2D platformer | | `game.camera({chase: id, boom: 13, height: 2.4, pitch: -0.22, lag: 0.3, recenter: 1.2, speed_tighten: 0.15})` | **the racing camera in ONE line** — third_person's rig plus engine-side ease-behind-the-target. `lag` = ease time-constant (s); `speed_tighten` tightens it with the target's speed; the kid's drag takes over instantly and the rig resumes `recenter` s after the drag ends (wheel zoom is never fought). Angle wrapping is handled engine-side — do NOT hand-roll yaw math on top. `chase: 0` stops the easing, keeping the rig for the mouse | | `game.set_cam_yaw(a)` / `game.set_cam_pitch(p)` / `game.set_cam_dist(d)` / `game.set_cam_fov(f)` | WRITE the camera — the same state the mouse drags. Writes stick: under a chase rig a write becomes the new camera state and easing continues from there (a scripted look-at burst just works) | | `game.cam_yaw()` `game.cam_pitch()` `game.cam_dist()` `game.cam_fov()` `game.cam_dragging()` | read the whole camera pose (preserve the kid's wheel zoom before scripting it) | | `game.cam_shake(0.4)` | impact shake — decays over ~half a second, stacks | | `game.text("You win!")` | big center banner; `""` clears. Named slots: `game.text("lap", "LAP 2/3", {anchor: "top_right", color, size})` — anchors `top_left top top_right center bottom_left bottom bottom_right`; slots stack per anchor. `"hint"`/`"top"`/`"center"` keep their classic homes | | `game.bar("speed", 0.62, {color, anchor})` | a gauge (speedometer, boost). Negative fraction removes it | | `game.format(3.14159, 2)` | → "3.14" — lap times without hand-rolled math | | `game.crosshair(true)` | center aiming dot (shooting games) | Blob shadows under movers, label outlines, near-camera clipping (a creature overlapping the lens clips open instead of filling the screen) are automatic. House style: give every creature a face (`game.part` eyes) and a name (`game.label`) — two lines each, do it without being asked. Build big characters from many parts and animate them with `move_part`/`scale`/`glow`. ## The stock library — ~4,700 CC0 models Arcade ships Kenney's low-poly library: houses, vehicles, trees, rocks, walls, furniture, food, weapons, road and dungeon tiles, and rigged characters. **Reach for these before `game.box`.** A scene of coloured primitives reads as a prototype; the same layout with real models reads as a game. | call | meaning | |---|---| | `game.find_model("pine tree", {count: 4})` | search → **a LIST of DISTINCT model ids**. Options: `count`, `spread` (`mixed` default / `kinds` / `variants`), `seed`, `kind` (`model`/`sound`), `rigged: true`, `max_size` | | `game.find_palette("village", 7)` | → `{pack, group: [ids], ...}` — a MATCHED set from ONE art pack | | `game.model(id, {pos, yaw, scale, collide, tag})` | place one. Ids come from `find_model` — never invent one; a wrong id reports near-misses and places nothing | | `game.kits()` | → `[{pack, tiles, tile_size, roles}]` — the packs whose tiles snap together | | `game.cast()` | → `[{joints, members, states}]` — rigged characters, grouped by shared rig (one animation set drives every member of a group) | ### The three rules that decide whether it looks good 1. **Never place result #1 five times.** This is the single most common way to make a scene look cheap. `find_model` returns a list precisely so you can walk it — a village wants five different houses, a wood wants several species and several variants of each. 2. **One art pack per region.** Spreading across the whole library gives a suburban house beside a hex-tile house beside a sci-fi house. Use `find_palette` for a region, or keep to one `pack`. 3. **Generate layouts; don't hand-place a hundred things.** `game.town`, `game.dungeon` and `game.road_network` lay real tiles with correct corners and junctions, which is tedious and error-prone by hand. ``` // WRONG — one house, five times let h = game.find_model("house")[0] for i in 0..5 { game.model(h, {pos: vec3(i * 8.0, 0.0, 0.0)}) } // RIGHT — five different houses, all from one pack, all facing the street let houses = game.find_model("suburban house", {count: 5, seed: 3}) for i in 0..5 { game.model(houses[i], {pos: vec3(i * 8.0 - 16.0, 0.0, -10.0), yaw: 0.0}) } ``` ## Building a place (composition) Tiles from a kit snap onto a grid; the generators pick corners, junctions and dead ends from how the layout actually connects, so you never name a piece. | call | meaning | |---|---| | `game.road_network({kit, paths: [[vec3, vec3, ...], ...], seed})` | polylines → a road. Two paths that cross give a crossroad, one that tees gives a T — automatically | | `game.town({roads_kit, buildings_kit, props_kit, extent: 24, block: 4, density: 0.8, seed})` | a street grid with buildings FRONTING the streets | | `game.dungeon({kit, extent: 32, min_room: 5, depth: 4, seed})` | rooms + corridors, every room reachable. Returns `{tiles, entrance, exit}` — spawn the player at `entrance` | All three return `{tiles}` (a count) and take `collide` (default on for towns and dungeons, off for roads so cars drive over them). All are **seed-deterministic**: the same seed gives the same level every load. `modular-dungeon-kit`, `modular-cave-kit` and `modular-space-kit` share one role vocabulary — the same `game.dungeon` call gives a crypt, a cavern or a space station purely by swapping `kit`. ``` // A small place: a road, a village along it, and a dungeon to find. let d = game.dungeon({kit: "kenney/modular-dungeon-kit", extent: 24, seed: 5}) game.town({ roads_kit: "kenney/city-kit-roads", buildings_kit: "kenney/city-kit-suburban", extent: 20, block: 5, density: 0.7, seed: 5, }) let trees = game.find_model("pine tree", {count: 4, seed: 5}) for i in 0..24 { let a = i * 0.26 game.model(trees[i % 4], {pos: vec3(cos(a) * 34.0, 0.0, sin(a) * 34.0)}) } let hero = game.mover({pos: d.entrance, size: vec3(0.6, 1.7, 0.6)}) game.camera({third_person: hero, boom: 9, pitch: -0.3}) ``` ## Light and weather | call | meaning | |---|---| | `game.sun({time_of_day: 8.5})` | one sun for the whole scene: 0..24 local hours. Morning and evening are warm and throw long shadows, noon is white and short | | `game.sun({dir: vec3(0.3, 0.9, 0.2), color: vec3(1.0, 0.9, 0.7)})` | or aim it yourself. `ambient` lifts the shadow side, `shadow_alpha` (0..1) sets how dark cast shadows draw | Objects cast real shadows that stretch and swing as the sun moves — you get them for free, there is nothing to turn on. ## Particles (device-local — never affects the game) | call | meaning | |---|---| | `game.particles(id, {kind: "smoke", rate: 20})` → emitter | a continuous emitter that FOLLOWS an entity: exhaust, fire, a dust trail | | `game.particles(vec3(x,y,z), {kind: "dust"})` → emitter | or pinned to a spot | | `game.burst(vec3(x,y,z), {kind: "spark", count: 16})` | a one-shot puff: impacts, pickups, explosions | | `game.particles_stop(emitter)` | stop one emitter | Kinds: `spark` (fast, falls), `smoke` (rises, grows), `dust` (drifts, settles), `trail` (fades in place). Tune with `life size color spread speed gravity`. Particles are **cosmetic only**. They never collide, never touch game state, and each device draws its own — so never make a rule depend on one. A phone may draw fewer than a PC in the same game, and that is fine by design. ## Sound (all synthesized — never files) | call | meaning | |---|---| | `game.sfx("jump")` | named bank: `jump shoot zap grab angry calm rescue shove board coin hurt win lose squeak roar bark moo clank whip`. Pitch: `game.sfx("bark", 1.4)` — animals sound distinct by pitch (chicken high, cow low) | | `game.beep({freq: 440, to: 880, ms: 120, wave: "square", gain: 0.25})` | one tone; `to` glides pitch; waves: sine square saw triangle noise | | `game.jingle("C5 E5 G5 C6", 100)` | note names at N ms/note (sharps: "F#5") | | `game.tone({freq: 80, wave: "saw", gain: 0.15})` → tone id | a SUSTAINED tone — the car-engine primitive. Starts and keeps sounding | | `game.tone_set(id, {freq: 80 + speed * 6})` | retune it per tick — smoothed, never retriggers | | `game.tone_stop(id)` | fade it out. Tones also stop on every reload (no stuck hums) | | `game.sfx_at(vec3(x,y,z), "clank", {range: 40})` | same bank, but POSITIONED: quieter with distance, panned left/right by where it is relative to the camera. Use it for anything with a place — impacts, engines, other players | Add sounds without being asked — jumps, pickups, winning. They make it real. ## Checking your work — ALWAYS 1. `./tools/ag errors` after every edit. Empty = your edit is live. 2. Playtest: `./tools/ag test 120 tools/tapes/selftest.json` — replays a frame-numbered input tape, writes `.agent/sheet.png` (frames over time) and `.agent/probe.txt` (pos/vel of probed tags every 15 frames). **Read the image, read the numbers** — "the jump clears the step" should be a probe line you saw. Same tape = same frames, byte-identical. 3. `./tools/ag peek` — 4 screenshots of the live game + entity state, without interrupting the player. 4. `./tools/ag logs` — your `game.log()` lines + eval reports. Tapes: `{"probe": ["player"], "events": [{"f":5,"press":"right"}, {"f":30,"press":"jump"},{"f":33,"release":"jump"}]}` — actions are the input names above (`left right up down jump shoot grab`). ## Gotchas found the hard way - Movers are ~0.8×1.6×0.8. Keep playfields within the terrain you built. - Use tags + `game.find` for groups (coins, enemies) — like scene groups. - `on_touch` fires every overlapping tick: latch with a bool or remove the sensor, or you'll play 60 win jingles a second. - `turn_rate: 0` on a mover means "NEVER auto-face" — steer its visual with `game.face` yourself (cars want this). One `game.face(id, yaw)` call takes over facing permanently; `game.face(id)` gives it back to auto-face. - Typos are loud now: an unknown `game.` verb FAILS the eval (the kid keeps the old world; the error gives the game.splash line, names the verb, and suggests the nearest real one); an unknown option key logs a warning to `.agent/game.log` and `ag errors` shows the warning count. If a thing you set "did nothing", check both — or `game.api()` to see the real keys. - Errors report REAL `game.splash` line numbers (`game.splash:118:9`) — trust them, jump straight there. - `game.time()` restarts at 0 on every reload — durable numbers (best laps, high scores) belong in `game.save`/`game.load`. - Small, visible changes. Tune constants and add shapes; avoid big rewrites. - Intercept AI (bodyguards): pick threats with TWO distance gates (threat-to-player AND threat-to-me), steer at `threat + (player-threat) .normalized() * 2`, act within a bonk range. The engine gives you `find`, `distance`, `pos` — the brains are yours. - Weeping-angel AI: freeze when watched — `game.cam_yaw()` gives the camera yaw; the look direction is `(sin(yaw), -cos(yaw))` on the ground plane (the same rotation `move_x/move_z` use); dot it with the direction to-me and gate on > 0.55. Note the camera yaw is NOT an entity yaw — entities face `(-sin(e_yaw), -cos(e_yaw))`; the x sign differs. Don't equate the two — that's why chase cams belong to `camera({chase})`, not hand-rolled math. ## Building blocks High-level prefabs. **The engine runs these at 60Hz** — you call the verb once to create and configure, then the block drives itself. Don't reimplement their behaviour in `on_tick`; steer them with `game.drive` or let them run. All block state is **Shared tier**: laps, scores, positions and control intent replicate to other players. Animation blending is Derived (recomputed from velocity), so it costs nothing over the network. | verb | what you get | |---|---| | `game.car({pos, color, player: true, top_speed, accel, grip, seats})` | A driveable raycast vehicle on a rigid chassis: suspension, grip, arcade steering, and it will not roll over. Returns the entity id. | | `game.character({pos, color, player: true, model, speed, jump, view: "third"})` | A walker on the mover sweep (0.55 step-up, jumps, camera-relative movement) with idle↔walk↔run blending driven by its own speed. | | `game.plane({pos, color, player: true, thrust, lift_speed})` | Arcade flight: lift from airspeed, auto-level, weathervane stability. Holding pitch loops; it cannot stall or spin. | | `game.drive(id, {steer, throttle, brake, handbrake, pitch, roll, move_x, move_z, jump})` | Feed control intent to any block — this is how script-driven or AI opponents are controlled. | | `game.autodrive(car, {points: [vec3, ...], pace})` | Hands a car a racing line to follow. `pace` is 0..1 of its top speed. | | `game.speed(id)` | Forward speed for a car, airspeed for a plane, planar speed otherwise. | ### Brains Attach an AI to any mover. Re-issuing a brain on the same entity replaces it. | verb | behaviour | |---|---| | `game.wander(id, {home, range, speed, pause})` | Amble to random points near home, pausing between trips. | | `game.chase(id, {tag, range, catch, speed})` | Hunt the nearest entity carrying `tag`. `game.caught(id)` returns who it reached this tick. | | `game.patrol(id, {points: [vec3, ...], speed})` | Walk a fixed route, looping forever. **Do not pass `loop:`** — `loop` is a reserved word and using it as an option key hangs the script until the instruction limit kills the eval, so the game never starts. Looping is the default; ping-pong is unavailable until the engine renames that key. | ### Race kit | verb | behaviour | |---|---| | `game.spawnpoint({pos, yaw})` | Declares a start slot; returns its index. | | `game.checkpoint({pos, size})` | Declares a gate. Gates are numbered in declaration order and **must be crossed in order** — cutting the course scores nothing. | | `game.place(id, slot)` | Puts an entity on a start slot and enters it in the race. | | `game.race({laps})` | (Re)starts lap tracking. Call it again to restart a race. | | `game.standings()` | `[{entity, lap, checkpoint, finished, score}]`, leader first. | | `game.lap(id)` / `game.rank(id)` / `game.finished(id)` | Per-racer progress. | | `game.score(id, points)` / `game.score_of(id)` | Scoring for non-racing games too. | A complete 4-car race is ~60 lines — see `examples/gamemaker/resources/fixtures/racing.splash`. ## Players (multiplayer) Every world has at least one player: id `0`, this device. A hosted room adds one per connected client, plus any bots the game creates. Blocks take `player:` to say who drives them. | verb | what it does | |---|---| | `game.players()` | all player ids, local first — **Shared** | | `game.player_name(p)` | display name — **Shared** | | `game.player_entity(p)` | the entity this player drives, 0 if none — **Shared** | | `game.player_input(p)` | that player's input object (same shape `on_tick` gets) | | `game.bot(name)` | add a host-side player with no device — **Shared** | | `game.on_join(fn(p))` | someone joined the room | | `game.on_leave(fn(p))` | someone left; their body is freed for you | Movement stays camera-relative per player: `move_x`/`move_z` are rotated by *that player's* camera yaw, which travels inside their input packet. A client's camera is presentation only and never replicates. **Replication tiers.** The host simulates; clients receive. What crosses the wire is decided per field, not per entity: - **Shared** — position, velocity, size, body kind, tag, score, lap progress. Host to clients, every tick. - **Derived** — facing yaw, walk-cycle blending, part animation, scale and glow easing, blob shadows. Never sent: a client recomputes these from Shared state, which is why 200 moving props cost nothing to rotate. - **Local** — camera, audio, particles. This device's business alone. Gameplay must not depend on Local state, or late joiners will see a different game from everyone else.