documentation for level builders: docs index, step-by-step guide, file format reference (wad, geometry, objects, textures, audio, testlevel chapters, engine limits), strat vm and script language with the arena as worked example, tools overview, developer notes; readme rewritten as project front page with screenshots; review notes moved to notes/dev
This commit is contained in:
1 parent
00b2880d67
commit
624718b893
41 files changed
+2175
-259
No files matched your search
@@ -0,0 +1,240 @@
|
||||
# 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`.
|
||||
Reference in new issue
Block a user