Files
llamamod/docs/strats/arena.md
T

12 KiB
Raw Blame History

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.

The arena: an alligator chasing the llama

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, and ArenaGator, 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);
  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 – 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). 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 EnemyCoins, 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

  • 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): 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

Win: bonus coins on the exit pad, then the tally

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.