Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Sled

Sled is a downhill racer. The player pulls a slingshot back and lets go, and the sled launches down the track. On the way down, the player steers and jumps past rocks, collects coins, and races to the finish gate. A rock stuns the sled or wipes it out. Sled has twelve tracks, eight on snow and four in the desert, and a finish unlocks the next one.

The following commands write a copy of Sled, install it and play it:

Terminal window
npx @spawnite/cli create my-sled --template sled
cd my-sled
pnpm install
pnpm dev

It opens on the lobby scene, where the player picks a look, buys Speed steps and picks a track, and its play button starts the run scene on that track. The same source is in the examples repository.

Sled builds on the following engine parts, which a racing or riding game can reuse:

  • TrackSurface: the slope, with trees from TrackScatter. Lighting lights it, and the scene mounts no <World>.
  • TrackMover: the rider’s ride. The sling fires it with launchTrackMover.
  • Systems: Sled’s own systems run around the engine’s move. One system before the move adds to the mover’s speed, as the TrackMover’s throttle does. The sling reads the player’s pull from the step’s input.
  • TrackTrigger: the rocks, the coins and the finish.
  • Stats: the Speed upgrade, a modifier on the rider’s stats. registerStatField hands the side and drag stats to the mover every step, and Sled’s own pullDownhill system reads downhill.
  • Machines: the run and its pace.
  • Round and levels: the run is a race, and the tracks are levels.
  • Saves: a save with scope: "world", because the rider is not a player’s character.
  • The chase camera along a track: the camera.

The rest of this page describes Sled’s own design: its tracks, its prices, its rider’s rig and its HUD.

The lobby, src/scenes/Lobby.tsx, is a 3D stage with the controls floating over it, and no panels. The stage, src/lobby/Stage.tsx, follows the selected track’s world:

  • On a snow track, a snowy clearing with the chalet and the store behind the rider, both whole on a phone held upright, and spruces and conifers round them. Out to the sides, which a wide screen shows, stand two more chalets and another store, turned and sized apart from the first two, one chalet half hidden by the trees.
  • On a desert track, a sandy camp with hoodoos, palms, barrel cacti and fallen palm trunks.

The rider stands in the middle: the animal on show on the ride on show, idling, breathing and glancing about. The run’s RiderModel draws it with the same rig and ride code, so a look bought or swapped shows at once. A look the player does not own yet draws in full colour, to show it off, under its price tag. As the look on show changes, by an arrow or a buy, the rider turns once round on its ride in 0.6 s, eased, and ends facing the camera again, as a mobile game’s character picker does: addTurn in src/ride/rig.ts lays the turn on the pose, and only the lobby passes RiderModel the turnKey that starts it, so a run never turns. The camera is fixed, a three-quarter front view from a little above, and its lens widens to keep at least 2.6 m across the rider on a phone held upright and 4.4 m top to bottom on a wide screen. The scene projects the rider and the ride through the camera, so each one’s arrows stand over its part of the stage at any screen shape.

The controls are the following, in src/lobby/:

  • The bank, at the top right, drawn as the run’s coin count.
  • Arrows either side of the animal cycle the animals, and arrows either side of the ride cycle the rides. A look the player owns is equipped as the arrows reach it. One they do not own shows its price tag between the arrows; a tap on the tag buys and equips it.
  • The Speed dial at the top centre: its + buys the next step for the price beside it.
  • The map sheet at the bottom: its tab shows the selected track’s number and the play button, and a tap or a drag on the tab raises the painted map of each world, where a tap on a pin selects that track.

The selected track starts as the last one played, or, on a fresh load, the first one not finished. The play button sends the ride.commands.play command, whose reader on the step that decides outcomes calls the engine’s playLevel and refuses locked for a track not unlocked yet, and goes to the run scene once it is accepted. The round screen’s home button comes back here.

Sled has twelve tracks, eight on snow and then four in the desert, each one of the old game’s 29 levels with its rocks and coins where they were. Each track brings one new thing. A finish unlocks the next one.

The bends ramp up from track 1 to track 12, because a fast rider sees less of what lies past a bend. Tracks 1 to 4 keep the old game’s gentle plans: one or two bends, none tighter than a 25 m radius. Tracks 5 to 8 take three to five bends, down to a 20 m radius. The desert’s straights bend into sweeps and esses, four to seven bends, tightest on tracks 10 to 12, down to 16 m on track 12. Each track gates on a climb of its own, as the next section says, and the gates fall at different places along the tracks:

  • Track 1, 66 m to the finish: all snow, one bend to the left. It climbs 1.2 m over its first 28 m, which a full pull crests and a launch from the half-draw does not, then drops to the line. Seven coins, a slab across the middle at 39 m and a boulder on the left at 60 m.
  • Track 2, 71 m: the narrowest lane, 3.9 m, on ice from 22 m, bending left and then right. It drops 3.5 m to 24 m, climbs 3.3 m on the ice to a crest at 42 m, and drops to the line. Three slabs, on the left at 18 m, on the right at 32 m and across the middle at 49 m, and 13 coins.
  • Track 3, 76 m: on ice from 23 m, bending right and then left. It falls 3 m to 36 m and climbs 3.1 m to a crest at 58 m, where a boulder stands on the right, then drops to the line. Four slabs, at 10, 22, 34 and 47.5 m, from one side to the other, and 13 coins.
  • Track 4, 83 m: it climbs 0.6 m over its first 27 m, plunges 3.4 m onto ice by 46 m, climbs 3.4 m to a crest at 65 m, and bends left down to the line. A low rock on the left at 42 m, a boulder on the left of the climb at 62 m, and 8 coins.
  • Track 5, 85 m, the old level 5: the slalom, three bends, right, left and right, the tightest at a 22 m radius. It drops 3.6 m from the sling onto ice, to 30 m, then climbs 3.6 m to a crest at 54 m, the old level’s roller drawn longer, and drops to the line. Three low rocks alternate sides at 10, 26 and 42 m. 16 coins.
  • Track 6, 126 m, the old level 7: boulders to steer round, down three sweeps, right, left and right, the tightest at 21 m. It gates early: it drops 3.5 m to 19 m and climbs 3.3 m to a crest at 41 m, then falls 5.8 m to the line as ice and snow alternate. Four boulders, one just past the crest at 47 m and the last across the middle at 95 m. 10 coins.
  • Track 7, 139 m, the old level 8: the first pair, two rocks close enough that hitting the first throws the sled into the second. Four bends, the tightest at 20 m. It plunges from 40 m to 50 m, then climbs 5.3 m to a crest at 77 m, and the pair waits just past it at 79 and 84 m. The lane pinches to 3.4 m from 83 m to 112 m. Six low rocks, a boulder at 44 m and a slab at 100 m. 26 coins.
  • Track 8, 287 m, the old level 14: the switchbacks, a 165° turn to the left and a 205° turn to the right, five bends in all, the tightest at 20 m. All snow, on a 4.4 m lane that pinches to 3.4 m from 181 m to 255 m. It falls 5 m to 73 m and climbs 3.5 m to a crest at 102 m, with a boulder across the middle at 112 m, then falls about 10 m through both turns. 12 low rocks threaded through the bends, and 60 coins.
  • Track 9, 296 m, the old level 20, in the desert: gates, two hoodoos side by side with one slot, six of them and a chicane, on a dune stretch that runs from 39 m nearly to the finish. Five bends, the longest a 167° turn to the left, the tightest at 24 m. It falls 4 m to 49 m and climbs 4.1 m through the first gate to a crest at 80 m, then falls 7.3 m to the line. Six barrel cacti. 35 coins.
  • Track 10, 283 m, the old level 21: the funnel. All sand, the lane narrowing from 5.9 m to 2.5 m the whole way down, six gates in it and a fallen trunk in the throat at 241 m. Five bends, an S and then a 123° turn to the right, the tightest at 18 m. It falls 9.3 m to 167 m, with a steep drop at 141 m, climbs 3.9 m between two gates to a crest at 200 m, and falls to the line. 35 coins.
  • Track 11, 287 m, the old level 22: greed. Hoodoo lines split the lane, cacti to hop on one side, paid in coins, and the other side open and unpaid. Four bends, among them a 214° turn to the left in the middle, the tightest at 18 m. It falls 6.3 m to 106 m and climbs 4.4 m to a crest at 133 m, before the hoodoo at 138 m, then falls 7.5 m to the line. 13 barrel cacti and 6 hoodoos. 52 coins.
  • Track 12, 299 m, the old level 24: the summit, one of everything in the order it was taught, the lane pinching to 3.4 m. Seven bends that turn 468° in all, the tightest at 16 m. A 1.2 m hump at 53 m, then the track falls 9 m to 235 m, climbs 6.4 m to the summit at 265 m, and plunges 4.5 m to the line. Four fallen trunks, 11 barrel cacti and 6 hoodoos. 59 coins.

The other 17 old levels stay behind. Level 6’s narrows returns in track 10’s funnel. Level 9, one long sweep, gives its place to level 14, whose rocks the old game laid through its switchbacks, so track 8 is the snow’s curviest by design rather than a sweep bent by hand, and the climb in turning steps up to the desert. Track 7 keeps level 8, which teaches the pair. The other turning levels run 240 to 300 m, too long for tracks 5 to 7, and of the desert’s ten, these four rise in order to the summit, the old game’s review of everything it taught.

Each track carries the old game’s rocks, coins and trees, each at its old distance along the track and its old side, with its zones renamed and one last point 6.5 m on past its end, a little lower, so the sled glides to rest past the gate. A rock, a coin or a tree is placed by distance along the track, so it moves with the track wherever the track bends. Tracks 1 to 4 keep the old points across the ground. Tracks 5 to 12 bend further than the old game’s: each old track’s curve is drawn tighter, and sweeps are added on its straights, then the track is laid as a point every 6 m, each point taking its width and zone from the old points either side. The old points’ bank angles and walls are left out: the track draws neither.

Every track after the first is gated on the Speed curve: a clean full pull finishes it at the track’s expected step and runs out of steam three steps below. Each track gates on a climb of its own, at the place the list above names: a natural climb where the old track had one, such as track 5’s roller and track 12’s summit, and one added where it fits the track’s flow where it had none. A gate’s height is the middle of the heights the expected step clears and the step three below does not, 3.1 to 6.4 m at a grade of 12 to 21%. A step pulls only 2% harder down a slope than the one before, so a gate must take up most of the speed the expected step brings to it; the window between the two steps is 0.19 to 0.8 m of height. The drops before and after each gate are set so the expected step crosses the line at 9 to 13.5 m/s. The heights and the bends changed, and nothing else: no rock, coin or tree moved along the track or across it.

A track’s expected step is one past the highest step a player can buy with 60% of the coins of the tracks before it, so a track asks about one replay of the earlier ones. A step never falls below the track before’s, and never passes 25. The steps cost, by tier of five:

  • Steps 1 to 5: 1 coin each.
  • Steps 6 to 10: 3 coins each.
  • Steps 11 to 15: 8 coins each.
  • Steps 16 to 20: 20 coins each.
  • Steps 21 to 25: 50 coins each, 410 coins in all.

readExpectedSteps in src/ride/speed.ts holds the rule, with each number named, and levelSpeedSteps in src/levels/index.ts is its output over the tracks’ coins, so a change to a track’s coins changes the steps after it. The lobby reads the same function to show the step a track needs. The rule gives the following steps:

Track Coins 60% of the earlier coins Step those buy Expected step
1 7 0 none 0
2 13 4 4 5
3 13 12 7 8
4 8 19 9 10
5 16 24 10 11
6 10 34 11 12
7 26 40 12 13
8 60 55 14 15
9 35 91 16 17
10 35 112 17 18
11 52 133 18 19
12 59 165 20 21

test/levels/tuning.test.ts rides each track clean, with every rock left out and the rider down the middle, at its expected step, three steps below and five above. It checks the finish, the stall before the line, and that the fastest finish brakes to rest inside the run-out, and prints a table of all three. A run that rides the shoulder or hits a rock needs a step or two more than the expected one.

test/levels/sight.test.ts checks that every rock is fair, on a phone held upright, 390 by 844, and on a 16:9 screen. It rides each track’s clean line at its expected step and finds, for each rock, where the chase camera first has it in view for good. The camera stands as the engine stands it, on the centreline 6.8 m behind the rider and 3.5 m up, and looks at the centreline 4 m ahead and 1.5 m up, through the 52° lens at the screen’s aspect. The lens is 52° from the bottom of the screen to the top, so a phone held upright sees 12.7° to either side and a 16:9 screen 41°. A rock is in view while some of its top edge, end to end across its drawn width, is in that frame, and no stretch of the track between rises above the line from the eye to it. A rock a bend or a crest hides again counts as seen only once it comes back. The time from there to the rock must be at least 1.5 s, the old game’s warning floor; a rock in view from the sling, where the rider waits to fire, passes, and a rock never in view fails. The phone held upright is the tighter screen. Where a bend left a rock less than that on it, the stretch of the bend in the 25 m before the rock was eased as little as reached the floor, and no rock was moved: the second turn on track 8, the first bend and the long left turn on track 9, the right turn on track 10, both long turns on track 11, and four of track 12’s bends. No gate’s height changed. The tightest warning on the phone held upright is 1.52 s, on tracks 8, 9, 11 and 12, and on the 16:9 screen 2.48 s, a low rock at 42 m on track 4.

Each track names the map it is drawn and ridden on, snow or desert, in levelMaps in src/levels/index.ts, a table by level id rather than a field of the file, because the engine’s track level refuses a key it does not know. A level is a JSON file in src/levels, in the track level format. Track in src/levels/index.ts names each level’s id, level-1 to level-12, with defineLevels, and registers them, so useLevels types each id; the id is the key the save keeps the level under. A level’s track is a list of points, each with the lane’s width and its zone, snow or ice. The game builds the engine’s Track from them, one lane plus a 3 m snow shoulder on each side wide, so the ride stops at the fence.

The drawn surface is a cross-section laid every half metre: the lane, the shoulders, a 1.5 m fence and 60 m of hillside that climbs 10 m away from the track. Loose snow lies on the lane and deepens toward the fences, and the wind carves ridges into it without ever raising it above its authored depth. An ice stretch is swept bare, paints paler and shines more. Past the hillside, the engine’s terrain carries the ground on into mountains that reach the horizon: snow lies on whatever faces up, the steep faces show stone, and the far peaks fade into the sky’s lowest colour. The trees are the level’s own, plus nine rows across the hillside around them and five more past it on the terrain, at 62, 78, 96, 116 and 136 m, the same on every device. The engine’s Lighting lights the run with the Dusk look, its sun low and warm ahead of the run. The run drops the look’s ambient occlusion: open snow under fog shows no contact shade, and at a retina screen’s resolution the pass costs half the frame. The painted dawn panorama is drawn behind it as the sky, turned so its sun stands on the look’s; Dusk’s own image is the light every surface reflects.

A map, in src/maps.ts, holds everything that differs between the two: the lane’s paint in each zone, the ground tiles, what the cover costs the sled, the terrain’s colours and height, the look and sky, and the stands of models round the run. The points’ zones keep their names on both maps: snow is the loose cover, and ice the fast stretch. The snow map is everything above.

The desert map is a canyon at mid-afternoon:

  • The cover is sand, #e8c48c, the old game’s, on the snow’s tile of wind ridges. The fast stretch is a packed dune, painted a shade darker and pinker, that shines a little and, like ice, costs the sled nothing.
  • Sand costs the sled half again what snow does, 1.71/s per metre of depth: 0.06/s on the lane against snow’s 0.04/s, and 0.6/s at a shoulder’s edge against 0.4/s. The old game ran its desert on the snow’s numbers to keep 29 levels’ feel; the desert tracks are tuned for sand.
  • Past the hillside, the terrain climbs to 220 m rather than 340 m, sand on whatever faces up and red sandstone on the steep faces, fading into a dusty #e6c7a6.
  • The Noon look lights it, its sun at 14:30, high and ahead of the run, with a warm grade and fog. Noon’s own sky is drawn behind it.
  • Palms and hoodoos stand in the rows where the snow has its conifers and spruces, at the trees’ sizes, about 3 to 9 m tall, and the level’s own trees are palms and hoodoos too. Barrel cacti, about 0.6 to 1.7 m tall, stand in the same rows, between them.
  • The rocks are the desert’s own, as in the old game, on the same boxes and the same rules: a fallen palm trunk where the snow has a slab, three barrel cacti where it has a low rock, and a hoodoo where it has a boulder. The map’s rocks names each kind’s model, and Rock stretches it to the kind’s box. A low rock is three copies across its box on both maps, as the old game drew it, so it reads apart from a slab.

The rider stands 6 m down the track between two sling posts, with the band strung through it. S pulls the sling back, to a full pull in 0.8 s, and W eases it off, down to the half-draw. A and D aim the rider across the lane, up to 2.5 m either side of the centre, and the posts stand half a metre outside that reach. Space or Enter fires. The first key draws the sling halfway at once, so a bare Space launches from the half-draw. Level 1 climbs for its first 24 m, and a launch from the half-draw stops short of the crest, as it did in the old game, where the run ends on a stall. The rider trails the pull and the aim, so the sling feels weighted, and the pull is drawn only: the rider’s place on the track does not move back.

The launch speed comes from the pull asked for, not the one the rider has eased to: 1.5 m/s at none, 4 m/s at the half-draw, and 6.5 m/s at a full pull. It sets the engine’s TrackMover going with launchTrackMover. From there A and D steer and Space jumps, as the mover does for any rider, under gravity at 1.6 times Earth’s, the old sled’s pace. The snow under the sled costs it speed, through the track’s surface friction: 1.14 per second for each metre of snow, so about 0.04 per second on a snow lane, 0.4 per second at a shoulder’s outer edge, and nothing on ice. Drifting off the fast line costs speed as it happens. The rider rolls into the steer, up to 0.3 radians. A finger, a mouse or a trackpad plays it too, from anywhere on the world: on the sling, a drag pulls the rider back as far as it goes down, up to 30% of the screen’s shorter side for a full pull, and aims it as far as it goes across, and letting go fires it as drawn, from the slack when the drag only aimed; a tap fires the half-draw. On track 1 alone, “Drag back and let go to launch” stands over the rider until the launch, from src/components/LaunchHint.tsx. Riding, a drag across steers, a full steer at 30% of the shorter side, and a tap jumps. src/components/Controls.tsx reads the drag.

The rider is a skinned animal on a ride. Rider reads each from the save, as what the coins buy says, by id: rider from src/ride/riders.ts’s RiderId and ride from its RideId. The riders are the penguin, the bear, the fox, the cat and the rabbit, and the rides are the toboggan, the donut, the sardine tin and the bathtub. A rider sits low and tucked on the toboggan and on the donut, its legs folded, and lounges in the tin and the bathtub, leant back 0.25 radians with its arms along the rim. Each ride stands on its lowest point on the snow, and the lowest vertex of the rider’s folded skin rests on its deck or floor, a fraction of the fitted model’s height: the toboggan’s slats at 0.83 of it, under the curl, and the donut’s glaze at its top. The rig leans, rolls and squashes the body about that contact, so the seat never slides. The rider and its ride are drawn at 1.4 times their size on the track, and the toboggan 1.6 m long. test/components/RiderModel.test.ts builds each ride and checks it touches the snow, seats the skin and holds the contact while the body leans and squashes. The models carry a skeleton and no clips, so a procedural rig poses the bones every frame, as Unity’s Animation Rigging and Godot’s SkeletonModifier3D layer a pose over a rig:

  • In the air, the arms rise 0.7 radians over the first 0.45 s. A takeoff stretches the body 18% up and squashes it 12% across, about the seat, and a landing does the opposite; the pulse decays at 7 per second.
  • In a turn, the body rolls a further 0.18 radians past the sled’s own 0.3, the inside arm drops 0.9 radians and swings back 0.6, the outside arm lifts 0.35, and the head turns 0.3 radians into the turn.
  • With speed, the body leans forward 0.06 radians at rest and 0.28 at 18 m/s, and the head cancels 0.6 of that lean to keep the horizon. Above 9 m/s the rider shifts between arms wide and a racing tuck every 2 to 4 s: 0.18 radians deeper, with the arms down along the body.
  • On the sling, the body leans back 0.25 radians and the arms reach forward 0.6 radians at a full pull.
  • When hit, the body squashes 1.4 times as hard as a landing, the arms fly up 1.1 radians, the head goes back 0.5, and the body wobbles 0.22 radians at 3.5 Hz. All of it fades over the 0.8 s stun.

The rig is sled’s own behaviour in src/ride/rig.ts: its system keeps what the step decides, the eased speed, the seconds in the air and the impact pulse, on RigTrait, and readRiderPose turns those, the steer, the pull and the stun into the pose that RiderModel writes to the bones. test/ride/rig.test.ts drives a jump, a turn, a pull and a hit in the harness and checks the numbers. The pose runs after the move and the course, so a takeoff or a rock’s stun shows in the step it happens, which test/game.test.ts checks.

The camera is the engine’s chase camera along the track, at a 52 degree lens.

The rider declares four stats, so an upgrade adds a modifier and never a new number:

  • launch, base 6.5: the metres a second a full pull fires at. It scales the whole range, so the slack and the half-draw move with it. No upgrade raises it: every run fires at the same power.
  • side, base 3.5: the metres a second the mover steers across, at most.
  • drag, base 0.004: the air’s 1/m quadratic drag on the mover.
  • downhill, base 1: the multiple of the slope’s pull the sled rides down a slope with. Sled’s pullDownhill system, in the step’s motion phase before the engine’s move, adds (downhill − 1) × gravity along the slope to a riding sled’s speed on the ground going down. A climb’s pull, the sling and an ended run are left as the engine has them.

Sled declares side and drag with registerStatField, so each step hands both to the mover before the move. So the devtools inspector shows no sideSpeed or drag slider on the rider’s track mover, which the next step would set back: the Stats card slides the stats’ bases instead, and a modifier still applies over an edited base.

Sled has one upgrade track, Speed, in src/ride/speed.ts: 25 steps, each doing two things, compounding:

  • It adds more: 0.02 to downhill, so the slope pulls the sled 2% harder going down, to 1.02²⁵, about 1.64 times, at step 25. A climb pulls back as hard at every step, so an upgraded sled reaches a climb faster rather than climbing it worse. Hill Climb Racing’s engine upgrade is the model: the player feels it on every slope.
  • It adds more: 1 / 1.02² − 1, about −0.039, to drag. Top speed goes as one over the square root of drag, so the drag alone raises it 2% a step.

Together they raise the top speed on a long slope about 3% a step, to about 2.1 times at step 25. Steering does not scale. setSpeedStep(rider, step) sets a rider at a step, as one drag and one downhill modifier under the source speed that replace any step it had. levelSpeedSteps in src/levels/index.ts holds the step each track is tuned to finish at, as the tracks say. test/ride/speed.test.ts checks the 1.64 of each: the pull’s on the speed a rider gains down an ice slope in a quarter second from rest, and the drag’s on the top speed down a long ice straight. It also checks that a climb slows step 25 as hard as step 0, and that a rider on the sling or past the end of its run gets no pull.

The coins a run banks buy Speed steps and looks, in src/shop.ts, and the save keeps what they bought. A look is an animal or a ride, and changes nothing but how the rider is drawn. Each costs the following, in the old game’s order:

Animal Coins Ride Coins
Penguin free Toboggan free
Bear 50 Donut 75
Fox 100 Tin 150
Cat 200 Bathtub 250
Rabbit 300

lookPrices in src/ride/riders.ts holds the prices, and speedTierCosts in src/ride/speed.ts the Speed steps’, as the tracks list them. The lobby calls three actions, each on the world the bank is kept on:

  • buySpeedStep(world) takes the next step’s price from the bank and raises the step by one.
  • buyLook(world, look) takes the look’s price from the bank, and owns and equips the look.
  • equipLook(world, look) equips an animal or a ride the player owns.

Each returns { ok: true }, or { ok: false, reason } and changes nothing: reason is a Refusal, too few coins, the last step already bought, a look owned already or a look not owned yet. Each change calls save(), as a run’s end does. The game’s save keeps the progress beside the bank: the Speed step, the animals and rides owned, and the animal and ride equipped. A new player, and a save from before the shop, starts at step 0 on the penguin and the toboggan, which are free. The run’s Rider reads the progress as it mounts: it draws the equipped animal on the equipped ride and calls setSpeedStep with the step bought. test/shop.test.ts checks the actions, and test/save.test.ts the save, a rider spawned at a saved step 5, and that step 5 finishes track 2 where step 0 stalls.

Everything the rider meets is an engine TrackTrigger, a region in track coordinates. The level lists each kind under triggers: slabs, rocks and boulders for the three tiers of rock, and pickups for the coins. Track 1 has seven coins, a slab across the middle at 39 m and a boulder on the left at 60 m. The two rocks are there so that the first track already shows the rocks.

A rock’s box is its tier’s size in metres, across, up and along: a slab 4 by 0.3 by 1.4, a low rock 4 by 0.5 by 1.4, and a boulder 3 by 3 by 2.2. Each hits at 0.8 of that size, so a near miss reads as a miss. Along the track each is at least a metre deep, so a rider at 30 m/s cannot pass between two steps. Each region is padded by the sled’s half metre across and along. A jump clears a slab or a low rock once the rider rises above the rock’s hit height.

Only the entry is a hit. The first hit halves the speed and stuns the rider for 0.8 s, and the rider blinks 8 times a second while stunned. A second hit inside the stun wipes the run out, and so does any touch of a boulder. A wipeout stops the sled where it is. The course acts only on a run between the launch and its end, so the rocks leave a rider on the sling alone.

A coin floats 0.6 m over the snow, spins, and reaches 0.6 m across, 0.7 m up and 0.5 m along from its middle, plus the sled’s half metre. The rider who enters it takes it into the WalletTrait on the rider, the coin sound plays, and the coin leaves the world. A coin lifted over a rock pays only a rider in the air.

The finish is a gate at finish.at, and its region runs from there to the track’s end across the whole width. Crossing it finishes the run, and the sled glides on past it to rest: each track falls away past the gate for about 10 m, then flattens out for 5.5 to 10.5 m more to its end. On track 1 a full pull crosses the line at 12.35 m/s and comes to rest 10.73 m on, 1.77 s later, 5.4 m short of the end. Track 2’s run-out is the longest, because its fastest finish is: a full pull that misses every rock crosses at 17.7 m/s and rests 17.21 m on, 3.3 m short of the end.

The rules are sled’s own system in src/ride/course.ts, which runs after the engine’s move has marked the triggers. It applies each entry to the rider that entered, read from the trigger’s movers, so a second rider that rides into a rock the first still stands in is hit too. test/ride/course.test.ts replays the old game’s obstacle, stun and coin tests in the harness.

The rider carries two machines, both in src/ride/course.ts: the run and its pace.

The run, RunMachine on the rider as RunTrait, is aiming on the sling, riding from the sling’s fire, and stunned for 0.8 s after a hit, then back to riding. The sling’s fire sends LAUNCH; the course sends HIT, WIPE_OUT, FINISH and OUT_OF_STEAM. The course hits and the HUD warns of a stall by the run’s tags, such as RunMachine.is.stunned; the sling eases while the run’s record says aiming, and the rider blinks by the seconds its stun has left, from RunMachine.read.

The pace, PaceMachine as PaceTrait, is sliding, stalling below 2 m/s, and stalled once it has stalled for 1.5 s. Its when rules move it as the speed crosses 2 m/s, on every step between the launch and the end, a stun included; the course ends the run on a slow step once the pace holds its pace.stalled tag. The pace is a machine of its own because a machine counts one wait at a time: the stun is the run’s wait and the stall is the pace’s, so both count at once. spawnite play dump and the devtools show each machine’s state and the seconds its wait has left.

The run ends in one of three states:

  • finished: the rider crosses the finish gate.
  • wiped: two hits inside a stun, or any boulder.
  • crashed, out of steam: the rider stays under 2 m/s down the track for more than 1.5 s. Only the speed along the track counts, so a sled jumping in place still stalls. The stall counts on through a stun, so a slow rider that hits a rock runs out of steam on the same step as one that does not. The pace is stalled after 1.5 s, and the course ends the run on the next step the rider is still slow.

The round is an engine Round with ready set, and it carries the run’s end to the round screen. The sling’s fire starts it. The round is won on a finish or fails with the run’s end state, wiped or crashed, as its reason.

The round screen lets the world run on behind it, with pause={false}, so the end reads as a place on the run. A wipeout stops the sled where it hit, and a sled that wipes out in the air drops to the snow there before its mover stops. After a finish or out of steam, the sled glides on under the screen with its steer and jump let go, and brakes to rest at endBrake, 10 m/s² on top of the slope’s pull, in src/ride/rider.ts. A constant brake reaches rest in a few seconds, where the ride’s quadratic drag fades as the sled slows and leaves it creeping down the slope. Once it rests, its mover stops, so the slope cannot creep it on. On track 1, a finish faster than about 16 m/s reaches the track’s end, and the engine’s track mover stops it there. The menu’s pause still freezes the world. The finish sound plays on a finish, and the crash sound on either failure.

The HUD draws no panes: icons and outlined numbers float over the snow, each in a bare panel, in src/components/. RunHud reads the live run and hands RunHudView its numbers; the devtools take the top left in a debug build, and the HUD keeps off it. It shows three things, each as prominent as the player needs it:

  • The track, across the top from the left edge to the coins: a strip from the sling to a finish flag, with the track’s number as a numeral at its start and the head of the animal riding, the one equipped, sliding along it. It fills by the metres the rider has come from its spawn, 6 m down the track, over the metres from there to the level’s finish.at, so it starts empty on the sling and is full at the gate.
  • The coins, read at a glance: a gold coin and a large count at the top right, read from the rider’s wallet through useWallet(). Each pickup pops the coin and the count.
  • The speed, read now and then: a gauge icon and a small number in km/h under the coins, with no unit word, shown from the launch to the run’s end. Below the stall speed of 2 m/s, and only while the run is riding or stunned, the number turns red and shakes, the gauge’s needle drops into its red zone, and a screen reader hears “Stalling” for “Speed”.

Each number draws tabular digits in a minimum width, so nothing shifts at 10 coins or at 100 km/h.

The round screen, RoundResult, draws no pane either: a large picture of the ending, a finish flag over slow gold rays for a finish, a crash burst for a wipeout and a gauge in the red for out of steam, which a screen reader names “Finish”, “Wiped out” or “Out of steam”. Under it, the coins this run took as a coin and “+24” in gold, then the total banked as a smaller coin and number. Round icon buttons sit under them: Home goes back to the lobby, Retry restarts the track, and after a finish a larger gold Next track plays the next one, and track 1 after track 12; a crash or a wipeout offers Retry alone, drawn gold as the lead. Every ending banks the run’s coins on the world, in src/bank.ts, so a crash keeps them, and save() asks for a write. The game’s save, src/save.ts, keeps the bank and what the coins buy with a world-scope defineSave, since the rider is no character: the engine’s wallet save reads a character’s wallet, and the rider’s wallet holds only the run’s coins.

An agent brings either ending up from a fresh spawnite play start --scene run, run in the game’s folder, which goes past the lobby to the run. A bare Space launches from the half-draw and runs out of steam: spawnite play step --keys Space --steps 1, then spawnite play step --seconds 20, which steps past the end and lets the sled come to rest, then spawnite play screenshot. A full pull finishes track 1: spawnite play step --keys s --steps 90 first, then the same three commands.

A finish records the track through the engine’s levels, which unlocks the next one, and the game’s save keeps it by naming levels in include. After a reload, the lobby selects the first track not finished, or track 1 once all twelve are. test/ride/run.test.ts replays the old game’s run director tests.

The sling and the lean are sled’s own systems in src/ride. The sling runs between the engine’s input and its move, so it takes a jump pressed at the line as its fire before the move can jump; the lean runs after the move, on the steer the move used. A system between the input and the move lets go of an ended run’s steer and jump. The course hands every end to the round in the step it happens, before the round’s clock counts that step. test/ride replays the old game’s sling and lean tests in the harness.