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
@@ -304,18 +304,25 @@ The editor knows the parameters of these types (the rest show raw values):
|
|||||||
| **ForcePlayerMovement**, **WaterReboundPoints**, **ObjectDontDropInBox**, **FallOffNoHurt**, **RiverCurrentBox** | see [Force and water objects](11-cameras-triggers.md#force-and-water-objects) | points / box |
|
| **ForcePlayerMovement**, **WaterReboundPoints**, **ObjectDontDropInBox**, **FallOffNoHurt**, **RiverCurrentBox** | see [Force and water objects](11-cameras-triggers.md#force-and-water-objects) | points / box |
|
||||||
| **RubyIdolDoor** | Idols needed, Offset (sign distance) | – |
|
| **RubyIdolDoor** | Idols needed, Offset (sign distance) | – |
|
||||||
| **PressurePadDoor** | Pad flags (bit field), Door z, Camera, Camera hold | – |
|
| **PressurePadDoor** | Pad flags (bit field), Door z, Camera, Camera hold | – |
|
||||||
| **CityGuard** | View range, Chase range, Event bit, Wall collision | Patrol path (wraps to the first node) |
|
| **CityGuard** (and CityGuardWithRubyIdol, CowGuard, GorillaGuard, LizardGuard, WarthogGuard) | View range, Chase range, Event bit (0 = active at once), Wall collision (CityGuard only) | Walking boxes: node pairs; the guard must **start inside one box** |
|
||||||
| **JumpingBeetleBlue**, **JumpingBeetle** | Jump time, Wait, Jump height, Pause | Jump targets |
|
| **JumpingBeetleBlue**, **JumpingBeetle** | Jump time, Wait, Jump height, Pause | Jump targets |
|
||||||
| **Spider** | Range, Speed, Down time, Up time, Start down | Path |
|
| **Spider** | Range, Speed, Down time, Up time, Start down | Path |
|
||||||
| **Bunny** | Flee range | – |
|
| **Bunny** | Flee range | – |
|
||||||
|
|
||||||
|
> **Guards:** a guard walks around inside the boxes of its waypoint pairs and chases the llama
|
||||||
|
> only inside them. If it does not start inside one of its boxes, or the node count is odd, the
|
||||||
|
> game removes it when the level starts – it never appears. Placing a guard gives it one 4×4 box
|
||||||
|
> around itself and Event bit 0; the Inspector shows a warning and a **Box around it** button
|
||||||
|
> when the boxes are wrong. An **Event bit** above 0 keeps the guard hidden until another object
|
||||||
|
> with that event number (a talking character, a smash statue, a door pad …) sets the bit.
|
||||||
|
|
||||||
> **Tip:** For pickups, leave **Snap to floor** on: the game then drops the coin onto the floor
|
> **Tip:** For pickups, leave **Snap to floor** on: the game then drops the coin onto the floor
|
||||||
> when the level starts, so a coin placed slightly too high still ends up in the right place.
|
> when the level starts, so a coin placed slightly too high still ends up in the right place.
|
||||||
|
|
||||||
## Duplicating, copying and pasting
|
## Duplicating, copying and pasting
|
||||||
|
|
||||||
* **Edit → Duplicate** (**Ctrl+D**) copies the selection one cell east. Objects keep a copy of
|
* **Edit → Duplicate** (**Ctrl+D**) copies the selection one cell east. Objects keep a copy of
|
||||||
their waypoint chain, so a duplicated guard gets its own patrol path.
|
their waypoint chain, so a duplicated guard gets its own walking boxes.
|
||||||
* **Edit → Copy** / **Paste** (**Ctrl+C** / **Ctrl+V**) work inside the open level.
|
* **Edit → Copy** / **Paste** (**Ctrl+C** / **Ctrl+V**) work inside the open level.
|
||||||
* **Delete** removes objects; waypoint chains used only by the deleted objects are removed too.
|
* **Delete** removes objects; waypoint chains used only by the deleted objects are removed too.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Waypoints and paths
|
# Waypoints and paths
|
||||||
|
|
||||||
Many objects need positions besides their own: a guard walks along a patrol path, a camera zone
|
Many objects need positions besides their own: a guard walks inside its boxes, a beetle jumps along a patrol path, a camera zone
|
||||||
covers a box, a checkpoint stores where the llama respawns, a cut scene flies its camera along a
|
covers a box, a checkpoint stores where the llama respawns, a cut scene flies its camera along a
|
||||||
route. The game stores all of these as **waypoints** – points linked into **chains**. This
|
route. The game stores all of these as **waypoints** – points linked into **chains**. This
|
||||||
chapter explains how chains work and how to edit them with the **Waypoint paths** tool.
|
chapter explains how chains work and how to edit them with the **Waypoint paths** tool.
|
||||||
@@ -27,10 +27,10 @@ What the nodes mean depends on the object type. The Inspector names the role in
|
|||||||
|
|
||||||
| Role | Used by | Meaning of the nodes |
|
| Role | Used by | Meaning of the nodes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **patrol path** | CityGuard, JumpingBeetleBlue, Spider … | Points the character walks or jumps to; patrols wrap from the last node back to the first |
|
| **patrol path** | JumpingBeetleBlue, Spider … | Points the character walks or jumps to; patrols wrap from the last node back to the first |
|
||||||
| **camera positions** | Llama (first node), cut-scene cameras, CutSceneTrigger | Camera position; node yaw and pitch give the view direction |
|
| **camera positions** | Llama (first node), cut-scene cameras, CutSceneTrigger | Camera position; node yaw and pitch give the view direction |
|
||||||
| **respawn point** | CheckPointPost | Where the llama reappears, and which way it faces |
|
| **respawn point** | CheckPointPost | Where the llama reappears, and which way it faces |
|
||||||
| **box area (two opposite corners)** | Secret, CameraSide, CameraLift, ObjectDontDropInBox, FallOffNoHurt … | Two nodes span a rectangle on the ground (x and z only); some types use several pairs |
|
| **box area (two opposite corners)** | Secret, CameraSide, CameraLift, ObjectDontDropInBox, FallOffNoHurt, guards … | Two nodes span a rectangle on the ground (x and z only); some types use several pairs (guards walk inside their boxes and must start inside one) |
|
||||||
| **point list** | ForcePlayerMovement, WaterReboundPoints | Target points |
|
| **point list** | ForcePlayerMovement, WaterReboundPoints | Target points |
|
||||||
| **waypoint chain** | other types | Meaning unknown – keep the original layout |
|
| **waypoint chain** | other types | Meaning unknown – keep the original layout |
|
||||||
|
|
||||||
@@ -162,5 +162,7 @@ The Problems panel reports:
|
|||||||
| … needs n waypoint(s) (…), has m | The type reads more nodes than the chain has |
|
| … needs n waypoint(s) (…), has m | The type reads more nodes than the chain has |
|
||||||
| … uses only n waypoint(s), the chain has m (info) | Extra nodes are ignored by the game |
|
| … uses only n waypoint(s), the chain has m (info) | Extra nodes are ignored by the game |
|
||||||
| … box nodes come in pairs | Box zones need an even number of nodes |
|
| … box nodes come in pairs | Box zones need an even number of nodes |
|
||||||
|
| … never appears: the game removes it at level start (error) | A guard without boxes, with an odd node count or starting outside all of its boxes; use **Box around it** in the Inspector |
|
||||||
|
| … stays hidden until event bit n is set (info) | A guard waiting for an event; set Event bit to 0 to have it active at once |
|
||||||
| … has no waypoints; all original … objects have a chain | Most likely the type needs one |
|
| … has no waypoints; all original … objects have a chain | Most likely the type needs one |
|
||||||
| Respawn node is n cells away (info) | A checkpoint's respawn point far from the post |
|
| Respawn node is n cells away (info) | A checkpoint's respawn point far from the post |
|
||||||
@@ -76,10 +76,31 @@ values only.
|
|||||||
| ObjectDontDropInBox, FallOffNoHurt | box mode (+ game state) | box |
|
| ObjectDontDropInBox, FallOffNoHurt | box mode (+ game state) | box |
|
||||||
| WaterReboundPoints | active | rebound points |
|
| WaterReboundPoints | active | rebound points |
|
||||||
| RubyIdolDoor | p1 idols needed, p4 sign offset | – |
|
| RubyIdolDoor | p1 idols needed, p4 sign offset | – |
|
||||||
| CityGuard | p0 view range², p1 chase range², p2 event bit, p3 wall collision | patrol (wraps) |
|
| CityGuard family (high) | p0 view range², p1 chase range² (view capped to it), p2 event bit (0 = active at once), p3 wall collision (CityGuard only) | **boxes** (node pairs), must start inside one – see below |
|
||||||
| JumpingBeetleBlue, Spider, Bunny | times in seconds (×30), ranges² | patrol path |
|
| JumpingBeetleBlue, Spider, Bunny | times in seconds (×30), ranges² | patrol path |
|
||||||
|
|
||||||
|
### Guards (CityGuard, CityGuardWithRubyIdol, CowGuard(NoCoin), GorillaGuard, LizardGuard(NoCoin), WarthogGuard)
|
||||||
|
|
||||||
|
One compiled template (same code in t1l1m002 unit 20 and the others). Start of the script:
|
||||||
|
1. `if (p2 > 0) { hide; wait until g95 & (1 << (p2−1)); unhide } else wait until playerPtr ≠ 0` –
|
||||||
|
p2 is an **event bit**: bits of global 95 are set by talking characters (TipoVillage,
|
||||||
|
ChacaVillage), smash statues, LabBigCutscene, LabSlidingDoorPad … with the same number. All
|
||||||
|
village guards have p2 = 0; 4 lab guards wait for an event.
|
||||||
|
2. Waypoint scan: `wpFirst`, then pairs (`wpNext` twice): odd count → debug "Uneven number of
|
||||||
|
WPs" + **remove**; the start box = first pair whose x/z box (`inWaypointBoxXZ`, op 0x178,
|
||||||
|
inclusive, fixed point) contains the guard and whose first node has field +0x28 (`wpParam`,
|
||||||
|
editor `unk2`) = 0; none → "Guard not in WP box" + **remove**. So the waypoints are **walking
|
||||||
|
areas**, not a patrol path: the guard wanders (random turns) inside the current box; when it
|
||||||
|
leaves it, `waypointStep(2)` looks for another box containing it (boxes with `wpParam ≠ 0` only
|
||||||
|
from another such box), else it turns back (`clampToWaypointBox`). Chasing stays clamped to
|
||||||
|
the boxes; it gives up beyond p1 or when the player leaves.
|
||||||
|
All 41 original guards satisfy this (unit test `GuardsTest`). The editor: placing a guard gives
|
||||||
|
it one 4×4 box and p2 = 0; `ObjectChecks` reports an error when the game would remove it and an
|
||||||
|
info for p2 > 0; the Inspector shows the state plus a **Box around it** button (`Guards`,
|
||||||
|
`Setups.guardBox`). Verified in game: CityGuard with its box on a new level (base village 2)
|
||||||
|
appears and attacks the llama; a second one with a 4-node "square path" next to it never appears.
|
||||||
|
|
||||||
The rest (~150 placed types) shows raw values with the original statistics. Next candidates:
|
The rest (~150 placed types) shows raw values with the original statistics. Next candidates:
|
||||||
PressurePadDoor (bit field), TargetRopePlatform, CataSwingingBlade (speeds/angles), RiverLog.
|
Alligator (also box pairs: "Forcing alligator inside first WP box"), PressurePadDoor (bit field), TargetRopePlatform, CataSwingingBlade (speeds/angles), RiverLog.
|
||||||
Tooling used (not in the repo): Ghidra script decompiling the handler table, a Python sweep
|
Tooling used (not in the repo): Ghidra script decompiling the handler table, a Python sweep
|
||||||
disassembler with an op-name table, "parameter use" context dumps per type.
|
disassembler with an op-name table, "parameter use" context dumps per type.
|
||||||
@@ -12,7 +12,7 @@ screenshot tour `ObjectsTour` (test classes).
|
|||||||
|---|---|
|
|---|---|
|
||||||
| **Objects panel** (left dock) | every placeable type of the base with rendered thumbnail, category filter, search (name + schema summary), favourites (right-click, ★ filter), "parameters decoded" marker; drag into the viewport or click → Place tool. Quick setups: Player start, Checkpoint, Exit, Secret, Coin line, Coin arc |
|
| **Objects panel** (left dock) | every placeable type of the base with rendered thumbnail, category filter, search (name + schema summary), favourites (right-click, ★ filter), "parameters decoded" marker; drag into the viewport or click → Place tool. Quick setups: Player start, Checkpoint, Exit, Secret, Coin line, Coin arc |
|
||||||
| **Place tool** (P) | click = place on the collision floor under the cursor (snap to floor, grid snap), yaw = camera direction / facing the camera / 0° / type default; click + drag an object = move along the ground (its waypoints move along); drag the yellow knob = rotate (angle snap; a checkpoint's respawn node turns with it). Modes for the setups |
|
| **Place tool** (P) | click = place on the collision floor under the cursor (snap to floor, grid snap), yaw = camera direction / facing the camera / 0° / type default; click + drag an object = move along the ground (its waypoints move along); drag the yellow knob = rotate (angle snap; a checkpoint's respawn node turns with it). Modes for the setups |
|
||||||
| Setups | **Player start** moves the existing player (exactly one) or places a Llama with its chain (camera node 8 cells behind). **Checkpoint** = CheckPointPost + respawn node 0.4 ahead (floor height). **Exit** = LevelDoor in the cell centre + 1×1 FLOOR brush with collision material 16; target picked in the options bar (world/chapter names, "world finished 7,7,7"). **Secret** = Secret with a 3×3 box (2 nodes) |
|
| Setups | **Player start** moves the existing player (exactly one) or places a Llama with its chain (camera node 8 cells behind). **Checkpoint** = CheckPointPost + respawn node 0.4 ahead (floor height). **Exit** = LevelDoor in the cell centre + 1×1 FLOOR brush with collision material 16; target picked in the options bar (world/chapter names, "world finished 7,7,7"). **Secret** = Secret with a 3×3 box (2 nodes). Guards (any placement) get one 4×4 box and event bit 0 |
|
||||||
| **Coin tool** (O) | Line: two clicks, coins every *spacing*; Arc: centre + point on the circle, *count* coins over *arc°*; Coin/Coin5, height above floor, follow floor; live preview |
|
| **Coin tool** (O) | Line: two clicks, coins every *spacing*; Arc: centre + point on the circle, *count* coins over *arc°*; Coin/Coin5, height above floor, follow floor; live preview |
|
||||||
| **Waypoint tool** (N) | select an object (or node): click = add node after the selected node / at the chain end (creates the chain if missing), Ctrl+click = insert before, drag = move (keeps height above floor), Backspace/Del = remove and relink; Reverse; orient new nodes along the path |
|
| **Waypoint tool** (N) | select an object (or node): click = add node after the selected node / at the chain end (creates the chain if missing), Ctrl+click = insert before, drag = move (keeps height above floor), Backspace/Del = remove and relink; Reverse; orient new nodes along the path |
|
||||||
| **Zones & triggers** (J) | handles: 4 corners of box zones (node pairs: Secret, CameraSide/Lift, ObjectDontDropInBox, FallOffNoHurt …), radius knob for RADIUS parameters (CutSceneTrigger, guards, CameraFocusPoint) |
|
| **Zones & triggers** (J) | handles: 4 corners of box zones (node pairs: Secret, CameraSide/Lift, ObjectDontDropInBox, FallOffNoHurt …), radius knob for RADIUS parameters (CutSceneTrigger, guards, CameraFocusPoint) |
|
||||||
@@ -20,7 +20,7 @@ screenshot tour `ObjectsTour` (test classes).
|
|||||||
| Inspector "Waypoint chain" | node i of n, owner and role, ◀ ▶, select object, delete node, look through |
|
| Inspector "Waypoint chain" | node i of n, owner and role, ◀ ▶, select object, delete node, look through |
|
||||||
| Viewport layers | *Path directions* (arrows, dashed wrap segment for patrol paths, node yaw ticks), *Zone areas & trigger radii* (boxes for all zone objects, fills for the selection), *Camera frusta* (CameraSide/Lift: computed game camera from height/distance/node yaw; cut-scene cameras: a frustum per node from node yaw/pitch) |
|
| Viewport layers | *Path directions* (arrows, dashed wrap segment for patrol paths, node yaw ticks), *Zone areas & trigger radii* (boxes for all zone objects, fills for the selection), *Camera frusta* (CameraSide/Lift: computed game camera from height/distance/node yaw; cut-scene cameras: a frustum per node from node yaw/pitch) |
|
||||||
| Look through camera (Ctrl+Shift+L, View → Camera) | viewport takes the camera of the selected node / cut-scene camera / camera zone; while active, flying writes back node position + yaw/pitch, or for CameraSide the height/distance params and node yaw (one undo step per pilot session) |
|
| Look through camera (Ctrl+Shift+L, View → Camera) | viewport takes the camera of the selected node / cut-scene camera / camera zone; while active, flying writes back node position + yaw/pitch, or for CameraSide the height/distance params and node yaw (one undo step per pilot session) |
|
||||||
| Problems (Level → Check, Build) | chains not stored contiguously (error, Tools → Repair waypoint chains), param count, out-of-range values, missing required waypoints / odd box pairs, types whose originals always have a chain, LevelDoor without material-16 floor in its cell, impossible exit targets and (7,7,7) in TestLevel, far respawn nodes, several player starts |
|
| Problems (Level → Check, Build) | guards the game would remove (no start box / odd node count) and guards waiting for an event bit, chains not stored contiguously (error, Tools → Repair waypoint chains), param count, out-of-range values, missing required waypoints / odd box pairs, types whose originals always have a chain, LevelDoor without material-16 floor in its cell, impossible exit targets and (7,7,7) in TestLevel, far respawn nodes, several player starts |
|
||||||
| Copy / paste between levels | Edit → Copy objects (Ctrl+Shift+C) puts JSON (objects + chains + brushes, relative to their centre) on the system clipboard; Paste objects (Ctrl+Shift+V) at the view centre in any level – types are matched by name/variant in the target base (unknown types skipped with a message) |
|
| Copy / paste between levels | Edit → Copy objects (Ctrl+Shift+C) puts JSON (objects + chains + brushes, relative to their centre) on the system clipboard; Paste objects (Ctrl+Shift+V) at the view centre in any level – types are matched by name/variant in the target base (unknown types skipped with a message) |
|
||||||
| Prefabs | Tools → Save selection as prefab… / asset browser tab "Prefabs": `<data>/editor/prefabs/<name>.json` + thumbnail; double-click = place at view centre, drag = place under the cursor |
|
| Prefabs | Tools → Save selection as prefab… / asset browser tab "Prefabs": `<data>/editor/prefabs/<name>.json` + thumbnail; double-click = place at view centre, drag = place under the cursor |
|
||||||
| Delete | `edit.delete` is re-registered: a selection of waypoints is removed with the chain relinked |
|
| Delete | `edit.delete` is re-registered: a selection of waypoints is removed with the chain relinked |
|
||||||
@@ -50,7 +50,7 @@ UI (Xvfb, `ObjectsTour`): `img/editor/obj_camera_zone.jpg`, `obj_cutscene_frusta
|
|||||||
|
|
||||||
In game (apptest, display :7, new level on base t1l1m002 as TestLevel chapter 2; island of brushes,
|
In game (apptest, display :7, new level on base t1l1m002 as TestLevel chapter 2; island of brushes,
|
||||||
coin line + coin arc, checkpoint, exit to Village chapter 2, CameraSide zone at the exit,
|
coin line + coin arc, checkpoint, exit to Village chapter 2, CameraSide zone at the exit,
|
||||||
JumpingBeetleBlue with a 4-node patrol path, CityGuard with a square path):
|
JumpingBeetleBlue with a 4-node patrol path, CityGuard with two boxes):
|
||||||
|
|
||||||
* level loads, coins of line and arc stand where placed (`obj_game_coins.jpg`);
|
* level loads, coins of line and arc stand where placed (`obj_game_coins.jpg`);
|
||||||
* walking through the checkpoint shows "KONTROLLPUNKT"; falling into the sea respawns the llama at
|
* walking through the checkpoint shows "KONTROLLPUNKT"; falling into the sea respawns the llama at
|
||||||
@@ -58,18 +58,27 @@ JumpingBeetleBlue with a 4-node patrol path, CityGuard with a square path):
|
|||||||
* entering the camera zone switches to the zone camera (high view, `obj_game_camera_zone.jpg`);
|
* entering the camera zone switches to the zone camera (high view, `obj_game_camera_zone.jpg`);
|
||||||
* stepping on the exit floor shows the tally "TestLevel Kapitel 2"; Enter → "Dorf Kapitel 2"
|
* stepping on the exit floor shows the tally "TestLevel Kapitel 2"; Enter → "Dorf Kapitel 2"
|
||||||
loads = the chosen chapter (`obj_game_exit.jpg`);
|
loads = the chosen chapter (`obj_game_exit.jpg`);
|
||||||
* the beetle jumps back and forth along its path (`obj_game_patrol.jpg`).
|
* the beetle jumps back and forth along its path (`obj_game_patrol.jpg`);
|
||||||
|
* (2026-11-07, display :25) a CityGuard placed with the Place tool (one box) appears and attacks
|
||||||
|
the llama (`obj_game_guard.jpg`); one with a square path beside it never appears (editor:
|
||||||
|
`obj_guard_removed.jpg`). Village 1 with the door tile swapped open: start on the pad 42.5/3.5/49.37
|
||||||
|
→ tally "Dorf Kapitel 1" at once (`obj_game_village_door.jpg`); at the door's own position
|
||||||
|
42.5/3.5/49.8 or in the original (door closed) → nothing.
|
||||||
|
|
||||||
## Known limits
|
## Known limits
|
||||||
|
|
||||||
* Parameters are decoded for ~35 common types (confidence per type in the schema doc); the rest
|
* Parameters are decoded for ~35 common types (confidence per type in the schema doc); the rest
|
||||||
shows raw values with the statistics of the originals.
|
shows raw values with the statistics of the originals.
|
||||||
* The CityGuard (village-2 base) placed in the open did not show up in game – it probably waits for
|
* Solved 2026-11-07: the CityGuard "not showing up" was removed by its own script – guards need
|
||||||
a scene/event; the patrol test was done with JumpingBeetleBlue.
|
their start inside a waypoint box (node pairs), the tour had given it a 4-node square path
|
||||||
* In 8 original levels the exit check reports "no material 16 in the door cell": the exit floor
|
(editor-object-schemas.md "Guards"). The exit check "no material 16 in the door cell" in
|
||||||
polygons decode one cell off the door (e.g. village door cell 42,49, floor polygon at 43,48) –
|
originals came from the door floor being in the tile's swap model (RubyIdolDoor & co.); the
|
||||||
original tile collision positions vs. strat positions are not fully understood yet; brush-built
|
pad at 43,48 next to the village door is a house entrance. Tile cells follow the engine's
|
||||||
exits are correct (verified in game). Snap-to-floor on original levels uses the same polygons.
|
loader exactly (`Tile.cellX/cellZ` = ftol(x − 0.5), −ftol(z + 0.5)); `ObjectChecks` tests the
|
||||||
|
door cell like the engine (tiles listed in the cell, any height, swap models), same as
|
||||||
|
`GeometryChecks`. Tests: `GuardsTest`, `OriginalExitsTest`, `OriginalStartsTest`.
|
||||||
|
* Snap-to-floor (`Ground`) is geometric: it finds polygons that reach into a cell from a
|
||||||
|
neighbouring tile, which the engine ignores (`FloorFinder` follows the engine for the player).
|
||||||
* Zone heights are estimated (+1.2 above the lower node); the engine tests x/z only.
|
* Zone heights are estimated (+1.2 above the lower node); the engine tests x/z only.
|
||||||
* Camera preview of CameraSide assumes the player in the box centre and default distance 2 /
|
* Camera preview of CameraSide assumes the player in the box centre and default distance 2 /
|
||||||
height 1 when the parameters are 0; the game's FOV is approximated by 60°.
|
height 1 when the parameters are 0; the game's FOV is approximated by 60°.
|
||||||
|
|||||||
+3
-1
@@ -114,7 +114,9 @@ Order in the file:
|
|||||||
|
|
||||||
1. `u32 N (tiles), u32 W, u32 H` (grid)
|
1. `u32 N (tiles), u32 W, u32 H` (grid)
|
||||||
2. **A**: N × `{f32 x, f32 y, f32 z, u32 rot, u32 model, u32 animFlag}` (export space; x,z = cell centre .5).
|
2. **A**: N × `{f32 x, f32 y, f32 z, u32 rot, u32 model, u32 animFlag}` (export space; x,z = cell centre .5).
|
||||||
Only used to sort into the cell lists: cell = floor(x) + floor(−z)·W.
|
Only used to sort into the cell lists: cell = ftol(x − 0.5) − ftol(z + 0.5)·W (ftol truncates; 0x42ad4a,
|
||||||
|
constant 0x56e158 = 0.5) = floor(x) + floor(−z)·W for cell-centred tiles (all originals). The player/strat
|
||||||
|
cell of the collision tests is x>>12, z>>12 (FUN_004036d0).
|
||||||
3. u32 n + n×u32 (list, referenced by strats)
|
3. u32 n + n×u32 (list, referenced by strats)
|
||||||
4. u32 n + n×90 bytes (lights/cameras? – objects.md)
|
4. u32 n + n×90 bytes (lights/cameras? – objects.md)
|
||||||
5. u32 n + n×34 bytes (path nodes)
|
5. u32 n + n×34 bytes (path nodes)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
@@ -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.Scene;
|
||||||
import com.eliasmueller.llamamod.editor.model.Strat;
|
import com.eliasmueller.llamamod.editor.model.Strat;
|
||||||
import com.eliasmueller.llamamod.editor.model.StratCatalog;
|
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.ParamDef;
|
||||||
import com.eliasmueller.llamamod.editor.tools.objects.ParamSchema.TypeSchema;
|
import com.eliasmueller.llamamod.editor.tools.objects.ParamSchema.TypeSchema;
|
||||||
import com.eliasmueller.llamamod.editor.ui.LevelCheck;
|
import com.eliasmueller.llamamod.editor.ui.LevelCheck;
|
||||||
import com.eliasmueller.llamamod.editor.ui.Problem;
|
import com.eliasmueller.llamamod.editor.ui.Problem;
|
||||||
import com.eliasmueller.llamamod.save.Worlds;
|
import com.eliasmueller.llamamod.save.Worlds;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ final class ObjectChecks implements LevelCheck {
|
|||||||
+ " i+1): run Tools → Repair waypoint chains",
|
+ " i+1): run Tools → Repair waypoint chains",
|
||||||
broken));
|
broken));
|
||||||
}
|
}
|
||||||
Ground g = null;
|
List<Tile> doorTiles = null;
|
||||||
int players = 0;
|
int players = 0;
|
||||||
for (Strat st : s.strats()) {
|
for (Strat st : s.strats()) {
|
||||||
String type = st.type();
|
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(
|
out.add(Problem.warning(
|
||||||
label(st) + " needs " + sc.waypoints().min() + " waypoint(s) ("
|
label(st) + " needs " + sc.waypoints().min() + " waypoint(s) ("
|
||||||
+ sc.waypoints().role().label + "), has " + chain.size(),
|
+ sc.waypoints().role().label + "), has " + chain.size(),
|
||||||
@@ -67,7 +83,7 @@ final class ObjectChecks implements LevelCheck {
|
|||||||
+ chain.size(),
|
+ chain.size(),
|
||||||
st));
|
st));
|
||||||
}
|
}
|
||||||
if (sc.waypoints().pairs() && chain.size() % 2 == 1) {
|
if (sc.waypoints().pairs() && chain.size() % 2 == 1 && !Guards.is(type)) {
|
||||||
out.add(Problem.warning(
|
out.add(Problem.warning(
|
||||||
label(st) + ": box nodes come in pairs, the chain has " + chain.size(), st));
|
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 (type.equals("LevelDoor")) {
|
||||||
if (g == null) {
|
if (doorTiles == null) {
|
||||||
g = Ground.build(
|
doorTiles = new ArrayList<>(s.tiles());
|
||||||
s, s.brushes().isEmpty() ? null : BrushCompiler.compile(s, new java.util.ArrayList<>()));
|
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());
|
int cx = (int) Math.floor(st.gx()), cz = (int) Math.floor(st.gz());
|
||||||
if (!g.hasMaterial(cx, cz, 16)
|
if (!cellHasMaterial(doorTiles, cx, cz, 16)) {
|
||||||
&& s.tiles().stream()
|
|
||||||
.noneMatch(t -> t.cellX() == cx && t.cellZ() == cz && t.swapHasMaterial(16))) {
|
|
||||||
out.add(Problem.warning(
|
out.add(Problem.warning(
|
||||||
label(st) + " stands in cell " + cx + ", " + cz + " without exit floor (collision material"
|
label(st) + " stands in cell " + cx + ", " + cz + " without exit floor (collision material"
|
||||||
+ " 16): the door never fires",
|
+ " 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) {
|
static String label(Strat s) {
|
||||||
return s.type() + " (" + s.id() + ")";
|
return s.type() + " (" + s.id() + ")";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,9 +238,7 @@ public final class PlaceTool implements Tool {
|
|||||||
yield null;
|
yield null;
|
||||||
}
|
}
|
||||||
Double yaw = yawMode() == 3 ? null : yaw();
|
Double yaw = yawMode() == 3 ? null : yaw();
|
||||||
Command c = Edits.placeStrat(s, ses.type(), ses.variant(), x, y, z, yaw);
|
yield Setups.object(s, ses.type(), ses.variant(), x, y, z, yaw);
|
||||||
Strat st = Setups.lastStrat(c);
|
|
||||||
yield new Setups.Made(c, st);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (m == null) {
|
if (m == null) {
|
||||||
|
|||||||
@@ -336,13 +336,33 @@ public final class Schemas {
|
|||||||
.add();
|
.add();
|
||||||
|
|
||||||
// ---------------------------------------------------------- characters & enemies
|
// ---------------------------------------------------------- characters & enemies
|
||||||
t("CityGuard", "Guard patrolling its waypoints; chases the player when seen.")
|
String guardEvent = "0 = active from the start (all village guards). n > 0: hidden and inactive until"
|
||||||
.p("View range", Kind.RADIUS, 0, 50, "distance at which it sees the player")
|
+ " progress bit n (global 95) is set by another object with that event number (a talking"
|
||||||
.p("Chase range", Kind.RADIUS, 0, 50, "distance it chases the player (≥ view range)")
|
+ " character, smash statue, lab door pad …)";
|
||||||
.p("Event bit", Kind.EVENT_BIT, 0, 32, "")
|
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")
|
.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();
|
.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.")
|
t("JumpingBeetleBlue", "Jumping beetle following its path.")
|
||||||
.p("Jump time", Kind.FIXED, "")
|
.p("Jump time", Kind.FIXED, "")
|
||||||
.p("p1", Kind.RAW, "unused")
|
.p("p1", Kind.RAW, "unused")
|
||||||
|
|||||||
@@ -61,13 +61,74 @@ public final class Setups {
|
|||||||
p.setRaw("z", Edits.fixed(z));
|
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) {
|
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);
|
Command c = Edits.placeStrat(s, type, variant, x, y, z, yaw);
|
||||||
Strat st = lastStrat(c);
|
Strat st = lastStrat(c);
|
||||||
|
if (Guards.is(type)) {
|
||||||
|
c = guardSetup(s, (Batch) c, st, x, y, z);
|
||||||
|
}
|
||||||
return new Made(c, st);
|
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),
|
* 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).
|
* else places a Llama with its chain (first node = camera 8 cells behind, rest on the llama).
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package com.eliasmueller.llamamod.editor.tools.objects;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.eliasmueller.llamamod.editor.io.Bases;
|
||||||
|
import com.eliasmueller.llamamod.editor.io.Project;
|
||||||
|
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.cmd.Batch;
|
||||||
|
import com.eliasmueller.llamamod.editor.model.cmd.History;
|
||||||
|
import com.eliasmueller.llamamod.editor.ui.Problem;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guards are removed by their own code unless they start inside a box of a waypoint pair; LevelDoors
|
||||||
|
* need material 16 in their cell. All originals satisfy the object checks (needs -Dllamamod.cd).
|
||||||
|
*/
|
||||||
|
class GuardsTest {
|
||||||
|
@Test
|
||||||
|
void originalGuardsAndDoorsPassTheChecks() throws Exception {
|
||||||
|
int guards = 0, withEvent = 0;
|
||||||
|
Map<String, Integer> doorWarnings = new TreeMap<>();
|
||||||
|
try (var files = Files.list(ObjectToolsTest.cd().resolve("wads"))) {
|
||||||
|
for (Path p : files.filter(f -> f.getFileName().toString().matches("t1l\\dm00\\d\\.wad"))
|
||||||
|
.sorted()
|
||||||
|
.toList()) {
|
||||||
|
String base = p.getFileName().toString().replace(".wad", "");
|
||||||
|
Scene s = ObjectToolsTest.load(base);
|
||||||
|
for (Strat st : s.strats()) {
|
||||||
|
if (Guards.is(st.type())) {
|
||||||
|
guards++;
|
||||||
|
assertNull(Guards.removedBecause(s, st), base + " " + st.id());
|
||||||
|
withEvent += Guards.eventBit(st) > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Problem> out = new ArrayList<>();
|
||||||
|
new ObjectChecks(() -> null).check(s, out);
|
||||||
|
for (Problem pr : out) {
|
||||||
|
assertTrue(!pr.message().contains("never appears"), base + ": " + pr.message());
|
||||||
|
if (pr.message().contains("without exit floor")) {
|
||||||
|
doorWarnings.merge(base, 1, Integer::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(41, guards);
|
||||||
|
assertEquals(4, withEvent); // lab: cow, gorilla and lizard guards wait for scenes
|
||||||
|
// the two spare doors of t1l3m003 have no exit floor (their partners next to them do)
|
||||||
|
assertEquals(Map.of("t1l3m003", 2), doorWarnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void placedGuardStartsInItsBoxAndIsActive() throws Exception {
|
||||||
|
Scene s = Project.create(Bases.fromCd(ObjectToolsTest.cd()), "new", "t1l1m002")
|
||||||
|
.scene();
|
||||||
|
History h = new History(s);
|
||||||
|
Setups.Made g = Setups.object(s, "CityGuard", 0, 22, 0, 18, 0);
|
||||||
|
h.execute(g.command());
|
||||||
|
Strat guard = (Strat) g.main();
|
||||||
|
List<PathNode> chain = s.chain(guard.waypoint());
|
||||||
|
assertEquals(2, chain.size());
|
||||||
|
assertNull(Guards.removedBecause(s, guard));
|
||||||
|
assertEquals(0, Guards.eventBit(guard));
|
||||||
|
assertEquals(0, Guards.startBox(chain, guard));
|
||||||
|
assertTrue(ChainOps.contiguous(s));
|
||||||
|
assertTrue(problems(s).isEmpty(), problems(s).toString());
|
||||||
|
|
||||||
|
// a square "patrol path" around it: pairs (0,1) and (2,3) are flat strips, the guard is in neither
|
||||||
|
h.execute(new Batch("clear", ChainOps.removeChain(s, guard)));
|
||||||
|
List<PathNode> sq = new ArrayList<>();
|
||||||
|
for (double[] q : new double[][] {{20, 15}, {26, 15}, {26, 22}, {20, 22}}) {
|
||||||
|
sq.add(ChainOps.newNode(s, q[0], 0, q[1], 0, null));
|
||||||
|
}
|
||||||
|
h.execute(new Batch("square", ChainOps.newChain(s, guard, sq)));
|
||||||
|
assertNotNull(Guards.removedBecause(s, guard));
|
||||||
|
assertTrue(problems(s).stream()
|
||||||
|
.anyMatch(p ->
|
||||||
|
p.severity() == Problem.Severity.ERROR && p.message().contains("never appears")));
|
||||||
|
|
||||||
|
// the inspector's fix
|
||||||
|
h.execute(Setups.guardBox(s, guard));
|
||||||
|
assertNull(Guards.removedBecause(s, guard));
|
||||||
|
assertEquals(2, s.chain(guard.waypoint()).size());
|
||||||
|
assertTrue(ChainOps.contiguous(s));
|
||||||
|
|
||||||
|
// event bit set: still placed, but hidden (info)
|
||||||
|
int[] p = guard.params().clone();
|
||||||
|
p[Guards.EVENT_PARAM] = 3 << 12;
|
||||||
|
guard.setRaw("params", p);
|
||||||
|
assertTrue(problems(s).stream()
|
||||||
|
.anyMatch(q ->
|
||||||
|
q.severity() == Problem.Severity.INFO && q.message().contains("event bit 3")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void brushExitPassesTheDoorCheck() throws Exception {
|
||||||
|
Scene s = Project.create(Bases.fromCd(ObjectToolsTest.cd()), "new", "t1l1m002")
|
||||||
|
.scene();
|
||||||
|
Setups.exit(s, 12.2, 0, 26.2, 1, 1, 2).command().apply(s);
|
||||||
|
List<Problem> out = new ArrayList<>();
|
||||||
|
new ObjectChecks(() -> null).check(s, out);
|
||||||
|
assertTrue(out.stream().noneMatch(p -> p.message().contains("exit floor")), out.toString());
|
||||||
|
// moved one cell off its floor: the door never fires
|
||||||
|
Strat door = s.strats().stream()
|
||||||
|
.filter(st -> st.type().equals("LevelDoor"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
door.setRaw("x", door.x() + 4096);
|
||||||
|
out.clear();
|
||||||
|
new ObjectChecks(() -> null).check(s, out);
|
||||||
|
assertTrue(out.stream().anyMatch(p -> p.message().contains("without exit floor")), out.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Problem> problems(Scene s) {
|
||||||
|
List<Problem> out = new ArrayList<>();
|
||||||
|
new ObjectChecks(() -> null).check(s, out);
|
||||||
|
out.removeIf(p -> !(p.node() instanceof Strat st) || !Guards.is(st.type()));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,8 @@ public final class ObjectsTour {
|
|||||||
sleep(6000);
|
sleep(6000);
|
||||||
if (mode.startsWith("beetle:")) {
|
if (mode.startsWith("beetle:")) {
|
||||||
beetle();
|
beetle();
|
||||||
|
} else if (mode.startsWith("guard:")) {
|
||||||
|
guards();
|
||||||
} else if (mode.equals("village")) {
|
} else if (mode.equals("village")) {
|
||||||
village();
|
village();
|
||||||
} else {
|
} else {
|
||||||
@@ -140,7 +142,7 @@ public final class ObjectsTour {
|
|||||||
shot("obj_problems");
|
shot("obj_problems");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** New level on the village-2 base: island, coins, checkpoint, exit, guard with patrol path, camera zone. */
|
/** New level on the village-2 base: island, coins, checkpoint, exit, guard with two boxes, camera zone. */
|
||||||
static void build() throws Exception {
|
static void build() throws Exception {
|
||||||
edt(() -> {
|
edt(() -> {
|
||||||
Scene s = w.scene();
|
Scene s = w.scene();
|
||||||
@@ -171,7 +173,7 @@ public final class ObjectsTour {
|
|||||||
p[2] = 1 << 12;
|
p[2] = 1 << 12;
|
||||||
v.put(c, Map.of("params", p));
|
v.put(c, Map.of("params", p));
|
||||||
w.execute(Edits.set("Camera zone", v, null));
|
w.execute(Edits.set("Camera zone", v, null));
|
||||||
// guard with a square patrol path
|
// guard with two overlapping walking boxes (node pairs); it must start inside one
|
||||||
Setups.Made g = Setups.object(s, "CityGuard", 0, 22, 0, 18, 0);
|
Setups.Made g = Setups.object(s, "CityGuard", 0, 22, 0, 18, 0);
|
||||||
w.execute(g.command());
|
w.execute(g.command());
|
||||||
Strat guard = (Strat) g.main();
|
Strat guard = (Strat) g.main();
|
||||||
@@ -179,11 +181,11 @@ public final class ObjectsTour {
|
|||||||
new ArrayList<>(ChainOps.removeChain(s, guard));
|
new ArrayList<>(ChainOps.removeChain(s, guard));
|
||||||
w.execute(new Batch("clear", cmds));
|
w.execute(new Batch("clear", cmds));
|
||||||
List<PathNode> path = new ArrayList<>();
|
List<PathNode> path = new ArrayList<>();
|
||||||
double[][] pts = {{20, 15}, {26, 15}, {26, 22}, {20, 22}};
|
double[][] pts = {{20, 15}, {25, 20}, {23, 19}, {28, 23}};
|
||||||
for (double[] q : pts) {
|
for (double[] q : pts) {
|
||||||
path.add(ChainOps.newNode(s, q[0], 0, q[1], 0, null));
|
path.add(ChainOps.newNode(s, q[0], 0, q[1], 0, null));
|
||||||
}
|
}
|
||||||
w.execute(new Batch("Patrol path", ChainOps.newChain(s, guard, path)));
|
w.execute(new Batch("Guard boxes", ChainOps.newChain(s, guard, path)));
|
||||||
w.selection().set(guard, null);
|
w.selection().set(guard, null);
|
||||||
w.setTool(PathTool.ID);
|
w.setTool(PathTool.ID);
|
||||||
return null;
|
return null;
|
||||||
@@ -243,6 +245,33 @@ public final class ObjectsTour {
|
|||||||
shot("obj_newlevel_saved");
|
shot("obj_newlevel_saved");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Level with a correct guard (own box) and one outside its boxes: Inspector + Problems. */
|
||||||
|
static void guards() throws Exception {
|
||||||
|
edt(() -> {
|
||||||
|
Camera c = w.viewport().camera();
|
||||||
|
w.viewport().stopAnimation();
|
||||||
|
c.position.set(ToolEvent.toExport(19, 10, 8));
|
||||||
|
c.yaw = 0;
|
||||||
|
c.pitch = (float) Math.toRadians(-50);
|
||||||
|
w.viewport().requestRender();
|
||||||
|
List<Strat> g = w.scene().strats().stream()
|
||||||
|
.filter(st -> Guards.is(st.type()))
|
||||||
|
.toList();
|
||||||
|
w.selection().set(g.getLast(), null);
|
||||||
|
w.setTool("select");
|
||||||
|
w.run("level.check");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
sleep(2500);
|
||||||
|
shot("obj_guard_removed");
|
||||||
|
edt(() -> {
|
||||||
|
w.selection().set(first("CityGuard"), null);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
sleep(1500);
|
||||||
|
shot("obj_guard_box");
|
||||||
|
}
|
||||||
|
|
||||||
static void beetle() throws Exception {
|
static void beetle() throws Exception {
|
||||||
edt(() -> {
|
edt(() -> {
|
||||||
Scene s = w.scene();
|
Scene s = w.scene();
|
||||||
|
|||||||
Reference in new issue
Block a user