From 87b38ddf5ca0c61efb0700f1cb4302d83d1a334d Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:18:24 +0200 Subject: [PATCH 01/43] chore(setup): add new targets --- .../overview/MapOverviewInventory.java | 11 +++++++---- .../inventory/overview/OverviewType.java | 19 ++++++++++--------- .../inventory/overview/package-info.java | 4 ++++ 3 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/package-info.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java index 73fbcdb5..b1e8a818 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java @@ -8,11 +8,12 @@ import net.theevilreaper.aves.inventory.slot.ISlot; import net.theevilreaper.aves.inventory.util.LayoutCalculator; import net.theevilreaper.bounce.setup.builder.GameMapBuilder; +import net.theevilreaper.bounce.setup.inventory.slot.AreaOverviewSlot; import net.theevilreaper.bounce.setup.inventory.slot.MultiStringSlot; import net.theevilreaper.bounce.setup.inventory.slot.PositionSlot; +import net.theevilreaper.bounce.setup.inventory.slot.ShuffleIntervalOverviewSlot; import net.theevilreaper.bounce.setup.inventory.slot.StringSlot; import net.theevilreaper.bounce.setup.util.SetupItems; -import org.jetbrains.annotations.NotNull; /** * The {@link MapOverviewInventory} is a custom inventory implementation of the {@link PersonalInventoryBuilder} class. @@ -24,7 +25,7 @@ */ public final class MapOverviewInventory extends PersonalInventoryBuilder { - private static final int[] DATA_SLOT = LayoutCalculator.from(10, 12, 14, 16); + private static final int[] DATA_SLOT = LayoutCalculator.from(10, 11, 12, 13, 14, 15); private final GameMapBuilder builder; @@ -34,7 +35,7 @@ public final class MapOverviewInventory extends PersonalInventoryBuilder { * @param player the {@link Player} who is involved * @param builder the {@link GameMapBuilder} which contains the map data */ - public MapOverviewInventory(@NotNull Player player, @NotNull GameMapBuilder builder) { + public MapOverviewInventory(Player player, GameMapBuilder builder) { super(Component.text("Data view"), InventoryType.CHEST_3_ROW, player); this.builder = builder; InventoryLayout layout = InventoryLayout.fromType(getType()); @@ -62,12 +63,14 @@ public MapOverviewInventory(@NotNull Player player, @NotNull GameMapBuilder buil * @param type the {@link OverviewType} to map * @return the corresponding {@link ISlot} for the given type */ - private @NotNull ISlot getOverViewItem(@NotNull OverviewType type) { + private ISlot getOverViewItem(OverviewType type) { return switch (type) { case SPAWN -> new PositionSlot(type, this.builder.getSpawn()); case GAME_SPAWN -> new PositionSlot(type, this.builder.getGameSpawn()); case NAME -> new StringSlot(type, builder.getName()); case BUILDER -> new MultiStringSlot(type, builder.getBuilders()); + case AREA -> new AreaOverviewSlot(type, builder.getArea()); + case SHUFFLE_INTERVAL -> new ShuffleIntervalOverviewSlot(type, builder.getShuffleIntervalTicks()); }; } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java index 2fc6e090..c97c7a5a 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java @@ -5,7 +5,6 @@ import net.kyori.adventure.text.format.TextColor; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; -import org.jetbrains.annotations.NotNull; import java.util.EnumMap; import java.util.Map; @@ -23,7 +22,9 @@ public enum OverviewType { NAME("Map Name", Material.OAK_SIGN, NamedTextColor.YELLOW), BUILDER("Builder", Material.OAK_HANGING_SIGN, NamedTextColor.AQUA), SPAWN("Spawn Point", Material.COMPASS, NamedTextColor.RED), - GAME_SPAWN("Game Spawn Point", Material.RECOVERY_COMPASS, NamedTextColor.RED) + GAME_SPAWN("Game Spawn Point", Material.RECOVERY_COMPASS, NamedTextColor.RED), + AREA("Playing Area", Material.FILLED_MAP, NamedTextColor.GOLD), + SHUFFLE_INTERVAL("Reshuffle Interval", Material.CLOCK, NamedTextColor.LIGHT_PURPLE) ; @@ -41,7 +42,7 @@ public enum OverviewType { * @param material the material associated with this overview type * @param color the text color for this overview type */ - OverviewType(@NotNull String name, @NotNull Material material, @NotNull TextColor color) { + OverviewType(String name, Material material, TextColor color) { this.name = name; this.material = material; this.color = color; @@ -52,7 +53,7 @@ public enum OverviewType { * * @return the name of */ - public @NotNull String getName() { + public String getName() { return name; } @@ -61,7 +62,7 @@ public enum OverviewType { * * @return the material */ - public @NotNull Material getMaterial() { + public Material getMaterial() { return material; } @@ -70,7 +71,7 @@ public enum OverviewType { * * @return the text color */ - public @NotNull TextColor getColor() { + public TextColor getColor() { return color; } @@ -79,7 +80,7 @@ public enum OverviewType { * * @return the ItemStack for this overview type */ - public @NotNull ItemStack getItem() { + public ItemStack getItem() { return itemCache.computeIfAbsent(this, type -> ItemStack.builder(type.getMaterial()) .customName(Component.text(type.getName(), type.getColor())) .build()); @@ -90,11 +91,11 @@ public enum OverviewType { * * @return an array of all OverviewType values */ - public static @NotNull OverviewType[] getValues() { + public static OverviewType[] getValues() { return VALUES; } - public static @NotNull OverviewType fromOrdinal(int ordinal) { + public static OverviewType fromOrdinal(int ordinal) { if (ordinal < 0 || ordinal >= VALUES.length) { throw new IndexOutOfBoundsException("Invalid ordinal for OverviewType: " + ordinal); } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/package-info.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/package-info.java new file mode 100644 index 00000000..92161dd9 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.setup.inventory.overview; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file From 68adac24c0431f4f460dcceb057e43cd7cdb278a Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:19:45 +0200 Subject: [PATCH 02/43] chore(setup): add weight option --- .../inventory/ground/GroundValueInventory.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundValueInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundValueInventory.java index 7f54a760..e56ce83e 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundValueInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundValueInventory.java @@ -13,11 +13,11 @@ import net.theevilreaper.aves.inventory.util.LayoutCalculator; import net.theevilreaper.bounce.common.push.PushEntry; import net.theevilreaper.bounce.setup.builder.GameMapBuilder; +import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent; import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent.SwitchTarget; import net.theevilreaper.bounce.setup.inventory.slot.SwitchTargetSlot; import net.theevilreaper.bounce.setup.util.LoreHelper; -import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; @@ -28,11 +28,12 @@ public class GroundValueInventory extends PersonalInventoryBuilder { private static final Component TITLE = Component.text("Ground block"); private static final int BLOCK_SLOT = 11; + private static final int WEIGHT_SLOT = 13; private static final int VALUE_SLOT = 15; private final PushEntry pushEntry; - public GroundValueInventory(@NotNull Player player, @NotNull GameMapBuilder gameMapBuilder) { + public GroundValueInventory(Player player, GameMapBuilder gameMapBuilder) { super(TITLE, InventoryType.CHEST_3_ROW, player); InventoryLayout layout = InventoryLayout.fromType(getType()); layout.setItems(LayoutCalculator.quad(0, getType().getSize() - 1), DECORATION); @@ -47,18 +48,19 @@ public GroundValueInventory(@NotNull Player player, @NotNull GameMapBuilder game dataLayout.blank(LayoutCalculator.from(BLOCK_SLOT, VALUE_SLOT)); Material material = pushEntry.getBlock().material(); dataLayout.setItem(BLOCK_SLOT, ItemStack.builder(material).build(), this::handleBlockClick); + dataLayout.setItem(WEIGHT_SLOT, LoreHelper.getWeight(pushEntry), this::handleWeightButtonClick); dataLayout.setItem(VALUE_SLOT, LoreHelper.getPushValue(pushEntry), this::handlePushButtonClick); return dataLayout; }); } - private void handleBlockClick(@NotNull Player player, int slot, @NotNull Click clickType, @NotNull ItemStack stack, @NotNull Consumer result) { + private void handleBlockClick(Player player, int slot, Click clickType, ItemStack stack, Consumer result) { result.accept(ClickHolder.cancelClick()); player.closeInventory(); EventDispatcher.call(new SetupInventorySwitchEvent(player, SwitchTarget.GROUND_BLOCKS_OVERVIEW)); } - private void handlePushButtonClick(@NotNull Player player, int slot, @NotNull Click click, @NotNull ItemStack stack, @NotNull Consumer result) { + private void handlePushButtonClick(Player player, int slot, Click click, ItemStack stack, Consumer result) { result.accept(ClickHolder.cancelClick()); if ((!(click instanceof Click.Left || click instanceof Click.Right))) return; @@ -73,4 +75,12 @@ private void handlePushButtonClick(@NotNull Player player, int slot, @NotNull Cl this.invalidateDataLayout(); } } + + private void handleWeightButtonClick(Player player, int slot, Click click, ItemStack stack, Consumer result) { + result.accept(ClickHolder.cancelClick()); + if ((!(click instanceof Click.Left || click instanceof Click.Right))) return; + + player.setTag(net.theevilreaper.bounce.setup.util.SetupTags.PUSH_SLOT_INDEX, 0); + EventDispatcher.call(new PlayerDialogRequestEvent(player, PlayerDialogRequestEvent.Target.SETUP_BLOCK_WEIGHT)); + } } From 0471339b8c303fad9d7f125178b914b60265fcd7 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:19:56 +0200 Subject: [PATCH 03/43] feat(setup): add area inventory --- .../inventory/ground/AreaViewInventory.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventory.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventory.java new file mode 100644 index 00000000..7f02404a --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventory.java @@ -0,0 +1,139 @@ +package net.theevilreaper.bounce.setup.inventory.ground; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Player; +import net.minestom.server.inventory.InventoryType; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.theevilreaper.aves.inventory.PersonalInventoryBuilder; +import net.theevilreaper.aves.inventory.click.ClickHolder; +import net.theevilreaper.aves.inventory.layout.InventoryLayout; +import net.theevilreaper.aves.inventory.util.LayoutCalculator; +import net.theevilreaper.bounce.common.ground.Area; +import net.theevilreaper.bounce.common.ground.GroundArea; +import net.theevilreaper.bounce.common.push.PushEntry; +import net.theevilreaper.bounce.setup.builder.GameMapBuilder; +import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent.SwitchTarget; +import net.theevilreaper.bounce.setup.inventory.slot.SwitchTargetSlot; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static net.theevilreaper.bounce.setup.util.SetupItems.DECORATION; + +public final class AreaViewInventory extends PersonalInventoryBuilder { + + private static final Component TITLE = Component.text("Setup area"); + + private static final int POS1_SLOT = 11; + private static final int POS2_SLOT = 13; + private static final int CONFIRM_SLOT = 15; + + private final GameMapBuilder gameMapBuilder; + private @Nullable Vec pos1; + private @Nullable Vec pos2; + + public AreaViewInventory(@NotNull Player player, @NotNull GameMapBuilder gameMapBuilder) { + super(TITLE, InventoryType.CHEST_3_ROW, player); + this.gameMapBuilder = gameMapBuilder; + + Area existingArea = gameMapBuilder.getArea(); + if (existingArea != null) { + this.pos1 = existingArea.min(); + this.pos2 = existingArea.max(); + } + + InventoryLayout layout = InventoryLayout.fromType(getType()); + layout.setItems(LayoutCalculator.quad(0, getType().getSize() - 1), DECORATION); + layout.setItem(getType().getSize() - 1, new SwitchTargetSlot(SwitchTarget.MAP_OVERVIEW)); + this.setLayout(layout); + + this.setDataLayoutFunction(dataLayoutFunction -> { + InventoryLayout dataLayout = dataLayoutFunction == null ? InventoryLayout.fromType(getType()) : dataLayoutFunction; + dataLayout.blank(LayoutCalculator.from(POS1_SLOT, POS2_SLOT, CONFIRM_SLOT)); + + dataLayout.setItem(POS1_SLOT, getPosItem("Pos1", pos1), (p, slot, click, stack, result) -> { + result.accept(ClickHolder.cancelClick()); + if (click instanceof Click.Left) setPos1ToCurrentPosition(p); + }); + dataLayout.setItem(POS2_SLOT, getPosItem("Pos2", pos2), (p, slot, click, stack, result) -> { + result.accept(ClickHolder.cancelClick()); + if (click instanceof Click.Left) setPos2ToCurrentPosition(p); + }); + dataLayout.setItem(CONFIRM_SLOT, getConfirmItem(), (p, slot, click, stack, result) -> { + result.accept(ClickHolder.cancelClick()); + if (click instanceof Click.Left) confirm(p); + }); + + return dataLayout; + }); + } + + /** + * Sets Pos1 to the player's current position and refreshes the layout. + * + * @param player the player whose position is captured + */ + public void setPos1ToCurrentPosition(@NotNull Player player) { + this.pos1 = toVec(player.getPosition()); + this.invalidateDataLayout(); + } + + /** + * Sets Pos2 to the player's current position and refreshes the layout. + * + * @param player the player whose position is captured + */ + public void setPos2ToCurrentPosition(@NotNull Player player) { + this.pos2 = toVec(player.getPosition()); + this.invalidateDataLayout(); + } + + /** + * Builds a {@link GroundArea} from the captured positions and the current ground block/push data, and stores + * it on the {@link GameMapBuilder}. A no-op if either position is still unset. + * + * @param player the player to notify + */ + public void confirm(@NotNull Player player) { + if (pos1 == null || pos2 == null) return; + + PushEntry groundEntry = gameMapBuilder.getGroundBlockEntry(); + Area area = new GroundArea(pos1, pos2, groundEntry.getBlock(), gameMapBuilder.getPushDataBuilder().build()); + gameMapBuilder.area(area); + player.sendMessage(Component.text("Area saved.", NamedTextColor.GREEN)); + } + + private @NotNull Vec toVec(@NotNull Pos pos) { + return new Vec(pos.x(), pos.y(), pos.z()); + } + + private @NotNull ItemStack getPosItem(@NotNull String label, @Nullable Vec pos) { + ItemStack.Builder builder = ItemStack.builder(Material.STICK) + .customName(Component.text(label, NamedTextColor.AQUA)); + if (pos == null) { + return builder.lore( + Component.empty(), + Component.text("Not set", NamedTextColor.RED), + Component.empty(), + Component.text("Left-click: set to your position", NamedTextColor.GRAY) + ).build(); + } + return builder.lore( + Component.empty(), + Component.text("X: " + pos.x() + " Y: " + pos.y() + " Z: " + pos.z(), NamedTextColor.YELLOW), + Component.empty(), + Component.text("Left-click: set to your position", NamedTextColor.GRAY) + ).build(); + } + + private @NotNull ItemStack getConfirmItem() { + boolean ready = pos1 != null && pos2 != null; + return ItemStack.builder(ready ? Material.LIME_DYE : Material.GRAY_DYE) + .customName(Component.text(ready ? "Confirm area" : "Set both positions first", ready ? NamedTextColor.GREEN : NamedTextColor.RED)) + .build(); + } +} From 3e20140da05f55339a05ca4d9f34320c2dac124c Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:20:12 +0200 Subject: [PATCH 04/43] chore(setup): improve annotation handling --- .../ground/GroundBlockOverviewInventory.java | 3 +-- .../setup/inventory/ground/GroundViewInventory.java | 12 ++++++------ .../bounce/setup/inventory/ground/package-info.java | 4 ++++ 3 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/package-info.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundBlockOverviewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundBlockOverviewInventory.java index 4bcc4784..390986e3 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundBlockOverviewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundBlockOverviewInventory.java @@ -15,7 +15,6 @@ import net.theevilreaper.bounce.setup.event.ground.PlayerGroundBlockSelectEvent; import net.theevilreaper.bounce.setup.inventory.SetupBlocks; import net.theevilreaper.bounce.setup.inventory.slot.SwitchTargetSlot; -import org.jetbrains.annotations.NotNull; import java.util.Iterator; import java.util.function.Consumer; @@ -56,7 +55,7 @@ public GroundBlockOverviewInventory() { * @param stack the item stack that was clicked * @param result the consumer to handle the click result */ - private void handleClick(@NotNull Player player, int slot, @NotNull Click clickType, @NotNull ItemStack stack, @NotNull Consumer result) { + private void handleClick(Player player, int slot, Click clickType, ItemStack stack, Consumer result) { result.accept(ClickHolder.cancelClick()); EventDispatcher.call(new PlayerGroundBlockSelectEvent(player, stack.material())); } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundViewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundViewInventory.java index 9bca65c7..a4eadf7a 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundViewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundViewInventory.java @@ -20,7 +20,7 @@ import net.theevilreaper.bounce.setup.inventory.slot.SwitchTargetSlot; import net.theevilreaper.bounce.setup.util.SetupItems; import net.theevilreaper.bounce.setup.util.SetupMessages; -import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.function.Consumer; @@ -36,10 +36,10 @@ public class GroundViewInventory extends PersonalInventoryBuilder { Component.empty() ); - private GroundValueInventory groundValueInventory; + private @Nullable GroundValueInventory groundValueInventory; private final GameMapBuilder gameMapBuilder; - public GroundViewInventory(@NotNull Player player, @NotNull GameMapBuilder gameMapBuilder) { + public GroundViewInventory(Player player, GameMapBuilder gameMapBuilder) { super(Component.text("Setup playing area"), InventoryType.CHEST_3_ROW, player); this.gameMapBuilder = gameMapBuilder; InventoryLayout layout = InventoryLayout.fromType(getType()); @@ -81,7 +81,7 @@ public GroundViewInventory(@NotNull Player player, @NotNull GameMapBuilder gameM * @param stack the item stack that was clicked * @param result the consumer to handle the click result */ - private void handlePushButton(@NotNull Player player, int slot, @NotNull Click clickType, @NotNull ItemStack stack, @NotNull Consumer result) { + private void handlePushButton(Player player, int slot, Click clickType, ItemStack stack, Consumer result) { result.accept(ClickHolder.cancelClick()); if (!stack.hasTag(PUSH_SLOT_INDEX)) return; @@ -100,7 +100,7 @@ private void handlePushButton(@NotNull Player player, int slot, @NotNull Click c * @param stack the item stack that was clicked * @param result the consumer to handle the click result */ - private void handleGroundButton(@NotNull Player player, int slot, @NotNull Click clickType, @NotNull ItemStack stack, @NotNull Consumer result) { + private void handleGroundButton(Player player, int slot, Click clickType, ItemStack stack, Consumer result) { result.accept(ClickHolder.cancelClick()); player.closeInventory(); @@ -111,7 +111,7 @@ private void handleGroundButton(@NotNull Player player, int slot, @NotNull Click groundValueInventory.open(); } - private @NotNull ItemStack getSlotItem(@NotNull Material material, int slotId) { + private ItemStack getSlotItem(Material material, int slotId) { return ItemStack.builder(material) .customName(Component.translatable(material.translationKey(), NamedTextColor.AQUA)) .lore(PUSH_LORE) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/package-info.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/package-info.java new file mode 100644 index 00000000..04a5fd07 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.setup.inventory.ground; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file From afd669bc9f46818125f5f8ccd7b3cec838932169 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:20:59 +0200 Subject: [PATCH 05/43] chore(setup): add new target --- .../bounce/setup/event/SetupInventorySwitchEvent.java | 4 ++++ .../listener/inventory/SetupInventorySwitchListener.java | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/event/SetupInventorySwitchEvent.java b/setup/src/main/java/net/theevilreaper/bounce/setup/event/SetupInventorySwitchEvent.java index e235264d..d25adde7 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/event/SetupInventorySwitchEvent.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/event/SetupInventorySwitchEvent.java @@ -78,5 +78,9 @@ public enum SwitchTarget { * Switch to the overview of maps. */ MAP_OVERVIEW, + /** + * Switch to the area selection view of the map. + */ + AREA_VIEW, } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/inventory/SetupInventorySwitchListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/inventory/SetupInventorySwitchListener.java index 9fbec825..60ec6496 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/inventory/SetupInventorySwitchListener.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/inventory/SetupInventorySwitchListener.java @@ -1,9 +1,11 @@ package net.theevilreaper.bounce.setup.listener.inventory; import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; import net.onelitefeather.guira.data.SetupData; import net.onelitefeather.guira.functional.OptionalSetupDataGetter; import net.theevilreaper.bounce.setup.data.BounceData; +import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent; import net.theevilreaper.bounce.setup.inventory.InventoryService; import net.theevilreaper.bounce.setup.util.SetupMessages; @@ -68,5 +70,9 @@ public void accept(@NotNull SetupInventorySwitchEvent event) { if (event.getTarget() == SwitchTarget.GROUND_BLOCK_VIEW) { data.openGroundBlockView(); } + + if (event.getTarget() == SwitchTarget.AREA_VIEW) { + data.openAreaView(); + } } } From de5309cb06e3119e3fbe0919863819f2305ce8b5 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:21:22 +0200 Subject: [PATCH 06/43] chore(dialog): add new dialog types --- .../type/ShuffleIntervalInputDialog.java | 70 +++++++++++++++++++ .../setup/dialog/type/WeightInputDialog.java | 70 +++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/ShuffleIntervalInputDialog.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/WeightInputDialog.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/ShuffleIntervalInputDialog.java b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/ShuffleIntervalInputDialog.java new file mode 100644 index 00000000..7ef88528 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/ShuffleIntervalInputDialog.java @@ -0,0 +1,70 @@ +package net.theevilreaper.bounce.setup.dialog.type; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.dialog.*; +import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.server.common.ShowDialogPacket; +import net.theevilreaper.bounce.setup.dialog.AbstractDialogTemplate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class ShuffleIntervalInputDialog extends AbstractDialogTemplate { + + public static final Key DIALOG_KEY = Key.key("bounce", "shuffle_interval_setup_dialog"); + + public ShuffleIntervalInputDialog() { + super( + Component.text("Change shuffle interval"), + Component.text("Click to confirm"), + Component.text("Click to cancel") + ); + } + + @Override + public void open(@NotNull Player player) { + this.open(player, 100.0f); + } + + @Override + public void open(@NotNull Player player, @Nullable Float data) { + float initial = data != null ? data : 100.0f; + ShowDialogPacket packet = new ShowDialogPacket(new Dialog.Confirmation( + new DialogMetadata( + header, + null, + false, + false, + DialogAfterAction.CLOSE, + List.of( + new DialogBody.PlainMessage(Component.text("Reshuffle interval in ticks (20 ticks = 1 second):"), 320) + ), + List.of( + new DialogInput.NumberRange("interval_ticks", 320, Component.text("Interval"), "options.generic_value", 20f, 600f, initial, 10f) + ) + ), + new DialogActionButton( + submitComponent, + Component.text("Click to confirm", NamedTextColor.GREEN), + 155, + new DialogAction.DynamicCustom(DIALOG_KEY, CompoundBinaryTag.builder().build()) + ), + new DialogActionButton( + cancelComponent, + Component.text("Click to cancel", NamedTextColor.RED), + 155, + null + ) + )); + player.sendPacket(packet); + } + + @Override + public @NotNull Key key() { + return DIALOG_KEY; + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/WeightInputDialog.java b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/WeightInputDialog.java new file mode 100644 index 00000000..a71c499c --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/WeightInputDialog.java @@ -0,0 +1,70 @@ +package net.theevilreaper.bounce.setup.dialog.type; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.dialog.*; +import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.server.common.ShowDialogPacket; +import net.theevilreaper.bounce.setup.dialog.AbstractDialogTemplate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class WeightInputDialog extends AbstractDialogTemplate { + + public static final Key DIALOG_KEY = Key.key("bounce", "weight_setup_dialog"); + + public WeightInputDialog() { + super( + Component.text("Change spawn chance"), + Component.text("Click to confirm"), + Component.text("Click to cancel") + ); + } + + @Override + public void open(@NotNull Player player) { + this.open(player, 5.0f); + } + + @Override + public void open(@NotNull Player player, @Nullable Float data) { + float initial = data != null ? data : 5.0f; + ShowDialogPacket packet = new ShowDialogPacket(new Dialog.Confirmation( + new DialogMetadata( + header, + null, + false, + false, + DialogAfterAction.CLOSE, + List.of( + new DialogBody.PlainMessage(Component.text("Spawn probability in percent (0 - 100%):"), 320) + ), + List.of( + new DialogInput.NumberRange("weight_percentage", 320, Component.text("Chance"), "options.percent_value", 0f, 100f, initial, 0.1f) + ) + ), + new DialogActionButton( + submitComponent, + Component.text("Click to confirm", NamedTextColor.GREEN), + 155, + new DialogAction.DynamicCustom(DIALOG_KEY, CompoundBinaryTag.builder().build()) + ), + new DialogActionButton( + cancelComponent, + Component.text("Click to cancel", NamedTextColor.RED), + 155, + null + ) + )); + player.sendPacket(packet); + } + + @Override + public @NotNull Key key() { + return DIALOG_KEY; + } +} From 076a615c04974660632baa8d86c2b5bf7389345c Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:21:32 +0200 Subject: [PATCH 07/43] chore(slot): add shuffle interval overview slot --- .../slot/ShuffleIntervalOverviewSlot.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/ShuffleIntervalOverviewSlot.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/ShuffleIntervalOverviewSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/ShuffleIntervalOverviewSlot.java new file mode 100644 index 00000000..93085301 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/ShuffleIntervalOverviewSlot.java @@ -0,0 +1,43 @@ +package net.theevilreaper.bounce.setup.inventory.slot; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.theevilreaper.aves.inventory.click.ClickHolder; +import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; +import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +public final class ShuffleIntervalOverviewSlot extends AbstractDataSlot { + + private final int shuffleIntervalTicks; + + public ShuffleIntervalOverviewSlot(@NotNull OverviewType overviewType, int shuffleIntervalTicks) { + super(overviewType); + this.shuffleIntervalTicks = shuffleIntervalTicks; + } + + @Override + public ItemStack getItem() { + ItemStack overviewItem = this.type.getItem(); + double seconds = shuffleIntervalTicks / 20.0; + return asBuilder(overviewItem).lore( + Component.empty(), + Component.text(String.format(java.util.Locale.ROOT, "%d ticks (%.1fs)", shuffleIntervalTicks, seconds), NamedTextColor.YELLOW), + Component.empty(), + Component.text("Click to edit", NamedTextColor.GRAY), + Component.empty() + ).build(); + } + + @Override + protected void click(@NotNull Player player, int slot, @NotNull Click click, @NotNull ItemStack stack, @NotNull Consumer result) { + result.accept(ClickHolder.cancelClick()); + EventDispatcher.call(new PlayerDialogRequestEvent(player, PlayerDialogRequestEvent.Target.SETUP_SHUFFLE_INTERVAL)); + } +} From 4a3cf811a023fc4ff6e7f08b3e0070d5d2469ade Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:21:47 +0200 Subject: [PATCH 08/43] chore(registry): register new dialogs --- .../theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java index bad56987..b6aaebc3 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java @@ -39,6 +39,8 @@ public SetupDialogRegistry() { )); this.registerDialog(new DeleteDialog()); this.registerDialog(new ValueInputDialog()); + this.registerDialog(new WeightInputDialog()); + this.registerDialog(new ShuffleIntervalInputDialog()); } private void registerDialog(@NotNull DialogTemplate dialog) { From 8711c4c9b93235f4ea7f58e0f0be571c45e98a6c Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:22:40 +0200 Subject: [PATCH 09/43] chore(dialog): add new request targets --- .../event/PlayerDialogRequestEvent.java | 10 +++++- .../PlayerCustomClickEventListener.java | 33 +++++++++++++++++++ .../dialog/PlayerDialogRequestListener.java | 2 ++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/event/PlayerDialogRequestEvent.java b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/event/PlayerDialogRequestEvent.java index 3cc3ef0d..b58fcfc1 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/event/PlayerDialogRequestEvent.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/event/PlayerDialogRequestEvent.java @@ -58,7 +58,15 @@ public enum Target { /** * The target for the dialog request is to set up the block bounce. */ - SETUP_BLOCK_BOUNCE + SETUP_BLOCK_BOUNCE, + /** + * The target for the dialog request is to set up the block weight/chance. + */ + SETUP_BLOCK_WEIGHT, + /** + * The target for the dialog request is to set up the reshuffle interval. + */ + SETUP_SHUFFLE_INTERVAL ; } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java index d35db99f..149815cd 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java @@ -60,12 +60,45 @@ public void accept(@NotNull PlayerCustomClickEvent event) { case AuthorInputDialog ignored -> handleAuthorSet(data, dialogData); case DeleteDialog ignored -> this.handleDataDelete(data, dialogData); case ValueInputDialog ignored -> this.handleValueUpdate(player, data, dialogData); + case WeightInputDialog ignored -> this.handleWeightUpdate(player, data, dialogData); + case ShuffleIntervalInputDialog ignored -> this.handleShuffleIntervalSet(data, dialogData); default -> throw new IllegalStateException("Unexpected dialog type: " + dialogTemplate.getClass().getCanonicalName()); } }); } + /** + * Handles setting the reshuffle interval based on the dialog data provided. + * @param data the BounceData instance containing the map builder + * @param dialogData the dialog data containing the interval ticks to set + */ + private void handleShuffleIntervalSet(@NotNull BounceData data, @NotNull CompoundBinaryTag dialogData) { + int ticks = (int) dialogData.getFloat("interval_ticks", 100f); + if (ticks < 20) ticks = 20; + data.getMapBuilder().shuffleIntervalTicks(ticks); + data.triggerUpdate(); + } + + /** + * Handles the update of a weight/chance based on the dialog data provided. + * @param player the player who triggered the dialog + * @param data the BounceData instance containing the map builder + * @param dialogData the dialog data containing the weight percentage to update + */ + private void handleWeightUpdate(@NotNull Player player, @NotNull BounceData data, @NotNull CompoundBinaryTag dialogData) { + float percentage = dialogData.getFloat("weight_percentage", 5.0f); + double weight = Math.max(0.0, Math.min(1.0, Math.round((percentage / 100.0) * 1000.0) / 1000.0)); + int valueIndex = player.hasTag(SetupTags.PUSH_SLOT_INDEX) ? player.getTag(SetupTags.PUSH_SLOT_INDEX) : 0; + player.removeTag(SetupTags.PUSH_SLOT_INDEX); + data.getMapBuilder().getPushDataBuilder().getPushValues().get(valueIndex).setWeight(weight); + if (valueIndex == 0) { + data.triggerGroundViewUpdate(); + } else { + data.triggerPushValueUpdate(valueIndex); + } + } + /** * Handles the update of a value based on the dialog data provided. * @param player the player who triggered the dialog diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerDialogRequestListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerDialogRequestListener.java index 4853bc0f..57c11919 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerDialogRequestListener.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerDialogRequestListener.java @@ -33,6 +33,8 @@ public void accept(@NotNull PlayerDialogRequestEvent event) { case Target.SETUP_AUTHOR -> dialogTemplate = dialogRegistry.get(AuthorInputDialog.DIALOG_KEY); case Target.SETUP_REQUEST_AUTHOR -> dialogTemplate = dialogRegistry.get(AuthorRequestDialog.DIALOG_KEY); case Target.SETUP_BLOCK_BOUNCE -> dialogTemplate = dialogRegistry.get(ValueInputDialog.DIALOG_KEY); + case Target.SETUP_BLOCK_WEIGHT -> dialogTemplate = dialogRegistry.get(net.theevilreaper.bounce.setup.dialog.type.WeightInputDialog.DIALOG_KEY); + case Target.SETUP_SHUFFLE_INTERVAL -> dialogTemplate = dialogRegistry.get(net.theevilreaper.bounce.setup.dialog.type.ShuffleIntervalInputDialog.DIALOG_KEY); default -> throw new IllegalArgumentException("Unknown target: " + target); } From 34c673ca55569921e2f620a10312ede9dfae40f9 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:23:18 +0200 Subject: [PATCH 10/43] feat(area): add area related code --- .../bounce/common/ground/Area.java | 21 ++- .../bounce/common/ground/AreaFiller.java | 96 ++++++++++++ .../ground/AreaFillerIntegrationTest.java | 137 ++++++++++++++++++ .../common/ground/AreaIntegrationTest.java | 134 +++++++++++++++++ .../bounce/common/ground/AreaTest.java | 33 ----- .../inventory/slot/AreaOverviewSlot.java | 54 +++++++ .../AreaViewInventoryIntegrationTest.java | 68 +++++++++ 7 files changed, 508 insertions(+), 35 deletions(-) create mode 100644 common/src/main/java/net/theevilreaper/bounce/common/ground/AreaFiller.java create mode 100644 common/src/test/java/net/theevilreaper/bounce/common/ground/AreaFillerIntegrationTest.java create mode 100644 common/src/test/java/net/theevilreaper/bounce/common/ground/AreaIntegrationTest.java delete mode 100644 common/src/test/java/net/theevilreaper/bounce/common/ground/AreaTest.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AreaOverviewSlot.java create mode 100644 setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventoryIntegrationTest.java diff --git a/common/src/main/java/net/theevilreaper/bounce/common/ground/Area.java b/common/src/main/java/net/theevilreaper/bounce/common/ground/Area.java index bab02a4c..26535e93 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/ground/Area.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/ground/Area.java @@ -1,19 +1,28 @@ package net.theevilreaper.bounce.common.ground; import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.theevilreaper.bounce.common.push.PushData; +import java.util.List; + /** * The {@link Area} interface represents an area in the game. * * @author theEvilReaper - * @version 1.0.0 + * @version 1.1.0 * @since 0.1.0 */ public interface Area { - void calculatePositions(); + /** + * Scans the volume between {@link #min()} and {@link #max()} in the given instance and records every position + * whose block matches {@link #groundBlock()}. A no-op if positions were already calculated, see {@link #hasPositions()}. + * + * @param instance the instance to scan + */ + void calculatePositions(Instance instance); /** * Returns a boolean indicator if the are includes an amount of positions. @@ -22,6 +31,14 @@ public interface Area { */ boolean hasPositions(); + /** + * Returns the positions calculated by {@link #calculatePositions(Instance)}, or an empty list if it hasn't + * been called yet. + * + * @return an unmodifiable view of the calculated positions + */ + List positions(); + /** * Returns the minimum point of the area. * diff --git a/common/src/main/java/net/theevilreaper/bounce/common/ground/AreaFiller.java b/common/src/main/java/net/theevilreaper/bounce/common/ground/AreaFiller.java new file mode 100644 index 00000000..ea7ccc5e --- /dev/null +++ b/common/src/main/java/net/theevilreaper/bounce/common/ground/AreaFiller.java @@ -0,0 +1,96 @@ +package net.theevilreaper.bounce.common.ground; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.theevilreaper.bounce.common.push.PushEntry; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Fills an {@link Area}'s scanned positions with a weighted-random mix of its {@link PushEntry} blocks, and + * partially reshuffles that fill at runtime. + * + * @author theEvilReaper + * @version 1.0.0 + * @since 1.0.0 + */ +public final class AreaFiller { + + private AreaFiller() { + // Prevent instantiation + } + + /** + * Scans the area (if not already scanned) and fills every found position with a weighted-random block. + * + * @param instance the instance to place blocks in + * @param area the area to fill + */ + public static void fill(@NotNull Instance instance, @NotNull Area area) { + area.calculatePositions(instance); + + List entries = area.data().push(); + for (Vec position : area.positions()) { + instance.setBlock(position, pickWeightedBlock(entries, area.groundBlock())); + } + } + + /** + * Re-rolls {@code percentage} of the area's already scanned positions, skipping the position directly under + * any of the given players so nobody's ground changes under their feet. + * + * @param instance the instance to place blocks in + * @param area the area to reshuffle, must already have positions calculated (see {@link #fill}) + * @param percentage the fraction (0.0-1.0) of positions to re-roll + * @param players players whose current standing position must not be touched + */ + public static void reshuffle(@NotNull Instance instance, @NotNull Area area, double percentage, @NotNull Collection players) { + List positions = area.positions(); + if (positions.isEmpty()) return; + + Set excluded = new HashSet<>(); + for (Player player : players) { + Pos playerPosition = player.getPosition(); + excluded.add(new Vec(Math.floor(playerPosition.x()), Math.floor(playerPosition.y() - 1), Math.floor(playerPosition.z()))); + } + + List candidates = new ArrayList<>(); + for (Vec position : positions) { + if (!excluded.contains(position)) candidates.add(position); + } + if (candidates.isEmpty()) return; + + Collections.shuffle(candidates, ThreadLocalRandom.current()); + int amount = Math.min(candidates.size(), (int) Math.round(positions.size() * percentage)); + + List entries = area.data().push(); + for (int i = 0; i < amount; i++) { + Vec position = candidates.get(i); + instance.setBlock(position, pickWeightedBlock(entries, area.groundBlock())); + } + } + + private static @NotNull Block pickWeightedBlock(@NotNull List entries, @NotNull Block fallback) { + double roll = ThreadLocalRandom.current().nextDouble(); // 0.0 to 1.0 + double cumulative = 0.0; + for (PushEntry entry : entries) { + if (entry.isGround()) continue; + double p = Math.max(0.0, Math.min(1.0, entry.getWeight())); + cumulative += p; + if (roll < cumulative) { + return entry.getBlock(); + } + } + return fallback; + } +} diff --git a/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaFillerIntegrationTest.java b/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaFillerIntegrationTest.java new file mode 100644 index 00000000..c93b3302 --- /dev/null +++ b/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaFillerIntegrationTest.java @@ -0,0 +1,137 @@ +package net.theevilreaper.bounce.common.ground; + +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import net.theevilreaper.bounce.common.push.PushData; +import net.theevilreaper.bounce.common.push.PushEntry; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MicrotusExtension.class) +class AreaFillerIntegrationTest { + + @Test + void testFillFallsBackToGroundBlockWhenPushDataEmpty(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + for (int x = 0; x < 5; x++) { + instance.setBlock(x, 0, 0, Block.GLASS); + } + + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(4, 0, 0), Block.GLASS, new PushData(List.of())); + + AreaFiller.fill(instance, area); + + for (Vec position : area.positions()) { + assertTrue(instance.getBlock(position).compare(Block.GLASS)); + } + + env.destroyInstance(instance, true); + } + + @Test + void testFillNeverPicksAZeroWeightEntry(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + for (int x = 0; x < 30; x++) { + instance.setBlock(x, 0, 0, Block.GLASS); + } + + PushData pushData = new PushData(List.of( + PushEntry.groundEntry(Block.GLASS, 1, 1.0), + PushEntry.pushEntry(Block.DIAMOND_BLOCK, 5, 0.0) + )); + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(29, 0, 0), Block.GLASS, pushData); + + AreaFiller.fill(instance, area); + + for (Vec position : area.positions()) { + assertFalse(instance.getBlock(position).compare(Block.DIAMOND_BLOCK), "A weight of 0 must never be picked"); + } + + env.destroyInstance(instance, true); + } + + @Test + void testFillDistributionRoughlyFollowsWeights(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + for (int x = 0; x < 100; x++) { + instance.setBlock(x, 0, 0, Block.GLASS); + } + + PushData pushData = new PushData(List.of( + PushEntry.groundEntry(Block.GLASS, 1, 1.0), + PushEntry.pushEntry(Block.GOLD_BLOCK, 1, 0.8) // 80% gold, 20% glass + )); + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(99, 0, 0), Block.GLASS, pushData); + + AreaFiller.fill(instance, area); + + long goldCount = area.positions().stream().filter(pos -> instance.getBlock(pos).compare(Block.GOLD_BLOCK)).count(); + long glassCount = area.positions().stream().filter(pos -> instance.getBlock(pos).compare(Block.GLASS)).count(); + + assertEquals(100, goldCount + glassCount); + assertTrue(goldCount > glassCount, "Gold has 80% probability and should dominate the distribution"); + + env.destroyInstance(instance, true); + } + + @Test + void testReshuffleSkipsThePositionUnderAPlayer(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + for (int x = 0; x < 10; x++) { + instance.setBlock(x, 0, 0, Block.GLASS); + } + // A block no fill/reshuffle would ever place, so if it's still here afterwards we know the position was skipped + instance.setBlock(0, 0, 0, Block.WATER); + + Player player = env.createPlayer(instance); + player.teleport(new net.minestom.server.coordinate.Pos(0.5, 1, 0.5)).join(); + + PushData pushData = new PushData(List.of(PushEntry.groundEntry(Block.GLASS, 1, 1.0))); + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(9, 0, 0), Block.GLASS, pushData); + area.calculatePositions(instance); + + AreaFiller.reshuffle(instance, area, 1.0, List.of(player)); + + assertTrue(instance.getBlock(0, 0, 0).compare(Block.WATER), "The position under the player must be left untouched"); + + env.destroyInstance(instance, true); + } + + @Test + void testReshuffleExcludesPlayerPositionEvenWhenItIsAValidCandidate(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + for (int x = 0; x < 10; x++) { + instance.setBlock(x, 0, 0, Block.GLASS); + } + + Player player = env.createPlayer(instance); + player.teleport(new net.minestom.server.coordinate.Pos(0.5, 1, 0.5)).join(); + + // Only a non-ground entry with probability 1.0 is configured, so every reshuffled position is guaranteed to + // become DIAMOND_BLOCK unless it was excluded because a player stands on it. + PushData pushData = new PushData(List.of(PushEntry.pushEntry(Block.DIAMOND_BLOCK, 1, 1.0))); + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(9, 0, 0), Block.GLASS, pushData); + area.calculatePositions(instance); + + AreaFiller.reshuffle(instance, area, 1.0, List.of(player)); + + assertTrue(instance.getBlock(0, 0, 0).compare(Block.GLASS), "The position under the player was a valid candidate but must remain untouched"); + for (int x = 1; x < 10; x++) { + assertTrue(instance.getBlock(x, 0, 0).compare(Block.DIAMOND_BLOCK), "Every other position should have been reshuffled"); + } + + env.destroyInstance(instance, true); + } +} diff --git a/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaIntegrationTest.java b/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaIntegrationTest.java new file mode 100644 index 00000000..45348e98 --- /dev/null +++ b/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaIntegrationTest.java @@ -0,0 +1,134 @@ +package net.theevilreaper.bounce.common.ground; + +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import net.theevilreaper.bounce.common.push.PushData; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MicrotusExtension.class) +class AreaIntegrationTest { + + @Test + void testCalculatePositionsOnlyIncludesMatchingGroundBlock(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + instance.setBlock(0, 0, 0, Block.AMETHYST_BLOCK); + instance.setBlock(1, 0, 0, Block.AMETHYST_BLOCK); + instance.setBlock(2, 0, 0, Block.STONE); // not the configured ground block + + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(2, 0, 0), Block.AMETHYST_BLOCK, new PushData(List.of())); + assertFalse(area.hasPositions()); + + area.calculatePositions(instance); + + assertTrue(area.hasPositions()); + assertEquals(2, area.positions().size()); + assertTrue(area.positions().contains(new Vec(0, 0, 0))); + assertTrue(area.positions().contains(new Vec(1, 0, 0))); + assertFalse(area.positions().contains(new Vec(2, 0, 0))); + + env.destroyInstance(instance, true); + assertTrue(instance.getPlayers().isEmpty()); + } + + @Test + void testCalculatePositionsIsIdempotent(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + instance.setBlock(0, 0, 0, Block.GLASS); + + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(0, 0, 0), Block.GLASS, new PushData(List.of())); + area.calculatePositions(instance); + instance.setBlock(0, 0, 0, Block.STONE); // world changes after the first scan + + area.calculatePositions(instance); // second call must be a no-op + + assertEquals(1, area.positions().size(), "A second call must not re-scan or clear the already computed positions"); + + env.destroyInstance(instance, true); + } + + @Test + void testPositionsIsUnmodifiable(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + instance.setBlock(0, 0, 0, Block.GLASS); + + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(0, 0, 0), Block.GLASS, new PushData(List.of())); + area.calculatePositions(instance); + + assertThrows(UnsupportedOperationException.class, () -> area.positions().add(new Vec(9, 9, 9))); + + env.destroyInstance(instance, true); + } + + @Test + void testCalculatePositionsLoadsChunksInArea(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + // Coordinates spanning multiple chunks (e.g. chunk -2, 3 to chunk -1, 4) + Vec min = new Vec(-25, 60, -25); + Vec max = new Vec(25, 60, 25); + + Area area = new GroundArea(min, max, Block.STONE, new PushData(List.of())); + assertDoesNotThrow(() -> area.calculatePositions(instance)); + + env.destroyInstance(instance, true); + } + + @Test + void testCalculatePositionsIncludesConfiguredPushBlocks(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + instance.setBlock(0, 0, 0, Block.AMETHYST_BLOCK); // ground block + instance.setBlock(1, 0, 0, Block.GOLD_BLOCK); // existing push block + instance.setBlock(2, 0, 0, Block.STONE); // unrelated block + + PushData pushData = PushData.builder() + .add(net.theevilreaper.bounce.common.push.PushEntry.groundEntry(Block.AMETHYST_BLOCK, 1)) + .add(net.theevilreaper.bounce.common.push.PushEntry.pushEntry(Block.GOLD_BLOCK, 2)) + .build(); + + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(2, 0, 0), Block.AMETHYST_BLOCK, pushData); + area.calculatePositions(instance); + + assertEquals(2, area.positions().size()); + assertTrue(area.positions().contains(new Vec(0, 0, 0))); + assertTrue(area.positions().contains(new Vec(1, 0, 0))); + assertFalse(area.positions().contains(new Vec(2, 0, 0))); + + env.destroyInstance(instance, true); + } + + @Test + void testCalculatePositionsScansLayerBelowWhenStandingOnPlatform(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + // Platform block is placed at Y=63, air at Y=64 + instance.setBlock(0, 63, 0, Block.GLASS); + instance.setBlock(0, 64, 0, Block.AIR); + + // Area captured at player feet level (Y=64) + Area area = new GroundArea(new Vec(0, 64, 0), new Vec(0, 64, 0), Block.GLASS, new PushData(List.of())); + area.calculatePositions(instance); + + assertEquals(1, area.positions().size(), "Should fallback to Y=63 when Y=64 has no ground blocks"); + assertEquals(new Vec(0, 63, 0), area.positions().getFirst()); + + env.destroyInstance(instance, true); + } +} diff --git a/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaTest.java b/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaTest.java deleted file mode 100644 index c6f6aa73..00000000 --- a/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.theevilreaper.bounce.common.ground; - -import net.minestom.server.coordinate.Vec; -import net.minestom.server.instance.block.Block; -import net.theevilreaper.bounce.common.push.PushData; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -class AreaTest { - - @Test - void testArea() { - Area area = new GroundArea(Vec.ZERO, Vec.ONE, Block.AMETHYST_BLOCK, new PushData(List.of())); - - assertNotNull(area); - assertEquals(Vec.ZERO, area.min()); - assertEquals(Vec.ONE, area.max()); - assertEquals(Block.AMETHYST_BLOCK, area.groundBlock()); - assertNotNull(area.data()); - assertNotNull(area.data().push()); - assertTrue(area.data().push().isEmpty()); - assertEquals(0.0, area.data().getPush(area.groundBlock())); - - assertFalse(area.hasPositions()); - - area.calculatePositions(); - - assertTrue(area.hasPositions()); - } -} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AreaOverviewSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AreaOverviewSlot.java new file mode 100644 index 00000000..949a1b43 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AreaOverviewSlot.java @@ -0,0 +1,54 @@ +package net.theevilreaper.bounce.setup.inventory.slot; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.theevilreaper.aves.inventory.click.ClickHolder; +import net.theevilreaper.bounce.common.ground.Area; +import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent; +import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent.SwitchTarget; +import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public final class AreaOverviewSlot extends AbstractDataSlot { + + private final @Nullable Area area; + + public AreaOverviewSlot(@NotNull OverviewType overviewType, @Nullable Area area) { + super(overviewType); + this.area = area; + } + + @Override + public ItemStack getItem() { + ItemStack overviewItem = this.type.getItem(); + if (area == null) { + return asBuilder(overviewItem).lore( + Component.empty(), + Component.text("Not set", NamedTextColor.RED), + Component.empty(), + Component.text("Click to configure", NamedTextColor.GRAY), + Component.empty() + ).build(); + } + return asBuilder(overviewItem).lore( + Component.empty(), + Component.text("Configured", type.getColor()), + Component.empty(), + Component.text("Click to edit", NamedTextColor.GRAY), + Component.empty() + ).build(); + } + + @Override + protected void click(@NotNull Player player, int slot, @NotNull Click click, @NotNull ItemStack stack, @NotNull Consumer result) { + result.accept(ClickHolder.cancelClick()); + EventDispatcher.call(new SetupInventorySwitchEvent(player, SwitchTarget.AREA_VIEW)); + } +} diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventoryIntegrationTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventoryIntegrationTest.java new file mode 100644 index 00000000..1b628c9e --- /dev/null +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventoryIntegrationTest.java @@ -0,0 +1,68 @@ +package net.theevilreaper.bounce.setup.inventory.ground; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.item.Material; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import net.theevilreaper.aves.inventory.layout.InventoryLayout; +import net.theevilreaper.aves.inventory.slot.ISlot; +import net.theevilreaper.bounce.setup.builder.GameMapBuilder; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MicrotusExtension.class) +class AreaViewInventoryIntegrationTest { + + private static final int POS1_SLOT = 11; + private static final int POS2_SLOT = 13; + private static final int CONFIRM_SLOT = 15; + + @Test + void testConfirmSlotStartsDisabledAndBothPositionsAreUnset(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + GameMapBuilder gameMapBuilder = new GameMapBuilder(); + + AreaViewInventory inventory = new AreaViewInventory(player, gameMapBuilder); + inventory.open(); + env.tick(); + + InventoryLayout dataLayout = inventory.getDataLayout(); + assertEquals(Material.GRAY_DYE, dataLayout.getSlot(CONFIRM_SLOT).getItem().material()); + + env.destroyInstance(instance, true); + } + + @Test + void testClickingBothPosButtonsEnablesConfirmAndSavesArea(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + player.teleport(new Pos(1, 2, 3)).join(); + GameMapBuilder gameMapBuilder = new GameMapBuilder(); + + AreaViewInventory inventory = new AreaViewInventory(player, gameMapBuilder); + inventory.open(); + env.tick(); + + inventory.setPos1ToCurrentPosition(player); + inventory.setPos2ToCurrentPosition(player); + env.tick(); + + InventoryLayout dataLayout = inventory.getDataLayout(); + ISlot confirmSlot = dataLayout.getSlot(CONFIRM_SLOT); + assertEquals(Material.LIME_DYE, confirmSlot.getItem().material()); + + inventory.confirm(player); + + assertNotNull(gameMapBuilder.getArea()); + assertEquals(new net.minestom.server.coordinate.Vec(1, 2, 3), gameMapBuilder.getArea().min()); + assertEquals(new net.minestom.server.coordinate.Vec(1, 2, 3), gameMapBuilder.getArea().max()); + + env.destroyInstance(instance, true); + } +} From 9f3ce7c0e764f24ae1c345531c07757c37d68944 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:23:43 +0200 Subject: [PATCH 11/43] chore(map): wire area object --- .../bounce/common/map/GameMap.java | 41 +++++++++-- .../bounce/common/map/GameMapTest.java | 4 +- .../bounce/setup/builder/GameMapBuilder.java | 69 ++++++++++++++++--- .../setup/builder/GameMapBuilderTest.java | 34 +++++++++ 4 files changed, 131 insertions(+), 17 deletions(-) diff --git a/common/src/main/java/net/theevilreaper/bounce/common/map/GameMap.java b/common/src/main/java/net/theevilreaper/bounce/common/map/GameMap.java index c29c6127..96277974 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/map/GameMap.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/map/GameMap.java @@ -2,7 +2,9 @@ import net.minestom.server.coordinate.Pos; import net.theevilreaper.aves.map.BaseMap; +import net.theevilreaper.bounce.common.ground.Area; import net.theevilreaper.bounce.common.push.PushData; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -11,26 +13,33 @@ * It holds data about the used positions and other things. * * @author theEvilReaper - * @version 1.1.0 + * @version 1.2.0 * @since 0.1.0 */ public final class GameMap extends BaseMap { private final Pos gameSpawn; private final PushData pushData; + private final @Nullable Area area; + private final int shuffleIntervalTicks; /** * Creates a new reference from the map class. * - * @param name the name of the map - * @param spawn the spawn position - * @param gameSpawn the spawn position during the game - * @param pushData the {@link PushData} which includes information about push values + * @param name the name of the map + * @param spawn the spawn position + * @param gameSpawn the spawn position during the game + * @param pushData the {@link PushData} which includes information about push values + * @param builders the list of builders who worked on the map + * @param area the ground area which gets dynamically filled, or {@code null} for a fully manual map + * @param shuffleIntervalTicks the amount of ticks between two runtime reshuffles of the area */ - public GameMap(String name, Pos spawn, Pos gameSpawn, PushData pushData, List builders) { + public GameMap(String name, Pos spawn, Pos gameSpawn, PushData pushData, List builders, @Nullable Area area, int shuffleIntervalTicks) { super(name, spawn, builders); this.gameSpawn = gameSpawn; this.pushData = pushData; + this.area = area; + this.shuffleIntervalTicks = shuffleIntervalTicks; } /** @@ -50,4 +59,22 @@ public PushData getPushData() { public Pos getGameSpawn() { return gameSpawn; } -} \ No newline at end of file + + /** + * Returns the dynamically filled ground area of this map, or {@code null} for a fully manual map. + * + * @return the area, or {@code null} + */ + public @Nullable Area getArea() { + return area; + } + + /** + * Returns the amount of ticks between two runtime reshuffles of the area. + * + * @return the interval in ticks + */ + public int getShuffleIntervalTicks() { + return shuffleIntervalTicks; + } +} diff --git a/common/src/test/java/net/theevilreaper/bounce/common/map/GameMapTest.java b/common/src/test/java/net/theevilreaper/bounce/common/map/GameMapTest.java index 8a34c14e..0c06605e 100644 --- a/common/src/test/java/net/theevilreaper/bounce/common/map/GameMapTest.java +++ b/common/src/test/java/net/theevilreaper/bounce/common/map/GameMapTest.java @@ -13,7 +13,7 @@ class GameMapTest { @Test void testGameMap() { - GameMap gameMap = new GameMap("Test-Map", Pos.ZERO, new Pos(10, 0, 10), PushData.builder().build(), List.of()); + GameMap gameMap = new GameMap("Test-Map", Pos.ZERO, new Pos(10, 0, 10), PushData.builder().build(), List.of(), null, 100); assertNotNull(gameMap); assertInstanceOf(BaseMap.class, gameMap, "The GameMap should be rely on the BaseMap"); assertEquals("Test-Map", gameMap.name()); @@ -22,5 +22,7 @@ void testGameMap() { assertNotEquals(gameMap.getGameSpawn(), gameMap.spawn()); assertNotNull(gameMap.getPushData()); assertTrue(gameMap.getPushData().push().isEmpty()); + assertNull(gameMap.getArea(), "A map without an area should report null"); + assertEquals(100, gameMap.getShuffleIntervalTicks()); } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java index 60d2e4e7..8a4f2e5a 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java @@ -3,37 +3,48 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.instance.block.Block; import net.theevilreaper.aves.map.BaseMapBuilder; +import net.theevilreaper.bounce.common.ground.Area; import net.theevilreaper.bounce.common.map.GameMap; import net.theevilreaper.bounce.common.push.PushData; import net.theevilreaper.bounce.common.push.PushEntry; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public final class GameMapBuilder extends BaseMapBuilder { + private static final int DEFAULT_SHUFFLE_INTERVAL_TICKS = 100; + private final PushData.Builder pushDataBuilder; private Pos gameSpawn; + private @Nullable Area area; + private int shuffleIntervalTicks; public GameMapBuilder() { super(); + this.shuffleIntervalTicks = DEFAULT_SHUFFLE_INTERVAL_TICKS; this.pushDataBuilder = PushData.builder(); this.pushDataBuilder - .add(PushEntry.groundEntry(Block.GLASS, 1)) - .add(PushEntry.pushEntry(Block.GOLD_BLOCK, 1)) - .add(PushEntry.pushEntry(Block.DIAMOND_BLOCK, 1)) - .add(PushEntry.pushEntry(Block.EMERALD_BLOCK, 1)); + .add(PushEntry.groundEntry(Block.GLASS, 1, 1.0)) + .add(PushEntry.pushEntry(Block.GOLD_BLOCK, 1, 0.05)) + .add(PushEntry.pushEntry(Block.DIAMOND_BLOCK, 1, 0.03)) + .add(PushEntry.pushEntry(Block.EMERALD_BLOCK, 1, 0.02)); } public GameMapBuilder(@NotNull GameMap gameMap) { super(gameMap); this.gameSpawn = gameMap.getGameSpawn(); + this.area = gameMap.getArea(); + this.shuffleIntervalTicks = gameMap.getShuffleIntervalTicks() > 0 + ? gameMap.getShuffleIntervalTicks() + : DEFAULT_SHUFFLE_INTERVAL_TICKS; if (gameMap.getPushData() == null) { this.pushDataBuilder = PushData.builder(); this.pushDataBuilder - .add(PushEntry.groundEntry(Block.GLASS, 1)) - .add(PushEntry.pushEntry(Block.GOLD_BLOCK, 1)) - .add(PushEntry.pushEntry(Block.DIAMOND_BLOCK, 1)) - .add(PushEntry.pushEntry(Block.EMERALD_BLOCK, 1)); + .add(PushEntry.groundEntry(Block.GLASS, 1, 1.0)) + .add(PushEntry.pushEntry(Block.GOLD_BLOCK, 1, 0.05)) + .add(PushEntry.pushEntry(Block.DIAMOND_BLOCK, 1, 0.03)) + .add(PushEntry.pushEntry(Block.EMERALD_BLOCK, 1, 0.02)); } else{ this.pushDataBuilder = PushData.builder(gameMap.getPushData()); } @@ -62,6 +73,28 @@ public GameMapBuilder(@NotNull GameMap gameMap) { return this; } + /** + * Sets the ground area which gets dynamically filled. + * + * @param area the area, or {@code null} to disable dynamic filling + * @return this builder instance for chaining + */ + public @NotNull GameMapBuilder area(@Nullable Area area) { + this.area = area; + return this; + } + + /** + * Sets the amount of ticks between two runtime reshuffles of the area. + * + * @param shuffleIntervalTicks the interval in ticks + * @return this builder instance for chaining + */ + public @NotNull GameMapBuilder shuffleIntervalTicks(int shuffleIntervalTicks) { + this.shuffleIntervalTicks = shuffleIntervalTicks; + return this; + } + /** * Builds a new {@link GameMap} instance with the current properties. * @@ -69,7 +102,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { */ @Override public @NotNull GameMap build() { - return new GameMap(this.name, this.spawn, this.gameSpawn, pushDataBuilder.build(), this.builders); + return new GameMap(this.name, this.spawn, this.gameSpawn, pushDataBuilder.build(), this.builders, this.area, this.shuffleIntervalTicks); } /** @@ -81,6 +114,24 @@ public Pos getGameSpawn() { return gameSpawn; } + /** + * Returns the ground area which gets dynamically filled. + * + * @return the area, or {@code null} + */ + public @Nullable Area getArea() { + return area; + } + + /** + * Returns the amount of ticks between two runtime reshuffles of the area. + * + * @return the interval in ticks + */ + public int getShuffleIntervalTicks() { + return shuffleIntervalTicks; + } + /** * Returns the {@link PushData.Builder} instance used to build push data. * diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java index c2ed4e88..4de7217d 100644 --- a/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java @@ -1,7 +1,10 @@ package net.theevilreaper.bounce.setup.builder; import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; +import net.theevilreaper.bounce.common.ground.Area; +import net.theevilreaper.bounce.common.ground.GroundArea; import net.theevilreaper.bounce.common.map.GameMap; import net.theevilreaper.bounce.common.push.PushData; import net.theevilreaper.bounce.common.push.PushEntry; @@ -72,4 +75,35 @@ void testGameMapBuilderInitializationWithExistingData() { //assertTrue(anotherBuilder.getAuthors().contains("Test"), "Authors should contain 'Test'"); assertEquals(4, anotherBuilder.getPushDataBuilder().getPushValues().size(), "Push data should contain four entries"); } + + @Test + void testNewBuilderHasNoAreaAndDefaultInterval() { + GameMapBuilder builder = new GameMapBuilder(); + assertNull(builder.getArea()); + assertTrue(builder.getShuffleIntervalTicks() > 0, "A newly created map should have a sane default interval"); + } + + @Test + void testAreaAndShuffleIntervalRoundTripThroughBuild() { + GameMapBuilder builder = new GameMapBuilder(); + Area area = new GroundArea(Vec.ZERO, new Vec(5, 0, 5), Block.GLASS, PushData.builder().build()); + + builder.area(area).shuffleIntervalTicks(60); + + assertEquals(area, builder.getArea()); + assertEquals(60, builder.getShuffleIntervalTicks()); + + GameMap built = builder.build(); + assertEquals(area, built.getArea()); + assertEquals(60, built.getShuffleIntervalTicks()); + } + + @Test + void testReloadingExistingMapWithoutAreaKeepsDefaultInterval() { + GameMap gameMap = new GameMapBuilder().build(); + GameMapBuilder reloaded = new GameMapBuilder(gameMap); + + assertNull(reloaded.getArea()); + assertTrue(reloaded.getShuffleIntervalTicks() > 0); + } } \ No newline at end of file From 617b28dbe5091ef649f0679d24d42c471ae647ce Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:23:55 +0200 Subject: [PATCH 12/43] test(inventory): update slots --- .../inventory/overview/MapOverviewInventoryIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventoryIntegrationTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventoryIntegrationTest.java index fce0b8a0..42b0576b 100644 --- a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventoryIntegrationTest.java +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventoryIntegrationTest.java @@ -67,7 +67,7 @@ void testMapOverviewDataLayout(@NotNull Env env) { assertNotNull(dataLayout, "Data layout should not be null"); - int[] dataSlots = LayoutCalculator.from(10, 12, 14, 16); + int[] dataSlots = LayoutCalculator.from(10, 11, 12, 13, 14, 15); OverviewType[] overviewTypes = OverviewType.getValues(); for (int i = 0; i < overviewTypes.length && i < dataSlots.length; i++) { From 57e5fc9bcb4a4e51462bb3795abacd30a1eff56b Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:24:11 +0200 Subject: [PATCH 13/43] chore(push): add weight logic --- .../common/adapter/PushDataAdapter.java | 20 +++-- .../bounce/common/push/PushDataBuilder.java | 4 +- .../bounce/common/push/PushEntry.java | 90 +++++++++++++++++-- .../common/adapter/PushDataAdapterTest.java | 35 ++++++++ .../common/push/PushDataBuilderTest.java | 22 +++++ .../bounce/common/push/PushEntryTest.java | 50 +++++++++++ .../inventory/push/PushValueInventory.java | 13 ++- .../PushValueInventoryIntegrationTest.java | 43 +++++++++ 8 files changed, 260 insertions(+), 17 deletions(-) create mode 100644 common/src/test/java/net/theevilreaper/bounce/common/push/PushDataBuilderTest.java create mode 100644 setup/src/test/java/net/theevilreaper/bounce/setup/inventory/push/PushValueInventoryIntegrationTest.java diff --git a/common/src/main/java/net/theevilreaper/bounce/common/adapter/PushDataAdapter.java b/common/src/main/java/net/theevilreaper/bounce/common/adapter/PushDataAdapter.java index ac9e224b..b0f74038 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/adapter/PushDataAdapter.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/adapter/PushDataAdapter.java @@ -18,11 +18,14 @@ * Serializer and Deserializer implementation for {@link PushData} object. * * @author theEvilReaper - * @version 1.0.0 + * @version 1.1.0 * @since 1.0.0 */ public class PushDataAdapter implements JsonDeserializer, JsonSerializer { + private static final double DEFAULT_GROUND_WEIGHT = 1.0; + private static final double DEFAULT_PUSH_WEIGHT = 0.05; + @Override public PushData deserialize(JsonElement element, Type type, JsonDeserializationContext context) { JsonArray jsonArray = element.getAsJsonArray(); @@ -33,15 +36,19 @@ public PushData deserialize(JsonElement element, Type type, JsonDeserializationC } for (JsonElement jsonElement : jsonArray.asList()) { - Key blockKey = context.deserialize(jsonElement.getAsJsonObject().get("block"), Key.class); - int value = jsonElement.getAsJsonObject().get("value").getAsInt(); - boolean ground = jsonElement.getAsJsonObject().get("ground").getAsBoolean(); + JsonObject jsonObject = jsonElement.getAsJsonObject(); + Key blockKey = context.deserialize(jsonObject.get("block"), Key.class); + int value = jsonObject.get("value").getAsInt(); + boolean ground = jsonObject.get("ground").getAsBoolean(); + double weight = jsonObject.has("weight") + ? jsonObject.get("weight").getAsDouble() + : (ground ? DEFAULT_GROUND_WEIGHT : DEFAULT_PUSH_WEIGHT); Block block = Block.fromKey(blockKey); if (ground) { - builder.add(0, PushEntry.groundEntry(block, value)); + builder.add(0, PushEntry.groundEntry(block, value, weight)); } else { - builder.add(PushEntry.pushEntry(block, value)); + builder.add(PushEntry.pushEntry(block, value, weight)); } } @@ -58,6 +65,7 @@ public JsonElement serialize(PushData data, Type type, JsonSerializationContext jsonObject.add("block", context.serialize(blockKey, Key.class)); jsonObject.addProperty("value", pushEntry.getValue()); jsonObject.addProperty("ground", pushEntry.isGround()); + jsonObject.addProperty("weight", pushEntry.getWeight()); jsonArray.add(jsonObject); }); diff --git a/common/src/main/java/net/theevilreaper/bounce/common/push/PushDataBuilder.java b/common/src/main/java/net/theevilreaper/bounce/common/push/PushDataBuilder.java index d0c4356e..31a54ec3 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/push/PushDataBuilder.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/push/PushDataBuilder.java @@ -34,9 +34,9 @@ public PushDataBuilder(PushData pushData) { for (int i = 0; i < pushData.push().size(); i++) { PushEntry entry = pushData.push().get(i); if (entry.isGround()) { - this.blocks.add(PushEntry.groundEntry(entry.getBlock(), entry.getValue())); + this.blocks.add(PushEntry.groundEntry(entry.getBlock(), entry.getValue(), entry.getWeight())); } else { - this.blocks.add(PushEntry.pushEntry(entry.getBlock(), entry.getValue())); + this.blocks.add(PushEntry.pushEntry(entry.getBlock(), entry.getValue(), entry.getWeight())); } } } diff --git a/common/src/main/java/net/theevilreaper/bounce/common/push/PushEntry.java b/common/src/main/java/net/theevilreaper/bounce/common/push/PushEntry.java index 796bd17c..797880c6 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/push/PushEntry.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/push/PushEntry.java @@ -10,7 +10,7 @@ * This class is used to manage the push values in the game. * * @author Joltra - * @version 1.0.0 + * @version 1.1.0 * @since 0.1.0 */ public final class PushEntry { @@ -18,39 +18,77 @@ public final class PushEntry { private final boolean ground; private Block block; private int value; + private double weight; /** - * Constructs a new PushEntry with the specified block and value. + * Constructs a new PushEntry with the specified block and value. Uses default weight (1.0 for ground, 0.05 for push). * * @param block the block associated with this PushEntry * @param value the initial value for this PushEntry * @return a new PushEntry instance representing a ground block entry */ public static PushEntry groundEntry(Block block, int value) { - return new PushEntry(block, value, true); + return new PushEntry(block, value, 1.0, true); } /** - * Constructs a new PushEntry with the specified block and value. + * Constructs a new PushEntry with the specified block, value and weight. + * + * @param block the block associated with this PushEntry + * @param value the initial value for this PushEntry + * @param weight the probability (0.0 to 1.0) used when this entry is picked during area filling + * @return a new PushEntry instance representing a ground block entry + */ + public static PushEntry groundEntry(Block block, int value, double weight) { + return new PushEntry(block, value, weight, true); + } + + /** + * Constructs a new PushEntry with the specified block and value. Uses a default weight of {@code 0.05} (5%). * * @param block the block associated with this PushEntry * @param value the initial value for this PushEntry * @return a new PushEntry instance */ public static PushEntry pushEntry(Block block, int value) { - return new PushEntry(block, value, false); + return new PushEntry(block, value, 0.05, false); } /** - * Constructs a new PushEntry with the specified block and value. + * Constructs a new PushEntry with the specified block, value and weight. * - * @param block the block associated with this PushEntry - * @param value the initial value for this PushEntry + * @param block the block associated with this PushEntry + * @param value the initial value for this PushEntry + * @param weight the probability (0.0 to 1.0) used when this entry is picked during area filling + * @return a new PushEntry instance + */ + public static PushEntry pushEntry(Block block, int value, double weight) { + return new PushEntry(block, value, weight, false); + } + + /** + * Constructs a new PushEntry with a default weight. + * + * @param block the block associated with this PushEntry + * @param value the initial value for this PushEntry * @param ground indicates whether this entry is a ground block entry */ public PushEntry(Block block, int value, boolean ground) { + this(block, value, ground ? 1.0 : 0.05, ground); + } + + /** + * Constructs a new PushEntry with the specified block, value, weight and ground flag. + * + * @param block the block associated with this PushEntry + * @param value the initial value for this PushEntry + * @param weight the probability (0.0 to 1.0) used when this entry is picked during area filling + * @param ground indicates whether this entry is a ground block entry + */ + public PushEntry(Block block, int value, double weight, boolean ground) { this.block = block; this.value = value; + this.weight = clampWeight(weight); this.ground = ground; } @@ -74,6 +112,15 @@ public void setValue(int value) { this.value = value; } + /** + * Sets the weight (probability 0.0 to 1.0) for this PushEntry. + * + * @param weight the new weight to set + */ + public void setWeight(double weight) { + this.weight = clampWeight(weight); + } + /** * Increments the value of this PushEntry. * If the value is already at Integer.MAX_VALUE, it does nothing. @@ -92,6 +139,24 @@ public void decrementValue() { this.value--; } + /** + * Increments the weight by 0.01 (1%), clamped at 1.0. + */ + public void incrementWeight() { + this.weight = clampWeight(this.weight + 0.01); + } + + /** + * Decrements the weight by 0.01 (1%), clamped at 0.0. + */ + public void decrementWeight() { + this.weight = clampWeight(this.weight - 0.01); + } + + private static double clampWeight(double w) { + return Math.max(0.0, Math.min(1.0, Math.round(w * 100.0) / 100.0)); + } + /** * Gets the current value of this PushEntry. * This method should be used to retrieve the value for display or processing. @@ -102,6 +167,15 @@ public int getValue() { return value; } + /** + * Gets the current weight of this PushEntry as a probability between 0.0 and 1.0. + * + * @return the current weight of this PushEntry + */ + public double getWeight() { + return weight; + } + /** * Gets the block associated with this PushEntry. * This method should be used to retrieve the block for display or processing. diff --git a/common/src/test/java/net/theevilreaper/bounce/common/adapter/PushDataAdapterTest.java b/common/src/test/java/net/theevilreaper/bounce/common/adapter/PushDataAdapterTest.java index c91e6262..d36701dd 100644 --- a/common/src/test/java/net/theevilreaper/bounce/common/adapter/PushDataAdapterTest.java +++ b/common/src/test/java/net/theevilreaper/bounce/common/adapter/PushDataAdapterTest.java @@ -31,6 +31,20 @@ class PushDataAdapterTest { ] """; + private static final String TEST_JSON_WITH_WEIGHT = """ + [ + { + "block": { + "namespace": "minecraft", + "value": "slime_block" + }, + "ground": false, + "value": 1, + "weight": 0.15 + } + ] + """; + @Test void testPushDataWrite() { PushData pushData = PushData.builder() @@ -53,4 +67,25 @@ void testPushDataRead() { assertEquals(1, pushData.getPush(Block.SLIME_BLOCK)); assertEquals(2, pushData.getPush(Block.AMETHYST_BLOCK)); } + + @Test + void testPushDataReadDefaultsMissingWeightToOne() { + PushData pushData = GsonUtil.GSON.fromJson(TEST_JSON, PushData.class); + assertEquals(1.0, pushData.push().getFirst().getWeight(), "Old maps without a weight field must default to 1.0 for ground"); + } + + @Test + void testPushDataReadKeepsExplicitWeight() { + PushData pushData = GsonUtil.GSON.fromJson(TEST_JSON_WITH_WEIGHT, PushData.class); + assertEquals(0.15, pushData.push().getFirst().getWeight()); + } + + @Test + void testPushDataWriteIncludesWeight() { + PushData pushData = PushData.builder() + .add(PushEntry.pushEntry(Block.SLIME_BLOCK, 1, 0.25)) + .build(); + String json = GsonUtil.GSON.toJson(pushData); + assertTrue(json.contains("\"weight\": 0.25")); + } } diff --git a/common/src/test/java/net/theevilreaper/bounce/common/push/PushDataBuilderTest.java b/common/src/test/java/net/theevilreaper/bounce/common/push/PushDataBuilderTest.java new file mode 100644 index 00000000..bc431af2 --- /dev/null +++ b/common/src/test/java/net/theevilreaper/bounce/common/push/PushDataBuilderTest.java @@ -0,0 +1,22 @@ +package net.theevilreaper.bounce.common.push; + +import net.minestom.server.instance.block.Block; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class PushDataBuilderTest { + + @Test + void testCopyConstructorPreservesWeight() { + PushData original = PushData.builder() + .add(PushEntry.groundEntry(Block.GLASS, 1, 0.8)) + .add(PushEntry.pushEntry(Block.GOLD_BLOCK, 3, 0.2)) + .build(); + + PushData copy = PushData.builder(original).build(); + + assertEquals(0.8, copy.push().get(0).getWeight()); + assertEquals(0.2, copy.push().get(1).getWeight()); + } +} diff --git a/common/src/test/java/net/theevilreaper/bounce/common/push/PushEntryTest.java b/common/src/test/java/net/theevilreaper/bounce/common/push/PushEntryTest.java index 9d08f689..d24b4f27 100644 --- a/common/src/test/java/net/theevilreaper/bounce/common/push/PushEntryTest.java +++ b/common/src/test/java/net/theevilreaper/bounce/common/push/PushEntryTest.java @@ -67,4 +67,54 @@ void testEquality() { entry2.setBlock(Block.STONE); assertNotEquals(entry1, entry2, "Entries with different blocks should not be equal"); } + + @Test + void testWeightDefaults() { + PushEntry ground = PushEntry.groundEntry(Block.SAND, 5); + assertEquals(1.0, ground.getWeight(), "Ground weight should default to 1.0"); + + PushEntry push = PushEntry.pushEntry(Block.SAND, 5); + assertEquals(0.05, push.getWeight(), "Push weight should default to 0.05"); + } + + @Test + void testWeightConstructorOverload() { + PushEntry pushEntry = PushEntry.pushEntry(Block.SAND, 5, 0.25); + assertEquals(0.25, pushEntry.getWeight()); + } + + @Test + void testIncrementWeight() { + PushEntry pushEntry = PushEntry.pushEntry(Block.SAND, 5, 0.05); + pushEntry.incrementWeight(); + assertEquals(0.06, pushEntry.getWeight()); + } + + @Test + void testIncrementWeightMaxValue() { + PushEntry pushEntry = PushEntry.pushEntry(Block.SAND, 5, 1.0); + pushEntry.incrementWeight(); + assertEquals(1.0, pushEntry.getWeight(), "Weight must not exceed 1.0"); + } + + @Test + void testDecrementWeight() { + PushEntry pushEntry = PushEntry.pushEntry(Block.SAND, 5, 0.05); + pushEntry.decrementWeight(); + assertEquals(0.04, pushEntry.getWeight()); + } + + @Test + void testDecrementWeightNeverGoesNegative() { + PushEntry pushEntry = PushEntry.pushEntry(Block.SAND, 5, 0.0); + pushEntry.decrementWeight(); + assertEquals(0.0, pushEntry.getWeight(), "Weight of 0 must stay 0, it means the entry is never picked"); + } + + @Test + void testSetWeight() { + PushEntry pushEntry = PushEntry.pushEntry(Block.SAND, 5); + pushEntry.setWeight(0.75); + assertEquals(0.75, pushEntry.getWeight()); + } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/push/PushValueInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/push/PushValueInventory.java index 43bc65b6..dade6189 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/push/PushValueInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/push/PushValueInventory.java @@ -29,6 +29,7 @@ public final class PushValueInventory extends PersonalInventoryBuilder { private static final Component TITLE = Component.text("Push Value"); private static final int BLOCK_SLOT = 11; + private static final int WEIGHT_SLOT = 13; private static final int VALUE_SLOT = 15; private final GameMapBuilder gameMapBuilder; @@ -51,13 +52,14 @@ public void updateLayout(int index) { this.setDataLayoutFunction(dataLayoutFunction -> { InventoryLayout dataLayout = dataLayoutFunction == null ? InventoryLayout.fromType(getType()) : dataLayoutFunction; - dataLayout.blank(LayoutCalculator.from(BLOCK_SLOT, VALUE_SLOT)); + dataLayout.blank(LayoutCalculator.from(BLOCK_SLOT, WEIGHT_SLOT, VALUE_SLOT)); PushEntry pushEntry = this.gameMapBuilder.getPushDataBuilder().getPushValues().get(index); ItemStack stack = ItemStack.builder(pushEntry.getBlock().material()) .build(); dataLayout.setItem(BLOCK_SLOT, stack, this::handleBlockClick); + dataLayout.setItem(WEIGHT_SLOT, LoreHelper.getWeight(pushEntry), this::handleWeightButtonClick); dataLayout.setItem(VALUE_SLOT, LoreHelper.getPushValue(pushEntry), this::handlePushButtonClick); return dataLayout; @@ -81,4 +83,13 @@ private void handlePushButtonClick(@NotNull Player player, int slot, @NotNull Cl player.setTag(PUSH_SLOT_INDEX, index); EventDispatcher.call(new PlayerDialogRequestEvent(player, PlayerDialogRequestEvent.Target.SETUP_BLOCK_BOUNCE)); } + + private void handleWeightButtonClick(@NotNull Player player, int slot, @NotNull Click click, @NotNull ItemStack stack, @NotNull Consumer result) { + result.accept(ClickHolder.cancelClick()); + if ((!(click instanceof Click.Left || click instanceof Click.Right))) return; + + int index = player.getTag(PUSH_SLOT_INDEX); + player.setTag(PUSH_SLOT_INDEX, index); + EventDispatcher.call(new PlayerDialogRequestEvent(player, PlayerDialogRequestEvent.Target.SETUP_BLOCK_WEIGHT)); + } } diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/push/PushValueInventoryIntegrationTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/push/PushValueInventoryIntegrationTest.java new file mode 100644 index 00000000..00a85b7e --- /dev/null +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/push/PushValueInventoryIntegrationTest.java @@ -0,0 +1,43 @@ +package net.theevilreaper.bounce.setup.inventory.push; + +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import net.theevilreaper.aves.inventory.layout.InventoryLayout; +import net.theevilreaper.aves.inventory.slot.ISlot; +import net.theevilreaper.bounce.setup.builder.GameMapBuilder; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MicrotusExtension.class) +class PushValueInventoryIntegrationTest { + + private static final int WEIGHT_SLOT = 13; + + @Test + void testWeightSlotShowsCurrentWeight(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + GameMapBuilder gameMapBuilder = new GameMapBuilder(); + + PushValueInventory inventory = new PushValueInventory(player, gameMapBuilder); + inventory.open(); + inventory.updateLayout(1); // index 0 is the ground entry, 1 is the first push entry + env.tick(); + + InventoryLayout dataLayout = inventory.getDataLayout(); + ISlot slot = dataLayout.getSlot(WEIGHT_SLOT); + assertNotNull(slot); + ItemStack item = slot.getItem(); + assertNotNull(item); + assertEquals(Material.NETHER_STAR, item.material()); + + env.destroyInstance(instance, true); + } +} From aa93be4b41544fc54640faac925be9f41b208b51 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:24:25 +0200 Subject: [PATCH 14/43] chore(area): improve block lookup --- .../bounce/common/ground/GroundArea.java | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/net/theevilreaper/bounce/common/ground/GroundArea.java b/common/src/main/java/net/theevilreaper/bounce/common/ground/GroundArea.java index bb3fdf98..50aa01ab 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/ground/GroundArea.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/ground/GroundArea.java @@ -1,6 +1,8 @@ package net.theevilreaper.bounce.common.ground; import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.theevilreaper.bounce.common.push.PushData; import org.jetbrains.annotations.NotNull; @@ -8,7 +10,9 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; public final class GroundArea implements Area { @@ -32,7 +36,7 @@ public GroundArea(Vec min, Vec max, Block groundBlock, PushData pushData) { * {@inheritDoc} */ @Override - public void calculatePositions() { + public void calculatePositions(@NotNull Instance instance) { // Avoid double calculations if (!this.positions.isEmpty()) return; @@ -40,16 +44,60 @@ public void calculatePositions() { int maxX = (int) Math.floor(Math.max(min.x(), max.x())); int minZ = (int) Math.floor(Math.min(min.z(), max.z())); int maxZ = (int) Math.floor(Math.max(min.z(), max.z())); + int targetY = (int) Math.floor(min.y()); + int minChunkX = minX >> 4; + int maxChunkX = maxX >> 4; + int minChunkZ = minZ >> 4; + int maxChunkZ = maxZ >> 4; + + List> chunkFutures = new ArrayList<>(); + for (int cx = minChunkX; cx <= maxChunkX; cx++) { + for (int cz = minChunkZ; cz <= maxChunkZ; cz++) { + chunkFutures.add(instance.loadChunk(cx, cz)); + } + } + CompletableFuture.allOf(chunkFutures.toArray(new CompletableFuture[0])).join(); + + // Scan the single 2D plane at targetY for (int x = minX; x <= maxX; x++) { for (int z = minZ; z <= maxZ; z++) { - positions.add(new Vec(x, min.y(), z)); + if (isAreaBlock(instance.getBlock(x, targetY, z))) { + positions.add(new Vec(x, targetY, z)); + } + } + } + + // If no positions were found at targetY, try scanning targetY - 1 + // in case coordinates were captured while standing on top of the ground platform + if (positions.isEmpty()) { + int scanY = targetY - 1; + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + if (isAreaBlock(instance.getBlock(x, scanY, z))) { + positions.add(new Vec(x, scanY, z)); + } + } } } LOGGER.info("Calculated positions for area: {} to {} with {} positions", min, max, positions.size()); } + private boolean isAreaBlock(@NotNull Block block) { + if (block.compare(groundBlock) || block.compare(Block.REDSTONE_BLOCK)) { + return true; + } + if (data != null && data.push() != null) { + for (var entry : data.push()) { + if (block.compare(entry.getBlock())) { + return true; + } + } + } + return false; + } + /** * {@inheritDoc} */ @@ -58,6 +106,14 @@ public boolean hasPositions() { return !this.positions.isEmpty(); } + /** + * {@inheritDoc} + */ + @Override + public List positions() { + return Collections.unmodifiableList(this.positions); + } + /** * {@inheritDoc} */ From 5a8c20ee012cfbb10dc94a8a49aa62aa2d918b96 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:24:44 +0200 Subject: [PATCH 15/43] test(value): add new weight test --- .../GroundValueInventoryIntegrationTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/GroundValueInventoryIntegrationTest.java diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/GroundValueInventoryIntegrationTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/GroundValueInventoryIntegrationTest.java new file mode 100644 index 00000000..172ea01d --- /dev/null +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/GroundValueInventoryIntegrationTest.java @@ -0,0 +1,42 @@ +package net.theevilreaper.bounce.setup.inventory.ground; + +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import net.theevilreaper.aves.inventory.layout.InventoryLayout; +import net.theevilreaper.aves.inventory.slot.ISlot; +import net.theevilreaper.bounce.setup.builder.GameMapBuilder; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MicrotusExtension.class) +class GroundValueInventoryIntegrationTest { + + private static final int WEIGHT_SLOT = 13; + + @Test + void testWeightSlotShowsCurrentWeight(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + GameMapBuilder gameMapBuilder = new GameMapBuilder(); + + GroundValueInventory inventory = new GroundValueInventory(player, gameMapBuilder); + inventory.open(); + env.tick(); + + InventoryLayout dataLayout = inventory.getDataLayout(); + ISlot slot = dataLayout.getSlot(WEIGHT_SLOT); + assertNotNull(slot); + ItemStack item = slot.getItem(); + assertNotNull(item); + assertEquals(Material.NETHER_STAR, item.material()); + + env.destroyInstance(instance, true); + } +} From c923e6e39db0a23c898ccbe981d9f57021c0c269 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:25:00 +0200 Subject: [PATCH 16/43] chore(lore): add weight item method --- .../bounce/setup/util/LoreHelper.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/util/LoreHelper.java b/setup/src/main/java/net/theevilreaper/bounce/setup/util/LoreHelper.java index 202190e1..0a84becc 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/util/LoreHelper.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/util/LoreHelper.java @@ -16,6 +16,7 @@ public final class LoreHelper { private static final Component DISPLAY_NAME = Component.text("Boost Value", NamedTextColor.GREEN); + private static final Component WEIGHT_DISPLAY_NAME = Component.text("Weight", NamedTextColor.LIGHT_PURPLE); private static final Component CURRENT_VALUE = Component.text("Current:", NamedTextColor.GRAY).append(Component.space()); private static final Component LEFT_CLICK = miniMessage().deserialize("Left-click: Increase the value"); @@ -32,6 +33,20 @@ public static ItemStack getPushValue(PushEntry pushEntry) { .build(); } + public static ItemStack getWeight(PushEntry pushEntry) { + List lore = new ArrayList<>(); + lore.add(Component.empty()); + String formatted = String.format(java.util.Locale.ROOT, "%.1f%% (%.3f)", pushEntry.getWeight() * 100.0, pushEntry.getWeight()); + lore.add(CURRENT_VALUE.append(Component.text(formatted, NamedTextColor.YELLOW))); + lore.add(Component.empty()); + lore.add(miniMessage().deserialize("Click: Open dialog to edit chance")); + lore.add(Component.empty()); + return ItemStack.builder(Material.NETHER_STAR) + .customName(WEIGHT_DISPLAY_NAME) + .lore(lore) + .build(); + } + private LoreHelper() { // Prevent instantiation } From d3aceb0129df2941440ab4c3caf49065aacc3868 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:25:15 +0200 Subject: [PATCH 17/43] chore(map): add area usage --- .../net/theevilreaper/bounce/map/BounceMapProvider.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/net/theevilreaper/bounce/map/BounceMapProvider.java b/src/main/java/net/theevilreaper/bounce/map/BounceMapProvider.java index bf583e5f..af270452 100644 --- a/src/main/java/net/theevilreaper/bounce/map/BounceMapProvider.java +++ b/src/main/java/net/theevilreaper/bounce/map/BounceMapProvider.java @@ -7,6 +7,8 @@ import net.theevilreaper.aves.map.BaseMap; import net.theevilreaper.aves.map.MapEntry; import net.theevilreaper.aves.map.provider.AbstractMapProvider; +import net.theevilreaper.bounce.common.ground.Area; +import net.theevilreaper.bounce.common.ground.AreaFiller; import net.theevilreaper.bounce.common.map.GameMap; import net.theevilreaper.bounce.common.map.MapFilters; import net.theevilreaper.bounce.common.util.GsonUtil; @@ -36,6 +38,7 @@ public BounceMapProvider(Path path) { throw new IllegalStateException("An error occurred while loading the map"); } + GameMap gameMap = loadedDataMap.get(); this.activeMap = loadedDataMap.get(); this.falcoAnvilLoader = new FalcoAnvilLoader(mapEntry.getDirectoryRoot(), DimensionType.OVERWORLD.key()); this.activeInstance.setChunkLoader(this.falcoAnvilLoader); @@ -45,6 +48,11 @@ public BounceMapProvider(Path path) { defaultClock.rate(0f); } MinecraftServer.getInstanceManager().registerInstance(this.activeInstance); + + Area area = gameMap.getArea(); + if (area != null) { + AreaFiller.fill(this.activeInstance, area); + } } @Override From e1fb71ebc415f1a7071261e8872e15bac9449944 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:25:40 +0200 Subject: [PATCH 18/43] chore(data): add view inventory usage --- .../bounce/setup/data/BounceData.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java b/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java index c59cc714..8e4d7821 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java @@ -5,7 +5,6 @@ import net.minestom.server.entity.Player; import net.minestom.server.event.EventDispatcher; import net.minestom.server.instance.InstanceContainer; -import net.minestom.server.instance.anvil.AnvilLoader; import net.minestom.server.world.DimensionType; import net.onelitefeather.falco.anvil.FalcoAnvilLoader; import net.onelitefeather.guira.data.SetupData; @@ -14,11 +13,11 @@ import net.theevilreaper.bounce.common.map.GameMap; import net.theevilreaper.bounce.common.util.GsonUtil; import net.theevilreaper.bounce.setup.builder.GameMapBuilder; +import net.theevilreaper.bounce.setup.inventory.ground.AreaViewInventory; import net.theevilreaper.bounce.setup.inventory.ground.GroundViewInventory; import net.theevilreaper.bounce.setup.inventory.overview.MapOverviewInventory; import net.theevilreaper.bounce.setup.inventory.push.PushValueInventory; import net.theevilreaper.bounce.setup.util.SetupTags; -import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; @@ -38,6 +37,7 @@ public final class BounceData implements SetupData { private MapOverviewInventory overviewInventory; private GroundViewInventory groundViewInventory; private PushValueInventory pushValueInventory; + private AreaViewInventory areaViewInventory; public BounceData(UUID owner, MapEntry mapEntry) { this.owner = owner; @@ -107,6 +107,9 @@ public void loadData() { this.pushValueInventory = new PushValueInventory(this.player, this.gameMapBuilder); this.pushValueInventory.register(); + this.areaViewInventory = new AreaViewInventory(this.player, this.gameMapBuilder); + this.areaViewInventory.register(); + this.instance = MinecraftServer.getInstanceManager().createInstanceContainer(); this.loader = new FalcoAnvilLoader(this.mapEntry.getDirectoryRoot(), DimensionType.OVERWORLD.key()); this.instance.setChunkLoader(this.loader); @@ -169,6 +172,13 @@ public void openGroundBlockView() { this.groundViewInventory.openGroundBlockValueInventory(); } + /** + * Opens the {@link AreaViewInventory} for the player which owns the data. + */ + public void openAreaView() { + this.areaViewInventory.open(); + } + /** * {@inheritDoc} */ From dcf9bc06ba0cd1d30f87b09d373f60050fb5e9b8 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:40:36 +0200 Subject: [PATCH 19/43] chore(map): update item handling --- .../setup/inventory/overview/MapOverviewInventory.java | 3 ++- .../bounce/setup/inventory/overview/OverviewType.java | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java index b1e8a818..1243525c 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java @@ -46,7 +46,7 @@ public MapOverviewInventory(Player player, GameMapBuilder builder) { InventoryLayout dataLayout = dataLayoutFunction == null ? InventoryLayout.fromType(getType()) : dataLayoutFunction; dataLayout.blank(DATA_SLOT); - OverviewType[] overviewTypes = OverviewType.getValues(); + OverviewType[] overviewTypes = OverviewType.MAP_OVERVIEW_TYPES; for (int i = 0; i < overviewTypes.length && i < DATA_SLOT.length; i++) { OverviewType currentType = overviewTypes[i]; @@ -71,6 +71,7 @@ private ISlot getOverViewItem(OverviewType type) { case BUILDER -> new MultiStringSlot(type, builder.getBuilders()); case AREA -> new AreaOverviewSlot(type, builder.getArea()); case SHUFFLE_INTERVAL -> new ShuffleIntervalOverviewSlot(type, builder.getShuffleIntervalTicks()); + default -> throw new IllegalArgumentException("Unsupported overview type: " + type); }; } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java index c97c7a5a..718fa2e9 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java @@ -24,10 +24,16 @@ public enum OverviewType { SPAWN("Spawn Point", Material.COMPASS, NamedTextColor.RED), GAME_SPAWN("Game Spawn Point", Material.RECOVERY_COMPASS, NamedTextColor.RED), AREA("Playing Area", Material.FILLED_MAP, NamedTextColor.GOLD), - SHUFFLE_INTERVAL("Reshuffle Interval", Material.CLOCK, NamedTextColor.LIGHT_PURPLE) + SHUFFLE_INTERVAL("Reshuffle Interval", Material.CLOCK, NamedTextColor.LIGHT_PURPLE), + LEFT_AREA_CORNER("Left Corner", Material.GREEN_WOOL, NamedTextColor.AQUA), + RIGHT_AREA_CORNER("Right Corner", Material.RED_WOOL, NamedTextColor.AQUA) ; + public static final OverviewType[] MAP_OVERVIEW_TYPES = { + NAME, BUILDER, SPAWN, GAME_SPAWN, AREA, SHUFFLE_INTERVAL + }; + private final String name; private final Material material; private final TextColor color; From ff4e7daa6cc4a999ddb441a1060e4faded7fec82 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 24 Aug 2026 23:46:23 +0200 Subject: [PATCH 20/43] feat(setup): add custom player --- .../bounce/setup/player/SetupPlayer.java | 33 +++++++++++++++++++ .../bounce/setup/player/package-info.java | 4 +++ 2 files changed, 37 insertions(+) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/player/SetupPlayer.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/player/package-info.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/player/SetupPlayer.java b/setup/src/main/java/net/theevilreaper/bounce/setup/player/SetupPlayer.java new file mode 100644 index 00000000..fbf2edf1 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/player/SetupPlayer.java @@ -0,0 +1,33 @@ +package net.theevilreaper.bounce.setup.player; + +import net.minestom.server.coordinate.BlockVec; +import net.minestom.server.entity.Player; +import net.minestom.server.network.player.GameProfile; +import net.minestom.server.network.player.PlayerConnection; +import org.jetbrains.annotations.Nullable; + +public class SetupPlayer extends Player { + + private @Nullable BlockVec leftCorner; + private @Nullable BlockVec rightCorner; + + public SetupPlayer(PlayerConnection playerConnection, GameProfile gameProfile) { + super(playerConnection, gameProfile); + } + + public void setLeftCorner(@Nullable BlockVec leftCorner) { + this.leftCorner = leftCorner; + } + + public void setRightCorner(@Nullable BlockVec rightCorner) { + this.rightCorner = rightCorner; + } + + public @Nullable BlockVec getRightCorner() { + return rightCorner; + } + + public @Nullable BlockVec getLeftCorner() { + return leftCorner; + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/player/package-info.java b/setup/src/main/java/net/theevilreaper/bounce/setup/player/package-info.java new file mode 100644 index 00000000..b7713ad1 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/player/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.setup.player; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file From 139c08ad696d113d0724026f130fc2f625f5a0f5 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:05:38 +0200 Subject: [PATCH 21/43] feat(map): custom Instance implementation --- .../bounce/map/BounceInstance.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/main/java/net/theevilreaper/bounce/map/BounceInstance.java diff --git a/src/main/java/net/theevilreaper/bounce/map/BounceInstance.java b/src/main/java/net/theevilreaper/bounce/map/BounceInstance.java new file mode 100644 index 00000000..2923fa41 --- /dev/null +++ b/src/main/java/net/theevilreaper/bounce/map/BounceInstance.java @@ -0,0 +1,45 @@ +package net.theevilreaper.bounce.map; + +import net.minestom.server.instance.InstanceContainer; +import net.minestom.server.registry.RegistryKey; +import net.minestom.server.world.DimensionType; +import net.theevilreaper.bounce.common.ground.Area; +import net.theevilreaper.bounce.common.ground.AreaFiller; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Custom {@link InstanceContainer} which reshuffles its configured {@link Area} on its own tick instead of relying + * on a separately scheduled task. + * + * @author theEvilReaper + * @version 1.0.0 + * @since 1.0.0 + */ +public final class BounceInstance extends InstanceContainer { + + private final @Nullable Area area; + private final int shuffleIntervalTicks; + private final double reshufflePercentage; + + public BounceInstance(UUID uuid, RegistryKey dimensionType, @Nullable Area area, int shuffleIntervalTicks, double reshufflePercentage) { + super(uuid, dimensionType); + this.area = area; + this.shuffleIntervalTicks = shuffleIntervalTicks; + this.reshufflePercentage = reshufflePercentage; + } + + /** + * {@inheritDoc} + */ + @Override + public void tick(long time) { + super.tick(time); + if (area == null || shuffleIntervalTicks <= 0) return; + if (getWorldAge() % shuffleIntervalTicks == 0) { + AreaFiller.reshuffle(this, area, reshufflePercentage, getPlayers()); + } + } +} From ad7998ffa4980134897528a21e1e21d5bd99bfa5 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:07:26 +0200 Subject: [PATCH 22/43] chore(map): add shuffle percentage value --- .../bounce/common/map/GameMap.java | 14 +++++++++- .../bounce/common/map/GameMapTest.java | 3 +- .../bounce/setup/builder/GameMapBuilder.java | 28 ++++++++++++++++++- .../setup/builder/GameMapBuilderTest.java | 6 +++- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/net/theevilreaper/bounce/common/map/GameMap.java b/common/src/main/java/net/theevilreaper/bounce/common/map/GameMap.java index 96277974..146d3ef4 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/map/GameMap.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/map/GameMap.java @@ -22,6 +22,7 @@ public final class GameMap extends BaseMap { private final PushData pushData; private final @Nullable Area area; private final int shuffleIntervalTicks; + private final double reshufflePercentage; /** * Creates a new reference from the map class. @@ -33,13 +34,15 @@ public final class GameMap extends BaseMap { * @param builders the list of builders who worked on the map * @param area the ground area which gets dynamically filled, or {@code null} for a fully manual map * @param shuffleIntervalTicks the amount of ticks between two runtime reshuffles of the area + * @param reshufflePercentage the fraction (0.0-1.0) of the area's positions to re-roll on each reshuffle */ - public GameMap(String name, Pos spawn, Pos gameSpawn, PushData pushData, List builders, @Nullable Area area, int shuffleIntervalTicks) { + public GameMap(String name, Pos spawn, Pos gameSpawn, PushData pushData, List builders, @Nullable Area area, int shuffleIntervalTicks, double reshufflePercentage) { super(name, spawn, builders); this.gameSpawn = gameSpawn; this.pushData = pushData; this.area = area; this.shuffleIntervalTicks = shuffleIntervalTicks; + this.reshufflePercentage = reshufflePercentage; } /** @@ -77,4 +80,13 @@ public Pos getGameSpawn() { public int getShuffleIntervalTicks() { return shuffleIntervalTicks; } + + /** + * Returns the fraction of the area's positions which get re-rolled on each runtime reshuffle. + * + * @return the percentage as a fraction between 0.0 and 1.0 + */ + public double getReshufflePercentage() { + return reshufflePercentage; + } } diff --git a/common/src/test/java/net/theevilreaper/bounce/common/map/GameMapTest.java b/common/src/test/java/net/theevilreaper/bounce/common/map/GameMapTest.java index 0c06605e..be5284f9 100644 --- a/common/src/test/java/net/theevilreaper/bounce/common/map/GameMapTest.java +++ b/common/src/test/java/net/theevilreaper/bounce/common/map/GameMapTest.java @@ -13,7 +13,7 @@ class GameMapTest { @Test void testGameMap() { - GameMap gameMap = new GameMap("Test-Map", Pos.ZERO, new Pos(10, 0, 10), PushData.builder().build(), List.of(), null, 100); + GameMap gameMap = new GameMap("Test-Map", Pos.ZERO, new Pos(10, 0, 10), PushData.builder().build(), List.of(), null, 100, 0.25); assertNotNull(gameMap); assertInstanceOf(BaseMap.class, gameMap, "The GameMap should be rely on the BaseMap"); assertEquals("Test-Map", gameMap.name()); @@ -24,5 +24,6 @@ void testGameMap() { assertTrue(gameMap.getPushData().push().isEmpty()); assertNull(gameMap.getArea(), "A map without an area should report null"); assertEquals(100, gameMap.getShuffleIntervalTicks()); + assertEquals(0.25, gameMap.getReshufflePercentage()); } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java index 8a4f2e5a..8a669f52 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java @@ -13,15 +13,18 @@ public final class GameMapBuilder extends BaseMapBuilder { private static final int DEFAULT_SHUFFLE_INTERVAL_TICKS = 100; + private static final double DEFAULT_RESHUFFLE_PERCENTAGE = 0.1; private final PushData.Builder pushDataBuilder; private Pos gameSpawn; private @Nullable Area area; private int shuffleIntervalTicks; + private double reshufflePercentage; public GameMapBuilder() { super(); this.shuffleIntervalTicks = DEFAULT_SHUFFLE_INTERVAL_TICKS; + this.reshufflePercentage = DEFAULT_RESHUFFLE_PERCENTAGE; this.pushDataBuilder = PushData.builder(); this.pushDataBuilder .add(PushEntry.groundEntry(Block.GLASS, 1, 1.0)) @@ -37,6 +40,9 @@ public GameMapBuilder(@NotNull GameMap gameMap) { this.shuffleIntervalTicks = gameMap.getShuffleIntervalTicks() > 0 ? gameMap.getShuffleIntervalTicks() : DEFAULT_SHUFFLE_INTERVAL_TICKS; + this.reshufflePercentage = gameMap.getReshufflePercentage() > 0 + ? gameMap.getReshufflePercentage() + : DEFAULT_RESHUFFLE_PERCENTAGE; if (gameMap.getPushData() == null) { this.pushDataBuilder = PushData.builder(); @@ -95,6 +101,17 @@ public GameMapBuilder(@NotNull GameMap gameMap) { return this; } + /** + * Sets the fraction of the area's positions to re-roll on each runtime reshuffle. + * + * @param reshufflePercentage the percentage as a fraction between 0.0 and 1.0 + * @return this builder instance for chaining + */ + public @NotNull GameMapBuilder reshufflePercentage(double reshufflePercentage) { + this.reshufflePercentage = reshufflePercentage; + return this; + } + /** * Builds a new {@link GameMap} instance with the current properties. * @@ -102,7 +119,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { */ @Override public @NotNull GameMap build() { - return new GameMap(this.name, this.spawn, this.gameSpawn, pushDataBuilder.build(), this.builders, this.area, this.shuffleIntervalTicks); + return new GameMap(this.name, this.spawn, this.gameSpawn, pushDataBuilder.build(), this.builders, this.area, this.shuffleIntervalTicks, this.reshufflePercentage); } /** @@ -132,6 +149,15 @@ public int getShuffleIntervalTicks() { return shuffleIntervalTicks; } + /** + * Returns the fraction of the area's positions to re-roll on each runtime reshuffle. + * + * @return the percentage as a fraction between 0.0 and 1.0 + */ + public double getReshufflePercentage() { + return reshufflePercentage; + } + /** * Returns the {@link PushData.Builder} instance used to build push data. * diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java index 4de7217d..473ea20a 100644 --- a/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java @@ -81,6 +81,7 @@ void testNewBuilderHasNoAreaAndDefaultInterval() { GameMapBuilder builder = new GameMapBuilder(); assertNull(builder.getArea()); assertTrue(builder.getShuffleIntervalTicks() > 0, "A newly created map should have a sane default interval"); + assertTrue(builder.getReshufflePercentage() > 0, "A newly created map should have a sane default reshuffle percentage"); } @Test @@ -88,14 +89,16 @@ void testAreaAndShuffleIntervalRoundTripThroughBuild() { GameMapBuilder builder = new GameMapBuilder(); Area area = new GroundArea(Vec.ZERO, new Vec(5, 0, 5), Block.GLASS, PushData.builder().build()); - builder.area(area).shuffleIntervalTicks(60); + builder.area(area).shuffleIntervalTicks(60).reshufflePercentage(0.4); assertEquals(area, builder.getArea()); assertEquals(60, builder.getShuffleIntervalTicks()); + assertEquals(0.4, builder.getReshufflePercentage()); GameMap built = builder.build(); assertEquals(area, built.getArea()); assertEquals(60, built.getShuffleIntervalTicks()); + assertEquals(0.4, built.getReshufflePercentage()); } @Test @@ -105,5 +108,6 @@ void testReloadingExistingMapWithoutAreaKeepsDefaultInterval() { assertNull(reloaded.getArea()); assertTrue(reloaded.getShuffleIntervalTicks() > 0); + assertTrue(reloaded.getReshufflePercentage() > 0); } } \ No newline at end of file From 5f7df0644aa63b61cd1207cedac0e4e4ab73f37a Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:18:41 +0200 Subject: [PATCH 23/43] chore(map): outsource area data code --- .../setup/inventory/overview/MapOverviewInventory.java | 4 +--- .../bounce/setup/inventory/overview/OverviewType.java | 7 ++----- .../overview/MapOverviewInventoryIntegrationTest.java | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java index 1243525c..17af307f 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java @@ -11,7 +11,6 @@ import net.theevilreaper.bounce.setup.inventory.slot.AreaOverviewSlot; import net.theevilreaper.bounce.setup.inventory.slot.MultiStringSlot; import net.theevilreaper.bounce.setup.inventory.slot.PositionSlot; -import net.theevilreaper.bounce.setup.inventory.slot.ShuffleIntervalOverviewSlot; import net.theevilreaper.bounce.setup.inventory.slot.StringSlot; import net.theevilreaper.bounce.setup.util.SetupItems; @@ -25,7 +24,7 @@ */ public final class MapOverviewInventory extends PersonalInventoryBuilder { - private static final int[] DATA_SLOT = LayoutCalculator.from(10, 11, 12, 13, 14, 15); + private static final int[] DATA_SLOT = LayoutCalculator.from(10, 11, 12, 13, 14); private final GameMapBuilder builder; @@ -70,7 +69,6 @@ private ISlot getOverViewItem(OverviewType type) { case NAME -> new StringSlot(type, builder.getName()); case BUILDER -> new MultiStringSlot(type, builder.getBuilders()); case AREA -> new AreaOverviewSlot(type, builder.getArea()); - case SHUFFLE_INTERVAL -> new ShuffleIntervalOverviewSlot(type, builder.getShuffleIntervalTicks()); default -> throw new IllegalArgumentException("Unsupported overview type: " + type); }; } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java index 718fa2e9..7ea5d9fb 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java @@ -23,15 +23,12 @@ public enum OverviewType { BUILDER("Builder", Material.OAK_HANGING_SIGN, NamedTextColor.AQUA), SPAWN("Spawn Point", Material.COMPASS, NamedTextColor.RED), GAME_SPAWN("Game Spawn Point", Material.RECOVERY_COMPASS, NamedTextColor.RED), - AREA("Playing Area", Material.FILLED_MAP, NamedTextColor.GOLD), - SHUFFLE_INTERVAL("Reshuffle Interval", Material.CLOCK, NamedTextColor.LIGHT_PURPLE), - LEFT_AREA_CORNER("Left Corner", Material.GREEN_WOOL, NamedTextColor.AQUA), - RIGHT_AREA_CORNER("Right Corner", Material.RED_WOOL, NamedTextColor.AQUA) + AREA("Playing Area", Material.FILLED_MAP, NamedTextColor.GOLD) ; public static final OverviewType[] MAP_OVERVIEW_TYPES = { - NAME, BUILDER, SPAWN, GAME_SPAWN, AREA, SHUFFLE_INTERVAL + NAME, BUILDER, SPAWN, GAME_SPAWN, AREA }; private final String name; diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventoryIntegrationTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventoryIntegrationTest.java index 42b0576b..1c79a731 100644 --- a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventoryIntegrationTest.java +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventoryIntegrationTest.java @@ -67,7 +67,7 @@ void testMapOverviewDataLayout(@NotNull Env env) { assertNotNull(dataLayout, "Data layout should not be null"); - int[] dataSlots = LayoutCalculator.from(10, 11, 12, 13, 14, 15); + int[] dataSlots = LayoutCalculator.from(10, 11, 12, 13, 14); OverviewType[] overviewTypes = OverviewType.getValues(); for (int i = 0; i < overviewTypes.length && i < dataSlots.length; i++) { From 814b729352ff78aecc57054ad161a50ea3168c56 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:20:25 +0200 Subject: [PATCH 24/43] chore(type): remove static enumeration cache --- .../bounce/setup/inventory/overview/OverviewType.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java index 7ea5d9fb..406a513b 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java @@ -27,10 +27,6 @@ public enum OverviewType { ; - public static final OverviewType[] MAP_OVERVIEW_TYPES = { - NAME, BUILDER, SPAWN, GAME_SPAWN, AREA - }; - private final String name; private final Material material; private final TextColor color; From c631b3b907b8084ad717c84aefddb9cfe7953931 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:20:43 +0200 Subject: [PATCH 25/43] feat(setup): add dedicated area package --- .../{ground => area}/AreaViewInventory.java | 65 ++++++++++--- .../setup/inventory/area/AreaViewType.java | 96 +++++++++++++++++++ .../setup/inventory/area/package-info.java | 4 + .../AreaViewInventoryIntegrationTest.java | 39 +++++++- 4 files changed, 190 insertions(+), 14 deletions(-) rename setup/src/main/java/net/theevilreaper/bounce/setup/inventory/{ground => area}/AreaViewInventory.java (61%) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/package-info.java rename setup/src/test/java/net/theevilreaper/bounce/setup/inventory/{ground => area}/AreaViewInventoryIntegrationTest.java (59%) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventory.java similarity index 61% rename from setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventory.java rename to setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventory.java index 7f02404a..2fec948c 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventory.java @@ -1,10 +1,11 @@ -package net.theevilreaper.bounce.setup.inventory.ground; +package net.theevilreaper.bounce.setup.inventory.area; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; import net.minestom.server.inventory.InventoryType; import net.minestom.server.inventory.click.Click; import net.minestom.server.item.ItemStack; @@ -17,6 +18,7 @@ import net.theevilreaper.bounce.common.ground.GroundArea; import net.theevilreaper.bounce.common.push.PushEntry; import net.theevilreaper.bounce.setup.builder.GameMapBuilder; +import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent.SwitchTarget; import net.theevilreaper.bounce.setup.inventory.slot.SwitchTargetSlot; import org.jetbrains.annotations.NotNull; @@ -28,15 +30,17 @@ public final class AreaViewInventory extends PersonalInventoryBuilder { private static final Component TITLE = Component.text("Setup area"); + private static final int SHUFFLE_INTERVAL_SLOT = 10; private static final int POS1_SLOT = 11; private static final int POS2_SLOT = 13; private static final int CONFIRM_SLOT = 15; + private static final int RESHUFFLE_PERCENTAGE_SLOT = 16; private final GameMapBuilder gameMapBuilder; private @Nullable Vec pos1; private @Nullable Vec pos2; - public AreaViewInventory(@NotNull Player player, @NotNull GameMapBuilder gameMapBuilder) { + public AreaViewInventory(Player player, GameMapBuilder gameMapBuilder) { super(TITLE, InventoryType.CHEST_3_ROW, player); this.gameMapBuilder = gameMapBuilder; @@ -53,13 +57,17 @@ public AreaViewInventory(@NotNull Player player, @NotNull GameMapBuilder gameMap this.setDataLayoutFunction(dataLayoutFunction -> { InventoryLayout dataLayout = dataLayoutFunction == null ? InventoryLayout.fromType(getType()) : dataLayoutFunction; - dataLayout.blank(LayoutCalculator.from(POS1_SLOT, POS2_SLOT, CONFIRM_SLOT)); + dataLayout.blank(LayoutCalculator.from(SHUFFLE_INTERVAL_SLOT, POS1_SLOT, POS2_SLOT, CONFIRM_SLOT, RESHUFFLE_PERCENTAGE_SLOT)); - dataLayout.setItem(POS1_SLOT, getPosItem("Pos1", pos1), (p, slot, click, stack, result) -> { + dataLayout.setItem(SHUFFLE_INTERVAL_SLOT, getShuffleIntervalItem(), (p, slot, click, stack, result) -> { + result.accept(ClickHolder.cancelClick()); + EventDispatcher.call(new PlayerDialogRequestEvent(p, PlayerDialogRequestEvent.Target.SETUP_SHUFFLE_INTERVAL)); + }); + dataLayout.setItem(POS1_SLOT, getPosItem(AreaViewType.LEFT_AREA_CORNER, pos1), (p, slot, click, stack, result) -> { result.accept(ClickHolder.cancelClick()); if (click instanceof Click.Left) setPos1ToCurrentPosition(p); }); - dataLayout.setItem(POS2_SLOT, getPosItem("Pos2", pos2), (p, slot, click, stack, result) -> { + dataLayout.setItem(POS2_SLOT, getPosItem(AreaViewType.RIGHT_AREA_CORNER, pos2), (p, slot, click, stack, result) -> { result.accept(ClickHolder.cancelClick()); if (click instanceof Click.Left) setPos2ToCurrentPosition(p); }); @@ -67,6 +75,10 @@ public AreaViewInventory(@NotNull Player player, @NotNull GameMapBuilder gameMap result.accept(ClickHolder.cancelClick()); if (click instanceof Click.Left) confirm(p); }); + dataLayout.setItem(RESHUFFLE_PERCENTAGE_SLOT, getReshufflePercentageItem(), (p, slot, click, stack, result) -> { + result.accept(ClickHolder.cancelClick()); + EventDispatcher.call(new PlayerDialogRequestEvent(p, PlayerDialogRequestEvent.Target.SETUP_RESHUFFLE_PERCENTAGE)); + }); return dataLayout; }); @@ -77,7 +89,7 @@ public AreaViewInventory(@NotNull Player player, @NotNull GameMapBuilder gameMap * * @param player the player whose position is captured */ - public void setPos1ToCurrentPosition(@NotNull Player player) { + public void setPos1ToCurrentPosition(Player player) { this.pos1 = toVec(player.getPosition()); this.invalidateDataLayout(); } @@ -87,7 +99,7 @@ public void setPos1ToCurrentPosition(@NotNull Player player) { * * @param player the player whose position is captured */ - public void setPos2ToCurrentPosition(@NotNull Player player) { + public void setPos2ToCurrentPosition(Player player) { this.pos2 = toVec(player.getPosition()); this.invalidateDataLayout(); } @@ -98,7 +110,7 @@ public void setPos2ToCurrentPosition(@NotNull Player player) { * * @param player the player to notify */ - public void confirm(@NotNull Player player) { + public void confirm(Player player) { if (pos1 == null || pos2 == null) return; PushEntry groundEntry = gameMapBuilder.getGroundBlockEntry(); @@ -107,13 +119,13 @@ public void confirm(@NotNull Player player) { player.sendMessage(Component.text("Area saved.", NamedTextColor.GREEN)); } - private @NotNull Vec toVec(@NotNull Pos pos) { + private Vec toVec(Pos pos) { return new Vec(pos.x(), pos.y(), pos.z()); } - private @NotNull ItemStack getPosItem(@NotNull String label, @Nullable Vec pos) { - ItemStack.Builder builder = ItemStack.builder(Material.STICK) - .customName(Component.text(label, NamedTextColor.AQUA)); + private ItemStack getPosItem(AreaViewType type, @Nullable Vec pos) { + ItemStack.Builder builder = ItemStack.builder(type.getMaterial()) + .customName(Component.text(type.getName(), type.getColor())); if (pos == null) { return builder.lore( Component.empty(), @@ -130,10 +142,37 @@ public void confirm(@NotNull Player player) { ).build(); } - private @NotNull ItemStack getConfirmItem() { + private ItemStack getConfirmItem() { boolean ready = pos1 != null && pos2 != null; return ItemStack.builder(ready ? Material.LIME_DYE : Material.GRAY_DYE) .customName(Component.text(ready ? "Confirm area" : "Set both positions first", ready ? NamedTextColor.GREEN : NamedTextColor.RED)) .build(); } + + private ItemStack getShuffleIntervalItem() { + int ticks = gameMapBuilder.getShuffleIntervalTicks(); + double seconds = ticks / 20.0; + return ItemStack.builder(Material.CLOCK) + .customName(Component.text("Reshuffle Interval", NamedTextColor.LIGHT_PURPLE)) + .lore( + Component.empty(), + Component.text(String.format(java.util.Locale.ROOT, "%d ticks (%.1fs)", ticks, seconds), NamedTextColor.YELLOW), + Component.empty(), + Component.text("Click to edit", NamedTextColor.GRAY), + Component.empty() + ).build(); + } + + private ItemStack getReshufflePercentageItem() { + double percentage = gameMapBuilder.getReshufflePercentage() * 100.0; + return ItemStack.builder(Material.TARGET) + .customName(Component.text("Reshuffle Percentage", NamedTextColor.LIGHT_PURPLE)) + .lore( + Component.empty(), + Component.text(String.format(java.util.Locale.ROOT, "%.1f%%", percentage), NamedTextColor.YELLOW), + Component.empty(), + Component.text("Click to edit", NamedTextColor.GRAY), + Component.empty() + ).build(); + } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java new file mode 100644 index 00000000..24db07c1 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java @@ -0,0 +1,96 @@ +package net.theevilreaper.bounce.setup.inventory.area; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; + +import java.util.EnumMap; +import java.util.Map; + +/** + * The {@link AreaViewType} enum represents different types of items that can be displayed in the + * {@link AreaViewInventory}. + */ +public enum AreaViewType { + + LEFT_AREA_CORNER("Left Corner", Material.GREEN_WOOL, NamedTextColor.AQUA), + RIGHT_AREA_CORNER("Right Corner", Material.RED_WOOL, NamedTextColor.AQUA) + + ; + + private final String name; + private final Material material; + private final TextColor color; + + private static final Map itemCache = new EnumMap<>(AreaViewType.class); + private static final AreaViewType[] VALUES = values(); + + /** + * Constructs a new AreaViewType with the specified name, material, and color. + * + * @param name the name of the area view type + * @param material the material associated with this area view type + * @param color the text color for this area view type + */ + AreaViewType(String name, Material material, TextColor color) { + this.name = name; + this.material = material; + this.color = color; + } + + /** + * Gets the name of this area view type. + * + * @return the name of + */ + public String getName() { + return name; + } + + /** + * Gets the material associated with this area view type. + * + * @return the material + */ + public Material getMaterial() { + return material; + } + + /** + * Gets the text color associated with this area view type. + * + * @return the text color + */ + public TextColor getColor() { + return color; + } + + /** + * Gets the ItemStack representation of this area view type. + * + * @return the ItemStack for this area view type + */ + public ItemStack getItem() { + return itemCache.computeIfAbsent(this, type -> ItemStack.builder(type.getMaterial()) + .customName(Component.text(type.getName(), type.getColor())) + .build()); + } + + /** + * Gets all available area view types. + * + * @return an array of all AreaViewType values + */ + public static AreaViewType[] getValues() { + return VALUES; + } + + public static AreaViewType fromOrdinal(int ordinal) { + if (ordinal < 0 || ordinal >= VALUES.length) { + throw new IndexOutOfBoundsException("Invalid ordinal for AreaViewType: " + ordinal); + } + return VALUES[ordinal]; + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/package-info.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/package-info.java new file mode 100644 index 00000000..7f00e85e --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.setup.inventory.area; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventoryIntegrationTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventoryIntegrationTest.java similarity index 59% rename from setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventoryIntegrationTest.java rename to setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventoryIntegrationTest.java index 1b628c9e..fbad8358 100644 --- a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/ground/AreaViewInventoryIntegrationTest.java +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventoryIntegrationTest.java @@ -1,4 +1,4 @@ -package net.theevilreaper.bounce.setup.inventory.ground; +package net.theevilreaper.bounce.setup.inventory.area; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Player; @@ -18,9 +18,11 @@ @ExtendWith(MicrotusExtension.class) class AreaViewInventoryIntegrationTest { + private static final int SHUFFLE_INTERVAL_SLOT = 10; private static final int POS1_SLOT = 11; private static final int POS2_SLOT = 13; private static final int CONFIRM_SLOT = 15; + private static final int RESHUFFLE_PERCENTAGE_SLOT = 16; @Test void testConfirmSlotStartsDisabledAndBothPositionsAreUnset(@NotNull Env env) { @@ -38,6 +40,41 @@ void testConfirmSlotStartsDisabledAndBothPositionsAreUnset(@NotNull Env env) { env.destroyInstance(instance, true); } + @Test + void testPosSlotsUseTheAreaViewTypeMaterials(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + GameMapBuilder gameMapBuilder = new GameMapBuilder(); + + AreaViewInventory inventory = new AreaViewInventory(player, gameMapBuilder); + inventory.open(); + env.tick(); + + InventoryLayout dataLayout = inventory.getDataLayout(); + assertEquals(AreaViewType.LEFT_AREA_CORNER.getMaterial(), dataLayout.getSlot(POS1_SLOT).getItem().material()); + assertEquals(AreaViewType.RIGHT_AREA_CORNER.getMaterial(), dataLayout.getSlot(POS2_SLOT).getItem().material()); + + env.destroyInstance(instance, true); + } + + @Test + void testShuffleIntervalAndReshufflePercentageSlotsShowBuilderValues(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + GameMapBuilder gameMapBuilder = new GameMapBuilder(); + gameMapBuilder.shuffleIntervalTicks(60).reshufflePercentage(0.25); + + AreaViewInventory inventory = new AreaViewInventory(player, gameMapBuilder); + inventory.open(); + env.tick(); + + InventoryLayout dataLayout = inventory.getDataLayout(); + assertEquals(Material.CLOCK, dataLayout.getSlot(SHUFFLE_INTERVAL_SLOT).getItem().material()); + assertEquals(Material.TARGET, dataLayout.getSlot(RESHUFFLE_PERCENTAGE_SLOT).getItem().material()); + + env.destroyInstance(instance, true); + } + @Test void testClickingBothPosButtonsEnablesConfirmAndSavesArea(@NotNull Env env) { Instance instance = env.createFlatInstance(); From d46e3096e691b5c5240606fa05e53ae0abc8611e Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:21:02 +0200 Subject: [PATCH 26/43] chore(game): update used instance --- .../java/net/theevilreaper/bounce/map/BounceMapProvider.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/theevilreaper/bounce/map/BounceMapProvider.java b/src/main/java/net/theevilreaper/bounce/map/BounceMapProvider.java index af270452..63c50563 100644 --- a/src/main/java/net/theevilreaper/bounce/map/BounceMapProvider.java +++ b/src/main/java/net/theevilreaper/bounce/map/BounceMapProvider.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Optional; +import java.util.UUID; public class BounceMapProvider extends AbstractMapProvider { @@ -24,7 +25,6 @@ public class BounceMapProvider extends AbstractMapProvider { public BounceMapProvider(Path path) { super(GsonUtil.GSON_FILE_HANDLER, MapFilters::filterMapsForGame); this.loadMapEntries(path.resolve("maps")); - this.activeInstance = MinecraftServer.getInstanceManager().createInstanceContainer(); MapEntry mapEntry = this.getEntries().getFirst(); @@ -39,7 +39,8 @@ public BounceMapProvider(Path path) { } GameMap gameMap = loadedDataMap.get(); - this.activeMap = loadedDataMap.get(); + this.activeMap = gameMap; + this.activeInstance = new BounceInstance(UUID.randomUUID(), DimensionType.OVERWORLD, gameMap.getArea(), gameMap.getShuffleIntervalTicks(), gameMap.getReshufflePercentage()); this.falcoAnvilLoader = new FalcoAnvilLoader(mapEntry.getDirectoryRoot(), DimensionType.OVERWORLD.key()); this.activeInstance.setChunkLoader(this.falcoAnvilLoader); this.activeInstance.enableAutoChunkLoad(true); From 8c330586404ce324ec2d3b4c6a1ece5d29c6b57f Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:21:11 +0200 Subject: [PATCH 27/43] chore(setup): update value access --- .../bounce/setup/inventory/overview/MapOverviewInventory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java index 17af307f..144af9b7 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java @@ -45,7 +45,7 @@ public MapOverviewInventory(Player player, GameMapBuilder builder) { InventoryLayout dataLayout = dataLayoutFunction == null ? InventoryLayout.fromType(getType()) : dataLayoutFunction; dataLayout.blank(DATA_SLOT); - OverviewType[] overviewTypes = OverviewType.MAP_OVERVIEW_TYPES; + OverviewType[] overviewTypes = OverviewType.getValues(); for (int i = 0; i < overviewTypes.length && i < DATA_SLOT.length; i++) { OverviewType currentType = overviewTypes[i]; From ad30cbde1c2899068093d14f7e1d407bc28ac461 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:21:22 +0200 Subject: [PATCH 28/43] chore(dialog): update dialog handling --- .../bounce/setup/dialog/SetupDialogRegistry.java | 1 + .../dialog/event/PlayerDialogRequestEvent.java | 6 +++++- .../dialog/PlayerCustomClickEventListener.java | 15 ++++++++++++++- .../dialog/PlayerDialogRequestListener.java | 1 + 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java index b6aaebc3..915de6f2 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java @@ -41,6 +41,7 @@ public SetupDialogRegistry() { this.registerDialog(new ValueInputDialog()); this.registerDialog(new WeightInputDialog()); this.registerDialog(new ShuffleIntervalInputDialog()); + this.registerDialog(new ReshufflePercentageInputDialog()); } private void registerDialog(@NotNull DialogTemplate dialog) { diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/event/PlayerDialogRequestEvent.java b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/event/PlayerDialogRequestEvent.java index b58fcfc1..63ea1b4c 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/event/PlayerDialogRequestEvent.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/event/PlayerDialogRequestEvent.java @@ -66,7 +66,11 @@ public enum Target { /** * The target for the dialog request is to set up the reshuffle interval. */ - SETUP_SHUFFLE_INTERVAL + SETUP_SHUFFLE_INTERVAL, + /** + * The target for the dialog request is to set up the reshuffle percentage. + */ + SETUP_RESHUFFLE_PERCENTAGE ; } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java index 149815cd..dbe2e945 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java @@ -62,6 +62,7 @@ public void accept(@NotNull PlayerCustomClickEvent event) { case ValueInputDialog ignored -> this.handleValueUpdate(player, data, dialogData); case WeightInputDialog ignored -> this.handleWeightUpdate(player, data, dialogData); case ShuffleIntervalInputDialog ignored -> this.handleShuffleIntervalSet(data, dialogData); + case ReshufflePercentageInputDialog ignored -> this.handleReshufflePercentageSet(data, dialogData); default -> throw new IllegalStateException("Unexpected dialog type: " + dialogTemplate.getClass().getCanonicalName()); } @@ -77,7 +78,19 @@ private void handleShuffleIntervalSet(@NotNull BounceData data, @NotNull Compoun int ticks = (int) dialogData.getFloat("interval_ticks", 100f); if (ticks < 20) ticks = 20; data.getMapBuilder().shuffleIntervalTicks(ticks); - data.triggerUpdate(); + data.triggerAreaViewUpdate(); + } + + /** + * Handles setting the reshuffle percentage based on the dialog data provided. + * @param data the BounceData instance containing the map builder + * @param dialogData the dialog data containing the reshuffle percentage to set + */ + private void handleReshufflePercentageSet(@NotNull BounceData data, @NotNull CompoundBinaryTag dialogData) { + float percentage = dialogData.getFloat("reshuffle_percentage", 10.0f); + double reshufflePercentage = Math.max(0.0, Math.min(1.0, percentage / 100.0)); + data.getMapBuilder().reshufflePercentage(reshufflePercentage); + data.triggerAreaViewUpdate(); } /** diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerDialogRequestListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerDialogRequestListener.java index 57c11919..97f9767a 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerDialogRequestListener.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerDialogRequestListener.java @@ -35,6 +35,7 @@ public void accept(@NotNull PlayerDialogRequestEvent event) { case Target.SETUP_BLOCK_BOUNCE -> dialogTemplate = dialogRegistry.get(ValueInputDialog.DIALOG_KEY); case Target.SETUP_BLOCK_WEIGHT -> dialogTemplate = dialogRegistry.get(net.theevilreaper.bounce.setup.dialog.type.WeightInputDialog.DIALOG_KEY); case Target.SETUP_SHUFFLE_INTERVAL -> dialogTemplate = dialogRegistry.get(net.theevilreaper.bounce.setup.dialog.type.ShuffleIntervalInputDialog.DIALOG_KEY); + case Target.SETUP_RESHUFFLE_PERCENTAGE -> dialogTemplate = dialogRegistry.get(net.theevilreaper.bounce.setup.dialog.type.ReshufflePercentageInputDialog.DIALOG_KEY); default -> throw new IllegalArgumentException("Unknown target: " + target); } From a114687df69de06acaabefd9c7620f2756c6e061 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:21:40 +0200 Subject: [PATCH 29/43] chore(setup): update import --- .../net/theevilreaper/bounce/setup/data/BounceData.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java b/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java index 8e4d7821..f0660acf 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java @@ -13,7 +13,7 @@ import net.theevilreaper.bounce.common.map.GameMap; import net.theevilreaper.bounce.common.util.GsonUtil; import net.theevilreaper.bounce.setup.builder.GameMapBuilder; -import net.theevilreaper.bounce.setup.inventory.ground.AreaViewInventory; +import net.theevilreaper.bounce.setup.inventory.area.AreaViewInventory; import net.theevilreaper.bounce.setup.inventory.ground.GroundViewInventory; import net.theevilreaper.bounce.setup.inventory.overview.MapOverviewInventory; import net.theevilreaper.bounce.setup.inventory.push.PushValueInventory; @@ -144,6 +144,10 @@ public void triggerUpdate() { this.overviewInventory.invalidateDataLayout(); } + public void triggerAreaViewUpdate() { + this.areaViewInventory.invalidateDataLayout(); + } + public void triggerGroundViewUpdate() { this.groundViewInventory.invalidateDataLayout(); this.groundViewInventory.invalidateGroundValueInventory(); From 74a0b822ce4ea0da324754b2ed9fd5c0329fc842 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:32:50 +0200 Subject: [PATCH 30/43] feat(slot): add are slot implementations --- .../inventory/slot/area/AbstractAreaSlot.java | 51 ++++++++++++++++ .../inventory/slot/area/AreaCornerSlot.java | 60 +++++++++++++++++++ .../slot/{ => area}/AreaOverviewSlot.java | 7 ++- .../slot/area/ReshufflePercentageSlot.java | 50 ++++++++++++++++ .../slot/area/ShuffleIntervalSlot.java | 50 ++++++++++++++++ .../inventory/slot/area/package-info.java | 4 ++ 6 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AbstractAreaSlot.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaCornerSlot.java rename setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/{ => area}/AreaOverviewSlot.java (85%) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ReshufflePercentageSlot.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ShuffleIntervalSlot.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/package-info.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AbstractAreaSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AbstractAreaSlot.java new file mode 100644 index 00000000..dd255474 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AbstractAreaSlot.java @@ -0,0 +1,51 @@ +package net.theevilreaper.bounce.setup.inventory.slot.area; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.component.DataComponents; +import net.minestom.server.entity.Player; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.theevilreaper.aves.inventory.click.ClickHolder; +import net.theevilreaper.aves.inventory.slot.Slot; +import net.theevilreaper.bounce.setup.inventory.area.AreaViewInventory; +import net.theevilreaper.bounce.setup.inventory.area.AreaViewType; + +import java.util.function.Consumer; + +/** + * Base class for {@link AreaViewType}-bound slots shown in the {@link AreaViewInventory}. + */ +public abstract class AbstractAreaSlot extends Slot { + + protected static final Component CLICK_TO_EDIT = Component.text("Click to edit", NamedTextColor.GRAY); + protected final AreaViewType type; + + protected AbstractAreaSlot(AreaViewType type) { + this.type = type; + this.setClick(this::click); + } + + protected abstract void click( + Player player, + int slot, + Click clickType, + ItemStack stack, + Consumer result + ); + + /** + * Converts the given ItemStack to a builder, allowing for modifications + * without altering the original stack. + * + * @param stack the ItemStack to convert + * @return a new ItemStack.Builder with the same material and custom name + */ + protected ItemStack.Builder asBuilder(ItemStack stack) { + ItemStack.Builder builder = ItemStack.builder(stack.material()); + if (stack.has(DataComponents.CUSTOM_NAME)) { + builder.customName(stack.get(DataComponents.CUSTOM_NAME)); + } + return builder; + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaCornerSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaCornerSlot.java new file mode 100644 index 00000000..91334987 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaCornerSlot.java @@ -0,0 +1,60 @@ +package net.theevilreaper.bounce.setup.inventory.slot.area; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Player; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.theevilreaper.aves.inventory.click.ClickHolder; +import net.theevilreaper.bounce.setup.inventory.area.AreaViewType; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Displays one corner of the ground area (see {@link AreaViewType#LEFT_AREA_CORNER}/{@link AreaViewType#RIGHT_AREA_CORNER}) + * and captures the player's current position for it on left-click. + */ +public final class AreaCornerSlot extends AbstractAreaSlot { + + private final @Nullable Vec position; + private final Consumer onSet; + + public AreaCornerSlot(AreaViewType type, @Nullable Vec position, Consumer onSet) { + super(type); + this.position = position; + this.onSet = onSet; + } + + /** + * {@inheritDoc} + */ + @Override + public ItemStack getItem() { + ItemStack baseItem = this.type.getItem(); + if (position == null) { + return asBuilder(baseItem).lore( + Component.empty(), + Component.text("Not set", NamedTextColor.RED), + Component.empty(), + Component.text("Left-click: set to your position", NamedTextColor.GRAY) + ).build(); + } + return asBuilder(baseItem).lore( + Component.empty(), + Component.text("X: " + position.x() + " Y: " + position.y() + " Z: " + position.z(), NamedTextColor.YELLOW), + Component.empty(), + Component.text("Left-click: set to your position", NamedTextColor.GRAY) + ).build(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void click(Player player, int slot, Click click, ItemStack stack, Consumer result) { + result.accept(ClickHolder.cancelClick()); + if (click instanceof Click.Left) onSet.accept(player); + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AreaOverviewSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaOverviewSlot.java similarity index 85% rename from setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AreaOverviewSlot.java rename to setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaOverviewSlot.java index 949a1b43..e8201218 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AreaOverviewSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaOverviewSlot.java @@ -1,4 +1,4 @@ -package net.theevilreaper.bounce.setup.inventory.slot; +package net.theevilreaper.bounce.setup.inventory.slot.area; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -11,6 +11,7 @@ import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent; import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent.SwitchTarget; import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; +import net.theevilreaper.bounce.setup.inventory.slot.AbstractDataSlot; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -20,7 +21,7 @@ public final class AreaOverviewSlot extends AbstractDataSlot { private final @Nullable Area area; - public AreaOverviewSlot(@NotNull OverviewType overviewType, @Nullable Area area) { + public AreaOverviewSlot(OverviewType overviewType, @Nullable Area area) { super(overviewType); this.area = area; } @@ -47,7 +48,7 @@ public ItemStack getItem() { } @Override - protected void click(@NotNull Player player, int slot, @NotNull Click click, @NotNull ItemStack stack, @NotNull Consumer result) { + protected void click(Player player, int slot, Click click, ItemStack stack, Consumer result) { result.accept(ClickHolder.cancelClick()); EventDispatcher.call(new SetupInventorySwitchEvent(player, SwitchTarget.AREA_VIEW)); } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ReshufflePercentageSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ReshufflePercentageSlot.java new file mode 100644 index 00000000..c998b574 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ReshufflePercentageSlot.java @@ -0,0 +1,50 @@ +package net.theevilreaper.bounce.setup.inventory.slot.area; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.theevilreaper.aves.inventory.click.ClickHolder; +import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; +import net.theevilreaper.bounce.setup.inventory.area.AreaViewType; +import org.jetbrains.annotations.NotNull; + +import java.util.Locale; +import java.util.function.Consumer; + +public final class ReshufflePercentageSlot extends AbstractAreaSlot { + + private final double reshufflePercentage; + + public ReshufflePercentageSlot(AreaViewType type, double reshufflePercentage) { + super(type); + this.reshufflePercentage = reshufflePercentage; + } + + /** + * {@inheritDoc} + */ + @Override + public ItemStack getItem() { + ItemStack baseItem = this.type.getItem(); + double percentage = reshufflePercentage * 100.0; + return asBuilder(baseItem).lore( + Component.empty(), + Component.text(String.format(Locale.ROOT, "%.1f%%", percentage), NamedTextColor.YELLOW), + Component.empty(), + CLICK_TO_EDIT, + Component.empty() + ).build(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void click(Player player, int slot, Click click, ItemStack stack, Consumer result) { + result.accept(ClickHolder.cancelClick()); + EventDispatcher.call(new PlayerDialogRequestEvent(player, PlayerDialogRequestEvent.Target.SETUP_RESHUFFLE_PERCENTAGE)); + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ShuffleIntervalSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ShuffleIntervalSlot.java new file mode 100644 index 00000000..ef78bc0b --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ShuffleIntervalSlot.java @@ -0,0 +1,50 @@ +package net.theevilreaper.bounce.setup.inventory.slot.area; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.theevilreaper.aves.inventory.click.ClickHolder; +import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; +import net.theevilreaper.bounce.setup.inventory.area.AreaViewType; +import org.jetbrains.annotations.NotNull; + +import java.util.Locale; +import java.util.function.Consumer; + +public final class ShuffleIntervalSlot extends AbstractAreaSlot { + + private final int shuffleIntervalTicks; + + public ShuffleIntervalSlot(AreaViewType type, int shuffleIntervalTicks) { + super(type); + this.shuffleIntervalTicks = shuffleIntervalTicks; + } + + /** + * {@inheritDoc} + */ + @Override + public ItemStack getItem() { + ItemStack baseItem = this.type.getItem(); + double seconds = shuffleIntervalTicks / 20.0; + return asBuilder(baseItem).lore( + Component.empty(), + Component.text(String.format(Locale.ROOT, "%d ticks (%.1fs)", shuffleIntervalTicks, seconds), NamedTextColor.YELLOW), + Component.empty(), + Component.text("Click to edit", NamedTextColor.GRAY), + Component.empty() + ).build(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void click(Player player, int slot, Click click, ItemStack stack, Consumer result) { + result.accept(ClickHolder.cancelClick()); + EventDispatcher.call(new PlayerDialogRequestEvent(player, PlayerDialogRequestEvent.Target.SETUP_SHUFFLE_INTERVAL)); + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/package-info.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/package-info.java new file mode 100644 index 00000000..de65f7d0 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.setup.inventory.slot.area; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file From dddaec69927427e11e098f12455715d5e259c85d Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:33:02 +0200 Subject: [PATCH 31/43] chore(map): add new getter --- .../bounce/setup/builder/GameMapBuilder.java | 48 +++++++++++++++++++ .../setup/builder/GameMapBuilderTest.java | 41 ++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java index 8a669f52..c663e613 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java @@ -1,6 +1,7 @@ package net.theevilreaper.bounce.setup.builder; import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import net.theevilreaper.aves.map.BaseMapBuilder; import net.theevilreaper.bounce.common.ground.Area; @@ -20,6 +21,8 @@ public final class GameMapBuilder extends BaseMapBuilder { private @Nullable Area area; private int shuffleIntervalTicks; private double reshufflePercentage; + private @Nullable Vec pos1; + private @Nullable Vec pos2; public GameMapBuilder() { super(); @@ -44,6 +47,11 @@ public GameMapBuilder(@NotNull GameMap gameMap) { ? gameMap.getReshufflePercentage() : DEFAULT_RESHUFFLE_PERCENTAGE; + if (this.area != null) { + this.pos1 = this.area.min(); + this.pos2 = this.area.max(); + } + if (gameMap.getPushData() == null) { this.pushDataBuilder = PushData.builder(); this.pushDataBuilder @@ -112,6 +120,28 @@ public GameMapBuilder(@NotNull GameMap gameMap) { return this; } + /** + * Sets the first captured corner of the ground area. + * + * @param pos1 the corner position, or {@code null} to clear it + * @return this builder instance for chaining + */ + public @NotNull GameMapBuilder pos1(@Nullable Vec pos1) { + this.pos1 = pos1; + return this; + } + + /** + * Sets the second captured corner of the ground area. + * + * @param pos2 the corner position, or {@code null} to clear it + * @return this builder instance for chaining + */ + public @NotNull GameMapBuilder pos2(@Nullable Vec pos2) { + this.pos2 = pos2; + return this; + } + /** * Builds a new {@link GameMap} instance with the current properties. * @@ -158,6 +188,24 @@ public double getReshufflePercentage() { return reshufflePercentage; } + /** + * Returns the first captured corner of the ground area. + * + * @return the corner position, or {@code null} if not set + */ + public @Nullable Vec getPos1() { + return pos1; + } + + /** + * Returns the second captured corner of the ground area. + * + * @return the corner position, or {@code null} if not set + */ + public @Nullable Vec getPos2() { + return pos2; + } + /** * Returns the {@link PushData.Builder} instance used to build push data. * diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java index 473ea20a..927dd5c1 100644 --- a/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java @@ -110,4 +110,45 @@ void testReloadingExistingMapWithoutAreaKeepsDefaultInterval() { assertTrue(reloaded.getShuffleIntervalTicks() > 0); assertTrue(reloaded.getReshufflePercentage() > 0); } + + @Test + void testNewBuilderHasNoAreaCorners() { + GameMapBuilder builder = new GameMapBuilder(); + assertNull(builder.getPos1()); + assertNull(builder.getPos2()); + } + + @Test + void testAreaCornersRoundTripThroughSetters() { + GameMapBuilder builder = new GameMapBuilder(); + Vec pos1 = new Vec(1, 2, 3); + Vec pos2 = new Vec(4, 5, 6); + + builder.pos1(pos1).pos2(pos2); + + assertEquals(pos1, builder.getPos1()); + assertEquals(pos2, builder.getPos2()); + } + + @Test + void testReloadingExistingMapWithAreaRestoresCorners() { + GameMapBuilder builder = new GameMapBuilder(); + Area area = new GroundArea(new Vec(1, 2, 3), new Vec(4, 5, 6), Block.GLASS, PushData.builder().build()); + builder.area(area); + + GameMap gameMap = builder.build(); + GameMapBuilder reloaded = new GameMapBuilder(gameMap); + + assertEquals(area.min(), reloaded.getPos1()); + assertEquals(area.max(), reloaded.getPos2()); + } + + @Test + void testReloadingExistingMapWithoutAreaHasNoCorners() { + GameMap gameMap = new GameMapBuilder().build(); + GameMapBuilder reloaded = new GameMapBuilder(gameMap); + + assertNull(reloaded.getPos1()); + assertNull(reloaded.getPos2()); + } } \ No newline at end of file From ba5d3d24282c053f5a19d6cf9bd79479705f2de9 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:33:18 +0200 Subject: [PATCH 32/43] chore(setup): improve inventory handling --- .../inventory/area/AreaViewInventory.java | 128 ++++-------------- .../setup/inventory/area/AreaViewType.java | 4 +- .../overview/MapOverviewInventory.java | 3 +- .../slot/ShuffleIntervalOverviewSlot.java | 43 ------ .../AreaViewInventoryIntegrationTest.java | 40 +++--- 5 files changed, 45 insertions(+), 173 deletions(-) delete mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/ShuffleIntervalOverviewSlot.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventory.java index 2fec948c..684324f2 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventory.java @@ -1,28 +1,22 @@ package net.theevilreaper.bounce.setup.inventory.area; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Player; -import net.minestom.server.event.EventDispatcher; import net.minestom.server.inventory.InventoryType; -import net.minestom.server.inventory.click.Click; -import net.minestom.server.item.ItemStack; -import net.minestom.server.item.Material; +import net.kyori.adventure.text.Component; import net.theevilreaper.aves.inventory.PersonalInventoryBuilder; -import net.theevilreaper.aves.inventory.click.ClickHolder; import net.theevilreaper.aves.inventory.layout.InventoryLayout; import net.theevilreaper.aves.inventory.util.LayoutCalculator; import net.theevilreaper.bounce.common.ground.Area; import net.theevilreaper.bounce.common.ground.GroundArea; import net.theevilreaper.bounce.common.push.PushEntry; import net.theevilreaper.bounce.setup.builder.GameMapBuilder; -import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent.SwitchTarget; import net.theevilreaper.bounce.setup.inventory.slot.SwitchTargetSlot; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import net.theevilreaper.bounce.setup.inventory.slot.area.AreaCornerSlot; +import net.theevilreaper.bounce.setup.inventory.slot.area.ReshufflePercentageSlot; +import net.theevilreaper.bounce.setup.inventory.slot.area.ShuffleIntervalSlot; import static net.theevilreaper.bounce.setup.util.SetupItems.DECORATION; @@ -33,23 +27,14 @@ public final class AreaViewInventory extends PersonalInventoryBuilder { private static final int SHUFFLE_INTERVAL_SLOT = 10; private static final int POS1_SLOT = 11; private static final int POS2_SLOT = 13; - private static final int CONFIRM_SLOT = 15; private static final int RESHUFFLE_PERCENTAGE_SLOT = 16; private final GameMapBuilder gameMapBuilder; - private @Nullable Vec pos1; - private @Nullable Vec pos2; public AreaViewInventory(Player player, GameMapBuilder gameMapBuilder) { super(TITLE, InventoryType.CHEST_3_ROW, player); this.gameMapBuilder = gameMapBuilder; - Area existingArea = gameMapBuilder.getArea(); - if (existingArea != null) { - this.pos1 = existingArea.min(); - this.pos2 = existingArea.max(); - } - InventoryLayout layout = InventoryLayout.fromType(getType()); layout.setItems(LayoutCalculator.quad(0, getType().getSize() - 1), DECORATION); layout.setItem(getType().getSize() - 1, new SwitchTargetSlot(SwitchTarget.MAP_OVERVIEW)); @@ -57,122 +42,57 @@ public AreaViewInventory(Player player, GameMapBuilder gameMapBuilder) { this.setDataLayoutFunction(dataLayoutFunction -> { InventoryLayout dataLayout = dataLayoutFunction == null ? InventoryLayout.fromType(getType()) : dataLayoutFunction; - dataLayout.blank(LayoutCalculator.from(SHUFFLE_INTERVAL_SLOT, POS1_SLOT, POS2_SLOT, CONFIRM_SLOT, RESHUFFLE_PERCENTAGE_SLOT)); - - dataLayout.setItem(SHUFFLE_INTERVAL_SLOT, getShuffleIntervalItem(), (p, slot, click, stack, result) -> { - result.accept(ClickHolder.cancelClick()); - EventDispatcher.call(new PlayerDialogRequestEvent(p, PlayerDialogRequestEvent.Target.SETUP_SHUFFLE_INTERVAL)); - }); - dataLayout.setItem(POS1_SLOT, getPosItem(AreaViewType.LEFT_AREA_CORNER, pos1), (p, slot, click, stack, result) -> { - result.accept(ClickHolder.cancelClick()); - if (click instanceof Click.Left) setPos1ToCurrentPosition(p); - }); - dataLayout.setItem(POS2_SLOT, getPosItem(AreaViewType.RIGHT_AREA_CORNER, pos2), (p, slot, click, stack, result) -> { - result.accept(ClickHolder.cancelClick()); - if (click instanceof Click.Left) setPos2ToCurrentPosition(p); - }); - dataLayout.setItem(CONFIRM_SLOT, getConfirmItem(), (p, slot, click, stack, result) -> { - result.accept(ClickHolder.cancelClick()); - if (click instanceof Click.Left) confirm(p); - }); - dataLayout.setItem(RESHUFFLE_PERCENTAGE_SLOT, getReshufflePercentageItem(), (p, slot, click, stack, result) -> { - result.accept(ClickHolder.cancelClick()); - EventDispatcher.call(new PlayerDialogRequestEvent(p, PlayerDialogRequestEvent.Target.SETUP_RESHUFFLE_PERCENTAGE)); - }); + dataLayout.blank(LayoutCalculator.from(SHUFFLE_INTERVAL_SLOT, POS1_SLOT, POS2_SLOT, RESHUFFLE_PERCENTAGE_SLOT)); + + dataLayout.setItem(SHUFFLE_INTERVAL_SLOT, new ShuffleIntervalSlot(AreaViewType.SHUFFLE_INTERVAL, gameMapBuilder.getShuffleIntervalTicks())); + dataLayout.setItem(POS1_SLOT, new AreaCornerSlot(AreaViewType.LEFT_AREA_CORNER, gameMapBuilder.getPos1(), this::setPos1ToCurrentPosition)); + dataLayout.setItem(POS2_SLOT, new AreaCornerSlot(AreaViewType.RIGHT_AREA_CORNER, gameMapBuilder.getPos2(), this::setPos2ToCurrentPosition)); + dataLayout.setItem(RESHUFFLE_PERCENTAGE_SLOT, new ReshufflePercentageSlot(AreaViewType.RESHUFFLE_PERCENTAGE, gameMapBuilder.getReshufflePercentage())); return dataLayout; }); } /** - * Sets Pos1 to the player's current position and refreshes the layout. + * Sets Pos1 on the {@link GameMapBuilder} to the player's current position, rebuilds the area once both + * corners are known, and refreshes the layout. * * @param player the player whose position is captured */ public void setPos1ToCurrentPosition(Player player) { - this.pos1 = toVec(player.getPosition()); + gameMapBuilder.pos1(toVec(player.getPosition())); + rebuildAreaIfBothCornersSet(); this.invalidateDataLayout(); } /** - * Sets Pos2 to the player's current position and refreshes the layout. + * Sets Pos2 on the {@link GameMapBuilder} to the player's current position, rebuilds the area once both + * corners are known, and refreshes the layout. * * @param player the player whose position is captured */ public void setPos2ToCurrentPosition(Player player) { - this.pos2 = toVec(player.getPosition()); + gameMapBuilder.pos2(toVec(player.getPosition())); + rebuildAreaIfBothCornersSet(); this.invalidateDataLayout(); } /** - * Builds a {@link GroundArea} from the captured positions and the current ground block/push data, and stores - * it on the {@link GameMapBuilder}. A no-op if either position is still unset. - * - * @param player the player to notify + * Builds a {@link GroundArea} from the captured corners and the current ground block/push data, and stores + * it on the {@link GameMapBuilder}. A no-op if either corner is still unset. Final validation that an area + * is actually configured happens when the map itself is saved. */ - public void confirm(Player player) { + private void rebuildAreaIfBothCornersSet() { + Vec pos1 = gameMapBuilder.getPos1(); + Vec pos2 = gameMapBuilder.getPos2(); if (pos1 == null || pos2 == null) return; PushEntry groundEntry = gameMapBuilder.getGroundBlockEntry(); Area area = new GroundArea(pos1, pos2, groundEntry.getBlock(), gameMapBuilder.getPushDataBuilder().build()); gameMapBuilder.area(area); - player.sendMessage(Component.text("Area saved.", NamedTextColor.GREEN)); } private Vec toVec(Pos pos) { return new Vec(pos.x(), pos.y(), pos.z()); } - - private ItemStack getPosItem(AreaViewType type, @Nullable Vec pos) { - ItemStack.Builder builder = ItemStack.builder(type.getMaterial()) - .customName(Component.text(type.getName(), type.getColor())); - if (pos == null) { - return builder.lore( - Component.empty(), - Component.text("Not set", NamedTextColor.RED), - Component.empty(), - Component.text("Left-click: set to your position", NamedTextColor.GRAY) - ).build(); - } - return builder.lore( - Component.empty(), - Component.text("X: " + pos.x() + " Y: " + pos.y() + " Z: " + pos.z(), NamedTextColor.YELLOW), - Component.empty(), - Component.text("Left-click: set to your position", NamedTextColor.GRAY) - ).build(); - } - - private ItemStack getConfirmItem() { - boolean ready = pos1 != null && pos2 != null; - return ItemStack.builder(ready ? Material.LIME_DYE : Material.GRAY_DYE) - .customName(Component.text(ready ? "Confirm area" : "Set both positions first", ready ? NamedTextColor.GREEN : NamedTextColor.RED)) - .build(); - } - - private ItemStack getShuffleIntervalItem() { - int ticks = gameMapBuilder.getShuffleIntervalTicks(); - double seconds = ticks / 20.0; - return ItemStack.builder(Material.CLOCK) - .customName(Component.text("Reshuffle Interval", NamedTextColor.LIGHT_PURPLE)) - .lore( - Component.empty(), - Component.text(String.format(java.util.Locale.ROOT, "%d ticks (%.1fs)", ticks, seconds), NamedTextColor.YELLOW), - Component.empty(), - Component.text("Click to edit", NamedTextColor.GRAY), - Component.empty() - ).build(); - } - - private ItemStack getReshufflePercentageItem() { - double percentage = gameMapBuilder.getReshufflePercentage() * 100.0; - return ItemStack.builder(Material.TARGET) - .customName(Component.text("Reshuffle Percentage", NamedTextColor.LIGHT_PURPLE)) - .lore( - Component.empty(), - Component.text(String.format(java.util.Locale.ROOT, "%.1f%%", percentage), NamedTextColor.YELLOW), - Component.empty(), - Component.text("Click to edit", NamedTextColor.GRAY), - Component.empty() - ).build(); - } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java index 24db07c1..1fc3c604 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java @@ -16,7 +16,9 @@ public enum AreaViewType { LEFT_AREA_CORNER("Left Corner", Material.GREEN_WOOL, NamedTextColor.AQUA), - RIGHT_AREA_CORNER("Right Corner", Material.RED_WOOL, NamedTextColor.AQUA) + RIGHT_AREA_CORNER("Right Corner", Material.RED_WOOL, NamedTextColor.AQUA), + SHUFFLE_INTERVAL("Reshuffle Interval", Material.CLOCK, NamedTextColor.LIGHT_PURPLE), + RESHUFFLE_PERCENTAGE("Reshuffle Percentage", Material.TARGET, NamedTextColor.LIGHT_PURPLE) ; diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java index 144af9b7..98eaa221 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java @@ -8,7 +8,7 @@ import net.theevilreaper.aves.inventory.slot.ISlot; import net.theevilreaper.aves.inventory.util.LayoutCalculator; import net.theevilreaper.bounce.setup.builder.GameMapBuilder; -import net.theevilreaper.bounce.setup.inventory.slot.AreaOverviewSlot; +import net.theevilreaper.bounce.setup.inventory.slot.area.AreaOverviewSlot; import net.theevilreaper.bounce.setup.inventory.slot.MultiStringSlot; import net.theevilreaper.bounce.setup.inventory.slot.PositionSlot; import net.theevilreaper.bounce.setup.inventory.slot.StringSlot; @@ -69,7 +69,6 @@ private ISlot getOverViewItem(OverviewType type) { case NAME -> new StringSlot(type, builder.getName()); case BUILDER -> new MultiStringSlot(type, builder.getBuilders()); case AREA -> new AreaOverviewSlot(type, builder.getArea()); - default -> throw new IllegalArgumentException("Unsupported overview type: " + type); }; } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/ShuffleIntervalOverviewSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/ShuffleIntervalOverviewSlot.java deleted file mode 100644 index 93085301..00000000 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/ShuffleIntervalOverviewSlot.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.theevilreaper.bounce.setup.inventory.slot; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.minestom.server.entity.Player; -import net.minestom.server.event.EventDispatcher; -import net.minestom.server.inventory.click.Click; -import net.minestom.server.item.ItemStack; -import net.theevilreaper.aves.inventory.click.ClickHolder; -import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; -import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; -import org.jetbrains.annotations.NotNull; - -import java.util.function.Consumer; - -public final class ShuffleIntervalOverviewSlot extends AbstractDataSlot { - - private final int shuffleIntervalTicks; - - public ShuffleIntervalOverviewSlot(@NotNull OverviewType overviewType, int shuffleIntervalTicks) { - super(overviewType); - this.shuffleIntervalTicks = shuffleIntervalTicks; - } - - @Override - public ItemStack getItem() { - ItemStack overviewItem = this.type.getItem(); - double seconds = shuffleIntervalTicks / 20.0; - return asBuilder(overviewItem).lore( - Component.empty(), - Component.text(String.format(java.util.Locale.ROOT, "%d ticks (%.1fs)", shuffleIntervalTicks, seconds), NamedTextColor.YELLOW), - Component.empty(), - Component.text("Click to edit", NamedTextColor.GRAY), - Component.empty() - ).build(); - } - - @Override - protected void click(@NotNull Player player, int slot, @NotNull Click click, @NotNull ItemStack stack, @NotNull Consumer result) { - result.accept(ClickHolder.cancelClick()); - EventDispatcher.call(new PlayerDialogRequestEvent(player, PlayerDialogRequestEvent.Target.SETUP_SHUFFLE_INTERVAL)); - } -} diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventoryIntegrationTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventoryIntegrationTest.java index fbad8358..bfea5405 100644 --- a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventoryIntegrationTest.java +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewInventoryIntegrationTest.java @@ -1,13 +1,13 @@ package net.theevilreaper.bounce.setup.inventory.area; import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.item.Material; import net.minestom.testing.Env; import net.minestom.testing.extension.MicrotusExtension; import net.theevilreaper.aves.inventory.layout.InventoryLayout; -import net.theevilreaper.aves.inventory.slot.ISlot; import net.theevilreaper.bounce.setup.builder.GameMapBuilder; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -21,11 +21,10 @@ class AreaViewInventoryIntegrationTest { private static final int SHUFFLE_INTERVAL_SLOT = 10; private static final int POS1_SLOT = 11; private static final int POS2_SLOT = 13; - private static final int CONFIRM_SLOT = 15; private static final int RESHUFFLE_PERCENTAGE_SLOT = 16; @Test - void testConfirmSlotStartsDisabledAndBothPositionsAreUnset(@NotNull Env env) { + void testPosSlotsUseTheAreaViewTypeMaterials(@NotNull Env env) { Instance instance = env.createFlatInstance(); Player player = env.createPlayer(instance); GameMapBuilder gameMapBuilder = new GameMapBuilder(); @@ -35,48 +34,50 @@ void testConfirmSlotStartsDisabledAndBothPositionsAreUnset(@NotNull Env env) { env.tick(); InventoryLayout dataLayout = inventory.getDataLayout(); - assertEquals(Material.GRAY_DYE, dataLayout.getSlot(CONFIRM_SLOT).getItem().material()); + assertEquals(AreaViewType.LEFT_AREA_CORNER.getMaterial(), dataLayout.getSlot(POS1_SLOT).getItem().material()); + assertEquals(AreaViewType.RIGHT_AREA_CORNER.getMaterial(), dataLayout.getSlot(POS2_SLOT).getItem().material()); env.destroyInstance(instance, true); } @Test - void testPosSlotsUseTheAreaViewTypeMaterials(@NotNull Env env) { + void testShuffleIntervalAndReshufflePercentageSlotsShowBuilderValues(@NotNull Env env) { Instance instance = env.createFlatInstance(); Player player = env.createPlayer(instance); GameMapBuilder gameMapBuilder = new GameMapBuilder(); + gameMapBuilder.shuffleIntervalTicks(60).reshufflePercentage(0.25); AreaViewInventory inventory = new AreaViewInventory(player, gameMapBuilder); inventory.open(); env.tick(); InventoryLayout dataLayout = inventory.getDataLayout(); - assertEquals(AreaViewType.LEFT_AREA_CORNER.getMaterial(), dataLayout.getSlot(POS1_SLOT).getItem().material()); - assertEquals(AreaViewType.RIGHT_AREA_CORNER.getMaterial(), dataLayout.getSlot(POS2_SLOT).getItem().material()); + assertEquals(Material.CLOCK, dataLayout.getSlot(SHUFFLE_INTERVAL_SLOT).getItem().material()); + assertEquals(Material.TARGET, dataLayout.getSlot(RESHUFFLE_PERCENTAGE_SLOT).getItem().material()); env.destroyInstance(instance, true); } @Test - void testShuffleIntervalAndReshufflePercentageSlotsShowBuilderValues(@NotNull Env env) { + void testSettingOnlyOneCornerDoesNotBuildAnArea(@NotNull Env env) { Instance instance = env.createFlatInstance(); Player player = env.createPlayer(instance); + player.teleport(new Pos(1, 2, 3)).join(); GameMapBuilder gameMapBuilder = new GameMapBuilder(); - gameMapBuilder.shuffleIntervalTicks(60).reshufflePercentage(0.25); AreaViewInventory inventory = new AreaViewInventory(player, gameMapBuilder); inventory.open(); env.tick(); - InventoryLayout dataLayout = inventory.getDataLayout(); - assertEquals(Material.CLOCK, dataLayout.getSlot(SHUFFLE_INTERVAL_SLOT).getItem().material()); - assertEquals(Material.TARGET, dataLayout.getSlot(RESHUFFLE_PERCENTAGE_SLOT).getItem().material()); + inventory.setPos1ToCurrentPosition(player); + + assertNull(gameMapBuilder.getArea(), "An area must not be built until both corners are set"); env.destroyInstance(instance, true); } @Test - void testClickingBothPosButtonsEnablesConfirmAndSavesArea(@NotNull Env env) { + void testSettingBothCornersAutomaticallyBuildsTheArea(@NotNull Env env) { Instance instance = env.createFlatInstance(); Player player = env.createPlayer(instance); player.teleport(new Pos(1, 2, 3)).join(); @@ -88,17 +89,10 @@ void testClickingBothPosButtonsEnablesConfirmAndSavesArea(@NotNull Env env) { inventory.setPos1ToCurrentPosition(player); inventory.setPos2ToCurrentPosition(player); - env.tick(); - - InventoryLayout dataLayout = inventory.getDataLayout(); - ISlot confirmSlot = dataLayout.getSlot(CONFIRM_SLOT); - assertEquals(Material.LIME_DYE, confirmSlot.getItem().material()); - - inventory.confirm(player); - assertNotNull(gameMapBuilder.getArea()); - assertEquals(new net.minestom.server.coordinate.Vec(1, 2, 3), gameMapBuilder.getArea().min()); - assertEquals(new net.minestom.server.coordinate.Vec(1, 2, 3), gameMapBuilder.getArea().max()); + assertNotNull(gameMapBuilder.getArea(), "The area must be built automatically once both corners are set"); + assertEquals(new Vec(1, 2, 3), gameMapBuilder.getArea().min()); + assertEquals(new Vec(1, 2, 3), gameMapBuilder.getArea().max()); env.destroyInstance(instance, true); } From 56d1450abb77bc98b5d3ecf4bf4baf949155e939 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:34:11 +0200 Subject: [PATCH 33/43] feat(dialog): add shuffle dialog --- .../type/ReshufflePercentageInputDialog.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/ReshufflePercentageInputDialog.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/ReshufflePercentageInputDialog.java b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/ReshufflePercentageInputDialog.java new file mode 100644 index 00000000..fc3f945e --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/ReshufflePercentageInputDialog.java @@ -0,0 +1,70 @@ +package net.theevilreaper.bounce.setup.dialog.type; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.dialog.*; +import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.server.common.ShowDialogPacket; +import net.theevilreaper.bounce.setup.dialog.AbstractDialogTemplate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class ReshufflePercentageInputDialog extends AbstractDialogTemplate { + + public static final Key DIALOG_KEY = Key.key("bounce", "reshuffle_percentage_setup_dialog"); + + public ReshufflePercentageInputDialog() { + super( + Component.text("Change reshuffle percentage"), + Component.text("Click to confirm"), + Component.text("Click to cancel") + ); + } + + @Override + public void open(@NotNull Player player) { + this.open(player, 10.0f); + } + + @Override + public void open(@NotNull Player player, @Nullable Float data) { + float initial = data != null ? data : 10.0f; + ShowDialogPacket packet = new ShowDialogPacket(new Dialog.Confirmation( + new DialogMetadata( + header, + null, + false, + false, + DialogAfterAction.CLOSE, + List.of( + new DialogBody.PlainMessage(Component.text("Percentage of the area re-rolled\non each reshuffle (0 - 100%):"), 320) + ), + List.of( + new DialogInput.NumberRange("reshuffle_percentage", 320, Component.text("Percentage"), "options.percent_value", 0f, 100f, initial, 0.1f) + ) + ), + new DialogActionButton( + submitComponent, + Component.text("Click to confirm", NamedTextColor.GREEN), + 155, + new DialogAction.DynamicCustom(DIALOG_KEY, CompoundBinaryTag.builder().build()) + ), + new DialogActionButton( + cancelComponent, + Component.text("Click to cancel", NamedTextColor.RED), + 155, + null + ) + )); + player.sendPacket(packet); + } + + @Override + public @NotNull Key key() { + return DIALOG_KEY; + } +} From 88990b15df7779ff186644720a534076c837e521 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:44:34 +0200 Subject: [PATCH 34/43] feat(setup): add generic layer over the data types --- .../bounce/setup/inventory/DataType.java | 36 +++++++++++++++++++ .../setup/inventory/area/AreaViewType.java | 23 ++++-------- .../overview/MapOverviewInventory.java | 4 +-- .../inventory/overview/OverviewType.java | 7 +++- 4 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/DataType.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/DataType.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/DataType.java new file mode 100644 index 00000000..4d4dd819 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/DataType.java @@ -0,0 +1,36 @@ +package net.theevilreaper.bounce.setup.inventory; + +import net.kyori.adventure.text.format.TextColor; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; + +public interface DataType { + + /** + * Gets the name of this overview type. + * + * @return the name of + */ + String getName(); + + /** + * Gets the material associated with this overview type. + * + * @return the material + */ + Material getMaterial(); + + /** + * Gets the text color associated with this overview type. + * + * @return the text color + */ + TextColor getColor(); + + /** + * Gets the ItemStack representation of this data type. + * + * @return the ItemStack for this data type + */ + ItemStack getItem(); +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java index 1fc3c604..3eeab2af 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewType.java @@ -5,6 +5,7 @@ import net.kyori.adventure.text.format.TextColor; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; +import net.theevilreaper.bounce.setup.inventory.DataType; import java.util.EnumMap; import java.util.Map; @@ -13,7 +14,7 @@ * The {@link AreaViewType} enum represents different types of items that can be displayed in the * {@link AreaViewInventory}. */ -public enum AreaViewType { +public enum AreaViewType implements DataType { LEFT_AREA_CORNER("Left Corner", Material.GREEN_WOOL, NamedTextColor.AQUA), RIGHT_AREA_CORNER("Right Corner", Material.RED_WOOL, NamedTextColor.AQUA), @@ -47,6 +48,7 @@ public enum AreaViewType { * * @return the name of */ + @Override public String getName() { return name; } @@ -56,6 +58,7 @@ public String getName() { * * @return the material */ + @Override public Material getMaterial() { return material; } @@ -65,6 +68,7 @@ public Material getMaterial() { * * @return the text color */ + @Override public TextColor getColor() { return color; } @@ -74,25 +78,10 @@ public TextColor getColor() { * * @return the ItemStack for this area view type */ + @Override public ItemStack getItem() { return itemCache.computeIfAbsent(this, type -> ItemStack.builder(type.getMaterial()) .customName(Component.text(type.getName(), type.getColor())) .build()); } - - /** - * Gets all available area view types. - * - * @return an array of all AreaViewType values - */ - public static AreaViewType[] getValues() { - return VALUES; - } - - public static AreaViewType fromOrdinal(int ordinal) { - if (ordinal < 0 || ordinal >= VALUES.length) { - throw new IndexOutOfBoundsException("Invalid ordinal for AreaViewType: " + ordinal); - } - return VALUES[ordinal]; - } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java index 98eaa221..8eb9424c 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/MapOverviewInventory.java @@ -64,8 +64,8 @@ public MapOverviewInventory(Player player, GameMapBuilder builder) { */ private ISlot getOverViewItem(OverviewType type) { return switch (type) { - case SPAWN -> new PositionSlot(type, this.builder.getSpawn()); - case GAME_SPAWN -> new PositionSlot(type, this.builder.getGameSpawn()); + case SPAWN -> new PositionSlot<>(type, this.builder.getSpawn()); + case GAME_SPAWN -> new PositionSlot<>(type, this.builder.getGameSpawn()); case NAME -> new StringSlot(type, builder.getName()); case BUILDER -> new MultiStringSlot(type, builder.getBuilders()); case AREA -> new AreaOverviewSlot(type, builder.getArea()); diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java index 406a513b..c08dd510 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/overview/OverviewType.java @@ -5,6 +5,7 @@ import net.kyori.adventure.text.format.TextColor; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; +import net.theevilreaper.bounce.setup.inventory.DataType; import java.util.EnumMap; import java.util.Map; @@ -17,7 +18,7 @@ * @version 1.0.0 * @since 0.1.0 */ -public enum OverviewType { +public enum OverviewType implements DataType { NAME("Map Name", Material.OAK_SIGN, NamedTextColor.YELLOW), BUILDER("Builder", Material.OAK_HANGING_SIGN, NamedTextColor.AQUA), @@ -52,6 +53,7 @@ public enum OverviewType { * * @return the name of */ + @Override public String getName() { return name; } @@ -61,6 +63,7 @@ public String getName() { * * @return the material */ + @Override public Material getMaterial() { return material; } @@ -70,6 +73,7 @@ public Material getMaterial() { * * @return the text color */ + @Override public TextColor getColor() { return color; } @@ -79,6 +83,7 @@ public TextColor getColor() { * * @return the ItemStack for this overview type */ + @Override public ItemStack getItem() { return itemCache.computeIfAbsent(this, type -> ItemStack.builder(type.getMaterial()) .customName(Component.text(type.getName(), type.getColor())) From 199d5f758c76762d40bbb230cbed041d890daf19 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:44:51 +0200 Subject: [PATCH 35/43] chore(slot): simplify slot implementation --- .../inventory/slot/AbstractDataSlot.java | 28 ++++++++++++------- .../setup/inventory/slot/EmptyPushSlot.java | 6 ++-- .../setup/inventory/slot/MaterialSlot.java | 8 ++++-- .../setup/inventory/slot/MultiStringSlot.java | 17 +++++++---- .../setup/inventory/slot/PositionSlot.java | 21 +++++++------- .../setup/inventory/slot/StringSlot.java | 13 ++++++--- .../inventory/slot/SwitchTargetSlot.java | 15 ++++------ .../inventory/slot/area/AreaCornerSlot.java | 3 +- .../inventory/slot/area/AreaOverviewSlot.java | 5 ++-- .../slot/area/ReshufflePercentageSlot.java | 6 ++-- .../slot/area/ShuffleIntervalSlot.java | 6 ++-- .../setup/inventory/slot/package-info.java | 4 +++ 12 files changed, 81 insertions(+), 51 deletions(-) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/package-info.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AbstractDataSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AbstractDataSlot.java index a70d00ea..f14ece68 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AbstractDataSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/AbstractDataSlot.java @@ -6,26 +6,34 @@ import net.minestom.server.item.ItemStack; import net.theevilreaper.aves.inventory.click.ClickHolder; import net.theevilreaper.aves.inventory.slot.Slot; -import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; -import org.jetbrains.annotations.NotNull; +import net.theevilreaper.bounce.setup.inventory.DataType; import java.util.function.Consumer; -public abstract class AbstractDataSlot extends Slot { +public abstract class AbstractDataSlot extends Slot { - protected final OverviewType type; + protected final T type; - protected AbstractDataSlot(@NotNull OverviewType type) { + protected AbstractDataSlot(T type) { this.type = type; this.setClick(this::click); } + /** + * Handles what happen when a player clicks + * + * @param player who clicked + * @param slot was clicked + * @param clickType was involved + * @param stack was involved + * @param result of the click + */ protected abstract void click( - @NotNull Player player, + Player player, int slot, - @NotNull Click clickType, - @NotNull ItemStack stack, - @NotNull Consumer result + Click clickType, + ItemStack stack, + Consumer result ); /** @@ -35,7 +43,7 @@ protected abstract void click( * @param stack the ItemStack to convert * @return a new ItemStack.Builder with the same material and custom name */ - protected @NotNull ItemStack.Builder asBuilder(@NotNull ItemStack stack) { + protected ItemStack.Builder asBuilder(ItemStack stack) { ItemStack.Builder builder = ItemStack.builder(stack.material()); if (stack.has(DataComponents.CUSTOM_NAME)) { builder.customName(stack.get(DataComponents.CUSTOM_NAME)); diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/EmptyPushSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/EmptyPushSlot.java index 025d329b..79eeb532 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/EmptyPushSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/EmptyPushSlot.java @@ -3,7 +3,6 @@ import net.minestom.server.item.ItemStack; import net.theevilreaper.aves.inventory.slot.Slot; import net.theevilreaper.aves.inventory.util.InventoryConstants; -import org.jetbrains.annotations.NotNull; /** * The {@link EmptyPushSlot} represents a {@link Slot} implementation that is used to indicate that a given push slot is not set up with any data. @@ -19,11 +18,14 @@ public final class EmptyPushSlot extends Slot { * * @param itemStack the {@link ItemStack} to be used for this slot, typically representing an empty or default state. */ - public EmptyPushSlot(@NotNull ItemStack itemStack) { + public EmptyPushSlot(ItemStack itemStack) { setItemStack(itemStack); setClick(InventoryConstants.CANCEL_CLICK); } + /** + * {@inheritDoc} + */ @Override public ItemStack getItem() { return this.itemStack; diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/MaterialSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/MaterialSlot.java index d999e90a..4a8b16ca 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/MaterialSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/MaterialSlot.java @@ -6,7 +6,6 @@ import net.minestom.server.item.Material; import net.theevilreaper.aves.inventory.slot.Slot; import net.theevilreaper.bounce.setup.util.SetupMessages; -import org.jetbrains.annotations.NotNull; public class MaterialSlot extends Slot { @@ -17,7 +16,7 @@ public class MaterialSlot extends Slot { * * @param material the {@link Material} to be used for this slot, typically representing a block or item. */ - public MaterialSlot(@NotNull Material material) { + public MaterialSlot(Material material) { this.stack = ItemStack.builder(material) .lore(Component.empty(), SetupMessages.NO_SPACE_SEPARATOR.append(Component.space()).append(Component.text("Ground block", NamedTextColor.GRAY))) .build(); @@ -28,10 +27,13 @@ public MaterialSlot(@NotNull Material material) { * * @param stack the {@link ItemStack} to be used for this slot, typically representing a material. */ - public MaterialSlot(@NotNull ItemStack stack) { + public MaterialSlot(ItemStack stack) { this.stack = stack; } + /** + * {@inheritDoc} + */ @Override public ItemStack getItem() { return stack; diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/MultiStringSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/MultiStringSlot.java index 84f78360..95f0f863 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/MultiStringSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/MultiStringSlot.java @@ -9,7 +9,6 @@ import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; import net.theevilreaper.bounce.setup.event.map.PlayerDeletePromptEvent; import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; @@ -18,20 +17,23 @@ import static net.theevilreaper.bounce.setup.util.SetupMessages.DELETE_CLICK; import static net.theevilreaper.bounce.setup.util.SetupMessages.NO_SPACE_SEPARATOR; -public class MultiStringSlot extends AbstractDataSlot { +public class MultiStringSlot extends AbstractDataSlot { private final List data; - public MultiStringSlot(@NotNull OverviewType overviewType, @Nullable List data) { + public MultiStringSlot(OverviewType overviewType, List data) { super(overviewType); this.data = data; } + /** + * {@inheritDoc} + */ @Override public ItemStack getItem() { ItemStack overviewItem = this.type.getItem(); - if (data == null || data.isEmpty()) { + if (data.isEmpty()) { return overviewItem; } return asBuilder(overviewItem).lore( @@ -44,11 +46,14 @@ public ItemStack getItem() { .build(); } + /** + * {@inheritDoc} + */ @Override - protected void click(@NotNull Player player, int slot, @NotNull Click click, @NotNull ItemStack stack, @NotNull Consumer result) { + protected void click(Player player, int slot, Click click, ItemStack stack, Consumer result) { result.accept(ClickHolder.cancelClick()); - if (data == null || data.isEmpty()) { + if (data.isEmpty()) { EventDispatcher.call(new PlayerDialogRequestEvent(player, PlayerDialogRequestEvent.Target.SETUP_REQUEST_AUTHOR)); return; } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/PositionSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/PositionSlot.java index da9ede1e..e26c9308 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/PositionSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/PositionSlot.java @@ -10,8 +10,8 @@ import net.theevilreaper.aves.inventory.click.ClickHolder; import net.theevilreaper.aves.util.Components; import net.theevilreaper.bounce.setup.event.map.PlayerDeletePromptEvent; +import net.theevilreaper.bounce.setup.inventory.DataType; import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.math.RoundingMode; @@ -25,7 +25,7 @@ import static net.theevilreaper.bounce.setup.util.SetupMessages.DELETE_CLICK; import static net.theevilreaper.bounce.setup.util.SetupMessages.TELEPORT_CLICK; -public class PositionSlot extends AbstractDataSlot { +public class PositionSlot extends AbstractDataSlot { private static final DecimalFormat DECIMAL_FORMAT; @@ -35,10 +35,10 @@ public class PositionSlot extends AbstractDataSlot { DECIMAL_FORMAT.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ROOT)); } - private final Pos position; + private final @Nullable Pos position; - public PositionSlot(@NotNull OverviewType overviewType, @Nullable Pos position) { - super(overviewType); + public PositionSlot(T type, @Nullable Pos position) { + super(type); this.position = position; } @@ -46,9 +46,8 @@ public PositionSlot(@NotNull OverviewType overviewType, @Nullable Pos position) public ItemStack getItem() { ItemStack overviewItem = this.type.getItem(); - if (position == null) { - return overviewItem; - } + if (position == null) return overviewItem; + List lore = new ArrayList<>(); lore.add(Component.empty()); lore.addAll(Components.pointToLore(MiniMessage.miniMessage(), position, DECIMAL_FORMAT)); @@ -61,7 +60,7 @@ public ItemStack getItem() { } @Override - protected void click(@NotNull Player player, int slot, @NotNull Click click, @NotNull ItemStack stack, @NotNull Consumer result) { + protected void click(Player player, int slot, Click click, ItemStack stack, Consumer result) { result.accept(ClickHolder.cancelClick()); if ((!(click instanceof Click.Left || click instanceof Click.Right)) || position == null) return; if (click instanceof Click.Left) { @@ -70,6 +69,8 @@ protected void click(@NotNull Player player, int slot, @NotNull Click click, @No return; } - EventDispatcher.call(new PlayerDeletePromptEvent(player, this.type)); + if (this.type instanceof OverviewType overviewType) { + EventDispatcher.call(new PlayerDeletePromptEvent(player, overviewType)); + } } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/StringSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/StringSlot.java index 354c2eba..90254f53 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/StringSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/StringSlot.java @@ -9,7 +9,6 @@ import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; import net.theevilreaper.bounce.setup.event.map.PlayerDeletePromptEvent; import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; @@ -17,15 +16,18 @@ import static net.theevilreaper.bounce.setup.util.SetupMessages.DELETE_CLICK; import static net.theevilreaper.bounce.setup.util.SetupMessages.NO_SPACE_SEPARATOR; -public class StringSlot extends AbstractDataSlot { +public class StringSlot extends AbstractDataSlot { private final String data; - public StringSlot(@NotNull OverviewType overviewType, @Nullable String data) { + public StringSlot(OverviewType overviewType, @Nullable String data) { super(overviewType); this.data = data; } + /** + * {@inheritDoc} + */ @Override public ItemStack getItem() { ItemStack overviewItem = this.type.getItem(); @@ -43,8 +45,11 @@ public ItemStack getItem() { .build(); } + /** + * {@inheritDoc} + */ @Override - protected void click(@NotNull Player player, int slot, @NotNull Click click, @NotNull ItemStack stack, @NotNull Consumer result) { + protected void click(Player player, int slot, Click click, ItemStack stack, Consumer result) { result.accept(ClickHolder.cancelClick()); if (data == null || data.isEmpty()) { diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/SwitchTargetSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/SwitchTargetSlot.java index e8f4e974..b6e6019f 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/SwitchTargetSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/SwitchTargetSlot.java @@ -11,7 +11,6 @@ import net.theevilreaper.aves.inventory.slot.Slot; import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent; import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent.SwitchTarget; -import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; @@ -20,7 +19,7 @@ public final class SwitchTargetSlot extends Slot { private final ItemStack stack; private final SwitchTarget switchTarget; - public SwitchTargetSlot(@NotNull SwitchTarget target) { + public SwitchTargetSlot(SwitchTarget target) { this.stack = ItemStack.builder(Material.BARRIER) .customName(Component.text("Back", NamedTextColor.RED)) .build(); @@ -29,19 +28,17 @@ public SwitchTargetSlot(@NotNull SwitchTarget target) { } /** - * Handles the click event for the back slot. - * - * @param player the player who clicked - * @param slot the slot that was clicked - * @param clickType the type of click - * @param result the result of the click condition + * {@inheritDoc} */ - private void handleClick(@NotNull Player player, int slot, @NotNull Click clickType, @NotNull ItemStack stack, @NotNull Consumer result) { + private void handleClick(Player player, int slot, Click clickType, ItemStack stack, Consumer result) { result.accept(ClickHolder.cancelClick()); player.closeInventory(); EventDispatcher.call(new SetupInventorySwitchEvent(player, this.switchTarget)); } + /** + * {@inheritDoc} + */ @Override public ItemStack getItem() { return this.stack; diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaCornerSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaCornerSlot.java index 91334987..77501f42 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaCornerSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaCornerSlot.java @@ -8,6 +8,7 @@ import net.minestom.server.item.ItemStack; import net.theevilreaper.aves.inventory.click.ClickHolder; import net.theevilreaper.bounce.setup.inventory.area.AreaViewType; +import net.theevilreaper.bounce.setup.inventory.slot.AbstractDataSlot; import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; @@ -16,7 +17,7 @@ * Displays one corner of the ground area (see {@link AreaViewType#LEFT_AREA_CORNER}/{@link AreaViewType#RIGHT_AREA_CORNER}) * and captures the player's current position for it on left-click. */ -public final class AreaCornerSlot extends AbstractAreaSlot { +public final class AreaCornerSlot extends AbstractDataSlot { private final @Nullable Vec position; private final Consumer onSet; diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaOverviewSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaOverviewSlot.java index e8201218..f521e29c 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaOverviewSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AreaOverviewSlot.java @@ -12,12 +12,13 @@ import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent.SwitchTarget; import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; import net.theevilreaper.bounce.setup.inventory.slot.AbstractDataSlot; +import net.theevilreaper.bounce.setup.util.SetupMessages; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; -public final class AreaOverviewSlot extends AbstractDataSlot { +public final class AreaOverviewSlot extends AbstractDataSlot { private final @Nullable Area area; @@ -42,7 +43,7 @@ public ItemStack getItem() { Component.empty(), Component.text("Configured", type.getColor()), Component.empty(), - Component.text("Click to edit", NamedTextColor.GRAY), + SetupMessages.CLICK_TO_EDIT, Component.empty() ).build(); } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ReshufflePercentageSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ReshufflePercentageSlot.java index c998b574..8f47a1dc 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ReshufflePercentageSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ReshufflePercentageSlot.java @@ -9,12 +9,14 @@ import net.theevilreaper.aves.inventory.click.ClickHolder; import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; import net.theevilreaper.bounce.setup.inventory.area.AreaViewType; +import net.theevilreaper.bounce.setup.inventory.slot.AbstractDataSlot; +import net.theevilreaper.bounce.setup.util.SetupMessages; import org.jetbrains.annotations.NotNull; import java.util.Locale; import java.util.function.Consumer; -public final class ReshufflePercentageSlot extends AbstractAreaSlot { +public final class ReshufflePercentageSlot extends AbstractDataSlot { private final double reshufflePercentage; @@ -34,7 +36,7 @@ public ItemStack getItem() { Component.empty(), Component.text(String.format(Locale.ROOT, "%.1f%%", percentage), NamedTextColor.YELLOW), Component.empty(), - CLICK_TO_EDIT, + SetupMessages.CLICK_TO_EDIT, Component.empty() ).build(); } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ShuffleIntervalSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ShuffleIntervalSlot.java index ef78bc0b..4376dd4f 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ShuffleIntervalSlot.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/ShuffleIntervalSlot.java @@ -9,12 +9,14 @@ import net.theevilreaper.aves.inventory.click.ClickHolder; import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; import net.theevilreaper.bounce.setup.inventory.area.AreaViewType; +import net.theevilreaper.bounce.setup.inventory.slot.AbstractDataSlot; +import net.theevilreaper.bounce.setup.util.SetupMessages; import org.jetbrains.annotations.NotNull; import java.util.Locale; import java.util.function.Consumer; -public final class ShuffleIntervalSlot extends AbstractAreaSlot { +public final class ShuffleIntervalSlot extends AbstractDataSlot { private final int shuffleIntervalTicks; @@ -34,7 +36,7 @@ public ItemStack getItem() { Component.empty(), Component.text(String.format(Locale.ROOT, "%d ticks (%.1fs)", shuffleIntervalTicks, seconds), NamedTextColor.YELLOW), Component.empty(), - Component.text("Click to edit", NamedTextColor.GRAY), + SetupMessages.CLICK_TO_EDIT, Component.empty() ).build(); } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/package-info.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/package-info.java new file mode 100644 index 00000000..fed92122 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.setup.inventory.slot; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file From 235e259ff1029207c3aa4c2b5adc02e421258fcc Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:46:33 +0200 Subject: [PATCH 36/43] chore(map): add missing fields getter --- .../bounce/setup/builder/GameMapBuilder.java | 61 ++++++++++++++----- .../bounce/setup/builder/package-info.java | 4 ++ .../setup/builder/GameMapBuilderTest.java | 33 ++++++++++ 3 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/builder/package-info.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java index c663e613..a5d6a0a6 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java @@ -8,16 +8,18 @@ import net.theevilreaper.bounce.common.map.GameMap; import net.theevilreaper.bounce.common.push.PushData; import net.theevilreaper.bounce.common.push.PushEntry; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; + public final class GameMapBuilder extends BaseMapBuilder { private static final int DEFAULT_SHUFFLE_INTERVAL_TICKS = 100; private static final double DEFAULT_RESHUFFLE_PERCENTAGE = 0.1; private final PushData.Builder pushDataBuilder; - private Pos gameSpawn; + private @Nullable Pos gameSpawn; private @Nullable Area area; private int shuffleIntervalTicks; private double reshufflePercentage; @@ -36,7 +38,7 @@ public GameMapBuilder() { .add(PushEntry.pushEntry(Block.EMERALD_BLOCK, 1, 0.02)); } - public GameMapBuilder(@NotNull GameMap gameMap) { + public GameMapBuilder(GameMap gameMap) { super(gameMap); this.gameSpawn = gameMap.getGameSpawn(); this.area = gameMap.getArea(); @@ -59,7 +61,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { .add(PushEntry.pushEntry(Block.GOLD_BLOCK, 1, 0.05)) .add(PushEntry.pushEntry(Block.DIAMOND_BLOCK, 1, 0.03)) .add(PushEntry.pushEntry(Block.EMERALD_BLOCK, 1, 0.02)); - } else{ + } else { this.pushDataBuilder = PushData.builder(gameMap.getPushData()); } } @@ -70,7 +72,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { * @param groundBlock the block to set as the ground block * @return this builder instance for chaining */ - public @NotNull GameMapBuilder groundBlock(Block groundBlock) { + public GameMapBuilder groundBlock(Block groundBlock) { PushEntry pushEntry = this.pushDataBuilder.getPushValues().getFirst(); pushEntry.setBlock(groundBlock); return this; @@ -82,7 +84,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { * @param gameSpawn the spawn position * @return this builder instance for chaining */ - public @NotNull GameMapBuilder gameSpawn(Pos gameSpawn) { + public GameMapBuilder gameSpawn(Pos gameSpawn) { this.gameSpawn = gameSpawn; return this; } @@ -93,7 +95,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { * @param area the area, or {@code null} to disable dynamic filling * @return this builder instance for chaining */ - public @NotNull GameMapBuilder area(@Nullable Area area) { + public GameMapBuilder area(@Nullable Area area) { this.area = area; return this; } @@ -104,7 +106,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { * @param shuffleIntervalTicks the interval in ticks * @return this builder instance for chaining */ - public @NotNull GameMapBuilder shuffleIntervalTicks(int shuffleIntervalTicks) { + public GameMapBuilder shuffleIntervalTicks(int shuffleIntervalTicks) { this.shuffleIntervalTicks = shuffleIntervalTicks; return this; } @@ -115,7 +117,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { * @param reshufflePercentage the percentage as a fraction between 0.0 and 1.0 * @return this builder instance for chaining */ - public @NotNull GameMapBuilder reshufflePercentage(double reshufflePercentage) { + public GameMapBuilder reshufflePercentage(double reshufflePercentage) { this.reshufflePercentage = reshufflePercentage; return this; } @@ -126,7 +128,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { * @param pos1 the corner position, or {@code null} to clear it * @return this builder instance for chaining */ - public @NotNull GameMapBuilder pos1(@Nullable Vec pos1) { + public GameMapBuilder pos1(@Nullable Vec pos1) { this.pos1 = pos1; return this; } @@ -137,7 +139,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { * @param pos2 the corner position, or {@code null} to clear it * @return this builder instance for chaining */ - public @NotNull GameMapBuilder pos2(@Nullable Vec pos2) { + public GameMapBuilder pos2(@Nullable Vec pos2) { this.pos2 = pos2; return this; } @@ -148,7 +150,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { * @return a new GameMap instance */ @Override - public @NotNull GameMap build() { + public GameMap build() { return new GameMap(this.name, this.spawn, this.gameSpawn, pushDataBuilder.build(), this.builders, this.area, this.shuffleIntervalTicks, this.reshufflePercentage); } @@ -157,7 +159,7 @@ public GameMapBuilder(@NotNull GameMap gameMap) { * * @return the game spawn position */ - public Pos getGameSpawn() { + public @Nullable Pos getGameSpawn() { return gameSpawn; } @@ -220,11 +222,40 @@ public PushData.Builder getPushDataBuilder() { * * @return the ground block entry */ - public @NotNull PushEntry getGroundBlockEntry() { + public PushEntry getGroundBlockEntry() { return this.pushDataBuilder.getPushValues().getFirst(); } - public @NotNull Pos getSpawnOrDefault(@NotNull Pos defaultSpawn) { + /*** + * Returns the spawn position of the map or a default. + * + * @param defaultSpawn as fallback + * @return the spawn position + */ + public Pos getSpawnOrDefault(Pos defaultSpawn) { return this.spawn != null ? this.spawn : defaultSpawn; } + + /** + * Returns whether every field required to save this map is present. + * + * @return {@code true} if {@link #getMissingFieldNames()} is empty + */ + public boolean isReadyToSave() { + return getMissingFieldNames().isEmpty(); + } + + /** + * Returns the human-readable names of the required fields which are not yet set. + * + * @return an empty list if the map is ready to save + */ + public List getMissingFieldNames() { + List missing = new ArrayList<>(); + if (isDefaultName()) missing.add("Name"); + if (getSpawn() == null) missing.add("Spawn"); + if (getGameSpawn() == null) missing.add("Game Spawn"); + if (getArea() == null) missing.add("Area"); + return missing; + } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/package-info.java b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/package-info.java new file mode 100644 index 00000000..9a861877 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.setup.builder; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java index 927dd5c1..bfe10462 100644 --- a/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java @@ -10,6 +10,8 @@ import net.theevilreaper.bounce.common.push.PushEntry; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; class GameMapBuilderTest { @@ -151,4 +153,35 @@ void testReloadingExistingMapWithoutAreaHasNoCorners() { assertNull(reloaded.getPos1()); assertNull(reloaded.getPos2()); } + + @Test + void testNewBuilderIsNotReadyToSave() { + GameMapBuilder builder = new GameMapBuilder(); + + assertFalse(builder.isReadyToSave()); + assertEquals(List.of("Name", "Spawn", "Game Spawn", "Area"), builder.getMissingFieldNames()); + } + + @Test + void testBuilderIsReadyToSaveOnceAllRequiredFieldsAreSet() { + GameMapBuilder builder = new GameMapBuilder(); + Area area = new GroundArea(Vec.ZERO, new Vec(5, 0, 5), Block.GLASS, PushData.builder().build()); + + builder.name("Test Map"); + builder.spawn(new Pos(1, 2, 3)); + builder.gameSpawn(new Pos(4, 5, 6)); + builder.area(area); + + assertTrue(builder.getMissingFieldNames().isEmpty()); + assertTrue(builder.isReadyToSave()); + } + + @Test + void testBuilderIsNotReadyToSaveWhenOnlySomeFieldsAreSet() { + GameMapBuilder builder = new GameMapBuilder(); + builder.name("Test Map").spawn(new Pos(1, 2, 3)); + + assertEquals(List.of("Game Spawn", "Area"), builder.getMissingFieldNames()); + assertFalse(builder.isReadyToSave()); + } } \ No newline at end of file From 1ee1568c85af7c6b8f794ee6cbc4346657ff2312 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:46:45 +0200 Subject: [PATCH 37/43] chore(slot): remove doubled slot --- .../inventory/slot/area/AbstractAreaSlot.java | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AbstractAreaSlot.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AbstractAreaSlot.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AbstractAreaSlot.java deleted file mode 100644 index dd255474..00000000 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/slot/area/AbstractAreaSlot.java +++ /dev/null @@ -1,51 +0,0 @@ -package net.theevilreaper.bounce.setup.inventory.slot.area; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.minestom.server.component.DataComponents; -import net.minestom.server.entity.Player; -import net.minestom.server.inventory.click.Click; -import net.minestom.server.item.ItemStack; -import net.theevilreaper.aves.inventory.click.ClickHolder; -import net.theevilreaper.aves.inventory.slot.Slot; -import net.theevilreaper.bounce.setup.inventory.area.AreaViewInventory; -import net.theevilreaper.bounce.setup.inventory.area.AreaViewType; - -import java.util.function.Consumer; - -/** - * Base class for {@link AreaViewType}-bound slots shown in the {@link AreaViewInventory}. - */ -public abstract class AbstractAreaSlot extends Slot { - - protected static final Component CLICK_TO_EDIT = Component.text("Click to edit", NamedTextColor.GRAY); - protected final AreaViewType type; - - protected AbstractAreaSlot(AreaViewType type) { - this.type = type; - this.setClick(this::click); - } - - protected abstract void click( - Player player, - int slot, - Click clickType, - ItemStack stack, - Consumer result - ); - - /** - * Converts the given ItemStack to a builder, allowing for modifications - * without altering the original stack. - * - * @param stack the ItemStack to convert - * @return a new ItemStack.Builder with the same material and custom name - */ - protected ItemStack.Builder asBuilder(ItemStack stack) { - ItemStack.Builder builder = ItemStack.builder(stack.material()); - if (stack.has(DataComponents.CUSTOM_NAME)) { - builder.customName(stack.get(DataComponents.CUSTOM_NAME)); - } - return builder; - } -} From d7c30ce5e68fd14a4371f97e01425e32bbed4a7f Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:47:19 +0200 Subject: [PATCH 38/43] test: add new test cases --- .../inventory/area/AreaViewTypeTest.java | 30 +++++ .../map/BounceInstanceIntegrationTest.java | 116 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewTypeTest.java create mode 100644 src/test/java/net/theevilreaper/bounce/map/BounceInstanceIntegrationTest.java diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewTypeTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewTypeTest.java new file mode 100644 index 00000000..af74b6fd --- /dev/null +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/area/AreaViewTypeTest.java @@ -0,0 +1,30 @@ +package net.theevilreaper.bounce.setup.inventory.area; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.minestom.server.component.DataComponents; +import net.minestom.server.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.*; + +class AreaViewTypeTest { + + @ParameterizedTest(name = "Test item get for type: {0}") + @EnumSource(AreaViewType.class) + void testItemGet(@NotNull AreaViewType type) { + ItemStack item = type.getItem(); + assertNotNull(item, "Item should not be null for type: " + type); + assertEquals(type.getMaterial(), item.material(), "Material should match for type: " + type); + + assertTrue(item.has(DataComponents.CUSTOM_NAME), "Item should have a name component for type: " + type); + + Component nameComponent = item.get(DataComponents.CUSTOM_NAME); + assertNotNull(nameComponent, "Custom name component should not be null for type: " + type); + + String name = PlainTextComponentSerializer.plainText().serialize(nameComponent); + assertTrue(name.contains(type.getName()), "Name should match for type: " + type); + } +} diff --git a/src/test/java/net/theevilreaper/bounce/map/BounceInstanceIntegrationTest.java b/src/test/java/net/theevilreaper/bounce/map/BounceInstanceIntegrationTest.java new file mode 100644 index 00000000..e52d13ce --- /dev/null +++ b/src/test/java/net/theevilreaper/bounce/map/BounceInstanceIntegrationTest.java @@ -0,0 +1,116 @@ +package net.theevilreaper.bounce.map; + +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import net.minestom.server.world.DimensionType; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import net.theevilreaper.bounce.common.ground.Area; +import net.theevilreaper.bounce.common.ground.GroundArea; +import net.theevilreaper.bounce.common.push.PushData; +import net.theevilreaper.bounce.common.push.PushEntry; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MicrotusExtension.class) +class BounceInstanceIntegrationTest { + + @Test + void testTickReshufflesAreaOnceIntervalElapses(@NotNull Env env) { + PushData pushData = new PushData(List.of(PushEntry.pushEntry(Block.DIAMOND_BLOCK, 1, 1.0))); + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(4, 0, 0), Block.GLASS, pushData); + + BounceInstance instance = new BounceInstance(UUID.randomUUID(), DimensionType.OVERWORLD, area, 5, 0.1); + instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); + env.process().instance().registerInstance(instance); + + for (int x = 0; x <= 4; x++) { + instance.setBlock(x, 0, 0, Block.GLASS); + } + area.calculatePositions(instance); + + for (int i = 0; i < 4; i++) { + env.tick(); + } + for (int x = 0; x <= 4; x++) { + assertTrue(instance.getBlock(x, 0, 0).compare(Block.GLASS), "Area must not reshuffle before the configured interval elapses"); + } + + env.tick(); // 5th tick reaches the configured interval + + long diamondCount = 0; + for (int x = 0; x <= 4; x++) { + if (instance.getBlock(x, 0, 0).compare(Block.DIAMOND_BLOCK)) diamondCount++; + } + assertEquals(1, diamondCount, "10% of the 5 positions must be reshuffled to diamond block once the configured interval elapses"); + + env.destroyInstance(instance, true); + } + + @Test + void testTickHonorsAConfiguredReshufflePercentage(@NotNull Env env) { + PushData pushData = new PushData(List.of(PushEntry.pushEntry(Block.DIAMOND_BLOCK, 1, 1.0))); + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(4, 0, 0), Block.GLASS, pushData); + + BounceInstance instance = new BounceInstance(UUID.randomUUID(), DimensionType.OVERWORLD, area, 5, 1.0); + instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); + env.process().instance().registerInstance(instance); + + for (int x = 0; x <= 4; x++) { + instance.setBlock(x, 0, 0, Block.GLASS); + } + area.calculatePositions(instance); + + for (int i = 0; i < 5; i++) { + env.tick(); + } + + for (int x = 0; x <= 4; x++) { + assertTrue(instance.getBlock(x, 0, 0).compare(Block.DIAMOND_BLOCK), "A 100% reshuffle percentage must reshuffle every position"); + } + + env.destroyInstance(instance, true); + } + + @Test + void testTickDoesNothingWhenNoAreaIsConfigured(@NotNull Env env) { + BounceInstance instance = new BounceInstance(UUID.randomUUID(), DimensionType.OVERWORLD, null, 5, 0.1); + instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); + env.process().instance().registerInstance(instance); + + assertDoesNotThrow(() -> { + for (int i = 0; i < 10; i++) { + env.tick(); + } + }); + + env.destroyInstance(instance, true); + } + + @Test + void testTickSkipsReshuffleWhenIntervalIsNotPositive(@NotNull Env env) { + PushData pushData = new PushData(List.of(PushEntry.pushEntry(Block.DIAMOND_BLOCK, 1, 1.0))); + Area area = new GroundArea(new Vec(0, 0, 0), new Vec(0, 0, 0), Block.GLASS, pushData); + + BounceInstance instance = new BounceInstance(UUID.randomUUID(), DimensionType.OVERWORLD, area, 0, 0.1); + instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); + env.process().instance().registerInstance(instance); + + instance.setBlock(0, 0, 0, Block.GLASS); + area.calculatePositions(instance); + + for (int i = 0; i < 10; i++) { + env.tick(); + } + + assertTrue(instance.getBlock(0, 0, 0).compare(Block.GLASS), "A non-positive interval must never trigger a reshuffle"); + + env.destroyInstance(instance, true); + } +} From 3946a86bb504b6c4e2dabb0683591b061124ac74 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:47:31 +0200 Subject: [PATCH 39/43] feat(setup): improve validation --- .../bounce/setup/BounceSetup.java | 6 ++ .../setup/dialog/SetupDialogRegistry.java | 1 + .../dialog/type/SaveValidationDialog.java | 80 +++++++++++++++++++ .../event/map/SaveValidationPromptEvent.java | 40 ++++++++++ .../setup/event/map/SetupDiscardEvent.java | 27 +++++++ .../setup/listener/PlayerItemListener.java | 9 +++ .../PlayerCustomClickEventListener.java | 3 + .../dialog/SaveValidationPromptListener.java | 32 ++++++++ .../listener/map/SetupDiscardListener.java | 28 +++++++ .../bounce/setup/util/SetupMessages.java | 2 + 10 files changed, 228 insertions(+) create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/SaveValidationDialog.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/event/map/SaveValidationPromptEvent.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/event/map/SetupDiscardEvent.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/SaveValidationPromptListener.java create mode 100644 setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupDiscardListener.java diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java b/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java index b953ea89..be73a16c 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java @@ -20,6 +20,8 @@ import net.theevilreaper.bounce.setup.event.ground.PlayerGroundBlockSelectEvent; import net.theevilreaper.bounce.setup.event.SetupInventorySwitchEvent; import net.theevilreaper.bounce.setup.event.map.PlayerDeletePromptEvent; +import net.theevilreaper.bounce.setup.event.map.SaveValidationPromptEvent; +import net.theevilreaper.bounce.setup.event.map.SetupDiscardEvent; import net.theevilreaper.bounce.setup.event.push.PlayerPushBlockSelectEvent; import net.theevilreaper.bounce.setup.event.push.PlayerPushIndexChangeEvent; import net.theevilreaper.bounce.setup.inventory.InventoryService; @@ -30,10 +32,12 @@ import net.theevilreaper.bounce.setup.listener.dialog.PlayerCustomClickEventListener; import net.theevilreaper.bounce.setup.listener.dialog.PlayerDeletePromptListener; import net.theevilreaper.bounce.setup.listener.dialog.PlayerDialogRequestListener; +import net.theevilreaper.bounce.setup.listener.dialog.SaveValidationPromptListener; import net.theevilreaper.bounce.setup.listener.entity.EntityAddToInstanceListener; import net.theevilreaper.bounce.setup.listener.ground.PlayerBlockSelectListener; import net.theevilreaper.bounce.setup.listener.inventory.SetupInventorySwitchListener; import net.theevilreaper.bounce.setup.listener.map.MapSetupSelectListener; +import net.theevilreaper.bounce.setup.listener.map.SetupDiscardListener; import net.theevilreaper.bounce.setup.listener.map.SetupFinishListener; import net.theevilreaper.bounce.setup.listener.push.PlayerPushBlockSelectListener; import net.theevilreaper.bounce.setup.listener.push.PlayerPushIndexChangeListener; @@ -93,6 +97,8 @@ private void registerListener(@NotNull EventNode node) { }; node.addListener(SetupFinishEvent.class, new SetupFinishListener(instanceSwitcher)); + node.addListener(SetupDiscardEvent.class, new SetupDiscardListener(instanceSwitcher)); + node.addListener(SaveValidationPromptEvent.class, new SaveValidationPromptListener(dialogRegistry)); node.addListener(PlayerGroundBlockSelectEvent.class, new PlayerBlockSelectListener(this.setupDataService::get)); node.addListener(SetupInventorySwitchEvent.class, new SetupInventorySwitchListener(this.inventoryService, this.setupDataService::get)); node.addListener(GameMapBuilderStateNotifyEvent.class, new GameMapBuilderStateNotifyListener()); diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java index 915de6f2..92615d03 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/SetupDialogRegistry.java @@ -42,6 +42,7 @@ public SetupDialogRegistry() { this.registerDialog(new WeightInputDialog()); this.registerDialog(new ShuffleIntervalInputDialog()); this.registerDialog(new ReshufflePercentageInputDialog()); + this.registerDialog(new SaveValidationDialog()); } private void registerDialog(@NotNull DialogTemplate dialog) { diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/SaveValidationDialog.java b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/SaveValidationDialog.java new file mode 100644 index 00000000..ffca4b27 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/dialog/type/SaveValidationDialog.java @@ -0,0 +1,80 @@ +package net.theevilreaper.bounce.setup.dialog.type; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.dialog.*; +import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.server.common.ShowDialogPacket; +import net.theevilreaper.bounce.setup.dialog.AbstractDialogTemplate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Shown when a player tries to save a map which is still missing required data. Offers to either go back and + * finish it, or to discard the whole setup process. + */ +public final class SaveValidationDialog extends AbstractDialogTemplate> { + + public static final Key DIALOG_KEY = Key.key("bounce", "save_validation_dialog"); + + public SaveValidationDialog() { + super( + Component.text("This map isn't ready yet"), + Component.text("Delete data"), + Component.text("Back") + ); + } + + @Override + public void open(@NotNull Player player, @Nullable List missingFields) { + List body = new ArrayList<>(); + body.add(new DialogBody.PlainMessage(Component.text("The following data is still missing:"), 200)); + body.add(new DialogBody.PlainMessage(Component.empty(), 1)); + + if (missingFields == null || missingFields.isEmpty()) { + body.add(new DialogBody.PlainMessage(Component.text("Unknown", NamedTextColor.RED), 200)); + } else { + for (String missingField : missingFields) { + body.add(new DialogBody.PlainMessage(Component.text("- " + missingField, NamedTextColor.RED), 200)); + } + } + + body.add(new DialogBody.PlainMessage(Component.empty(), 1)); + body.add(new DialogBody.PlainMessage(Component.text("Go back and fill them in, or delete this setup."), 200)); + + ShowDialogPacket packet = new ShowDialogPacket(new Dialog.Confirmation( + new DialogMetadata( + header, + null, + false, + false, + DialogAfterAction.CLOSE, + body, + List.of() + ), + new DialogActionButton( + submitComponent, + Component.text("Click to confirm", NamedTextColor.GREEN), + 155, + new DialogAction.DynamicCustom(DIALOG_KEY, CompoundBinaryTag.builder().build()) + ), + new DialogActionButton( + cancelComponent, + Component.text("Click to cancel", NamedTextColor.RED), + 155, + null + ) + )); + player.sendPacket(packet); + } + + @Override + public @NotNull Key key() { + return DIALOG_KEY; + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/event/map/SaveValidationPromptEvent.java b/setup/src/main/java/net/theevilreaper/bounce/setup/event/map/SaveValidationPromptEvent.java new file mode 100644 index 00000000..092a7907 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/event/map/SaveValidationPromptEvent.java @@ -0,0 +1,40 @@ +package net.theevilreaper.bounce.setup.event.map; + +import net.minestom.server.entity.Player; +import net.minestom.server.event.trait.PlayerEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * Fired when a player tries to save a map which is still missing required data. + */ +public class SaveValidationPromptEvent implements PlayerEvent { + + private final Player player; + private final List missingFields; + + public SaveValidationPromptEvent(@NotNull Player player, @NotNull List missingFields) { + this.player = player; + this.missingFields = missingFields; + } + + /** + * Gets the names of the required fields which are still missing. + * + * @return the missing field names + */ + public @NotNull List getMissingFields() { + return missingFields; + } + + /** + * Gets the player who triggered the save validation prompt event. + * + * @return the player who triggered the event + */ + @Override + public @NotNull Player getPlayer() { + return this.player; + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/event/map/SetupDiscardEvent.java b/setup/src/main/java/net/theevilreaper/bounce/setup/event/map/SetupDiscardEvent.java new file mode 100644 index 00000000..b8cef5f3 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/event/map/SetupDiscardEvent.java @@ -0,0 +1,27 @@ +package net.theevilreaper.bounce.setup.event.map; + +import net.minestom.server.event.Event; +import net.onelitefeather.guira.data.SetupData; +import org.jetbrains.annotations.NotNull; + +/** + * Fired when a player discards an in-progress setup instead of saving it, e.g. after confirming a + * {@link net.theevilreaper.bounce.setup.dialog.type.SaveValidationDialog}. + */ +public class SetupDiscardEvent implements Event { + + private final SetupData setupData; + + public SetupDiscardEvent(@NotNull SetupData setupData) { + this.setupData = setupData; + } + + /** + * Returns the setup data of the discarded setup process. + * + * @return the setup data + */ + public @NotNull SetupData getData() { + return setupData; + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/PlayerItemListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/PlayerItemListener.java index 0c12fac1..7098a2ca 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/PlayerItemListener.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/PlayerItemListener.java @@ -4,11 +4,14 @@ import net.onelitefeather.guira.functional.OptionalSetupDataGetter; import net.theevilreaper.aves.util.functional.PlayerConsumer; import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.player.PlayerUseItemEvent; import net.minestom.server.item.ItemStack; import net.theevilreaper.bounce.setup.data.BounceData; +import net.theevilreaper.bounce.setup.event.map.SaveValidationPromptEvent; import org.jetbrains.annotations.NotNull; +import java.util.List; import java.util.Optional; import java.util.function.Consumer; @@ -60,6 +63,12 @@ public void accept(@NotNull PlayerUseItemEvent event) { return; } + List missingFields = setupData.getMapBuilder().getMissingFieldNames(); + if (!missingFields.isEmpty()) { + EventDispatcher.call(new SaveValidationPromptEvent(player, missingFields)); + return; + } + setupData.save(); player.getInventory().setItemStack(0x0, ItemStack.AIR); } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java index dbe2e945..e47146cc 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/PlayerCustomClickEventListener.java @@ -4,11 +4,13 @@ import net.kyori.adventure.nbt.BinaryTag; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.player.PlayerCustomClickEvent; import net.onelitefeather.guira.functional.OptionalSetupDataGetter; import net.theevilreaper.bounce.setup.data.BounceData; import net.theevilreaper.bounce.setup.dialog.*; import net.theevilreaper.bounce.setup.dialog.type.*; +import net.theevilreaper.bounce.setup.event.map.SetupDiscardEvent; import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; import net.theevilreaper.bounce.setup.util.SetupTags; import org.jetbrains.annotations.NotNull; @@ -63,6 +65,7 @@ public void accept(@NotNull PlayerCustomClickEvent event) { case WeightInputDialog ignored -> this.handleWeightUpdate(player, data, dialogData); case ShuffleIntervalInputDialog ignored -> this.handleShuffleIntervalSet(data, dialogData); case ReshufflePercentageInputDialog ignored -> this.handleReshufflePercentageSet(data, dialogData); + case SaveValidationDialog ignored -> EventDispatcher.call(new SetupDiscardEvent(data)); default -> throw new IllegalStateException("Unexpected dialog type: " + dialogTemplate.getClass().getCanonicalName()); } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/SaveValidationPromptListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/SaveValidationPromptListener.java new file mode 100644 index 00000000..6332a8d7 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/dialog/SaveValidationPromptListener.java @@ -0,0 +1,32 @@ +package net.theevilreaper.bounce.setup.listener.dialog; + +import net.theevilreaper.bounce.setup.dialog.DialogRegistry; +import net.theevilreaper.bounce.setup.dialog.DialogTemplate; +import net.theevilreaper.bounce.setup.dialog.type.SaveValidationDialog; +import net.theevilreaper.bounce.setup.event.map.SaveValidationPromptEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +public class SaveValidationPromptListener implements Consumer { + + private final DialogRegistry dialogRegistry; + + public SaveValidationPromptListener(@NotNull DialogRegistry dialogRegistry) { + this.dialogRegistry = dialogRegistry; + } + + @Override + public void accept(@NotNull SaveValidationPromptEvent event) { + DialogTemplate dialog = dialogRegistry.get(SaveValidationDialog.DIALOG_KEY); + + if (dialog == null) { + throw new IllegalStateException("Dialog with key " + SaveValidationDialog.DIALOG_KEY + " not found in registry."); + } + + switch (dialog) { + case SaveValidationDialog saveValidationDialog -> saveValidationDialog.open(event.getPlayer(), event.getMissingFields()); + default -> throw new IllegalStateException("Unexpected dialog type: " + dialog.getClass().getCanonicalName()); + } + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupDiscardListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupDiscardListener.java new file mode 100644 index 00000000..d19efcd1 --- /dev/null +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupDiscardListener.java @@ -0,0 +1,28 @@ +package net.theevilreaper.bounce.setup.listener.map; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.entity.Player; +import net.onelitefeather.guira.data.SetupData; +import net.theevilreaper.aves.util.functional.PlayerConsumer; +import net.theevilreaper.bounce.setup.event.map.SetupDiscardEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +public class SetupDiscardListener implements Consumer { + + private final PlayerConsumer instanceSwitcher; + + public SetupDiscardListener(@NotNull PlayerConsumer instanceSwitcher) { + this.instanceSwitcher = instanceSwitcher; + } + + @Override + public void accept(@NotNull SetupDiscardEvent event) { + SetupData setupData = event.getData(); + + Player player = MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(setupData.getId()); + this.instanceSwitcher.accept(player); + setupData.reset(); + } +} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/util/SetupMessages.java b/setup/src/main/java/net/theevilreaper/bounce/setup/util/SetupMessages.java index a13439d3..0b87b06a 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/util/SetupMessages.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/util/SetupMessages.java @@ -12,11 +12,13 @@ public class SetupMessages extends Messages { public static final Component TELEPORT_CLICK; public static final Component DELETE_CLICK; + public static final Component CLICK_TO_EDIT; static { NO_SPACE_SEPARATOR = Component.text("ยป", NamedTextColor.GRAY); SELECT_MAP_FIRST = withPrefix(Component.text("Please select a map first!", NamedTextColor.RED)); INVALID_NAME = withPrefix(Component.text("Invalid name for the map", NamedTextColor.RED)); + CLICK_TO_EDIT = Component.text("Click to edit", NamedTextColor.GRAY); TELEPORT_CLICK = NO_SPACE_SEPARATOR .append(Component.space()) From 60debedebfcada5682e14b10988625964fdcd6b7 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:49:07 +0200 Subject: [PATCH 40/43] chore: cleanup unused imports --- .../java/net/theevilreaper/bounce/common/config/GameConfig.java | 1 - src/main/java/net/theevilreaper/bounce/Bounce.java | 1 - src/main/java/net/theevilreaper/bounce/map/BounceInstance.java | 1 - 3 files changed, 3 deletions(-) diff --git a/common/src/main/java/net/theevilreaper/bounce/common/config/GameConfig.java b/common/src/main/java/net/theevilreaper/bounce/common/config/GameConfig.java index 16c7e1da..c7e97024 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/config/GameConfig.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/config/GameConfig.java @@ -1,7 +1,6 @@ package net.theevilreaper.bounce.common.config; import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; /** * The {@link GameConfig} interface represents the structure for a configuration which is used by the game. diff --git a/src/main/java/net/theevilreaper/bounce/Bounce.java b/src/main/java/net/theevilreaper/bounce/Bounce.java index 8a0252ba..d808721a 100644 --- a/src/main/java/net/theevilreaper/bounce/Bounce.java +++ b/src/main/java/net/theevilreaper/bounce/Bounce.java @@ -15,7 +15,6 @@ import net.minestom.server.event.player.PlayerDisconnectEvent; import net.minestom.server.event.player.PlayerSpawnEvent; import net.minestom.server.instance.block.BlockManager; -import net.theevilreaper.aves.map.provider.MapProvider; import net.theevilreaper.bounce.block.BlockLoader; import net.theevilreaper.bounce.block.type.lantern.LanternBlockFactory; import net.theevilreaper.bounce.commands.StartCommand; diff --git a/src/main/java/net/theevilreaper/bounce/map/BounceInstance.java b/src/main/java/net/theevilreaper/bounce/map/BounceInstance.java index 2923fa41..b5f41a26 100644 --- a/src/main/java/net/theevilreaper/bounce/map/BounceInstance.java +++ b/src/main/java/net/theevilreaper/bounce/map/BounceInstance.java @@ -5,7 +5,6 @@ import net.minestom.server.world.DimensionType; import net.theevilreaper.bounce.common.ground.Area; import net.theevilreaper.bounce.common.ground.AreaFiller; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; From f60d41f0d5dbffb20d2111a7bfd8f7c1ad0550fa Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 13:52:25 +0200 Subject: [PATCH 41/43] fix(setup): improve setup finish / leave --- .../java/net/theevilreaper/bounce/setup/BounceSetup.java | 4 ++-- .../net/theevilreaper/bounce/setup/data/BounceData.java | 1 + .../bounce/setup/listener/map/SetupDiscardListener.java | 6 +++++- .../bounce/setup/listener/map/SetupFinishListener.java | 6 +++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java b/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java index be73a16c..2d53f03a 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java @@ -96,8 +96,8 @@ private void registerListener(@NotNull EventNode node) { SetupItems.setOverViewItem(player); }; - node.addListener(SetupFinishEvent.class, new SetupFinishListener(instanceSwitcher)); - node.addListener(SetupDiscardEvent.class, new SetupDiscardListener(instanceSwitcher)); + node.addListener(SetupFinishEvent.class, new SetupFinishListener(instanceSwitcher, this.setupDataService::remove)); + node.addListener(SetupDiscardEvent.class, new SetupDiscardListener(instanceSwitcher, this.setupDataService::remove)); node.addListener(SaveValidationPromptEvent.class, new SaveValidationPromptListener(dialogRegistry)); node.addListener(PlayerGroundBlockSelectEvent.class, new PlayerBlockSelectListener(this.setupDataService::get)); node.addListener(SetupInventorySwitchEvent.class, new SetupInventorySwitchListener(this.inventoryService, this.setupDataService::get)); diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java b/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java index f0660acf..dee452fe 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java @@ -74,6 +74,7 @@ public void reset() { this.overviewInventory.unregister(); this.groundViewInventory.unregister(); this.pushValueInventory.unregister(); + this.areaViewInventory.unregister(); MinecraftServer.getSchedulerManager().scheduleNextTick(() -> { MinecraftServer.getInstanceManager().unregisterInstance(this.instance); diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupDiscardListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupDiscardListener.java index d19efcd1..a50806a1 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupDiscardListener.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupDiscardListener.java @@ -3,6 +3,7 @@ import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; import net.onelitefeather.guira.data.SetupData; +import net.onelitefeather.guira.functional.OptionalSetupDataGetter; import net.theevilreaper.aves.util.functional.PlayerConsumer; import net.theevilreaper.bounce.setup.event.map.SetupDiscardEvent; import org.jetbrains.annotations.NotNull; @@ -12,9 +13,11 @@ public class SetupDiscardListener implements Consumer { private final PlayerConsumer instanceSwitcher; + private final OptionalSetupDataGetter setupDataRemover; - public SetupDiscardListener(@NotNull PlayerConsumer instanceSwitcher) { + public SetupDiscardListener(@NotNull PlayerConsumer instanceSwitcher, @NotNull OptionalSetupDataGetter setupDataRemover) { this.instanceSwitcher = instanceSwitcher; + this.setupDataRemover = setupDataRemover; } @Override @@ -24,5 +27,6 @@ public void accept(@NotNull SetupDiscardEvent event) { Player player = MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(setupData.getId()); this.instanceSwitcher.accept(player); setupData.reset(); + this.setupDataRemover.get(setupData.getId()); } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupFinishListener.java b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupFinishListener.java index 165fd3d8..651f8fda 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupFinishListener.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/listener/map/SetupFinishListener.java @@ -4,6 +4,7 @@ import net.minestom.server.entity.Player; import net.onelitefeather.guira.data.SetupData; import net.onelitefeather.guira.event.SetupFinishEvent; +import net.onelitefeather.guira.functional.OptionalSetupDataGetter; import net.theevilreaper.aves.util.functional.PlayerConsumer; import org.jetbrains.annotations.NotNull; @@ -12,9 +13,11 @@ public class SetupFinishListener implements Consumer { private final PlayerConsumer instanceSwitcher; + private final OptionalSetupDataGetter setupDataRemover; - public SetupFinishListener(@NotNull PlayerConsumer instanceSwitcher) { + public SetupFinishListener(@NotNull PlayerConsumer instanceSwitcher, @NotNull OptionalSetupDataGetter setupDataRemover) { this.instanceSwitcher = instanceSwitcher; + this.setupDataRemover = setupDataRemover; } @Override @@ -24,5 +27,6 @@ public void accept(@NotNull SetupFinishEvent event) { Player player = MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(setupData.getId()); this.instanceSwitcher.accept(player); setupData.reset(); + this.setupDataRemover.get(setupData.getId()); } } From 6eb58f58be717c8f300b6ea22dfaf99b91857098 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 14:17:53 +0200 Subject: [PATCH 42/43] chore(setup): improve validation and setup cleanup --- .../bounce/setup/builder/GameMapBuilder.java | 16 ++++++++++ .../bounce/setup/data/BounceData.java | 1 + .../inventory/ground/GroundViewInventory.java | 9 ++++++ .../setup/builder/GameMapBuilderTest.java | 30 +++++++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java index a5d6a0a6..190c1118 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/builder/GameMapBuilder.java @@ -256,6 +256,22 @@ public List getMissingFieldNames() { if (getSpawn() == null) missing.add("Spawn"); if (getGameSpawn() == null) missing.add("Game Spawn"); if (getArea() == null) missing.add("Area"); + if (!hasValidPushData()) missing.add("Push Data"); return missing; } + + /** + * Checks whether the push data is usable: every entry needs a positive value, and at least one non-ground + * entry needs a positive weight so something can actually be placed besides the ground block. + * + * @return {@code true} if the push data satisfies both conditions + */ + private boolean hasValidPushData() { + boolean hasWeightedPushEntry = false; + for (PushEntry entry : pushDataBuilder.getPushValues()) { + if (entry.getValue() <= 0) return false; + if (!entry.isGround() && entry.getWeight() > 0) hasWeightedPushEntry = true; + } + return hasWeightedPushEntry; + } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java b/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java index dee452fe..ba8e812f 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/data/BounceData.java @@ -72,6 +72,7 @@ public void reset() { player.removeTag(SetupTags.SETUP_TAG); player.removeTag(SetupTags.PUSH_SLOT_INDEX); this.overviewInventory.unregister(); + this.groundViewInventory.unregisterGroundValueInventory(); this.groundViewInventory.unregister(); this.pushValueInventory.unregister(); this.areaViewInventory.unregister(); diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundViewInventory.java b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundViewInventory.java index a4eadf7a..6da6f874 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundViewInventory.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/inventory/ground/GroundViewInventory.java @@ -125,6 +125,15 @@ public void invalidateGroundValueInventory() { } } + /** + * Unregisters the lazily-created ground value inventory, if one was ever opened for this setup session. + */ + public void unregisterGroundValueInventory() { + if (groundValueInventory != null) { + groundValueInventory.unregister(); + } + } + /** * Opens the ground value inventory for the player. * This method checks if the ground value inventory is initialized before attempting to open it. diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java index bfe10462..e9d287f1 100644 --- a/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/builder/GameMapBuilderTest.java @@ -184,4 +184,34 @@ void testBuilderIsNotReadyToSaveWhenOnlySomeFieldsAreSet() { assertEquals(List.of("Game Spawn", "Area"), builder.getMissingFieldNames()); assertFalse(builder.isReadyToSave()); } + + private GameMapBuilder readyBuilderExceptPushData() { + GameMapBuilder builder = new GameMapBuilder(); + Area area = new GroundArea(Vec.ZERO, new Vec(5, 0, 5), Block.GLASS, PushData.builder().build()); + builder.name("Test Map"); + builder.spawn(new Pos(1, 2, 3)); + builder.gameSpawn(new Pos(4, 5, 6)); + builder.area(area); + return builder; + } + + @Test + void testBuilderIsNotReadyToSaveWhenNoPushEntryHasWeight() { + GameMapBuilder builder = readyBuilderExceptPushData(); + for (PushEntry entry : builder.getPushDataBuilder().getPushValues()) { + if (!entry.isGround()) entry.setWeight(0.0); + } + + assertEquals(List.of("Push Data"), builder.getMissingFieldNames()); + assertFalse(builder.isReadyToSave()); + } + + @Test + void testBuilderIsNotReadyToSaveWhenAnEntryHasNoValue() { + GameMapBuilder builder = readyBuilderExceptPushData(); + builder.getPushDataBuilder().getPushValues().get(1).setValue(0); + + assertEquals(List.of("Push Data"), builder.getMissingFieldNames()); + assertFalse(builder.isReadyToSave()); + } } \ No newline at end of file From b0b4e37ffb0f638ab94d9ea5e58a02765fa9537b Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 25 Aug 2026 14:22:40 +0200 Subject: [PATCH 43/43] chore: cleanup code --- .../theevilreaper/bounce/common/ground/AreaFiller.java | 9 ++++----- .../theevilreaper/bounce/common/ground/GroundArea.java | 5 ++--- .../theevilreaper/bounce/common/push/PushEntry.java | 2 +- .../common/ground/AreaFillerIntegrationTest.java | 3 --- .../bounce/common/ground/AreaIntegrationTest.java | 10 ++-------- 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/common/src/main/java/net/theevilreaper/bounce/common/ground/AreaFiller.java b/common/src/main/java/net/theevilreaper/bounce/common/ground/AreaFiller.java index ea7ccc5e..bc154ab2 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/ground/AreaFiller.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/ground/AreaFiller.java @@ -6,7 +6,6 @@ import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.theevilreaper.bounce.common.push.PushEntry; -import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collection; @@ -36,7 +35,7 @@ private AreaFiller() { * @param instance the instance to place blocks in * @param area the area to fill */ - public static void fill(@NotNull Instance instance, @NotNull Area area) { + public static void fill(Instance instance, Area area) { area.calculatePositions(instance); List entries = area.data().push(); @@ -54,7 +53,7 @@ public static void fill(@NotNull Instance instance, @NotNull Area area) { * @param percentage the fraction (0.0-1.0) of positions to re-roll * @param players players whose current standing position must not be touched */ - public static void reshuffle(@NotNull Instance instance, @NotNull Area area, double percentage, @NotNull Collection players) { + public static void reshuffle(Instance instance, Area area, double percentage, Collection players) { List positions = area.positions(); if (positions.isEmpty()) return; @@ -80,12 +79,12 @@ public static void reshuffle(@NotNull Instance instance, @NotNull Area area, dou } } - private static @NotNull Block pickWeightedBlock(@NotNull List entries, @NotNull Block fallback) { + private static Block pickWeightedBlock(List entries, Block fallback) { double roll = ThreadLocalRandom.current().nextDouble(); // 0.0 to 1.0 double cumulative = 0.0; for (PushEntry entry : entries) { if (entry.isGround()) continue; - double p = Math.max(0.0, Math.min(1.0, entry.getWeight())); + double p = Math.clamp(entry.getWeight(), 0.0, 1.0); cumulative += p; if (roll < cumulative) { return entry.getBlock(); diff --git a/common/src/main/java/net/theevilreaper/bounce/common/ground/GroundArea.java b/common/src/main/java/net/theevilreaper/bounce/common/ground/GroundArea.java index 50aa01ab..31b993b3 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/ground/GroundArea.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/ground/GroundArea.java @@ -5,7 +5,6 @@ import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.theevilreaper.bounce.common.push.PushData; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,7 +35,7 @@ public GroundArea(Vec min, Vec max, Block groundBlock, PushData pushData) { * {@inheritDoc} */ @Override - public void calculatePositions(@NotNull Instance instance) { + public void calculatePositions(Instance instance) { // Avoid double calculations if (!this.positions.isEmpty()) return; @@ -84,7 +83,7 @@ public void calculatePositions(@NotNull Instance instance) { LOGGER.info("Calculated positions for area: {} to {} with {} positions", min, max, positions.size()); } - private boolean isAreaBlock(@NotNull Block block) { + private boolean isAreaBlock(Block block) { if (block.compare(groundBlock) || block.compare(Block.REDSTONE_BLOCK)) { return true; } diff --git a/common/src/main/java/net/theevilreaper/bounce/common/push/PushEntry.java b/common/src/main/java/net/theevilreaper/bounce/common/push/PushEntry.java index 797880c6..67978849 100644 --- a/common/src/main/java/net/theevilreaper/bounce/common/push/PushEntry.java +++ b/common/src/main/java/net/theevilreaper/bounce/common/push/PushEntry.java @@ -154,7 +154,7 @@ public void decrementWeight() { } private static double clampWeight(double w) { - return Math.max(0.0, Math.min(1.0, Math.round(w * 100.0) / 100.0)); + return Math.clamp(Math.round(w * 100.0) / 100.0, 0.0, 1.0); } /** diff --git a/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaFillerIntegrationTest.java b/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaFillerIntegrationTest.java index c93b3302..913f6949 100644 --- a/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaFillerIntegrationTest.java +++ b/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaFillerIntegrationTest.java @@ -22,7 +22,6 @@ class AreaFillerIntegrationTest { @Test void testFillFallsBackToGroundBlockWhenPushDataEmpty(@NotNull Env env) { Instance instance = env.createFlatInstance(); - Player player = env.createPlayer(instance); for (int x = 0; x < 5; x++) { instance.setBlock(x, 0, 0, Block.GLASS); } @@ -41,7 +40,6 @@ void testFillFallsBackToGroundBlockWhenPushDataEmpty(@NotNull Env env) { @Test void testFillNeverPicksAZeroWeightEntry(@NotNull Env env) { Instance instance = env.createFlatInstance(); - Player player = env.createPlayer(instance); for (int x = 0; x < 30; x++) { instance.setBlock(x, 0, 0, Block.GLASS); } @@ -64,7 +62,6 @@ void testFillNeverPicksAZeroWeightEntry(@NotNull Env env) { @Test void testFillDistributionRoughlyFollowsWeights(@NotNull Env env) { Instance instance = env.createFlatInstance(); - Player player = env.createPlayer(instance); for (int x = 0; x < 100; x++) { instance.setBlock(x, 0, 0, Block.GLASS); } diff --git a/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaIntegrationTest.java b/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaIntegrationTest.java index 45348e98..dac2a097 100644 --- a/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaIntegrationTest.java +++ b/common/src/test/java/net/theevilreaper/bounce/common/ground/AreaIntegrationTest.java @@ -1,7 +1,6 @@ package net.theevilreaper.bounce.common.ground; import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.testing.Env; @@ -21,7 +20,6 @@ class AreaIntegrationTest { @Test void testCalculatePositionsOnlyIncludesMatchingGroundBlock(@NotNull Env env) { Instance instance = env.createFlatInstance(); - Player player = env.createPlayer(instance); instance.setBlock(0, 0, 0, Block.AMETHYST_BLOCK); instance.setBlock(1, 0, 0, Block.AMETHYST_BLOCK); @@ -45,7 +43,6 @@ void testCalculatePositionsOnlyIncludesMatchingGroundBlock(@NotNull Env env) { @Test void testCalculatePositionsIsIdempotent(@NotNull Env env) { Instance instance = env.createFlatInstance(); - Player player = env.createPlayer(instance); instance.setBlock(0, 0, 0, Block.GLASS); Area area = new GroundArea(new Vec(0, 0, 0), new Vec(0, 0, 0), Block.GLASS, new PushData(List.of())); @@ -62,13 +59,13 @@ void testCalculatePositionsIsIdempotent(@NotNull Env env) { @Test void testPositionsIsUnmodifiable(@NotNull Env env) { Instance instance = env.createFlatInstance(); - Player player = env.createPlayer(instance); instance.setBlock(0, 0, 0, Block.GLASS); Area area = new GroundArea(new Vec(0, 0, 0), new Vec(0, 0, 0), Block.GLASS, new PushData(List.of())); area.calculatePositions(instance); - assertThrows(UnsupportedOperationException.class, () -> area.positions().add(new Vec(9, 9, 9))); + Vec vec = new Vec(9, 9, 9); + assertThrows(UnsupportedOperationException.class, () -> area.positions().add(vec)); env.destroyInstance(instance, true); } @@ -76,7 +73,6 @@ void testPositionsIsUnmodifiable(@NotNull Env env) { @Test void testCalculatePositionsLoadsChunksInArea(@NotNull Env env) { Instance instance = env.createFlatInstance(); - Player player = env.createPlayer(instance); // Coordinates spanning multiple chunks (e.g. chunk -2, 3 to chunk -1, 4) Vec min = new Vec(-25, 60, -25); @@ -91,7 +87,6 @@ void testCalculatePositionsLoadsChunksInArea(@NotNull Env env) { @Test void testCalculatePositionsIncludesConfiguredPushBlocks(@NotNull Env env) { Instance instance = env.createFlatInstance(); - Player player = env.createPlayer(instance); instance.setBlock(0, 0, 0, Block.AMETHYST_BLOCK); // ground block instance.setBlock(1, 0, 0, Block.GOLD_BLOCK); // existing push block @@ -116,7 +111,6 @@ void testCalculatePositionsIncludesConfiguredPushBlocks(@NotNull Env env) { @Test void testCalculatePositionsScansLayerBelowWhenStandingOnPlatform(@NotNull Env env) { Instance instance = env.createFlatInstance(); - Player player = env.createPlayer(instance); // Platform block is placed at Y=63, air at Y=64 instance.setBlock(0, 63, 0, Block.GLASS);