193 lines
12 KiB
Markdown
193 lines
12 KiB
Markdown
# 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.
|
||
|
||

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

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

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