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

193 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.