guards need their position inside a pair of waypoint boxes (else the game removes them): placing a guard gives it a box and event bit 0, check and inspector state with box around it button; exit and object checks use the engine's cell rule (tiles listed in the cell, swap models), tile cell from the map loader formula
This commit is contained in:
1 parent
a6708e5fff
commit
d9f1b8c2fe
15 files changed
+424
-40
No files matched your search
@@ -0,0 +1,72 @@
|
||||
package com.eliasmueller.llamamod.editor.tools.objects;
|
||||
|
||||
import com.eliasmueller.llamamod.editor.model.PathNode;
|
||||
import com.eliasmueller.llamamod.editor.model.Scene;
|
||||
import com.eliasmueller.llamamod.editor.model.Strat;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Guard family (CityGuard, CityGuardWithRubyIdol, CowGuard, GorillaGuard, …: one compiled template).
|
||||
* Strat code at start: the waypoints are walked in pairs; an odd count ("Uneven number of WPs") or a
|
||||
* start position outside every pair's x/z box whose first node has field unk2 = 0 ("Guard not in WP
|
||||
* box") removes the guard before it is ever drawn. Parameter 2 > 0 keeps it hidden until progress
|
||||
* bit p2 − 1 of global 95 is set; 0 = active as soon as the player exists.
|
||||
*/
|
||||
final class Guards {
|
||||
static final List<String> TYPES = List.of(
|
||||
"CityGuard",
|
||||
"CityGuardWithRubyIdol",
|
||||
"CowGuard",
|
||||
"CowGuardNoCoin",
|
||||
"GorillaGuard",
|
||||
"LizardGuard",
|
||||
"LizardGuardNoCoin",
|
||||
"WarthogGuard");
|
||||
|
||||
static final int EVENT_PARAM = 2;
|
||||
|
||||
/** Half size of the box a newly placed guard gets (cells). */
|
||||
static final double NEW_BOX_HALF = 2.0;
|
||||
|
||||
private Guards() {}
|
||||
|
||||
static boolean is(String type) {
|
||||
return type != null && TYPES.contains(type.endsWith("(spawn)") ? type.substring(0, type.length() - 7) : type);
|
||||
}
|
||||
|
||||
/** Index of the first node of the start box (pair), or −1 if the guard starts in none. */
|
||||
static int startBox(List<PathNode> chain, Strat st) {
|
||||
for (int k = 0; k + 1 < chain.size(); k += 2) {
|
||||
PathNode a = chain.get(k), b = chain.get(k + 1);
|
||||
if (a.unk2() == 0
|
||||
&& st.x() >= Math.min(a.x(), b.x())
|
||||
&& st.x() <= Math.max(a.x(), b.x())
|
||||
&& st.z() >= Math.min(a.z(), b.z())
|
||||
&& st.z() <= Math.max(a.z(), b.z())) {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Event bit (0 = none) of a guard. */
|
||||
static int eventBit(Strat st) {
|
||||
int[] p = st.params();
|
||||
return p.length > EVENT_PARAM ? p[EVENT_PARAM] >> 12 : 0;
|
||||
}
|
||||
|
||||
/** Why the game removes the guard at level start, or null if it stays. */
|
||||
static String removedBecause(Scene s, Strat st) {
|
||||
List<PathNode> chain = st.waypoint() != null && s.contains(st.waypoint()) ? s.chain(st.waypoint()) : List.of();
|
||||
if (chain.isEmpty()) {
|
||||
return "it has no waypoint boxes";
|
||||
}
|
||||
if (chain.size() % 2 == 1) {
|
||||
return "its waypoint count is odd (" + chain.size() + "; boxes are node pairs)";
|
||||
}
|
||||
if (startBox(chain, st) < 0) {
|
||||
return "it does not start inside one of its waypoint boxes";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import com.eliasmueller.llamamod.editor.model.PathNode;
|
||||
import com.eliasmueller.llamamod.editor.model.Scene;
|
||||
import com.eliasmueller.llamamod.editor.model.Strat;
|
||||
import com.eliasmueller.llamamod.editor.model.StratCatalog;
|
||||
import com.eliasmueller.llamamod.editor.model.Tile;
|
||||
import com.eliasmueller.llamamod.editor.tools.objects.ParamSchema.ParamDef;
|
||||
import com.eliasmueller.llamamod.editor.tools.objects.ParamSchema.TypeSchema;
|
||||
import com.eliasmueller.llamamod.editor.ui.LevelCheck;
|
||||
import com.eliasmueller.llamamod.editor.ui.Problem;
|
||||
import com.eliasmueller.llamamod.save.Worlds;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -30,7 +32,7 @@ final class ObjectChecks implements LevelCheck {
|
||||
+ " i+1): run Tools → Repair waypoint chains",
|
||||
broken));
|
||||
}
|
||||
Ground g = null;
|
||||
List<Tile> doorTiles = null;
|
||||
int players = 0;
|
||||
for (Strat st : s.strats()) {
|
||||
String type = st.type();
|
||||
@@ -55,7 +57,21 @@ final class ObjectChecks implements LevelCheck {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sc.waypoints().required() && chain.size() < sc.waypoints().min()) {
|
||||
String gone = Guards.is(type) ? Guards.removedBecause(s, st) : null;
|
||||
if (gone != null) {
|
||||
out.add(Problem.error(
|
||||
label(st) + " never appears: the game removes it at level start because " + gone
|
||||
+ " (Waypoint tool: give it a box of 2 nodes around its position)",
|
||||
st));
|
||||
} else if (Guards.is(type) && Guards.eventBit(st) > 0) {
|
||||
out.add(Problem.info(
|
||||
label(st) + " stays hidden until event bit " + Guards.eventBit(st)
|
||||
+ " is set by another object; set Event bit to 0 to have it active from the start",
|
||||
st));
|
||||
}
|
||||
if (!Guards.is(type)
|
||||
&& sc.waypoints().required()
|
||||
&& chain.size() < sc.waypoints().min()) {
|
||||
out.add(Problem.warning(
|
||||
label(st) + " needs " + sc.waypoints().min() + " waypoint(s) ("
|
||||
+ sc.waypoints().role().label + "), has " + chain.size(),
|
||||
@@ -67,7 +83,7 @@ final class ObjectChecks implements LevelCheck {
|
||||
+ chain.size(),
|
||||
st));
|
||||
}
|
||||
if (sc.waypoints().pairs() && chain.size() % 2 == 1) {
|
||||
if (sc.waypoints().pairs() && chain.size() % 2 == 1 && !Guards.is(type)) {
|
||||
out.add(Problem.warning(
|
||||
label(st) + ": box nodes come in pairs, the chain has " + chain.size(), st));
|
||||
}
|
||||
@@ -84,14 +100,15 @@ final class ObjectChecks implements LevelCheck {
|
||||
}
|
||||
}
|
||||
if (type.equals("LevelDoor")) {
|
||||
if (g == null) {
|
||||
g = Ground.build(
|
||||
s, s.brushes().isEmpty() ? null : BrushCompiler.compile(s, new java.util.ArrayList<>()));
|
||||
if (doorTiles == null) {
|
||||
doorTiles = new ArrayList<>(s.tiles());
|
||||
if (!s.brushes().isEmpty()) {
|
||||
doorTiles.addAll(
|
||||
BrushCompiler.compile(s, new ArrayList<>()).tiles());
|
||||
}
|
||||
}
|
||||
int cx = (int) Math.floor(st.gx()), cz = (int) Math.floor(st.gz());
|
||||
if (!g.hasMaterial(cx, cz, 16)
|
||||
&& s.tiles().stream()
|
||||
.noneMatch(t -> t.cellX() == cx && t.cellZ() == cz && t.swapHasMaterial(16))) {
|
||||
if (!cellHasMaterial(doorTiles, cx, cz, 16)) {
|
||||
out.add(Problem.warning(
|
||||
label(st) + " stands in cell " + cx + ", " + cz + " without exit floor (collision material"
|
||||
+ " 16): the door never fires",
|
||||
@@ -126,6 +143,22 @@ final class ObjectChecks implements LevelCheck {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The engine's floor test walks only the tiles listed in the player's grid cell (MAP cell lists,
|
||||
* cell = floor of the tile origin), at any height; a door's exit floor may also be in the swap
|
||||
* model a door object switches the tile to (village 1: RubyIdolDoor).
|
||||
*/
|
||||
static boolean cellHasMaterial(List<Tile> tiles, int cx, int cz, int material) {
|
||||
for (Tile t : tiles) {
|
||||
if (t.cellX() == cx
|
||||
&& t.cellZ() == cz
|
||||
&& (t.model() != null && t.model().hasCollisionMaterial(material) || t.swapHasMaterial(material))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static String label(Strat s) {
|
||||
return s.type() + " (" + s.id() + ")";
|
||||
}
|
||||
|
||||
@@ -238,9 +238,7 @@ public final class PlaceTool implements Tool {
|
||||
yield null;
|
||||
}
|
||||
Double yaw = yawMode() == 3 ? null : yaw();
|
||||
Command c = Edits.placeStrat(s, ses.type(), ses.variant(), x, y, z, yaw);
|
||||
Strat st = Setups.lastStrat(c);
|
||||
yield new Setups.Made(c, st);
|
||||
yield Setups.object(s, ses.type(), ses.variant(), x, y, z, yaw);
|
||||
}
|
||||
};
|
||||
if (m == null) {
|
||||
|
||||
@@ -336,13 +336,33 @@ public final class Schemas {
|
||||
.add();
|
||||
|
||||
// ---------------------------------------------------------- characters & enemies
|
||||
t("CityGuard", "Guard patrolling its waypoints; chases the player when seen.")
|
||||
.p("View range", Kind.RADIUS, 0, 50, "distance at which it sees the player")
|
||||
.p("Chase range", Kind.RADIUS, 0, 50, "distance it chases the player (≥ view range)")
|
||||
.p("Event bit", Kind.EVENT_BIT, 0, 32, "")
|
||||
String guardEvent = "0 = active from the start (all village guards). n > 0: hidden and inactive until"
|
||||
+ " progress bit n (global 95) is set by another object with that event number (a talking"
|
||||
+ " character, smash statue, lab door pad …)";
|
||||
String guardBoxes = "walking areas: boxes of node pairs (x/z). The guard must start inside a box,"
|
||||
+ " otherwise the game removes it at level start; overlapping boxes let it walk on";
|
||||
t(
|
||||
"CityGuard",
|
||||
"Guard walking around inside its waypoint boxes; chases the player within the boxes when"
|
||||
+ " it sees it.")
|
||||
.p("View range", Kind.RADIUS, 0, 50, "distance at which it sees the player (in front of it)")
|
||||
.p("Chase range", Kind.RADIUS, 0, 50, "distance at which it gives up (view range is capped to it)")
|
||||
.p("Event bit", Kind.EVENT_BIT, 0, 32, guardEvent)
|
||||
.p("Wall collision", Kind.BOOL, "strat flag 0x40000")
|
||||
.wp(Role.PATROL, 1, 0, "patrol path (wraps to the first node)")
|
||||
.wpPairs(Role.BOX, 2, guardBoxes)
|
||||
.conf("high")
|
||||
.add();
|
||||
for (String g : Guards.TYPES) {
|
||||
if (!g.equals("CityGuard")) {
|
||||
t(g, "Guard of the CityGuard family: walks inside its waypoint boxes, chases the player.")
|
||||
.p("View range", Kind.RADIUS, 0, 50, "distance at which it sees the player")
|
||||
.p("Chase range", Kind.RADIUS, 0, 50, "distance at which it gives up")
|
||||
.p("Event bit", Kind.EVENT_BIT, 0, 32, guardEvent)
|
||||
.wpPairs(Role.BOX, 2, guardBoxes)
|
||||
.conf(g.equals("CityGuardWithRubyIdol") ? "high" : "medium")
|
||||
.add();
|
||||
}
|
||||
}
|
||||
t("JumpingBeetleBlue", "Jumping beetle following its path.")
|
||||
.p("Jump time", Kind.FIXED, "")
|
||||
.p("p1", Kind.RAW, "unused")
|
||||
|
||||
@@ -61,13 +61,74 @@ public final class Setups {
|
||||
p.setRaw("z", Edits.fixed(z));
|
||||
}
|
||||
|
||||
/** Places an object; params with schema kind BOOL named "Snap to floor" are switched on. */
|
||||
/**
|
||||
* Places an object (yaw in degrees). Guards ({@link Guards}) get one box around them instead of the prototype's
|
||||
* boxes and event bit 0, so they are active from the start.
|
||||
*/
|
||||
public static Made object(Scene s, String type, int variant, double x, double y, double z, double yaw) {
|
||||
return object(s, type, variant, x, y, z, Double.valueOf(yaw));
|
||||
}
|
||||
|
||||
/** As above; yaw null = the prototype's yaw. */
|
||||
public static Made object(Scene s, String type, int variant, double x, double y, double z, Double yaw) {
|
||||
Command c = Edits.placeStrat(s, type, variant, x, y, z, yaw);
|
||||
Strat st = lastStrat(c);
|
||||
if (Guards.is(type)) {
|
||||
c = guardSetup(s, (Batch) c, st, x, y, z);
|
||||
}
|
||||
return new Made(c, st);
|
||||
}
|
||||
|
||||
private static Command guardSetup(Scene s, Batch placed, Strat st, double x, double y, double z) {
|
||||
double h = Guards.NEW_BOX_HALF;
|
||||
List<PathNode> nodes = addedNodes(placed);
|
||||
List<Command> cmds = new ArrayList<>();
|
||||
if (nodes.size() >= 2) {
|
||||
for (Command k : placed.commands()) {
|
||||
if (!(k instanceof AddNode a && a.node() instanceof PathNode p && nodes.indexOf(p) >= 2)) {
|
||||
cmds.add(k);
|
||||
}
|
||||
}
|
||||
pos(nodes.get(0), x - h, y, z - h);
|
||||
pos(nodes.get(1), x + h, y, z + h);
|
||||
nodes.get(0).setRaw("unk2", 0);
|
||||
nodes.get(1).setRaw("unk2", 0);
|
||||
nodes.get(1).setRaw("next", null);
|
||||
} else {
|
||||
cmds.addAll(placed.commands());
|
||||
cmds.addAll(ChainOps.newChain(
|
||||
s,
|
||||
st,
|
||||
List.of(
|
||||
ChainOps.newNode(s, x - h, y, z - h, 0, null),
|
||||
ChainOps.newNode(s, x + h, y, z + h, 0, null))));
|
||||
}
|
||||
int[] p = st.params().clone();
|
||||
if (p.length > Guards.EVENT_PARAM) {
|
||||
p[Guards.EVENT_PARAM] = 0;
|
||||
st.setRaw("params", p);
|
||||
}
|
||||
return new Batch(placed.label(), cmds);
|
||||
}
|
||||
|
||||
/** Replaces a guard's waypoints with one box of 2 nodes around it (it then starts inside). */
|
||||
public static Command guardBox(Scene s, Strat st) {
|
||||
double h = Guards.NEW_BOX_HALF;
|
||||
List<Command> cmds = new ArrayList<>();
|
||||
if (st.waypoint() != null
|
||||
&& s.contains(st.waypoint())
|
||||
&& ChainOps.owners(s, st.waypoint()).size() == 1) {
|
||||
cmds.addAll(ChainOps.removeChain(s, st));
|
||||
}
|
||||
cmds.addAll(ChainOps.newChain(
|
||||
s,
|
||||
st,
|
||||
List.of(
|
||||
ChainOps.newNode(s, st.gx() - h, st.gy(), st.gz() - h, 0, null),
|
||||
ChainOps.newNode(s, st.gx() + h, st.gy(), st.gz() + h, 0, null))));
|
||||
return new Batch("Guard box", cmds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Player start: moves the existing player object if there is one (a level has exactly one),
|
||||
* else places a Llama with its chain (first node = camera 8 cells behind, rest on the llama).
|
||||
|
||||
Reference in new issue
Block a user