alligator arena minigame: walled pit in a testlevel chapter, waves of alligators with several lives (coins pop out on every hit, blink, back off), bites cost energy, exit opens after the last wave; options for lives, waves, energy, coins per hit and chapter
This commit is contained in:
1 parent
38c679b319
commit
ec3c619120
13 files changed
+1385
No files matched your search
@@ -23,6 +23,7 @@ public final class ModRegistry {
|
||||
"com.eliasmueller.llamamod.mods.cheats.CheatsMod",
|
||||
"com.eliasmueller.llamamod.mods.practice.PracticeMod",
|
||||
"com.eliasmueller.llamamod.mods.freecam.FreeCamMod",
|
||||
"com.eliasmueller.llamamod.mods.arena.ArenaMod",
|
||||
};
|
||||
|
||||
/** {@link ModSource}s loaded by class name (dynamic mods, e.g. level projects). */
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.eliasmueller.llamamod.mods.arena;
|
||||
|
||||
import static com.eliasmueller.llamamod.levelbuild.Opts.NONE;
|
||||
import static com.eliasmueller.llamamod.levelbuild.Opts.o;
|
||||
|
||||
import com.eliasmueller.llamamod.levelbuild.LevelBuilder;
|
||||
import com.eliasmueller.llamamod.levelbuild.LevelScript;
|
||||
import com.eliasmueller.llamamod.levelbuild.PyRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Alligator arena (based on the village base level like the test world): a walled 8 × 8 pit on a
|
||||
* small island, the llama at the south end, the exit pad at the north end. The {@link
|
||||
* ArenaScripts#MASTER} strat on the exit pad spawns the alligator waves and opens the exit after
|
||||
* the last one.
|
||||
*
|
||||
* <p>Script options (tests): {@code probe=true} places an {@link ArenaScripts#PROBE} 1.6 units in
|
||||
* front of the llama; {@code start=x,z,rot} moves the llama; {@code master=false} leaves out
|
||||
* the master (no alligators).
|
||||
*/
|
||||
public final class Arena implements LevelScript {
|
||||
/** Source level of the alligator (models, animations, sounds, EnemyCoin). */
|
||||
static final String SOURCE = "wads/t1l4m002.wad";
|
||||
|
||||
static final ArenaScripts.Layout LAYOUT = new ArenaScripts.Layout(
|
||||
16, 16, 24, 24, 0, 20.5, 23.5, List.of(new double[] {20.5, 22.2}, new double[] {18.0, 21.8}, new double[] {
|
||||
23.0, 21.8
|
||||
}));
|
||||
|
||||
/** Player start (x, z, yaw): south end, facing north (+z). */
|
||||
static final double[] START = {20.5, 18.0, 0};
|
||||
|
||||
/** Collision material of the exit floor. */
|
||||
private static final int DOOR_MAT = 16;
|
||||
|
||||
private static final double WALL = 1.1;
|
||||
|
||||
private final ArenaScripts.Settings settings;
|
||||
private ArenaImport.Merged merged;
|
||||
|
||||
public Arena(ArenaScripts.Settings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String levelName() {
|
||||
return "Alligator Arena";
|
||||
}
|
||||
|
||||
@Override
|
||||
public long seed() {
|
||||
return 11;
|
||||
}
|
||||
|
||||
ArenaImport.Merged merged() {
|
||||
return merged;
|
||||
}
|
||||
|
||||
/** The script types in unit order (the master spawns the gator, so the gator comes first). */
|
||||
List<ArenaImport.Unit> units(boolean probe, boolean debug) {
|
||||
List<ArenaImport.Unit> u = new ArrayList<>();
|
||||
u.add(new ArenaImport.Unit(
|
||||
ArenaScripts.GATOR,
|
||||
ArenaScripts.gator(settings, LAYOUT),
|
||||
ArenaScripts.GATOR_VARS,
|
||||
5,
|
||||
0xC000,
|
||||
ArenaScripts.GATOR_BONES));
|
||||
u.add(new ArenaImport.Unit(
|
||||
ArenaScripts.MASTER, ArenaScripts.master(settings, LAYOUT, debug), ArenaScripts.MASTER_VARS, 0, 0, 0));
|
||||
if (probe) {
|
||||
u.add(new ArenaImport.Unit(ArenaScripts.PROBE, ArenaScripts.probe(), 3, 1, 0x28000, 0));
|
||||
}
|
||||
return u;
|
||||
}
|
||||
|
||||
/** Imports the arena types from {@link #SOURCE} into the base level, then builds as usual. */
|
||||
@Override
|
||||
public byte[] buildWad(byte[] baseWad, Function<String, byte[]> wads, Map<String, String> opts) {
|
||||
byte[] src = wads.apply(SOURCE);
|
||||
if (src == null) {
|
||||
throw new IllegalStateException(SOURCE + " missing (needed for the alligator)");
|
||||
}
|
||||
boolean probe = opts != null && "true".equals(opts.get("probe"));
|
||||
merged = ArenaImport.merge(
|
||||
baseWad, src, "t1l4m002", units(probe, opts != null && "true".equals(opts.get("debug"))));
|
||||
return LevelScript.super.buildWad(merged.wad(), wads, opts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void build(LevelBuilder l) {
|
||||
ArenaScripts.Layout a = LAYOUT;
|
||||
PyRandom rnd = new PyRandom(5);
|
||||
l.sun.azimuth = 200;
|
||||
l.sun.elevation = 55;
|
||||
l.sun.strength = 0.7;
|
||||
|
||||
// island in the sea (death plane), arena in the middle
|
||||
double ix0 = 10, iz0 = 10, ix1 = 30, iz1 = 30;
|
||||
l.sea(0, 0, 40, 40, -1.5, new double[] {ix0 - 3, iz0 - 3, ix1 + 3, iz1 + 3});
|
||||
l.box(
|
||||
ix0,
|
||||
iz0,
|
||||
ix1,
|
||||
iz1,
|
||||
-2.2,
|
||||
0,
|
||||
o().top(NONE).side("earth2").sideTop("grass_edge").solid(false));
|
||||
l.floor(ix0, iz0, ix1, a.z0(), 0, "grass");
|
||||
l.floor(ix0, a.z1(), ix1, iz1, 0, "grass");
|
||||
l.floor(ix0, a.z0(), a.x0(), a.z1(), 0, "grass");
|
||||
l.floor(a.x1(), a.z0(), ix1, a.z1(), 0, "grass");
|
||||
|
||||
// pit floor: sand with a stone ring
|
||||
l.floor(a.x0(), a.z0(), a.x1(), a.z1(), a.floor(), "sand");
|
||||
l.floor(a.x0() + 1.5, a.z0() + 1.5, a.x1() - 1.5, a.z1() - 1.5, a.floor() + 0.01, "sand2", o().coll(false));
|
||||
// walls (inner faces visible), corner towers
|
||||
double t = 0.4;
|
||||
l.box(
|
||||
a.x0() - t,
|
||||
a.z0() - t,
|
||||
a.x1() + t,
|
||||
a.z0(),
|
||||
0,
|
||||
WALL,
|
||||
o().top("stone").side("stone_moss"));
|
||||
l.box(
|
||||
a.x0() - t,
|
||||
a.z1(),
|
||||
a.x1() + t,
|
||||
a.z1() + t,
|
||||
0,
|
||||
WALL,
|
||||
o().top("stone").side("stone_moss"));
|
||||
l.box(a.x0() - t, a.z0(), a.x0(), a.z1(), 0, WALL, o().top("stone").side("stone_moss"));
|
||||
l.box(a.x1(), a.z0(), a.x1() + t, a.z1(), 0, WALL, o().top("stone").side("stone_moss"));
|
||||
for (double[] p : new double[][] {
|
||||
{a.x0() - t / 2, a.z0() - t / 2}, {a.x1() + t / 2, a.z0() - t / 2},
|
||||
{a.x0() - t / 2, a.z1() + t / 2}, {a.x1() + t / 2, a.z1() + t / 2}
|
||||
}) {
|
||||
l.pillar(p[0], p[1], 0, WALL + 0.7, 0.7, o().side("inca").top("stone"));
|
||||
}
|
||||
// exit pad (1 × 1 cell, exit floor material) under a gate on the north wall
|
||||
double ex = a.exitX(), ez = a.exitZ();
|
||||
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));
|
||||
for (double x : new double[] {ex - 0.7, ex + 0.7}) {
|
||||
l.pillar(x, a.z1() + t / 2, WALL, 2.4, 0.3, o().side("inca").top("stone"));
|
||||
}
|
||||
l.box(
|
||||
ex - 0.85,
|
||||
a.z1(),
|
||||
ex + 0.85,
|
||||
a.z1() + t,
|
||||
2.4,
|
||||
2.65,
|
||||
o().top("stone").side("stone_edge").rot(0));
|
||||
|
||||
// decoration outside
|
||||
for (double[] p : new double[][] {{12, 12.5}, {28, 12}, {12.5, 27.5}, {27.5, 28}, {13, 20}, {27, 19.5}}) {
|
||||
l.tree(p[0], p[1], 0, rnd.uniform(1.3, 1.9), 1.1);
|
||||
}
|
||||
|
||||
// objects
|
||||
double[] st = START;
|
||||
String s = l.opt("start", "");
|
||||
if (!s.isEmpty()) {
|
||||
String[] v = s.split(",");
|
||||
st = new double[] {Double.parseDouble(v[0]), Double.parseDouble(v[1]), Double.parseDouble(v[2])};
|
||||
}
|
||||
l.player(st[0], st[1], st[2], null, 1.6);
|
||||
l.checkpoint(a.x0() + 0.8, a.z0() + 0.8);
|
||||
if (!"false".equals(l.opt("master", "true"))) {
|
||||
l.strat(ArenaScripts.MASTER, ex, ez, a.floor(), 0.0, null, LevelBuilder.Waypoints.NONE, 0, false);
|
||||
}
|
||||
if ("true".equals(l.opt("probe", "false"))) {
|
||||
double r = Math.toRadians(st[2]);
|
||||
l.strat(
|
||||
ArenaScripts.PROBE,
|
||||
st[0] + 1.6 * Math.sin(r),
|
||||
st[1] + 1.6 * Math.cos(r),
|
||||
a.floor(),
|
||||
0.0,
|
||||
null,
|
||||
LevelBuilder.Waypoints.NONE,
|
||||
0,
|
||||
false);
|
||||
}
|
||||
// a few coins along the walls to start the counter
|
||||
l.coins(List.of(new double[] {a.x0() + 0.6, 20}, new double[] {a.x1() - 0.6, 20}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package com.eliasmueller.llamamod.mods.arena;
|
||||
|
||||
import com.eliasmueller.llamamod.format.Bytes;
|
||||
import com.eliasmueller.llamamod.format.Level;
|
||||
import com.eliasmueller.llamamod.format.MapChunk;
|
||||
import com.eliasmueller.llamamod.format.TextChunk;
|
||||
import com.eliasmueller.llamamod.format.Wad;
|
||||
import com.eliasmueller.llamamod.format.stpcmerge.DialogMerge;
|
||||
import com.eliasmueller.llamamod.format.stpcmerge.SoundMerge;
|
||||
import com.eliasmueller.llamamod.format.stpcmerge.StpcImage;
|
||||
import com.eliasmueller.llamamod.format.stpcmerge.StpcMerge;
|
||||
import com.eliasmueller.llamamod.format.stpcmerge.TextMerge;
|
||||
import com.eliasmueller.llamamod.format.strat.StratAssembler;
|
||||
import com.eliasmueller.llamamod.format.strat.StratCompiler;
|
||||
import com.eliasmueller.llamamod.format.strat.StratListing;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Compiles script types against a source level (whose models, animations, sounds and strats they
|
||||
* use by index) and imports them into a base level with {@link StpcMerge}, like the editor's
|
||||
* cross-world import: the closure (models, animation sets, textures, sounds, spawned strats such as
|
||||
* EnemyCoin) comes along. One unplaced prototype MAP record per type lets the level builder place it.
|
||||
*/
|
||||
final class ArenaImport {
|
||||
/** A script type: compiled as its own unit; {@code nVars}/{@code slots}/{@code typeFlags} go into the prototype. */
|
||||
record Unit(String name, String source, int nVars, int slots, int typeFlags, int extra) {}
|
||||
|
||||
/** Merged base level and the entry code of every type in it. */
|
||||
record Merged(byte[] wad, Map<String, Integer> codes, StpcMerge.Stats stats) {}
|
||||
|
||||
/** Units appended to a source STPC: new data and entry offsets (source STPC offsets). */
|
||||
record Appended(byte[] stpc, Map<String, Integer> entries, Map<String, Integer> unitIndex) {}
|
||||
|
||||
private ArenaImport() {}
|
||||
|
||||
/**
|
||||
* Appends the units in order. A source may name an earlier unit as {@code unit:@Name}; it is
|
||||
* replaced by {@code unit:N+off} (index and entry offset of that unit).
|
||||
*/
|
||||
static Appended append(byte[] stpc, int wfpc, List<Unit> units) {
|
||||
byte[] d = stpc;
|
||||
Map<String, Integer> entries = new LinkedHashMap<>();
|
||||
Map<String, Integer> index = new LinkedHashMap<>();
|
||||
Map<String, String> refs = new LinkedHashMap<>();
|
||||
for (Unit u : units) {
|
||||
StpcImage cur = new StpcImage(d, wfpc);
|
||||
int base = d.length + 4;
|
||||
String src = u.source();
|
||||
for (var e : refs.entrySet()) {
|
||||
src = src.replace("unit:@" + e.getKey(), e.getValue());
|
||||
}
|
||||
if (src.contains("unit:@")) {
|
||||
throw new IllegalArgumentException(u.name() + ": unknown unit reference in " + src);
|
||||
}
|
||||
StratAssembler.Result r =
|
||||
StratAssembler.assemble(StratCompiler.compile(src), StratListing.layout(cur, base));
|
||||
Integer at = r.labels().get(u.name());
|
||||
if (at == null) {
|
||||
throw new IllegalArgumentException("no strat " + u.name() + " in its source");
|
||||
}
|
||||
byte[] words = r.bytes();
|
||||
byte[] nd = Arrays.copyOf(d, d.length + 4 + words.length);
|
||||
putI32(nd, d.length, words.length / 4);
|
||||
System.arraycopy(words, 0, nd, d.length + 4, words.length);
|
||||
putI32(nd, cur.codeStart, Bytes.i32(nd, cur.codeStart) + 1);
|
||||
int idx = cur.units.size();
|
||||
entries.put(u.name(), base + at);
|
||||
index.put(u.name(), idx);
|
||||
refs.put(u.name(), "unit:" + idx + "+0x" + Integer.toHexString(at));
|
||||
d = nd;
|
||||
}
|
||||
return new Appended(d, entries, index);
|
||||
}
|
||||
|
||||
/** Imports the units (compiled against {@code sourceWad}) into {@code baseWad}. */
|
||||
static Merged merge(byte[] baseWad, byte[] sourceWad, String sourceName, List<Unit> units) {
|
||||
Level src = Level.parse(sourceWad);
|
||||
Appended ap = append(src.wad().require("STPC"), src.wfpc(), units);
|
||||
TreeSet<Integer> roots = new TreeSet<>();
|
||||
src.map().strats.forEach(s -> roots.add(s.code()));
|
||||
roots.addAll(ap.entries().values());
|
||||
StpcMerge.Analysis sa = new StpcMerge.Analysis(new StpcImage(ap.stpc(), src.wfpc()), roots);
|
||||
if (!sa.code.errors.isEmpty()) {
|
||||
throw new IllegalStateException("arena units do not decode: " + sa.code.errors.get(0));
|
||||
}
|
||||
Wad sw = src.wad();
|
||||
StpcMerge.Source source = new StpcMerge.Source(
|
||||
sourceName,
|
||||
sa,
|
||||
TextMerge.Textures.of(sw.require("TEXT")),
|
||||
SoundMerge.parse(sw.get("SMPC")),
|
||||
sw.get("LGPC") == null ? null : DialogMerge.Texts.parse(sw.get("LGPC")),
|
||||
DialogMerge.Speech.parse(sw.get("SRPC")));
|
||||
|
||||
Level lv = Level.parse(baseWad);
|
||||
Wad wad = lv.wad();
|
||||
TreeSet<Integer> troots = new TreeSet<>();
|
||||
lv.map().strats.forEach(s -> troots.add(s.code()));
|
||||
StpcMerge.Analysis tgt = new StpcMerge.Analysis(new StpcImage(wad.require("STPC"), lv.wfpc()), troots);
|
||||
byte[] text = wad.require("TEXT");
|
||||
TextMerge.Textures tex = TextMerge.Textures.of(text);
|
||||
int origTail = 8 + 8 * tex.entries.size();
|
||||
for (TextMerge.Anim a : tex.anims) {
|
||||
origTail += 3 + a.data().length;
|
||||
}
|
||||
List<SoundMerge.Sample> snd = SoundMerge.parse(wad.get("SMPC"));
|
||||
DialogMerge.Texts texts = wad.get("LGPC") == null ? null : DialogMerge.Texts.parse(wad.get("LGPC"));
|
||||
DialogMerge.Speech speech = DialogMerge.Speech.parse(wad.get("SRPC"));
|
||||
List<StpcMerge.Request> reqs = new ArrayList<>();
|
||||
for (Unit u : units) {
|
||||
reqs.add(new StpcMerge.Request(source, ap.entries().get(u.name())));
|
||||
}
|
||||
StpcMerge.Result res = StpcMerge.merge(tgt, reqs, new StpcMerge.Target(tex, snd, texts, speech));
|
||||
if (res.stats().addedTexts() != 0) {
|
||||
throw new IllegalStateException("arena types must not need dialogue texts");
|
||||
}
|
||||
MapChunk map = lv.map();
|
||||
List<MapChunk.Strat> recs = new ArrayList<>();
|
||||
for (MapChunk.Strat s : map.strats) {
|
||||
int[] a = s.toArray();
|
||||
a[6] = res.target().map(s.code());
|
||||
recs.add(MapChunk.Strat.of(a));
|
||||
}
|
||||
Map<String, Integer> codes = new LinkedHashMap<>();
|
||||
for (int i = 0; i < units.size(); i++) {
|
||||
Unit u = units.get(i);
|
||||
int code = res.entries().get(reqs.get(i));
|
||||
codes.put(u.name(), code);
|
||||
// rx ry rz x y z code nParams paramIndex nVars slots typeFlags extra waypoint zero flags last
|
||||
recs.add(MapChunk.Strat.of(new int[] {
|
||||
0, 0, 0, 0, 0, 0, code, 0, -1, u.nVars(), u.slots(), u.typeFlags(), u.extra(), -1, 0, 0, 0
|
||||
}));
|
||||
}
|
||||
map.strats.clear();
|
||||
map.strats.addAll(recs);
|
||||
Wad out = new Wad(wad.chunks());
|
||||
out.set("STPC", res.stpc());
|
||||
out.set("MAP", map.build());
|
||||
out.set("TEXT", text(text, tex, origTail));
|
||||
if (wad.get("SMPC") != null) {
|
||||
out.set("SMPC", SoundMerge.build(snd));
|
||||
}
|
||||
return new Merged(out.toBytes(), codes, res.stats());
|
||||
}
|
||||
|
||||
/** TEXT chunk with the (extended) pages, entries and animations of tex; the rest is kept. */
|
||||
private static byte[] text(byte[] orig, TextMerge.Textures tex, int origTailHead) {
|
||||
TextChunk tc = TextChunk.parse(orig);
|
||||
byte[] rest = Arrays.copyOfRange(tc.tail, origTailHead, tc.tail.length);
|
||||
ByteBuffer b = ByteBuffer.allocate(8 + 8 * tex.entries.size() + tex.anims.size() * 64 + rest.length + 1024)
|
||||
.order(ByteOrder.LITTLE_ENDIAN);
|
||||
b.putInt(tex.entries.size());
|
||||
for (TextChunk.TextureEntry e : tex.entries) {
|
||||
b.putShort((short) e.flags())
|
||||
.put((byte) e.page())
|
||||
.put((byte) e.page2())
|
||||
.put((byte) e.u0())
|
||||
.put((byte) e.u1())
|
||||
.put((byte) e.v0())
|
||||
.put((byte) e.v1());
|
||||
}
|
||||
b.putInt(tex.anims.size());
|
||||
for (TextMerge.Anim a : tex.anims) {
|
||||
b.putShort((short) a.texture()).put((byte) a.type()).put(a.data());
|
||||
}
|
||||
b.put(rest);
|
||||
tc.pages.clear();
|
||||
tc.pages.addAll(tex.pages);
|
||||
tc.tail = Arrays.copyOf(b.array(), b.position());
|
||||
return tc.build();
|
||||
}
|
||||
|
||||
private static void putI32(byte[] d, int o, int v) {
|
||||
ByteBuffer.wrap(d, o, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(v);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.eliasmueller.llamamod.mods.arena;
|
||||
|
||||
import com.eliasmueller.llamamod.format.CorEncoder;
|
||||
import com.eliasmueller.llamamod.levelbuild.LevelBuilder;
|
||||
import com.eliasmueller.llamamod.mods.Mod;
|
||||
import com.eliasmueller.llamamod.mods.ModContext;
|
||||
import com.eliasmueller.llamamod.mods.ModOption;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.GradientPaint;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/** Alligator Arena minigame on a TestLevel (world 9) chapter. */
|
||||
public final class ArenaMod implements Mod {
|
||||
@Override
|
||||
public String id() {
|
||||
return "arena";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "Alligator Arena";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String category() {
|
||||
return LEVELS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "A minigame on a TestLevel chapter: alligators come in waves into a walled pit. Each kick or"
|
||||
+ " jump attack (jump, then Action) costs one of their lives and coins pop out; the bite costs"
|
||||
+ " your energy. Beat every wave and the exit pad opens. Unlock TestLevel on the Save games tab.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ModOption> options() {
|
||||
return List.of(
|
||||
ModOption.integer("lives", "Alligator lives", "Hits each alligator takes.", 5, 1, 10),
|
||||
ModOption.integer("waves", "Waves", "Wave n brings n alligators (at most 2 at once).", 3, 1, 5),
|
||||
ModOption.integer(
|
||||
"energy", "Llama energy", "Energy cap of the llama in the arena (5 = normal).", 4, 1, 5),
|
||||
ModOption.integer("coins", "Coins per hit", "Coins that pop out per hit (5 more at the end).", 1, 0, 5),
|
||||
ModOption.integer("chapter", "TestLevel chapter", "Chapter of world 9 the arena replaces.", 8, 2, 8));
|
||||
}
|
||||
|
||||
/** After Test World (50) and level projects (60), so a clash with them is reported here. */
|
||||
@Override
|
||||
public int order() {
|
||||
return 65;
|
||||
}
|
||||
|
||||
static String wadPath(int chapter) {
|
||||
return String.format(Locale.ROOT, "wads/T1L9M%03d.wad", chapter);
|
||||
}
|
||||
|
||||
static ArenaScripts.Settings settings(ModContext ctx) {
|
||||
int chapter = ctx.optionInt("chapter");
|
||||
return new ArenaScripts.Settings(
|
||||
ctx.optionInt("lives"),
|
||||
ctx.optionInt("waves"),
|
||||
ctx.optionInt("energy"),
|
||||
ctx.optionInt("coins"),
|
||||
new int[] {1, 9, chapter});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(ModContext ctx) throws IOException {
|
||||
apply(ctx, Map.of());
|
||||
}
|
||||
|
||||
/** Applies with level script options ({@link Arena}: probe, start; for tests). */
|
||||
static void apply(ModContext ctx, Map<String, String> scriptOpts) throws IOException {
|
||||
int chapter = ctx.optionInt("chapter");
|
||||
String target = wadPath(chapter);
|
||||
if (ctx.exists(target)) {
|
||||
Path op = ctx.originalCd().resolve(target);
|
||||
byte[] cur = ctx.read(target);
|
||||
if (!Files.exists(op) || !Arrays.equals(cur, Files.readAllBytes(op))) {
|
||||
throw new IOException("Alligator Arena: " + target + " is already changed by another mod (a level"
|
||||
+ " on TestLevel chapter " + chapter + "). Choose another chapter in the arena's options.");
|
||||
}
|
||||
}
|
||||
ctx.progress(0.0, "building alligator arena");
|
||||
byte[] base = ctx.read(LevelBuilder.BASE_WAD);
|
||||
byte[] wad = new Arena(settings(ctx))
|
||||
.buildWad(
|
||||
base,
|
||||
rel -> {
|
||||
try {
|
||||
return ctx.exists(rel) ? ctx.read(rel) : null;
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
},
|
||||
scriptOpts);
|
||||
ctx.write(target, wad);
|
||||
ctx.progress(0.9, "loading picture");
|
||||
ctx.write("T19" + chapter + ".COR", loadingPicture(chapter));
|
||||
ctx.progress(1.0, "arena done");
|
||||
}
|
||||
|
||||
static byte[] loadingPicture(int chapter) {
|
||||
BufferedImage img = new BufferedImage(640, 480, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = img.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
g.setPaint(new GradientPaint(0, 0, new Color(0x1f4d2b), 0, 480, new Color(0x0a140c)));
|
||||
g.fillRect(0, 0, 640, 480);
|
||||
g.setColor(new Color(0xc9a24a));
|
||||
g.fillRect(80, 300, 480, 8);
|
||||
String title = "Alligator Arena";
|
||||
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 58));
|
||||
FontMetrics fm = g.getFontMetrics();
|
||||
g.setColor(new Color(0, 0, 0, 160));
|
||||
g.drawString(title, (640 - fm.stringWidth(title)) / 2 + 2, 252);
|
||||
g.setColor(new Color(0xf5c542));
|
||||
g.drawString(title, (640 - fm.stringWidth(title)) / 2, 250);
|
||||
String sub = "TestLevel – chapter " + chapter;
|
||||
g.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 26));
|
||||
fm = g.getFontMetrics();
|
||||
g.setColor(new Color(0xe8f0e0));
|
||||
g.drawString(sub, (640 - fm.stringWidth(sub)) / 2, 350);
|
||||
g.dispose();
|
||||
return CorEncoder.encode(img);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
package com.eliasmueller.llamamod.mods.arena;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Strat scripts of the arena (script language 2), written against the source level t1l4m002
|
||||
* (Jungle by day 2), whose Alligator they reuse: model 47, animations 98–104, sounds 58–62 and the
|
||||
* EnemyCoin strat (unit 56 + 0xf0) that the original drops when it dies.
|
||||
*
|
||||
* <p>Combat facts from the original scripts: the llama's attacks set its {@code hitDamage}
|
||||
* (256 kick, 512 dive from a jump, 1024 charge); on contact each side's {@code hitDamage} is
|
||||
* copied into the other's {@code damageTaken} and both get a "touched" event. The original
|
||||
* Alligator has hitDamage 513 (= 512 | 1): the llama takes it as a successful hit if its own
|
||||
* hitDamage shares a bit (only the dive), otherwise bit 1 makes it lose a heart and bounce back.
|
||||
* ArenaGator uses 1793 (1 | 256 | 512 | 1024) so kick, dive and charge all count. The dive homes
|
||||
* in on the target that its strat writes into g17..g20 (the "aim" sub, ported from the original).
|
||||
*/
|
||||
final class ArenaScripts {
|
||||
static final String GATOR = "ArenaGator";
|
||||
static final String MASTER = "ArenaMaster";
|
||||
static final String PROBE = "ArenaProbe";
|
||||
|
||||
/** Variables of a spawned gator (spawn operand) and of the master (MAP prototype). */
|
||||
static final int GATOR_VARS = 16;
|
||||
|
||||
static final int MASTER_VARS = 12;
|
||||
|
||||
/**
|
||||
* Collision bones of the gator = last spawn operand / MAP "extra" (SpawnedAlligator: 2 bones and 2,
|
||||
* Alligator: 3 and 3). Too small corrupts the strat heap (page fault in FUN_0041ae20).
|
||||
*/
|
||||
static final int GATOR_BONES = 3;
|
||||
|
||||
/** Strat hitDamage bits of the llama's attacks: kick, dive, charge. */
|
||||
static final int ATTACK_BITS = 256 | 512 | 1024;
|
||||
|
||||
/** Most alligators alive at once (wave n has min(n, this)). */
|
||||
static final int MAX_AT_ONCE = 2;
|
||||
|
||||
/**
|
||||
* @param lives hits an alligator takes
|
||||
* @param waves number of waves (wave n: min(n, 2) alligators)
|
||||
* @param energy llama energy cap (5 = the game's normal)
|
||||
* @param coinsPerHit EnemyCoins popping out per hit (5 more when it is defeated)
|
||||
* @param door exit target (tribe, world, chapter), ×0x1000 = raw words
|
||||
*/
|
||||
record Settings(int lives, int waves, int energy, int coinsPerHit, int[] door) {
|
||||
Settings {
|
||||
if (lives < 1 || waves < 1 || energy < 1 || energy > 5 || coinsPerHit < 0 || door.length != 3) {
|
||||
throw new IllegalArgumentException("bad arena settings");
|
||||
}
|
||||
door = door.clone();
|
||||
}
|
||||
|
||||
static Settings defaults(int chapter) {
|
||||
return new Settings(5, 3, 4, 1, new int[] {1, 9, chapter});
|
||||
}
|
||||
}
|
||||
|
||||
/** Arena bounds and spots (game units). */
|
||||
record Layout(
|
||||
double x0,
|
||||
double z0,
|
||||
double x1,
|
||||
double z1,
|
||||
double floor,
|
||||
double exitX,
|
||||
double exitZ,
|
||||
List<double[]> spawns) {}
|
||||
|
||||
private ArenaScripts() {}
|
||||
|
||||
static int aliveInWave(int wave) {
|
||||
return Math.min(wave, MAX_AT_ONCE);
|
||||
}
|
||||
|
||||
static int totalAlligators(int waves) {
|
||||
int n = 0;
|
||||
for (int w = 1; w <= waves; w++) {
|
||||
n += aliveInWave(w);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static String num(double v) {
|
||||
String s = String.format(Locale.ROOT, "%.3f", v);
|
||||
return s.replaceAll("0+$", "").replaceAll("\\.$", ".0");
|
||||
}
|
||||
|
||||
/** One EnemyCoin, offset from the gator (forward, right) and back. */
|
||||
private static String coin(String ind, double fwd, double right) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
if (fwd != 0) {
|
||||
b.append(ind).append("moveForwardQ(").append(num(fwd)).append(")\n");
|
||||
}
|
||||
if (right != 0) {
|
||||
b.append(ind).append("moveRightQ(").append(num(right)).append(")\n");
|
||||
}
|
||||
b.append(ind).append("spawn<1, 3, 1, 0x28000, 0>(0, unit:56+0xf0)\n");
|
||||
if (right != 0) {
|
||||
b.append(ind).append("moveLeftQ(").append(num(right)).append(")\n");
|
||||
}
|
||||
if (fwd != 0) {
|
||||
b.append(ind).append("moveBackwardQ(").append(num(fwd)).append(")\n");
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
/** The alligator: N lives, chases and lunges, blinks and backs off when hit, drops coins. */
|
||||
static String gator(Settings s, Layout a) {
|
||||
double m = 0.6;
|
||||
StringBuilder hitCoins = new StringBuilder();
|
||||
double[][] offs = {{0.35, 0}, {0.2, 0.3}, {0.2, -0.3}, {0.45, 0.2}, {0.45, -0.2}};
|
||||
for (int i = 0; i < s.coinsPerHit(); i++) {
|
||||
double[] o = offs[i % offs.length];
|
||||
hitCoins.append(coin(" ", o[0], o[1]));
|
||||
}
|
||||
return """
|
||||
strat ArenaGator {
|
||||
alias lives = v0
|
||||
alias state = v1
|
||||
alias timer = v2
|
||||
alias speed = v3
|
||||
alias hit = v4
|
||||
alias aimH = v5
|
||||
alias aimD = v6
|
||||
alias cool = v7
|
||||
alias top = v8
|
||||
alias k = v9
|
||||
alias size = v10
|
||||
alias bit = v11
|
||||
alias rest = v12
|
||||
setModel(model:47)
|
||||
scale(1.5)
|
||||
self.hitDamage = %HITDAMAGE%
|
||||
collisionOn<16384>()
|
||||
collRadius(409r)
|
||||
collisionBone(0, 409r)
|
||||
collisionBone(6.0, 409r)
|
||||
collisionBone(10.0, 409r)
|
||||
on touched {
|
||||
if (self.damageTaken & %ATTACK%) {
|
||||
if (cool <= 0 && lives > 0) {
|
||||
hit = 1
|
||||
}
|
||||
} else {
|
||||
bit = 1
|
||||
}
|
||||
}
|
||||
emitterInit(0, 23r, 1.0, 0, 0, 0, 0)
|
||||
emitterSetColour(0, 0, 0, 0, packRgb(0, rnd(255.0), 0))
|
||||
emitterSetParams(0, 358r, 50.0, 0, 0)
|
||||
emitterInit(1.0, 23r, 1.0, 0, 0, 0, 0)
|
||||
emitterSetColour(1.0, 0, 0, 0, packRgb(0, rnd(255.0), 0))
|
||||
emitterSetParams(1.0, 0.075, 45.0, 0, 0)
|
||||
emitterInit(2.0, 23r, 1.0, 0, 0, 0, 0)
|
||||
emitterSetColour(2.0, 0, 0, 0, packRgb(0, rnd(255.0), 0))
|
||||
emitterSetParams(2.0, 256r, 40.0, 0, 0)
|
||||
lives = %LIVES%
|
||||
hit = 0
|
||||
bit = 0
|
||||
rest = 25
|
||||
cool = 0
|
||||
top = 0.03 + parent.v1 * 0.006
|
||||
self.y = %FLOOR%
|
||||
k = 0
|
||||
do {
|
||||
k = min(k + 2, 63)
|
||||
setAmbient(k, k, k)
|
||||
if (!self.animPlaying) {
|
||||
animPlay(anim:99)
|
||||
}
|
||||
call aim
|
||||
yield
|
||||
} while (k < 63)
|
||||
state = 0
|
||||
// the k-th alligator of a wave waits longer before its first attack
|
||||
timer = 10 + parent.v3 * 40
|
||||
loop {
|
||||
call aim
|
||||
if (hit) {
|
||||
hit = 0
|
||||
lives -= 1
|
||||
if (lives <= 0) {
|
||||
break
|
||||
}
|
||||
soundPlay(sound:62)
|
||||
savePos()
|
||||
call puff
|
||||
call puff
|
||||
call puff
|
||||
%HITCOINS%
|
||||
cool = 45
|
||||
state = 3
|
||||
timer = 10
|
||||
rest = 25
|
||||
setAnimSpeed(1.0)
|
||||
animPlay(anim:104)
|
||||
}
|
||||
if (bit) {
|
||||
// the llama was bitten: back off and rest a while
|
||||
bit = 0
|
||||
if (state != 3) {
|
||||
setAnimSpeed(1.0)
|
||||
animPlay(anim:104)
|
||||
state = 3
|
||||
timer = 12
|
||||
rest = 90
|
||||
}
|
||||
}
|
||||
if (cool > 0) {
|
||||
cool -= 1
|
||||
if (cool & 4) {
|
||||
self.alpha = 0.25
|
||||
setAmbient(255, 120, 120)
|
||||
} else {
|
||||
self.alpha = 1.0
|
||||
setAmbient(63, 63, 63)
|
||||
}
|
||||
}
|
||||
switch (state) {
|
||||
case 0:
|
||||
if (!self.animPlaying) {
|
||||
animPlay(anim:99)
|
||||
}
|
||||
turnTowardAngleY(self.angleToPlayer, 2deg)
|
||||
timer -= 1
|
||||
if (timer < 0 && self.distToPlayerSqXZ < 25 && abs(player.floorHeight - self.y) < 0.4) {
|
||||
soundPlay(sound:61)
|
||||
animPlay(anim:100)
|
||||
state = 1
|
||||
}
|
||||
break
|
||||
case 1:
|
||||
if (!self.animPlaying) {
|
||||
speed = 99r
|
||||
timer = 75
|
||||
state = 2
|
||||
}
|
||||
break
|
||||
case 2:
|
||||
if (!self.animPlaying) {
|
||||
animPlay(anim:98)
|
||||
}
|
||||
speed = min(speed + 6r, top)
|
||||
setAnimSpeed(speed * 0x1638e)
|
||||
turnTowardAngleY(self.angleToPlayer, 5.625deg)
|
||||
moveForwardQ(speed)
|
||||
timer -= 1
|
||||
if (self.distToPlayerSqXZ < 1.44) {
|
||||
soundPlay(sound:62)
|
||||
animPlay(anim:101)
|
||||
setAnimSpeed(1.0)
|
||||
speed = 243r
|
||||
state = 4
|
||||
} else if (timer < 0 || abs(player.floorHeight - self.y) > 0.4) {
|
||||
setAnimSpeed(1.0)
|
||||
animPlay(anim:104)
|
||||
timer = 30 + rnd(30)
|
||||
state = 0
|
||||
}
|
||||
break
|
||||
case 3:
|
||||
moveBackwardQ(0.07)
|
||||
timer -= 1
|
||||
if (timer < 0) {
|
||||
timer = rest
|
||||
state = 0
|
||||
}
|
||||
break
|
||||
case 4:
|
||||
if (!self.animPlaying) {
|
||||
animPlay(anim:102)
|
||||
}
|
||||
speed = max(speed - 2r, 175r)
|
||||
moveForwardQ(speed)
|
||||
if (speed <= 175r) {
|
||||
animPlay(anim:103)
|
||||
state = 5
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (!self.animPlaying) {
|
||||
animPlay(anim:98)
|
||||
}
|
||||
speed = max(speed - 4r, 99r)
|
||||
setAnimSpeed(speed * 0x1638e)
|
||||
moveForwardQ(speed)
|
||||
if (speed <= 99r) {
|
||||
setAnimSpeed(1.0)
|
||||
animPlay(anim:104)
|
||||
timer = 50 + rnd(30)
|
||||
state = 0
|
||||
}
|
||||
}
|
||||
self.x = clamp(%X0%, self.x, %X1%)
|
||||
self.z = clamp(%Z0%, self.z, %Z1%)
|
||||
self.y = %FLOOR%
|
||||
yield
|
||||
}
|
||||
collisionOff<16384>()
|
||||
soundPlay(sound:60)
|
||||
self.alpha = 1.0
|
||||
setAmbient(63, 63, 63)
|
||||
savePos()
|
||||
animStop()
|
||||
size = 1.5
|
||||
k = 0
|
||||
do {
|
||||
call shrink
|
||||
k += 1
|
||||
if (k == 1) {
|
||||
%DEATH1%
|
||||
} else if (k == 3) {
|
||||
%DEATH3%
|
||||
} else if (k == 5) {
|
||||
%DEATH5%
|
||||
} else if (k == 7) {
|
||||
%DEATH7%
|
||||
} else if (k == 9) {
|
||||
%DEATH9%
|
||||
}
|
||||
yield
|
||||
} while (size > 0)
|
||||
parent.v3 -= 1
|
||||
parent.v4 += 1
|
||||
remove
|
||||
}
|
||||
|
||||
// the llama's dive homes in on the nearest target below it (g17..g19, g20 = best distance)
|
||||
sub aim {
|
||||
if (g20 > 0) {
|
||||
aimH = player.y - (self.y + 0.15)
|
||||
if (aimH > 0 && aimH < 2) {
|
||||
aimD = distSqToPosXZ(player.x + ((sin(player.rotY) * aimH) >> 1), player.z + ((cos(player.rotY) * aimH) >> 1))
|
||||
aimH = aimH * aimH
|
||||
if (aimD < aimH) {
|
||||
aimD = aimD + aimH
|
||||
if (aimD < g20) {
|
||||
g20 = aimD
|
||||
g17 = self.x
|
||||
g18 = self.y + 0.15
|
||||
g19 = self.z
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub puff {
|
||||
moveRightQ(randSigned(0.15))
|
||||
moveForwardQ(rnd(0.7) - 0.4)
|
||||
emitParticlesVel(int(rnd(2.95)), 1.0, 0, 0, rnd(40r) + 20r, 0)
|
||||
resetPos()
|
||||
}
|
||||
|
||||
sub shrink {
|
||||
if (!self.animPlaying) {
|
||||
animPlay(anim:98)
|
||||
}
|
||||
call puff
|
||||
call puff
|
||||
call puff
|
||||
scaleX(3 - size)
|
||||
scaleZ(3 - size)
|
||||
scaleY(size)
|
||||
self.alpha = size * 2730r
|
||||
size = max(size - 409r, 0)
|
||||
}
|
||||
"""
|
||||
.replace("%HITDAMAGE%", Integer.toString(1 | ATTACK_BITS))
|
||||
.replace("%ATTACK%", Integer.toString(ATTACK_BITS))
|
||||
.replace("%LIVES%", Integer.toString(s.lives()))
|
||||
.replace("%FLOOR%", num(a.floor()))
|
||||
.replace("%X0%", num(a.x0() + m))
|
||||
.replace("%X1%", num(a.x1() - m))
|
||||
.replace("%Z0%", num(a.z0() + m))
|
||||
.replace("%Z1%", num(a.z1() - m))
|
||||
.replace("%HITCOINS%\n", hitCoins.toString())
|
||||
.replace("%DEATH1%\n", coin(" ", 0.32, 0))
|
||||
.replace("%DEATH3%\n", coin(" ", 0.16, 0))
|
||||
.replace("%DEATH5%\n", coin(" ", 0, 0))
|
||||
.replace("%DEATH7%\n", coin(" ", -0.16, 0))
|
||||
.replace("%DEATH9%\n", coin(" ", -0.32, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invisible referee at the exit: spawns the waves, keeps the llama's energy at the cap, and
|
||||
* after the last alligator opens the exit (LevelDoor logic: g101..g105 = target and cell,
|
||||
* endLevel when the llama stands on the exit floor in this cell) with bonus coins and sparkles.
|
||||
*/
|
||||
static String master(Settings s, Layout a) {
|
||||
return master(s, a, false);
|
||||
}
|
||||
|
||||
/** debug: the HUD coin counter shows kills·100 + wave·10 + alive. */
|
||||
static String master(Settings s, Layout a, boolean debug) {
|
||||
StringBuilder waves = new StringBuilder();
|
||||
for (int w = 1; w <= s.waves(); w++) {
|
||||
waves.append(" wave = ").append(w).append('\n');
|
||||
for (int i = 0; i < aliveInWave(w); i++) {
|
||||
double[] p = a.spawns().get(i % a.spawns().size());
|
||||
waves.append(" self.x = ").append(num(p[0])).append('\n');
|
||||
waves.append(" self.z = ").append(num(p[1])).append('\n');
|
||||
// the spawn copies the strat's matrix/translation block, updated only when the frame ends
|
||||
waves.append(" yield\n");
|
||||
waves.append(" spawn<0, ")
|
||||
.append(GATOR_VARS)
|
||||
.append(", 5, 50176, ")
|
||||
.append(GATOR_BONES)
|
||||
.append(">(unit:@")
|
||||
.append(GATOR)
|
||||
.append(")\n");
|
||||
waves.append(" alive += 1\n");
|
||||
}
|
||||
waves.append(" self.x = ").append(num(a.exitX())).append('\n');
|
||||
waves.append(" self.z = ").append(num(a.exitZ())).append('\n');
|
||||
waves.append(" while (alive > 0) {\n call cap\n yield\n }\n");
|
||||
waves.append(" t = 45\n while (t > 0) {\n t -= 1\n call cap\n yield\n }\n");
|
||||
}
|
||||
String cap = s.energy() >= 5
|
||||
? ""
|
||||
: " if (player.health2 > " + s.energy() + ") {\n player.health2 = " + s.energy()
|
||||
+ "\n }\n";
|
||||
int[] d = s.door();
|
||||
String door = (d[0] * 0x1000) + "r, " + (d[1] * 0x1000) + "r, " + (d[2] * 0x1000) + "r";
|
||||
return """
|
||||
strat ArenaMaster {
|
||||
alias wave = v1
|
||||
alias alive = v3
|
||||
alias kills = v4
|
||||
alias t = v5
|
||||
g104 = -1
|
||||
g105 = -1
|
||||
wave = 0
|
||||
alive = 0
|
||||
kills = 0
|
||||
t = 60
|
||||
while (t > 0) {
|
||||
t -= 1
|
||||
call cap
|
||||
yield
|
||||
}
|
||||
%WAVES%
|
||||
g101 = %D0%
|
||||
g102 = %D1%
|
||||
g103 = %D2%
|
||||
g104 = int(self.x)
|
||||
g105 = int(self.z)
|
||||
%BONUS%
|
||||
t = 0
|
||||
loop {
|
||||
call cap
|
||||
if (player.floorMaterial == 16 && int(self.x) == int(player.x) && int(self.z) == int(player.z)) {
|
||||
endLevel(%DOOR%)
|
||||
g1 += 1
|
||||
remove
|
||||
}
|
||||
t -= 1
|
||||
if (t < 0) {
|
||||
spawn<3, 6, 0, 0, 0>(10.0, 204r, 450r, unit:36+0x28)
|
||||
t = 8
|
||||
}
|
||||
yield
|
||||
}
|
||||
}
|
||||
|
||||
sub cap {
|
||||
%CAP%
|
||||
}
|
||||
"""
|
||||
.replace("%WAVES%\n", waves.toString())
|
||||
.replace("%BONUS%\n", coin(" ", 0, 0).repeat(5))
|
||||
.replace(
|
||||
"%CAP%\n",
|
||||
cap
|
||||
+ (debug
|
||||
? " self.levelRecord = kills * 100 + wave * 10 + alive\n"
|
||||
+ " self.hudTimer = 100\n"
|
||||
: ""))
|
||||
.replace("%D0%", (d[0] * 0x1000) + "r")
|
||||
.replace("%D1%", (d[1] * 0x1000) + "r")
|
||||
.replace("%D2%", (d[2] * 0x1000) + "r")
|
||||
.replace("%DOOR%", door);
|
||||
}
|
||||
|
||||
/** Event-handler probe: a big coin that bursts into three EnemyCoins when the llama touches it. */
|
||||
static String probe() {
|
||||
return """
|
||||
strat ArenaProbe {
|
||||
setModel(model:12)
|
||||
scale(3)
|
||||
collisionOn<0x20000>()
|
||||
collRadius(0.3)
|
||||
self.y = self.y + 0.3
|
||||
on touched {
|
||||
spawn<1, 3, 1, 0x28000, 0>(0, unit:56+0xf0)
|
||||
spawn<1, 3, 1, 0x28000, 0>(0, unit:56+0xf0)
|
||||
spawn<1, 3, 1, 0x28000, 0>(0, unit:56+0xf0)
|
||||
remove
|
||||
}
|
||||
loop {
|
||||
turnLeft(4deg)
|
||||
yield
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user