12 KiB
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, and an object (the Alligator) imported from another world. This page walks through the code.
| File | Role |
|---|---|
ArenaMod.java |
the mod: options, writes the WAD and the loading picture |
Arena.java |
the level script: geometry and object placement |
ArenaScripts.java |
the strat scripts ArenaMaster, ArenaGator (and a test probe) |
ArenaImport.java |
compiles the scripts and merges them into the base level |
ArenaTest.java |
unit tests (scripts compile for all option corners, the built level decodes) |
| 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, andArenaGator, a new alligator spawned by the master. - The level goes into a TestLevel chapter (why 32–49).
2. The level (Arena.build)
The level is a LevelScript
for the Java LevelBuilder,
the same builder the Test World uses. Coordinates are game units (one cell each):
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. 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:
merged = ArenaImport.merge(baseWad, src, "t1l4m002", units(probe, debug));
return LevelScript.super.buildWad(merged.wad(), wads, opts);
appendcompiles each unit withStratCompiler→StratAssembler, laid out as if it were the last unit of t1l4m002's STPC, and appends it there. Somodel:47is the Alligator's model andunit:56+0xf0is that level'sEnemyCoin. A later unit can name an earlier one asunit:@ArenaGator;appendreplaces that withunit:N+offset.mergeasksStpcMerge– 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 (EnemyCoinandSparklesalready exist in the village and are shared), relocates every operand and verifies the result.- 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 –
aliveisv3, which the gators change from outside withparent.v3 -= 1: the parent/child link is the communication channel. - Waiting = looping with yield: every wait is a
while … yieldthat also callscap, so the energy cap holds every frame (also after a respawn). - The spawn rule: move,
yield, thenspawn– 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).
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, coinsEnemyCoins, 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 breaks: collision off, the original's death (shrink
over a few frames with puffs, five EnemyCoins), 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
ArenaTestcompiles 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): a probe type (ArenaProbe, optionprobe=true) proved that compiledon touchedhandlers fire; then fights with default options, a quick win withlives=1, waves=1, and a defeat (energy 0 → Continue restarts the arena).
What to take from it
- Borrow assets by compiling against the level that has them and importing with
StpcMerge. - Keep event handlers short; set flags and react in the main loop.
- Communicate through
parent.vNand globals; spawn only after ayield. - Copy numbers (speeds, animation indices, type fields) from the original type's script.
- Write the exit yourself if you need a condition – material 16 + same cell +
endLevel.


