Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d32d197b54 | ||
|
|
0568ae7f24 | ||
|
|
64259ea15b | ||
|
|
322e36c0f3 | ||
|
|
52c70e817d |
No files matched your search
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.3 – 2026-11-07
|
||||||
|
|
||||||
|
- **Editor:** flying with the right mouse button + W A S D / Q E now works even when another part of
|
||||||
|
the window (a text field, a dock) still had the keyboard focus.
|
||||||
|
- **Editor:** the block, pillar and invisible-box icons sat too low in the toolbars (drawn outside
|
||||||
|
their box); a test now keeps every icon inside its 16 × 16 box.
|
||||||
|
- **Tutorials** say where the Objects panel is (the ⊙ tab next to *Outliner*).
|
||||||
|
|
||||||
## 0.2.2 – 2026-11-07
|
## 0.2.2 – 2026-11-07
|
||||||
|
|
||||||
- **English game version supported.** All exe mods (boot to level, cheats, intro skip, practice
|
- **English game version supported.** All exe mods (boot to level, cheats, intro skip, practice
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ plugins {
|
|||||||
|
|
||||||
group = 'com.eliasmueller.llamamod'
|
group = 'com.eliasmueller.llamamod'
|
||||||
// Release builds pass -PreleaseVersion=<tag without v> (see .gitea/workflows/release.yml).
|
// Release builds pass -PreleaseVersion=<tag without v> (see .gitea/workflows/release.yml).
|
||||||
version = findProperty('releaseVersion') ?: '0.2.2'
|
version = findProperty('releaseVersion') ?: '0.2.3'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|||||||
@@ -144,6 +144,12 @@ Turns the hidden TestLevel (world 9, chapter 1) into a playground island: a gall
|
|||||||
|
|
||||||
*Id* `testworld`
|
*Id* `testworld`
|
||||||
|
|
||||||
|
#### Village Chapter 1 (play-test)
|
||||||
|
|
||||||
|
Temporary play-test copy from the level editor – removed when the play-test ends. Changes the original level t1l1m001.
|
||||||
|
|
||||||
|
*Id* `playtest-village-chapter-1`
|
||||||
|
|
||||||
### Gameplay mods
|
### Gameplay mods
|
||||||
|
|
||||||
#### Boot to level
|
#### Boot to level
|
||||||
|
|||||||
@@ -40,8 +40,9 @@ Every change can be undone with **Ctrl+Z**.
|
|||||||
|
|
||||||
## Step 4: A coin trail
|
## Step 4: A coin trail
|
||||||
|
|
||||||
1. Open the **Objects** panel (left) and click **Coin line** under the quick setups – or press
|
1. Open the **Objects** tab in the left dock – the ⊙ icon right next to *Outliner* (inactive tabs show only
|
||||||
**O** for the **Coin line / arc** tool.
|
their icon) and click **Coin line** under **Quick setups** – or press **O** for the
|
||||||
|
**Coin line / arc** tool.
|
||||||
2. Click once at the start of the trail and once at its end. Coins appear every *spacing* cells
|
2. Click once at the start of the trail and once at its end. Coins appear every *spacing* cells
|
||||||
on the ground (also up the ramp).
|
on the ground (also up the ramp).
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ Every drag is one undo step – **Ctrl+Z** takes it back.
|
|||||||
|
|
||||||
## Step 3: Add a checkpoint
|
## Step 3: Add a checkpoint
|
||||||
|
|
||||||
1. Open the **Objects** panel (left dock) and click the quick setup **Checkpoint**.
|
1. Open the **Objects** tab in the left dock – the ⊙ icon right next to *Outliner* (inactive tabs show only
|
||||||
|
their icon) and click the quick setup **Checkpoint**.
|
||||||
2. The Place tool (**P**) is now active. Click on the ground where the checkpoint should stand.
|
2. The Place tool (**P**) is now active. Click on the ground where the checkpoint should stand.
|
||||||
|
|
||||||
The editor places a checkpoint post together with its respawn point a little in front of it.
|
The editor places a checkpoint post together with its respawn point a little in front of it.
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ plane shows as material 30. Look for:
|
|||||||
|
|
||||||
The llama should not have to start over after every fall.
|
The llama should not have to start over after every fall.
|
||||||
|
|
||||||
1. Open the **Objects** panel (left dock) and click **Checkpoint** in **Quick setups**.
|
1. Open the **Objects** tab in the left dock – the ⊙ icon right next to *Outliner* (inactive tabs show only
|
||||||
|
their icon) and click **Checkpoint** in **Quick setups**.
|
||||||
2. Click on a platform in the middle of the course.
|
2. Click on a platform in the middle of the course.
|
||||||
|
|
||||||
You get a checkpoint post with its respawn point just in front of it. Falling after passing it
|
You get a checkpoint post with its respawn point just in front of it. Falling after passing it
|
||||||
|
|||||||
+37
-15
@@ -81,27 +81,49 @@ nothing solid right behind the llama (the camera moves aside).
|
|||||||
| cameras | `CameraSide`, `CutSceneCameraDirectional`, `CutSceneTrigger` | camera zones and cut scenes |
|
| cameras | `CameraSide`, `CutSceneCameraDirectional`, `CutSceneTrigger` | camera zones and cut scenes |
|
||||||
| always present | `CheatTeleport`, `Wompy`, `CutSceneTrigger` | |
|
| always present | `CheatTeleport`, `Wompy`, `CutSceneTrigger` | |
|
||||||
|
|
||||||
### The exit (LevelDoor) – *certain*
|
### The exit (LevelDoor)
|
||||||
|
|
||||||
The LevelDoor has no model. Every frame it checks:
|
An exit is **two parts working together** (decompiled strat code; the details below were checked
|
||||||
|
in game in Jungle Day 2 with a debugger, the basic rule also in Village 1):
|
||||||
|
|
||||||
|
* The **LevelDoor** object (no model) writes its three parameters – the next level (tribe, world,
|
||||||
|
chapter) – to globals g101–g103 and its grid cell to g104/g105. It does **not** end the level
|
||||||
|
itself in the cases we tested (see the details below).
|
||||||
|
* **Kuzco's own script** ends the level when **all** of these hold:
|
||||||
|
|
||||||
```
|
```
|
||||||
game state is "running"
|
floor material under Kuzco == 16 ; the remembered floor (see "floor test" below)
|
||||||
and the floor polygon under the llama has collision material 16
|
and floor(x), floor(z) == the cell in g104/g105 ; the exit's grid cell, any height
|
||||||
and floor(llama.x) == floor(door.x) and floor(llama.z) == floor(door.z) ; same grid cell
|
and Kuzco is at most ~0.6 above that floor's height
|
||||||
-> endLevel(tribe, world, chapter) ; its three parameters
|
-> level change to g101..g103 (tally screen)
|
||||||
```
|
```
|
||||||
|
|
||||||
So an exit needs **a floor with collision material 16 in the door's grid cell** – height does not
|
On a material-16 floor **outside** the exit cell the same check is a house door: fade-out and
|
||||||
matter. In several original levels the pad only appears when a tile swap opens the door (Village 1:
|
the teleport of the environment zone of that cell.
|
||||||
the RubyIdolDoor swaps the tile to a model that has the material-16 pad). The parameters are the
|
|
||||||
next level; the originals use (1, world, chapter + 1), and **(7, 7, 7)** in the last chapter of a
|
|
||||||
world means "world finished" – in TestLevel that crashes, so custom levels use (1, 9, chapter).
|
|
||||||
`endLevel` shows the tally (coins x / LNFO total, secrets) and unlocks the next chapter.
|
|
||||||
|
|
||||||
A second path in the Llama's own code ends the level when it stands on material 16 in the cell
|
So a normal exit needs **a floor with collision material 16 in the exit's grid cell**. In several
|
||||||
stored in globals g104/g105 with the target in g101–g103 – the arena uses this
|
original levels that floor only exists after a tile swap opens the door (Village 1 and Jungle Day 2:
|
||||||
([Arena](../strats/arena.md)).
|
the RubyIdolDoor swaps the tile to a model that has the material-16 pad; checked in game in Village
|
||||||
|
1). The parameters of the originals are (1, world, chapter + 1); **(7, 7, 7)** in the last chapter of
|
||||||
|
a world means "world finished" – in TestLevel that crashes, so custom levels use (1, 9, chapter).
|
||||||
|
The tally shows coins x / LNFO total and secrets and unlocks the next chapter. The arena sets
|
||||||
|
g101–g105 itself to make its own exit ([Arena](../strats/arena.md)).
|
||||||
|
|
||||||
|
**Details checked in game (Jungle Day 2, t1l4m002, 2026-11-07):**
|
||||||
|
|
||||||
|
* The LevelDoor stores its parameters and cell in g101–g105, then checks global 0x21 **once**
|
||||||
|
(after 5 ticks); if it is not 3 the door removes itself. In Jungle Day 2 it was gone after the
|
||||||
|
start, so the Llama's check is what ends the level. That check needs floor material 16, the
|
||||||
|
door's cell, and the llama no more than 0.6 above the recorded floor height. Off the exit cell
|
||||||
|
the same material-16 check is the house door (fade-out and zone teleport).
|
||||||
|
* The floor test only looks at tiles in the player's cell and takes the **nearest upward floor
|
||||||
|
below the player, at any distance**. Material 17 is ignored for the llama. A llama hovering 3.7
|
||||||
|
units above a house-door pad records material 16.
|
||||||
|
* Each tick runs all strat scripts first and then the floor tests. A position change made after
|
||||||
|
the floor tests is seen by the next tick's scripts with the **old** floor record. So hovering
|
||||||
|
above any house-door pad and then moving the llama straight to floor height in the exit cell
|
||||||
|
ends the level, even though the exit cell has no material 16 yet (it appears only in the swap
|
||||||
|
model when the RubyIdolDoor opens). Moving there from normal ground does nothing.
|
||||||
|
|
||||||
## Environment zones (90 bytes, *certain* for the fields below)
|
## Environment zones (90 bytes, *certain* for the fields below)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
@@ -173,6 +173,11 @@ stairs, platforms register as shadow casters (`solids`). 1.0 = texture colour
|
|||||||
|
|
||||||
## Exit (LevelDoor) – how it works
|
## Exit (LevelDoor) – how it works
|
||||||
|
|
||||||
|
> **Corrected 2026-11-07 (in game, Jungle Day 2):** the listing below was the first reading. The
|
||||||
|
> LevelDoor only sets g101–g105 and checks global 0x21 *once*; the level is ended by the **Llama's
|
||||||
|
> script** (material 16 + exit cell + at most ~0.6 above the floor). See "Exit via stale floor
|
||||||
|
> material" further down and docs/reference/objects.md.
|
||||||
|
|
||||||
Strat bytecode (village, STPC 0x430332, 3 parameters, invisible, no model), decoded via the
|
Strat bytecode (village, STPC 0x430332, 3 parameters, invisible, no model), decoded via the
|
||||||
VM `FUN_0054d180` (opcode < 0x45: table 0x5796bc with 16-bit operand, otherwise 0x578ff4+op·4):
|
VM `FUN_0054d180` (opcode < 0x45: table 0x5796bc with 16-bit operand, otherwise 0x578ff4+op·4):
|
||||||
```
|
```
|
||||||
@@ -199,6 +204,46 @@ loop: if g21 == 3 ; game state "running"
|
|||||||
(zone teleport). t1l3m003: 2 of the 4 doors (cells 71,48/71,49) have no pad at all.
|
(zone teleport). t1l3m003: 2 of the 4 doors (cells 71,48/71,49) have no pad at all.
|
||||||
* Second path (strat code at 0x4252aa, player logic [likely]): floor material 16 and cell =
|
* Second path (strat code at 0x4252aa, player logic [likely]): floor material 16 and cell =
|
||||||
g104/g105 (set by the LevelDoor at start, Llama sets them to −1) → level change(g101..g103).
|
g104/g105 (set by the LevelDoor at start, Llama sets them to −1) → level change(g101..g103).
|
||||||
|
|
||||||
|
### Exit via stale floor material (hover + position jump, t1l4m002) – checked 2026-11-07
|
||||||
|
|
||||||
|
Speedrun video (position/hover hack): Kuzco hovers above a house-door stump, is moved to the exit and
|
||||||
|
the level ends. Reproduced in game (apptest, gdb through `winedbg --gdb`, position written by gdb):
|
||||||
|
* **Jungle Day chapter 2 = t1l4m002** (LNFO 95 coins / 2 secrets; tally "Dschungeltag Kapitel 2,
|
||||||
|
0/95, 0/2"). LevelDoor at 44.5/25.5 (cell 44,25), params (1,4,3); RubyIdolDoor in the same
|
||||||
|
cell. Cell 44,25 (tile 255) has material 16 **only in swap model 95** (pad x 44.67–45.0,
|
||||||
|
z 25.25–25.75, y 2.25); main model 94 has none → with the door closed the exit cannot fire.
|
||||||
|
House-door pads (material 16, main models) in cells 38,20 / 51,0 / 52,5 / 41,24 / 44,27 /
|
||||||
|
50,27 / 41,29 / 57,55 / 41,38 / 41,44 plus swap pads in 22,21 / 26,21 / 23,23 / 50,1 / 43,21 /
|
||||||
|
46,30 / 58,49 / 31,50; the zones pair them (e.g. 44,27 → 50.5/27.5, 50,27 → 44.5/27.5).
|
||||||
|
* **Floor test (FUN_004036d0):** only tiles of the player's own cell; per upward polygon the plane
|
||||||
|
height at (x,z) is compared with a probe point (y + 0.25, or y + (floor record +0x18)/2 with
|
||||||
|
+0xe8 flag 0x200000 as for the llama);
|
||||||
|
the **nearest floor below the probe wins, with no distance limit**. Material 17 is skipped for
|
||||||
|
the llama (+0xe8 = 0x24882a, no 0x8000000). If nothing is below: for the llama (+0xe8 & 0x40000)
|
||||||
|
material/height are **not** updated (stale) and x/z go back to the previous position.
|
||||||
|
In game: llama held at 44.9/**6.0**/27.5 (3.7 above the pad of the stump at 44,27) → floor
|
||||||
|
record h 2.30, **material 16**, tile 282 poly 15, every frame.
|
||||||
|
* **Order per tick (FUN_0054c4a0):** all strat scripts (VM FUN_0054d180) first, then the
|
||||||
|
collision pass (FUN_00403020 for every strat with +0xe8 & 0x8000). A position written after the
|
||||||
|
collision pass is seen by the scripts of the next tick with the **old floor record**.
|
||||||
|
In game: after hovering above the 44,27 pad, llama set to 44.5/2.3/25.5 right after its floor
|
||||||
|
test → next event endLevel `FUN_0054d620` (called from the VM, floor record still 16), before
|
||||||
|
any new floor test → tally (twice with screenshot, once traced). Control: same jump from normal
|
||||||
|
ground (material 0) → no exit; jump written just *before* the floor test → record becomes 0, no exit.
|
||||||
|
* **Who ends the level here: the Llama, not the LevelDoor.** LevelDoor bytecode: wait 5 ticks,
|
||||||
|
g101..g105 = params + cell, then **once** `if global 0x21 != 3 → end` (so "g21 == 3" above is a
|
||||||
|
one-time check, and global 0x21 is not the game state 0x5846e4), then the per-tick loop. In
|
||||||
|
t1l4m002 the LevelDoor was gone from the strat list (scan of 0x6d9e38) while g101..g105 =
|
||||||
|
1,4,3,44,25 and global 0x21 = 0. The Llama block at STPC 0x39ff4c–0x3a0064: if floorMaterial
|
||||||
|
== 16 and health > 0 and (y < floorHeight + 0.6 and global 0x21 != 2, or y < floorHeight + 0.025
|
||||||
|
and (g1 == 0 or global 0x23)): cell == (g104, g105) → level change(g101..g103), else fade-out +
|
||||||
|
`getDoorPos` (op 0x100, zone teleport = house door) + `forceCollision`. The 0.6 limit is why
|
||||||
|
hovering high above a house pad does not trigger the house teleport (bytecode; not separately
|
||||||
|
tested), while landing at floor height in the exit cell with the stale record does.
|
||||||
|
* Not explained: in two sessions with a moved player start (boot-to-level `start` 43.3/25.5) the
|
||||||
|
same jump did not end the level (Llama script ran with record 16 in cell 44,25, no endLevel).
|
||||||
|
* Images: `img/debug/exit-hover-trick.jpg` (top: video; bottom: our pool/door platform, tally).
|
||||||
* Parameters in the originals: (1, world, chapter+1) = next chapter; in the last chapter of a
|
* Parameters in the originals: (1, world, chapter+1) = next chapter; in the last chapter of a
|
||||||
world **(7,7,7)** = "world completed" → back to the selection map, unlock the next world.
|
world **(7,7,7)** = "world completed" → back to the selection map, unlock the next world.
|
||||||
* `FUN_0054d620`: remembers the current level (0x585008..14), sets the next (0x584eb4/b8/bc), shows the
|
* `FUN_0054d620`: remembers the current level (0x585008..14), sets the next (0x584eb4/b8/bc), shows the
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import java.awt.Color;
|
|||||||
import java.awt.Component;
|
import java.awt.Component;
|
||||||
import java.awt.Cursor;
|
import java.awt.Cursor;
|
||||||
import java.awt.Graphics;
|
import java.awt.Graphics;
|
||||||
|
import java.awt.KeyEventDispatcher;
|
||||||
|
import java.awt.KeyboardFocusManager;
|
||||||
import java.awt.event.ComponentAdapter;
|
import java.awt.event.ComponentAdapter;
|
||||||
import java.awt.event.ComponentEvent;
|
import java.awt.event.ComponentEvent;
|
||||||
import java.awt.event.InputEvent;
|
import java.awt.event.InputEvent;
|
||||||
@@ -93,6 +95,38 @@ public final class Viewport extends JPanel {
|
|||||||
private boolean dirty = true;
|
private boolean dirty = true;
|
||||||
private long lastTick = System.nanoTime();
|
private long lastTick = System.nanoTime();
|
||||||
private final Set<Integer> keys = new HashSet<>();
|
private final Set<Integer> keys = new HashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* While flying, WASD/QE/Shift reach the fly loop even if another component (a text field, a
|
||||||
|
* dock) still has the keyboard focus: with some window managers the heavyweight GL canvas does
|
||||||
|
* not get the focus from a right-click.
|
||||||
|
*/
|
||||||
|
private final KeyEventDispatcher flyKeys = e -> {
|
||||||
|
if (!this.flying) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (e.getID() == KeyEvent.KEY_PRESSED) {
|
||||||
|
keys.add(e.getKeyCode());
|
||||||
|
} else if (e.getID() == KeyEvent.KEY_RELEASED) {
|
||||||
|
keys.remove(e.getKeyCode());
|
||||||
|
}
|
||||||
|
// modifiers pass on (others track Shift, e.g. the play-here crosshair); typed keys do not
|
||||||
|
int k = e.getKeyCode();
|
||||||
|
return k != KeyEvent.VK_SHIFT && k != KeyEvent.VK_CONTROL && k != KeyEvent.VK_ALT && k != KeyEvent.VK_META;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addNotify() {
|
||||||
|
super.addNotify();
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(flyKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeNotify() {
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(flyKeys);
|
||||||
|
super.removeNotify();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean flying;
|
private boolean flying;
|
||||||
private BufferedImage capture;
|
private BufferedImage capture;
|
||||||
private boolean captureNext;
|
private boolean captureNext;
|
||||||
@@ -1131,6 +1165,7 @@ public final class Viewport extends JPanel {
|
|||||||
}
|
}
|
||||||
if (drag == Drag.FLY) {
|
if (drag == Drag.FLY) {
|
||||||
flying = false;
|
flying = false;
|
||||||
|
keys.clear(); // a key released after the button may have gone to another component
|
||||||
canvas.setCursor(Cursor.getDefaultCursor());
|
canvas.setCursor(Cursor.getDefaultCursor());
|
||||||
}
|
}
|
||||||
if (drag != Drag.NONE && buttonOf(drag, e)) {
|
if (drag != Drag.NONE && buttonOf(drag, e)) {
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ final class GeoIcons {
|
|||||||
private static final Color RED = new Color(0xD93F3F);
|
private static final Color RED = new Color(0xD93F3F);
|
||||||
|
|
||||||
static void register() {
|
static void register() {
|
||||||
Icons.register("prim-box", (g, fg) -> block(g, 2, 5, 12, 8, TOP, SIDE));
|
Icons.register("prim-box", (g, fg) -> block(g, 2, 1, 12, 8, TOP, SIDE));
|
||||||
Icons.register("prim-platform", (g, fg) -> block(g, 1, 4, 14, 4, TOP, SIDE));
|
Icons.register("prim-platform", (g, fg) -> block(g, 1, 2.5, 14, 4, TOP, SIDE));
|
||||||
Icons.register("prim-floor", (g, fg) -> {
|
Icons.register("prim-floor", (g, fg) -> {
|
||||||
Path2D p = poly(1, 10, 8, 6.5, 15, 10, 8, 13.5);
|
Path2D p = poly(1, 10, 8, 6.5, 15, 10, 8, 13.5);
|
||||||
g.setColor(TOP);
|
g.setColor(TOP);
|
||||||
@@ -47,7 +47,7 @@ final class GeoIcons {
|
|||||||
g.setColor(fg);
|
g.setColor(fg);
|
||||||
g.draw(p);
|
g.draw(p);
|
||||||
});
|
});
|
||||||
Icons.register("prim-pillar", (g, fg) -> block(g, 5, 2, 6, 12, STONE, STONE.darker()));
|
Icons.register("prim-pillar", (g, fg) -> block(g, 5, 0.5, 6, 12, STONE, STONE.darker()));
|
||||||
Icons.register("prim-cylinder", (g, fg) -> {
|
Icons.register("prim-cylinder", (g, fg) -> {
|
||||||
g.setColor(STONE.darker());
|
g.setColor(STONE.darker());
|
||||||
g.fill(new java.awt.geom.Rectangle2D.Double(2, 6, 12, 5));
|
g.fill(new java.awt.geom.Rectangle2D.Double(2, 6, 12, 5));
|
||||||
@@ -122,7 +122,7 @@ final class GeoIcons {
|
|||||||
Icons.register("prim-invbox", (g, fg) -> {
|
Icons.register("prim-invbox", (g, fg) -> {
|
||||||
g.setStroke(
|
g.setStroke(
|
||||||
new BasicStroke(1.2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1, new float[] {2, 1.5f}, 0));
|
new BasicStroke(1.2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1, new float[] {2, 1.5f}, 0));
|
||||||
outlineBlock(g, 2, 5, 12, 8, new Color(255, 60, 60, 60), RED);
|
outlineBlock(g, 2, 1, 12, 8, new Color(255, 60, 60, 60), RED);
|
||||||
});
|
});
|
||||||
Icons.register("geo-collision", (g, fg) -> {
|
Icons.register("geo-collision", (g, fg) -> {
|
||||||
Path2D p = poly(1, 10, 8, 6.5, 15, 10, 8, 13.5);
|
Path2D p = poly(1, 10, 8, 6.5, 15, 10, 8, 13.5);
|
||||||
@@ -221,6 +221,7 @@ final class GeoIcons {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Isometric block: x, y of the top-left, width w, side height h. */
|
/** Isometric block: x, y of the top-left, width w, side height h. */
|
||||||
|
/** An isometric block filling y .. y + w / 2 + h; keep that within the 16 × 16 icon box. */
|
||||||
private static void block(Graphics2D g, double x, double y, double w, double h, Color top, Color side) {
|
private static void block(Graphics2D g, double x, double y, double w, double h, Color top, Color side) {
|
||||||
Color fg = g.getColor();
|
Color fg = g.getColor();
|
||||||
double d = w / 4;
|
double d = w / 4;
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ public final class Icons {
|
|||||||
|
|
||||||
private Icons() {}
|
private Icons() {}
|
||||||
|
|
||||||
|
/** Names of all registered icons (sorted). */
|
||||||
|
public static java.util.List<String> names() {
|
||||||
|
return PAINTERS.keySet().stream().sorted().toList();
|
||||||
|
}
|
||||||
|
|
||||||
public static void register(String name, Painter p) {
|
public static void register(String name, Painter p) {
|
||||||
PAINTERS.put(name, p);
|
PAINTERS.put(name, p);
|
||||||
CACHE.keySet().removeIf(k -> k.startsWith(name + "@"));
|
CACHE.keySet().removeIf(k -> k.startsWith(name + "@"));
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.eliasmueller.llamamod.editor.tools.geometry;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.eliasmueller.llamamod.editor.ui.Icons;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Every vector icon stays inside its 16 × 16 box, so toolbar icons line up. */
|
||||||
|
class IconBoundsTest {
|
||||||
|
@Test
|
||||||
|
void iconsStayInsideTheirBox() {
|
||||||
|
GeoIcons.register();
|
||||||
|
List<String> outside = new ArrayList<>();
|
||||||
|
for (String name : Icons.names()) {
|
||||||
|
BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D g = img.createGraphics();
|
||||||
|
Icons.get(name, 16).paintIcon(null, g, 8, 8);
|
||||||
|
g.dispose();
|
||||||
|
int minY = 32, maxY = -1, minX = 32, maxX = -1;
|
||||||
|
for (int y = 0; y < 32; y++) {
|
||||||
|
for (int x = 0; x < 32; x++) {
|
||||||
|
if ((img.getRGB(x, y) >>> 24) > 40) {
|
||||||
|
minY = Math.min(minY, y);
|
||||||
|
maxY = Math.max(maxY, y);
|
||||||
|
minX = Math.min(minX, x);
|
||||||
|
maxX = Math.max(maxX, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxY >= 0 && (minY < 7 || maxY > 24 || minX < 7 || maxX > 24)) {
|
||||||
|
outside.add(name + " x " + (minX - 8) + ".." + (maxX - 8) + " y " + (minY - 8) + ".." + (maxY - 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(outside.isEmpty(), "icons drawn outside their 16x16 box: " + outside);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user