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

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

|
||
|
||

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