Files
llamamod/docs/strats/roller-coaster.md
T

12 KiB
Raw Blame History

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.

The ride: first drop, boost, jump, spin pad, climb, station

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 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.
  • 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) 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).

The two layouts in the editor's top view: Loop and Grand circuit

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).

The tally after the ride

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.