# Example: the roller coaster The *Llama Coaster* mod builds a new roller-coaster ride: a circuit with drops, climbs, boost arrows, a spin pad, a jump over a gap, a station where the ride starts and ends, and fireworks at the finish. Unlike the [arena](arena.md) it writes no new strat code: it **reuses the game's own coaster cart** (`RollerCoaster`) and only gives it new track data – new geometry and a new waypoint chain. This page explains how the original ride works and how the mod feeds it. ![The ride: first drop, boost, jump, spin pad, climb, station](../img/strats/coaster_ride.jpg) | File | Role | |---|---| | [`CoasterMod.java`](../../src/main/java/com/eliasmueller/llamamod/mods/coaster/CoasterMod.java) | the mod: options, chapter check, writes the WAD and the loading picture | | [`CoasterTrack.java`](../../src/main/java/com/eliasmueller/llamamod/mods/coaster/CoasterTrack.java) | the two layouts; turns legs and pieces into segments, corners and the waypoint chain | | [`CoasterLevel.java`](../../src/main/java/com/eliasmueller/llamamod/mods/coaster/CoasterLevel.java) | the level script: track bed, rails, pillars, station, ground, objects | | [`CoasterSpeed.java`](../../src/main/java/com/eliasmueller/llamamod/mods/coaster/CoasterSpeed.java) | patches the cart's speed constants for the Speed option | | [`CoasterSim.java`](../../src/main/java/com/eliasmueller/llamamod/mods/coaster/CoasterSim.java) | frame-by-frame model of the cart, to check a layout without the game | | [`CoasterTest.java`](../../src/test/java/com/eliasmueller/llamamod/mods/coaster/CoasterTest.java) | unit tests | | [notes/mod-coaster.md](../../notes/mod-coaster.md) | lab notes: engine facts, what was verified in the game | Options: **track** (Loop ~100 units, Grand circuit ~150 units), **laps** (1–3, default 2), **speed** (slow / normal / fast), **coins** (on/off), TestLevel **chapter** (32–49, default 48). See [manual chapter 18](../manual/18-mods.md#the-llama-coaster-ride) for players. ## 1. How the original ride works Catacombs chapters 1, 3 and 5 (`t1l7m001`, `t1l7m003`, `t1l7m005`) are roller-coaster rides. The ride music is track 25, `ROLLER` ([Music](../reference/audio.md#music)); the levels have the longest views of the game (fog end up to 23 cells, [Engine facts](../reference/engine-limits.md#drawing)). * **The cart is the player.** These levels have no `Llama` strat. The `RollerCoaster` strat calls `isPlayer()` and draws `model:0` – the cart *with* Kuzco the llama and Pacha in it – so the llama is not attached at run time, it is part of the cart model and its animations (lean, cheer, look back). See [Objects](../reference/objects.md#special-objects). * **The camera** is the ordinary `Camera` strat, spawned by the cart like the Llama spawns it. It follows the global `g29`; in a corner the cart sets `g29` to the heading *after* the turn, so the camera swings round first. * **Track following**: the cart drives straight along its heading and turns by exactly 90° at a waypoint. Its height comes from the floor under it. So a ride is a chain of straight legs on the grid with 90° corners; slopes, jumps and gaps are pure geometry. * **Floor materials** of the track bed trigger the effects: 5 boost arrows, 11 jump, 13 spin pad (the cart hops and turns round, riding backwards to the next corner), 16 the end, 30 the invisible death plane below (fall = restart). * **Start and end**: the first waypoints are *station* nodes – the cart crawls out with letterbox borders. On material 16, when the current waypoint is the last one, the strat waits 30 frames and calls `endLevel` with its own parameters 1–3 (tribe, world, chapter) → the tally screen. * Helpers: `RollerDoor` (gates), `FireworksTrigger` (rockets at its waypoint spots when the cart passes), `CoinRollerCoaster` (coins hovering over the track), `WompyRollerCoaster`. The river levels (`t1l3m001…4`) look similar but are not rail rides: their player `KuscoAndPachaTogetherOnATree` floats on water, is pushed by current zones, steers freely between the banks and leaves through a normal `LevelDoor`. That is why the mod uses the coaster. ### The cart script step by step Excerpts of `RollerCoaster` as the editor's script view shows it (`v19` = speed, `v22` = boost excess, `v18` = sideways offset, `v21` = 0 or 2048 when riding backwards): ``` call L_0200() // prepare the waypoints: // self.wpX = int(self.wpX * 2 + 0.5) / 2 x/z rounded to halves // setWaypointParam2((self.wpRotY + 512) & 3072) heading rounded to a quarter turn spawn<4, 41, 2, 0x248000, 0>(0.9, 1.0, 0, 0, unit:Camera) isPlayer() self.y = self.floorHeight showLevelStartInfo<1>() loop { call L_0074() // the end check (below) call L_012c() // left/right input → v18 (at most half the speed) moveRightQ(v18) moveForwardQ(v19) if (v22 > 2r) { v19 -= 2r; v22 -= 2r } // a boost wears off … else if (v19 > 0.15) { v19 = max(v19 - 2r, 0.15) } // … down to cruise 0.15 (614 raw) if (self.floorMaterial == 11) { … g22 = 163r } // jump: vertical speed else if (self.floorMaterial == 5) { v19 += 20r; v22 += 20r } // boost arrows else if (self.floorMaterial == 13) { … g22 = 61r; v20 = 128 } // spin: hop + half turn if (self.y - 204r - v19 * 1228r < self.floorHeight && self.floorMaterial != -1) { self.y = self.floorHeight // stay on the floor (a climb > ~0.1 per frame is lost) } else { … fall … } // corner: within 0.75 of the waypoint, steer round it and take the next one v15 = (self.x - self.wpX) * v4 + (self.z - self.wpZ) * v5 if (v15 < 0.75 | (self.wpParam && v15 < 2.0)) { … g29 = self.wpRotY … } … } L_0074: // the end if (v13 > 1) { v13 -= 1; if (v13 <= 1) { endLevel(v1, v2, v3) } } else if (self.floorMaterial == 16 && (self.wpEndFlags & 3r)) { v13 = 30 } ``` ## 2. The plan * **Base level = Catacombs 1 (`t1l7m001`)**, not the village: it already contains the cart, its camera, coins, gates, fireworks, textures, sounds and the ride music. Importing them into the village ([like the arena](arena.md#3-compiling-and-importing-arenaimport)) would only copy the same things over. The mod keeps the base level's code and replaces geometry and objects. * The **track** follows the cart's rules: straight legs along the axes, 90° corners, heights changing only on straights. * The **cart's waypoint chain** is generated from the same layout, so geometry and path cannot disagree. * The level goes into a TestLevel chapter ([why 32–49](../reference/testlevel.md)). ## 3. The track (`CoasterTrack`) A layout is a start corner and a list of legs; each leg is a heading and a list of pieces (kind, length in units, height change): ```java new CoasterTrack("loop", new double[] {10, 10, 24}, List.of( stationLeg(90, 4, 8, 4, 2), // run-in, END, brake, STATION, first drop leg(0, p(TRACK, 2), p(TRACK, 8, 2), p(TRACK, 2)), // climb 2 leg(270, p(TRACK, 2), p(SPIN, 1), p(TRACK, 1), p(TRACK, 4, -2)), leg(0, p(BOOST, 2), p(JUMP, 1), p(GAP, 2, -1), p(TRACK, 9)), // jump over a gap leg(270, p(TRACK, 3), p(TRACK, 5, -2), p(TRACK, 2)), leg(180, p(BOOST, 1), p(TRACK, 2), p(TRACK, 25, 7)))); // lift hill back up ``` The constructor lays the pieces out and refuses a layout that does not close, turns by anything but 90° or lacks exactly one STATION and one END on leg 0. Each kind maps to a floor material (TRACK 0, BOOST 5, JUMP 11, SPIN 13, END 16). ![The two layouts in the editor's top view: Loop and Grand circuit](../img/strats/coaster_tracks.jpg) **Waypoints** (`nodes(laps)`): node 0 one unit behind the cart and node 1 at the end of the station, both with parameter 1.0 (the crawl); then one node per corner, 0.5 before its centre with the heading after the turn, repeated for every lap; and a last node 9 units past the END pad. So the END pad only ends the ride in the last lap – in earlier laps the cart rolls over it. ## 4. The level (`CoasterLevel.build`) The level is a [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java) for the [`LevelBuilder`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java): * **Track bed**: per unit two 1 × 1 quads with the piece's texture and material (boost arrows, jump stripes, spin target), an underside, and **rails** 0.35 high on both sides – their inner faces are collision walls, so steering cannot leave the track. Gaps get end faces. * **Corners**: flat 2 × 2 squares with rails on the two outer sides. * **Pillars** every 5 units and under each corner, a **boarding platform** with posts left of the station, a **ground plane** at height 0 with material 30 (the death plane). * **Objects**: ```java l.strat("RollerCoaster", st[0], st[1], st[2], st[3], new int[] {0, tribe * FX, world * FX, chapter * FX}, // endLevel target = this chapter LevelBuilder.Waypoints.of(wps), 0, false); l.strat("RollerDoor", …); // gate at the station exit l.strat("FireworksTrigger", …, Waypoints.of(fw), …); // six rocket spots around the END pad l.strat("CoinRollerCoaster", …); // every 1.5 units on long straights ``` ## 5. Speed and checking a layout (`CoasterSpeed`, `CoasterSim`) The **Speed** option scales two constants of the cart: the cruise speed `pushShort 614` (six places: the floor after a boost and the cap in corners) and the boost `pushShort 20` (two places). `CoasterSpeed.find` locates them in the unit's listing (the instruction before and after must match) and rewrites only those 16-bit operands. A faster cart flies farther, and a jump that lands in a corner's turn zone misses the turn. `CoasterSim` replays the cart's speed and jump rules frame by frame on a layout and reports jumps that do not clear their gap or land too late, and climbs steeper than the floor probe allows. The tests run it for every track × speed × laps. ## 6. The mod (`CoasterMod`) `apply` refuses a chapter whose WAD another mod (or a level project) already changed, builds the level on `t1l7m001`, sets the tally total to the number of coins (`LevelTally.counted`) and draws a loading picture in Java (the title sits in the band the tally text leaves free). ![The tally after the ride](../img/strats/coaster_tally.jpg) ## 7. How it was tested * **Unit tests** (`CoasterTest`): both layouts close, contain every feature and do not overlap; bad layouts are refused; the waypoint chain follows the cart's rounding rules; every jump lands for all speeds and laps; the speed patch changes exactly the 8 operands; the built level has one RollerCoaster (and no Llama), the gate, the fireworks, the coins, the right exit target, the floor materials 0/5/11/13/16/30 and the tally total; a chapter another mod wrote is refused. * **In the game** (`tools/apptest.py`): Loop and Grand circuit at all three speeds, 1 and 2 laps – full rides from the station to the tally with a screenshot every 1.5 s, no crash. After the tally Enter restarts the ride; in the ride Esc → *Beenden und zur Auswahlkarte wechseln* goes back to the world map. ## What to take from it 1. **Reuse a ride type with new data** when its rules fit: a new geometry plus a new waypoint chain is a new ride, without writing strat code. 2. **Read the original strat first** – the 90° corners, the half-unit rounding and the 0.1 climb limit all come from its script, and the layout code enforces them. 3. **Generate path and geometry from one description**, so they always match. 4. **Model the physics offline** (`CoasterSim`) to catch bad jumps before starting the game. 5. **Constants can be tuned in place**: find them in the listing by their neighbours and patch the operand, instead of recompiling the strat.