Files
llamamod/docs/strats/arena.md
T

241 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 Alligator Arena
The *Alligator Arena* mod is a complete minigame built from scratch: a walled pit on an island,
alligators coming in waves, coins popping out on every hit, an exit that opens after the last wave.
It uses every technique of this documentation – a scripted level, two new strat types written in
the [script language](script-language.md), and an object (the Alligator) imported from another
world. This page walks through the code.
![The arena: an alligator chasing the llama](../manual/img/mod_arena.jpg)
| File | Role |
|---|---|
| [`ArenaMod.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaMod.java) | the mod: options, writes the WAD and the loading picture |
| [`Arena.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/Arena.java) | the level script: geometry and object placement |
| [`ArenaScripts.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaScripts.java) | the strat scripts `ArenaMaster`, `ArenaGator` (and a test probe) |
| [`ArenaImport.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaImport.java) | compiles the scripts and merges them into the base level |
| [`ArenaTest.java`](../../src/test/java/com/eliasmueller/llamamod/mods/arena/ArenaTest.java) | unit tests (scripts compile for all option corners, the built level decodes) |
| [notes/mod-arena.md](../../notes/mod-arena.md) | lab notes: engine facts found, what was verified in the game |
Options: alligator **lives** (1–10, default 5), **waves** (1–5, default 3; wave *n* brings
min(*n*, 2) alligators), llama **energy** cap (1–5, default 4), **coins** per hit (0–5, default 1),
TestLevel **chapter** (32–49, default 49).
## 1. The plan
* The **base level** is Village 1 (`t1l1m001`), like every new level: its code provides the
Llama, Coin, CheckPointPost and textures.
* The Alligator lives in **Jungle by Day 2** (`t1l4m002`). Its model, animations and sounds are
needed, so the new scripts are **compiled against that level** and then **imported** into the
village base together with everything they reference.
* Two new types: `ArenaMaster`, an invisible referee standing on the exit pad, and `ArenaGator`,
a new alligator spawned by the master.
* The level goes into a TestLevel chapter ([why 32–49](../reference/testlevel.md)).
## 2. The level (`Arena.build`)
The level is a [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java)
for the Java [`LevelBuilder`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java),
the same builder the Test World uses. Coordinates are game units (one cell each):
```java
l.sea(0, 0, 40, 40, -1.5, new double[] {ix0 - 3, iz0 - 3, ix1 + 3, iz1 + 3}); // sea + death plane
l.floor(a.x0(), a.z0(), a.x1(), a.z1(), a.floor(), "sand"); // pit floor 16..24
l.box(a.x0() - t, a.z0() - t, a.x1() + t, a.z0(), 0, WALL,
o().top("stone").side("stone_moss")); // one of four walls
// exit pad: one cell with collision material 16 under a gate
l.box(ex - 0.5, ez - 0.5, ex + 0.5, ez + 0.5, a.floor(), a.floor() + 0.06,
o().top("pattern_purple").side("inca").rot(0).sideColl(false).mat(DOOR_MAT));
l.player(st[0], st[1], st[2], null, 1.6); // llama + start camera node 1.6 behind it
l.checkpoint(a.x0() + 0.8, a.z0() + 0.8);
l.strat(ArenaScripts.MASTER, ex, ez, a.floor(), 0.0, null, LevelBuilder.Waypoints.NONE, 0, false);
l.coins(List.of(new double[] {a.x0() + 0.6, 20}, new double[] {a.x1() - 0.6, 20}));
```
Points to note:
* The **exit pad** is a 1 × 1 box whose top has collision **material 16** – the exit rule from
[Objects](../reference/objects.md#the-exit-leveldoor--certain). There is no LevelDoor; the master
takes its role.
* The walls are 1.1 high – too high to jump (0.35), so the fight stays in the pit.
* Texture names (`sand`, `stone_moss`, `inca`) are resolved on the base level by picture.
## 3. Compiling and importing (`ArenaImport`)
`Arena.buildWad` runs before the geometry is built:
```java
merged = ArenaImport.merge(baseWad, src, "t1l4m002", units(probe, debug));
return LevelScript.super.buildWad(merged.wad(), wads, opts);
```
1. **`append`** compiles each unit with `StratCompiler` → `StratAssembler`, laid out as if it were
the last unit of **t1l4m002's** STPC, and appends it there. So `model:47` is the Alligator's model
and `unit:56+0xf0` is that level's `EnemyCoin`. A later unit can name an earlier one as
`unit:@ArenaGator`; `append` replaces that with `unit:N+offset`.
2. **`merge`** asks [`StpcMerge`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/StpcMerge.java)
– the editor's cross-world import engine – to import the two entry points into the village:
it follows every reference and copies the model, 7 animation sets, textures, 3 sound samples and
the spawned strats (`EnemyCoin` and `Sparkles` already exist in the village and are shared),
relocates every operand and verifies the result.
3. It adds one **unplaced prototype MAP record** per type with its type fields
(`nVars`, `slots`, `typeFlags`, `extra`), so the level builder can place the type by name.
The type fields are declared in `Arena.units`:
| Type | vars | slots | type flags | extra | why |
|---|---|---|---|---|---|
| ArenaGator | 16 | 5 | 0xC000 | 3 | collision like the Alligator; 3 collision bones |
| ArenaMaster | 12 | 0 | 0 | 0 | invisible logic, no collision, no handlers |
## 4. The referee: `ArenaMaster`
`ArenaScripts.master()` generates the script; the waves are unrolled by Java. With 2 waves it
reads like this (simplified):
```
strat ArenaMaster {
alias wave = v1
alias alive = v3
alias kills = v4
alias t = v5
g104 = -1 // no exit cell yet (the llama's own exit check reads g104/g105)
g105 = -1
t = 60 // 2 seconds
while (t > 0) { t -= 1; call cap; yield }
wave = 1
self.x = 20.5 // move to a spawn spot …
self.z = 22.2
yield // … and let the engine update the matrix before spawning
spawn<0, 16, 5, 50176, 3>(unit:@ArenaGator)
alive += 1
self.x = 20.5 // back onto the exit pad
self.z = 23.5
while (alive > 0) { call cap; yield } // gators do parent.v3 -= 1 when they die
t = 45
while (t > 0) { t -= 1; call cap; yield }
wave = 2
… // two spawns this time
g101 = 4096r // exit target (1, 9, chapter) as raw 20.12 values
g102 = 36864r
g103 = 200704r
g104 = int(self.x) // the exit cell
g105 = int(self.z)
… // 5 bonus EnemyCoins
loop {
call cap
if (player.floorMaterial == 16 && int(self.x) == int(player.x)
&& int(self.z) == int(player.z)) {
endLevel(4096r, 36864r, 200704r) // the LevelDoor's rule
g1 += 1
remove
}
… // a Sparkles effect every 8 frames
yield
}
}
sub cap { // energy option: keep the llama's energy at the cap
if (player.health2 > 4) { player.health2 = 4 }
}
```
What it shows:
* **Variables as state** – `alive` is `v3`, which the gators change from outside with
`parent.v3 -= 1`: the parent/child link is the communication channel.
* **Waiting = looping with yield**: every wait is a `while … yield` that also calls `cap`, so the
energy cap holds every frame (also after a respawn).
* **The spawn rule**: move, `yield`, then `spawn` – without the yield the alligator would appear at
the master's old position (it copies the matrix of the last frame).
* **The exit** is the LevelDoor logic written out: material 16 under the llama in the same cell →
`endLevel(tribe, world, chapter)`. Setting g101–g105 additionally arms the Llama's built-in check
for the same cell.
## 5. The alligator: `ArenaGator`
`ArenaScripts.gator()` reuses the original Alligator's assets but has its own behaviour. Its parts:
**Setup** – model, size, collision and how it takes hits:
```
setModel(model:47)
scale(1.5)
self.hitDamage = 1793 // 1 | 256 | 512 | 1024
collisionOn<16384>()
collRadius(409r)
collisionBone(0, 409r) // three collision spheres along the body
collisionBone(6.0, 409r)
collisionBone(10.0, 409r)
on touched {
if (self.damageTaken & 1792) { // kick, dive or charge of the llama
if (cool <= 0 && lives > 0) { hit = 1 }
} else {
bit = 1 // contact without an attack: it bit the llama
}
}
```
`hitDamage` 1793 shares a bit with all three llama attacks, so each of them counts as the llama's
hit; bit 1 makes a plain contact hurt the llama ([Collision and damage](README.md#collision-and-damage)).
The handler only sets flags – the main loop reacts to them, which keeps the handler short.
**Fade-in** – `setAmbient` from 0 to 63 over 32 frames, playing the idle animation.
**Main loop** – a state machine in a `switch`, once per frame:
| State | Behaviour |
|---|---|
| 0 watch | turn towards the player (2° per frame); after a timer, if the llama is within 5 cells and on the same floor height (±0.4), play the snap animation and sound |
| 1 snap | wait for the animation, then start chasing |
| 2 chase | accelerate up to `0.03 + 0.006 · wave` per frame towards the llama; lunge when closer than 1.2, give up after 75 frames |
| 4, 5 lunge | fast forward move, then slow down (numbers from the original Alligator) |
| 3 back off | move backwards for a few frames, then rest and watch again |
Before the `switch`, the flags from the handler are handled:
* `hit`: lose a life, sound, three green particle puffs, *coins* `EnemyCoin`s, blink for 45
frames (alpha 0.25 and a red tint every 4 frames, no further hits meanwhile), back off.
* `bit`: back off and rest 90 frames, so the llama gets a breather.
After the `switch` the gator is clamped to the pit and to the floor height.
**Death** – when `lives` reaches 0 the loop `break`s: collision off, the original's death (shrink
over a few frames with puffs, five `EnemyCoin`s), then `parent.v3 -= 1` (one alive less for the
master), `parent.v4 += 1` (kills) and `remove`.
**`sub aim`** – ported from the original Alligator: each frame it publishes its position in
g17–g19 (and the distance in g20) if it is the best target below a jumping llama. That is what makes
the llama's **dive** home in on it.
## 6. The mod (`ArenaMod`)
`apply` refuses a chapter another mod already wrote, builds the WAD, rewrites the tally total
(`LevelTally.counted`: 2 placed coins + every coin the alligators can drop + 5 bonus), writes it
to `wads/T1L9M0nn.wad` and draws a loading picture with Java2D (`CorEncoder`). That is all a level
mod needs: LlamaMod's mod pipeline puts the files into the game's CD folder and links the empty
TestLevel chapters.
## 7. How it was tested
* `ArenaTest` compiles the scripts for every corner of the options, checks wave sizes, builds the
level from the CD and decodes the merged code (the master spawns the gator unit, the gator
spawns EnemyCoin).
* In the game ([`tools/apptest.py`](../../tools/apptest.py)): a probe type (`ArenaProbe`, option
`probe=true`) proved that compiled `on touched` handlers fire; then fights with default options,
a quick win with `lives=1, waves=1`, and a defeat (energy 0 → *Continue* restarts the arena).
![Defeated alligator: puffs and coins](../img/strats/arena_defeat.jpg)
![Win: bonus coins on the exit pad, then the tally](../img/strats/arena_win.jpg)
## What to take from it
1. Borrow assets by **compiling against the level that has them** and importing with `StpcMerge`.
2. Keep event handlers short; set flags and react in the main loop.
3. Communicate through `parent.vN` and globals; spawn only after a `yield`.
4. Copy numbers (speeds, animation indices, type fields) from the original type's script.
5. Write the exit yourself if you need a condition – material 16 + same cell + `endLevel`.