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 6a151b9..9e9c078 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java @@ -13,7 +13,6 @@ import net.theevilreaper.bounce.common.ListenerHandling; import net.theevilreaper.bounce.common.bootstrap.ServiceBootstrap; import net.theevilreaper.bounce.setup.command.GameModeCommand; -import net.theevilreaper.bounce.setup.command.SetupCommand; import net.theevilreaper.bounce.setup.dialog.event.PlayerDialogRequestEvent; import net.theevilreaper.bounce.setup.event.map.MapSetupSelectEvent; import net.theevilreaper.bounce.setup.event.ground.PlayerGroundBlockSelectEvent; @@ -71,7 +70,6 @@ public void initialize() { registerCancelListener(node); registerListener(node); - MinecraftServer.getCommandManager().register(new SetupCommand(this.setupDataService)); MinecraftServer.getCommandManager().register(new GameModeCommand()); } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/command/SetupCommand.java b/setup/src/main/java/net/theevilreaper/bounce/setup/command/SetupCommand.java deleted file mode 100644 index 08fc361..0000000 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/command/SetupCommand.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.theevilreaper.bounce.setup.command; - -import net.minestom.server.command.builder.Command; -import net.minestom.server.command.builder.condition.Conditions; -import net.onelitefeather.guira.SetupDataService; -import net.theevilreaper.bounce.setup.command.part.SetupPositionCommand; -import org.jetbrains.annotations.NotNull; - -/** - * The {@link SetupCommand} is the root command for the setup process in the game. - * - * @author theEvilReaper - * @version 1.0.0 - * @since 1.0.0 - */ -public class SetupCommand extends Command { - - /** - * Creates the command to allow players to initialize different parts of the game. - * - * @param dataService the service to get the setup data - */ - public SetupCommand(@NotNull SetupDataService dataService) { - super("setup"); - this.setCondition(Conditions::playerOnly); - this.addSubcommand(new SetupPositionCommand(dataService::get)); - } -} diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/command/part/SetupPositionCommand.java b/setup/src/main/java/net/theevilreaper/bounce/setup/command/part/SetupPositionCommand.java deleted file mode 100644 index 48b6992..0000000 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/command/part/SetupPositionCommand.java +++ /dev/null @@ -1,85 +0,0 @@ -package net.theevilreaper.bounce.setup.command.part; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.minestom.server.command.CommandSender; -import net.minestom.server.command.builder.Command; -import net.minestom.server.command.builder.CommandContext; -import net.minestom.server.command.builder.arguments.ArgumentType; -import net.minestom.server.command.builder.arguments.ArgumentWord; -import net.minestom.server.command.builder.condition.Conditions; -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.aves.util.Components; -import net.theevilreaper.bounce.common.util.Messages; -import net.theevilreaper.bounce.setup.data.BounceData; -import org.jetbrains.annotations.NotNull; - -import java.util.Optional; - -import static net.theevilreaper.bounce.setup.event.AbstractStateNotifyEvent.*; -import static net.theevilreaper.bounce.setup.util.SetupMessages.SELECT_MAP_FIRST; -import static net.theevilreaper.bounce.setup.util.SetupTags.SETUP_TAG; - -public final class SetupPositionCommand extends Command { - - private final OptionalSetupDataGetter setupDataFunction; - - public SetupPositionCommand(@NotNull OptionalSetupDataGetter setupDataFunction) { - super("position"); - this.setCondition(Conditions::playerOnly); - this.setupDataFunction = setupDataFunction; - ArgumentWord spawnType = ArgumentType.Word("spawnType").from("spawn", "game"); - this.addSyntax(this::handleSpawnSet, spawnType); - } - - private void handleSpawnSet(@NotNull CommandSender sender, @NotNull CommandContext context) { - if (!sender.hasTag(SETUP_TAG)) { - sender.sendMessage(SELECT_MAP_FIRST); - return; - } - - String type = context.get("spawnType"); - - Optional fetchedData = setupDataFunction.get(sender.identity().uuid()); - if (fetchedData.isEmpty()) { - sender.sendMessage(SELECT_MAP_FIRST); - return; - } - - Player player = (Player) sender; - BounceData setupData = ((BounceData) fetchedData.get()); - - if (setupData.getMapBuilder() == null) { - sender.sendMessage("No map is currently selected. Please select a map first."); - return; - } - GameMapBuilderState.StateChange stateChange; - switch (type) { - case "spawn": - setupData.getMapBuilder().spawn(player.getPosition()); - stateChange = GameMapBuilderState.StateChange.SPAWN; - break; - case "game": - setupData.getMapBuilder().gameSpawn(player.getPosition()); - stateChange = GameMapBuilderState.StateChange.GAME_SPAWN; - break; - default: - sender.sendMessage(Component.text("Invalid spawn type! Use 'spawn' or 'game'.", NamedTextColor.RED)); - return; - } - Component posAsComponent = Components.convertPoint(player.getPosition().asPos()); - Component argComponent = Component.text(type, NamedTextColor.GREEN); - Component message = Messages.withPrefix(Component.text("The ", NamedTextColor.GRAY) - .append(argComponent) - .append(Component.space()) - .append(Component.text("position of the map is now located at: ", NamedTextColor.GRAY)) - .append(posAsComponent) - ); - sender.sendMessage(message); - GameMapBuilderState state = new GameMapBuilderState(setupData, stateChange); - EventDispatcher.call(new GameMapBuilderStateNotifyEvent(state)); - } -} 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 8eb9424..a360a98 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,11 +64,31 @@ 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(), this::setSpawnToCurrentPosition); + case GAME_SPAWN -> new PositionSlot<>(type, this.builder.getGameSpawn(), this::setGameSpawnToCurrentPosition); case NAME -> new StringSlot(type, builder.getName()); case BUILDER -> new MultiStringSlot(type, builder.getBuilders()); case AREA -> new AreaOverviewSlot(type, builder.getArea()); }; } + + /** + * Sets the spawn position on the {@link GameMapBuilder} to the player's current position and refreshes the layout. + * + * @param player the player whose position is captured + */ + private void setSpawnToCurrentPosition(Player player) { + this.builder.spawn(player.getPosition()); + this.invalidateDataLayout(); + } + + /** + * Sets the game spawn position on the {@link GameMapBuilder} to the player's current position and refreshes the layout. + * + * @param player the player whose position is captured + */ + private void setGameSpawnToCurrentPosition(Player player) { + this.builder.gameSpawn(player.getPosition()); + this.invalidateDataLayout(); + } } 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 e26c930..879183e 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 @@ -23,6 +23,7 @@ import java.util.function.Consumer; import static net.theevilreaper.bounce.setup.util.SetupMessages.DELETE_CLICK; +import static net.theevilreaper.bounce.setup.util.SetupMessages.SET_CLICK; import static net.theevilreaper.bounce.setup.util.SetupMessages.TELEPORT_CLICK; public class PositionSlot extends AbstractDataSlot { @@ -36,17 +37,27 @@ public class PositionSlot extends AbstractDataSlot { } private final @Nullable Pos position; + private final @Nullable Consumer onSet; - public PositionSlot(T type, @Nullable Pos position) { + public PositionSlot(T type, @Nullable Pos position, @Nullable Consumer onSet) { super(type); this.position = position; + this.onSet = onSet; } @Override public ItemStack getItem() { ItemStack overviewItem = this.type.getItem(); - if (position == null) return overviewItem; + if (position == null) { + if (onSet == null) return overviewItem; + + List lore = new ArrayList<>(); + lore.add(Component.empty()); + lore.add(SET_CLICK); + lore.add(Component.empty()); + return asBuilder(overviewItem).lore(lore).build(); + } List lore = new ArrayList<>(); lore.add(Component.empty()); @@ -62,7 +73,13 @@ public ItemStack getItem() { @Override 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 || click instanceof Click.Right)) return; + + if (position == null) { + if (click instanceof Click.Left && onSet != null) onSet.accept(player); + return; + } + if (click instanceof Click.Left) { player.closeInventory(); player.teleport(position); 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 0b87b06..53cba9e 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,6 +12,7 @@ public class SetupMessages extends Messages { public static final Component TELEPORT_CLICK; public static final Component DELETE_CLICK; + public static final Component SET_CLICK; public static final Component CLICK_TO_EDIT; static { @@ -20,6 +21,16 @@ public class SetupMessages extends Messages { INVALID_NAME = withPrefix(Component.text("Invalid name for the map", NamedTextColor.RED)); CLICK_TO_EDIT = Component.text("Click to edit", NamedTextColor.GRAY); + SET_CLICK = NO_SPACE_SEPARATOR + .append(Component.space()) + .append(Component.text("Left", NamedTextColor.GREEN)) + .append(Component.space()) + .append(Component.text("click", NamedTextColor.GRAY)) + .append(Component.space()) + .append(Component.text("->", NamedTextColor.GRAY)) + .append(Component.space()) + .append(Component.text("set to your position", NamedTextColor.GREEN)); + TELEPORT_CLICK = NO_SPACE_SEPARATOR .append(Component.space()) .append(Component.text("Left", NamedTextColor.GREEN)) diff --git a/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/slot/PositionSlotTest.java b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/slot/PositionSlotTest.java new file mode 100644 index 0000000..2c5ae9f --- /dev/null +++ b/setup/src/test/java/net/theevilreaper/bounce/setup/inventory/slot/PositionSlotTest.java @@ -0,0 +1,100 @@ +package net.theevilreaper.bounce.setup.inventory.slot; + +import net.kyori.adventure.text.Component; +import net.minestom.server.component.DataComponents; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.instance.Instance; +import net.minestom.server.item.ItemStack; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import net.theevilreaper.aves.inventory.function.InventoryClick; +import net.theevilreaper.bounce.setup.inventory.overview.OverviewType; +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.concurrent.atomic.AtomicReference; + +import static net.theevilreaper.bounce.setup.util.SetupMessages.SET_CLICK; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MicrotusExtension.class) +class PositionSlotTest { + + private static final int SLOT_INDEX = 12; + + @Test + void testUnsetPositionWithSetterShowsHint(@NotNull Env env) { + PositionSlot slot = new PositionSlot<>(OverviewType.SPAWN, null, player -> {}); + ItemStack item = slot.getItem(); + + assertTrue(item.has(DataComponents.LORE)); + List lore = item.get(DataComponents.LORE); + + assertNotNull(lore); + assertEquals(3, lore.size(), "Lore should contain 3 components"); + assertAll( + "Unset position lore", + () -> assertEquals(Component.empty(), lore.getFirst(), "First lore component should be empty"), + () -> assertEquals(SET_CLICK, lore.get(1), "Middle lore component should hint at setting the position"), + () -> assertEquals(Component.empty(), lore.getLast(), "Last lore component should be empty") + ); + } + + @Test + void testUnsetPositionWithoutSetterHasNoLore(@NotNull Env env) { + PositionSlot slot = new PositionSlot<>(OverviewType.SPAWN, null, null); + ItemStack item = slot.getItem(); + + assertEquals(OverviewType.SPAWN.getItem(), item, "Without a setter the raw overview item should be rendered"); + } + + @Test + void testLeftClickOnUnsetPositionInvokesSetter(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + AtomicReference captured = new AtomicReference<>(); + + PositionSlot slot = new PositionSlot<>(OverviewType.SPAWN, null, captured::set); + InventoryClick click = slot.getClick(); + click.onClick(player, SLOT_INDEX, new Click.Left(SLOT_INDEX), slot.getItem(), holder -> {}); + + assertEquals(player, captured.get(), "Left-clicking an unset position should capture the clicking player"); + + env.destroyInstance(instance, true); + } + + @Test + void testRightClickOnUnsetPositionDoesNotInvokeSetter(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + AtomicReference captured = new AtomicReference<>(); + + PositionSlot slot = new PositionSlot<>(OverviewType.SPAWN, null, captured::set); + InventoryClick click = slot.getClick(); + click.onClick(player, SLOT_INDEX, new Click.Right(SLOT_INDEX), slot.getItem(), holder -> {}); + + assertNull(captured.get(), "Right-clicking an unset position must not invoke the setter"); + + env.destroyInstance(instance, true); + } + + @Test + void testLeftClickOnAlreadySetPositionDoesNotInvokeSetter(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + AtomicReference captured = new AtomicReference<>(); + Pos position = new Pos(1, 2, 3); + + PositionSlot slot = new PositionSlot<>(OverviewType.SPAWN, position, captured::set); + InventoryClick click = slot.getClick(); + click.onClick(player, SLOT_INDEX, new Click.Left(SLOT_INDEX), slot.getItem(), holder -> {}); + + assertNull(captured.get(), "Clicking an already-set position must teleport instead of invoking the setter"); + + env.destroyInstance(instance, true); + } +}