12 KiB
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 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.
| File | Role |
|---|---|
CoasterMod.java |
the mod: options, chapter check, writes the WAD and the loading picture |
CoasterTrack.java |
the two layouts; turns legs and pieces into segments, corners and the waypoint chain |
CoasterLevel.java |
the level script: track bed, rails, pillars, station, ground, objects |
CoasterSpeed.java |
patches the cart's speed constants for the Speed option |
CoasterSim.java |
frame-by-frame model of the cart, to check a layout without the game |
CoasterTest.java |
unit tests |
| 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 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); the levels have the
longest views of the game (fog end up to 23 cells, Engine facts).
- The cart is the player. These levels have no
Llamastrat. TheRollerCoasterstrat callsisPlayer()and drawsmodel: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. - The camera is the ordinary
Camerastrat, spawned by the cart like the Llama spawns it. It follows the globalg29; in a corner the cart setsg29to 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
endLevelwith 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) 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).
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):
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).
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
for the LevelBuilder:
- 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:
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).
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
- 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.
- 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.
- Generate path and geometry from one description, so they always match.
- Model the physics offline (
CoasterSim) to catch bad jumps before starting the game. - Constants can be tuned in place: find them in the listing by their neighbours and patch the operand, instead of recompiling the strat.


