From 29a22cee262f8eac29debf2bb6d73a0270580a85 Mon Sep 17 00:00:00 2001 From: Touchie771 Date: Sat, 29 Nov 2025 19:33:09 +0200 Subject: [PATCH 1/3] added builder pattern to slot item --- .../touchie771/minecraftGUI/api/SlotItem.java | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java index 6e16a57..84cd5f9 100644 --- a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java +++ b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java @@ -46,4 +46,140 @@ public record SlotItem( public SlotItem(@NotNull Component itemName, int itemSlot, @NotNull Material material, int quantity) { this(itemName, itemSlot, material, quantity, null, null, null, null, null); } + + /** + * Creates a new builder for constructing SlotItem instances. + * + * @param itemSlot The slot position where this item should be placed + * @return A new Builder instance + */ + public static Builder builder(int itemSlot) { + return new Builder(itemSlot); + } + + /** + * Builder class for creating SlotItem instances with a fluent API. + */ + public static class Builder { + private @Nullable Component itemName; + private final int itemSlot; + private @Nullable Material material; + private int quantity = 1; + private @Nullable List lore; + private @Nullable Map enchantments; + private @Nullable ItemStack customItemStack; + private @Nullable Integer customModelData; + private @Nullable Integer damage; + + private Builder(int itemSlot) { + this.itemSlot = itemSlot; + } + + /** + * Sets the display name of the item. + * + * @param itemName The display name component + * @return This builder instance for chaining + */ + public Builder itemName(@Nullable Component itemName) { + this.itemName = itemName; + return this; + } + + /** + * Sets the material type for this item. + * + * @param material The material type + * @return This builder instance for chaining + */ + public Builder material(@Nullable Material material) { + this.material = material; + return this; + } + + /** + * Sets the stack size of this item. + * + * @param quantity The stack size (1-64) + * @return This builder instance for chaining + */ + public Builder quantity(int quantity) { + this.quantity = quantity; + return this; + } + + /** + * Sets the lore lines for the item. + * + * @param lore The list of lore components + * @return This builder instance for chaining + */ + public Builder lore(@Nullable List lore) { + this.lore = lore; + return this; + } + + /** + * Sets the enchantments to apply to the item. + * + * @param enchantments The map of enchantments to levels + * @return This builder instance for chaining + */ + public Builder enchantments(@Nullable Map enchantments) { + this.enchantments = enchantments; + return this; + } + + /** + * Sets a custom ItemStack to use as base (e.g., for items with NBT). + * + * @param customItemStack The custom ItemStack + * @return This builder instance for chaining + */ + public Builder customItemStack(@Nullable ItemStack customItemStack) { + this.customItemStack = customItemStack; + return this; + } + + /** + * Sets the custom model data ID. + * + * @param customModelData The custom model data ID + * @return This builder instance for chaining + */ + public Builder customModelData(@Nullable Integer customModelData) { + this.customModelData = customModelData; + return this; + } + + /** + * Sets the damage/durability value. + * + * @param damage The damage value + * @return This builder instance for chaining + */ + public Builder damage(@Nullable Integer damage) { + this.damage = damage; + return this; + } + + /** + * Builds the SlotItem instance with the configured values. + * + * @return A new SlotItem instance + */ + public SlotItem build() { + return new SlotItem( + itemName, + itemSlot, + material, + quantity, + lore, + enchantments, + customItemStack, + customModelData, + damage + ); + } + } } \ No newline at end of file From da3dee2746b5ef271abb96fc37fbe29c4bb9bd54 Mon Sep 17 00:00:00 2001 From: Touchie771 Date: Sat, 29 Nov 2025 19:38:52 +0200 Subject: [PATCH 2/3] implemented lore support --- .../me/touchie771/minecraftGUI/api/Menu.java | 10 +++++-- .../touchie771/minecraftGUI/api/SlotItem.java | 30 +++++++++++++++++++ .../api/presets/ConfirmationMenu.java | 20 +++++-------- .../api/presets/PaginationMenu.java | 26 ++++++++-------- 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/main/java/me/touchie771/minecraftGUI/api/Menu.java b/src/main/java/me/touchie771/minecraftGUI/api/Menu.java index 447c345..b685b36 100644 --- a/src/main/java/me/touchie771/minecraftGUI/api/Menu.java +++ b/src/main/java/me/touchie771/minecraftGUI/api/Menu.java @@ -27,7 +27,10 @@ * Menu menu = Menu.newBuilder() * .size(27) * .title(Component.text("My Menu")) - * .items(new SlotItem(Component.text("Diamond"), (short) 0, Material.DIAMOND, 1)) + * .items(SlotItem.builder(0) + * .itemName(Component.text("Diamond")) + * .material(Material.DIAMOND) + * .build()) * .build(); * }

*/ @@ -276,7 +279,10 @@ public void unregisterEvents() { * Menu menu = new MenuBuilder() * .size(27) * .title(Component.text("My Menu")) - * .items(new SlotItem(Component.text("Diamond"), (short) 0, Material.DIAMOND, 1)) + * .items(SlotItem.builder(0) + * .itemName(Component.text("Diamond")) + * .material(Material.DIAMOND) + * .build()) * .build(); * }

*/ diff --git a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java index 84cd5f9..b8eb8d4 100644 --- a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java +++ b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java @@ -7,6 +7,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -119,6 +121,34 @@ public Builder lore(@Nullable List lore) { return this; } + /** + * Adds a single line of lore to the item. + * + * @param line The lore line to add + * @return This builder instance for chaining + */ + public Builder addLore(@NotNull Component line) { + if (this.lore == null) { + this.lore = new ArrayList<>(); + } + this.lore.add(line); + return this; + } + + /** + * Adds multiple lines of lore to the item. + * + * @param lines The lore lines to add + * @return This builder instance for chaining + */ + public Builder addLore(@NotNull Component... lines) { + if (this.lore == null) { + this.lore = new ArrayList<>(); + } + this.lore.addAll(Arrays.asList(lines)); + return this; + } + /** * Sets the enchantments to apply to the item. * diff --git a/src/main/java/me/touchie771/minecraftGUI/api/presets/ConfirmationMenu.java b/src/main/java/me/touchie771/minecraftGUI/api/presets/ConfirmationMenu.java index be7cc61..f62f261 100644 --- a/src/main/java/me/touchie771/minecraftGUI/api/presets/ConfirmationMenu.java +++ b/src/main/java/me/touchie771/minecraftGUI/api/presets/ConfirmationMenu.java @@ -29,19 +29,15 @@ public class ConfirmationMenu { * @return A configured Menu instance */ public static Menu create(@NotNull Plugin plugin, @NotNull Component title, @NotNull Runnable onConfirm, @NotNull Runnable onCancel) { - SlotItem confirmItem = new SlotItem( - Component.text("Confirm", NamedTextColor.GREEN), - (short) CONFIRM_SLOT, - Material.LIME_WOOL, - 1 - ); + SlotItem confirmItem = SlotItem.builder(CONFIRM_SLOT) + .itemName(Component.text("Confirm", NamedTextColor.GREEN)) + .material(Material.LIME_WOOL) + .build(); - SlotItem cancelItem = new SlotItem( - Component.text("Cancel", NamedTextColor.RED), - (short) CANCEL_SLOT, - Material.RED_WOOL, - 1 - ); + SlotItem cancelItem = SlotItem.builder(CANCEL_SLOT) + .itemName(Component.text("Cancel", NamedTextColor.RED)) + .material(Material.RED_WOOL) + .build(); ClickHandler confirmHandler = ClickHandler.newBuilder() .callback(event -> { diff --git a/src/main/java/me/touchie771/minecraftGUI/api/presets/PaginationMenu.java b/src/main/java/me/touchie771/minecraftGUI/api/presets/PaginationMenu.java index ec3f9eb..a37574a 100644 --- a/src/main/java/me/touchie771/minecraftGUI/api/presets/PaginationMenu.java +++ b/src/main/java/me/touchie771/minecraftGUI/api/presets/PaginationMenu.java @@ -79,7 +79,11 @@ private void update() { PageItem item = items.get(i); int slot = i - startIndex; - menu.addItems(new SlotItem(item.name(), (short) slot, item.material(), item.quantity())); + menu.addItems(SlotItem.builder(slot) + .itemName(item.name()) + .material(item.material()) + .quantity(item.quantity()) + .build()); if (item.action() != null) { menu.onClick(slot, ClickHandler.newBuilder() @@ -90,12 +94,10 @@ private void update() { } if (currentPage > 0) { - menu.addItems(new SlotItem( - Component.text("Previous Page", NamedTextColor.YELLOW), - (short) PREV_SLOT, - Material.ARROW, - 1 - )); + menu.addItems(SlotItem.builder(PREV_SLOT) + .itemName(Component.text("Previous Page", NamedTextColor.YELLOW)) + .material(Material.ARROW) + .build()); menu.onClick(PREV_SLOT, ClickHandler.newBuilder() .callback(e -> { currentPage--; @@ -106,12 +108,10 @@ private void update() { } if (currentPage < totalPages - 1) { - menu.addItems(new SlotItem( - Component.text("Next Page", NamedTextColor.YELLOW), - (short) NEXT_SLOT, - Material.ARROW, - 1 - )); + menu.addItems(SlotItem.builder(NEXT_SLOT) + .itemName(Component.text("Next Page", NamedTextColor.YELLOW)) + .material(Material.ARROW) + .build()); menu.onClick(NEXT_SLOT, ClickHandler.newBuilder() .callback(e -> { currentPage++; From e38e5bf7e91b9e57baf7ac2a936cb555c75ee741 Mon Sep 17 00:00:00 2001 From: Touchie771 Date: Sat, 29 Nov 2025 19:47:24 +0200 Subject: [PATCH 3/3] wrote tests for new slot item --- .../touchie771/minecraftGUI/api/SlotItem.java | 8 +- .../minecraftGUI/api/SlotItemTest.java | 283 +++++++++++++++--- 2 files changed, 249 insertions(+), 42 deletions(-) diff --git a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java index b8eb8d4..087cdb8 100644 --- a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java +++ b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java @@ -130,6 +130,8 @@ public Builder lore(@Nullable List lore) { public Builder addLore(@NotNull Component line) { if (this.lore == null) { this.lore = new ArrayList<>(); + } else if (!(this.lore instanceof ArrayList)) { + this.lore = new ArrayList<>(this.lore); } this.lore.add(line); return this; @@ -144,6 +146,8 @@ public Builder addLore(@NotNull Component line) { public Builder addLore(@NotNull Component... lines) { if (this.lore == null) { this.lore = new ArrayList<>(); + } else if (!(this.lore instanceof ArrayList)) { + this.lore = new ArrayList<>(this.lore); } this.lore.addAll(Arrays.asList(lines)); return this; @@ -204,8 +208,8 @@ public SlotItem build() { itemSlot, material, quantity, - lore, - enchantments, + lore != null ? List.copyOf(lore) : null, + enchantments != null ? Map.copyOf(enchantments) : null, customItemStack, customModelData, damage diff --git a/src/test/java/me/touchie771/minecraftGUI/api/SlotItemTest.java b/src/test/java/me/touchie771/minecraftGUI/api/SlotItemTest.java index 82a545b..ea90715 100644 --- a/src/test/java/me/touchie771/minecraftGUI/api/SlotItemTest.java +++ b/src/test/java/me/touchie771/minecraftGUI/api/SlotItemTest.java @@ -1,10 +1,12 @@ package me.touchie771.minecraftGUI.api; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; import java.util.List; import java.util.Map; @@ -15,64 +17,265 @@ class SlotItemTest { @Test - void testLegacyConstructor() { - Component name = Component.text("Test Item"); - SlotItem item = new SlotItem(name, (short) 0, Material.DIAMOND, 1); + @DisplayName("Test builder with minimal configuration") + void testBuilderMinimalConfiguration() { + int slot = 0; + SlotItem item = SlotItem.builder(slot) + .material(Material.DIAMOND) + .build(); - assertEquals(name, item.itemName()); - assertEquals(0, item.itemSlot()); + assertNull(item.itemName()); + assertEquals(slot, item.itemSlot()); assertEquals(Material.DIAMOND, item.material()); - assertEquals(1, item.quantity()); + assertEquals(1, item.quantity()); // Default quantity assertNull(item.lore()); assertNull(item.enchantments()); assertNull(item.customItemStack()); + assertNull(item.customModelData()); + assertNull(item.damage()); } @Test - void testFullConstructor() { - Component name = Component.text("Full Item"); - List lore = List.of(Component.text("Line 1")); - Map enchants = Map.of(Enchantment.UNBREAKING, 1); - - SlotItem item = new SlotItem( - name, - (short) 1, - Material.GOLD_INGOT, - 5, - lore, - enchants, - null, - 123, - 10 + @DisplayName("Test builder with full configuration") + void testBuilderFullConfiguration() { + Component name = Component.text("Full Item", NamedTextColor.GOLD); + List lore = List.of( + Component.text("First line"), + Component.text("Second line", NamedTextColor.RED) ); + Map enchants = Map.of( + Enchantment.UNBREAKING, 3, + Enchantment.SHARPNESS, 5 + ); + ItemStack customStack = mock(ItemStack.class); + + SlotItem item = SlotItem.builder(10) + .itemName(name) + .material(Material.NETHERITE_SWORD) + .quantity(1) + .lore(lore) + .enchantments(enchants) + .customItemStack(customStack) + .customModelData(1234) + .damage(50) + .build(); assertEquals(name, item.itemName()); - assertEquals(1, item.itemSlot()); - assertEquals(Material.GOLD_INGOT, item.material()); - assertEquals(5, item.quantity()); + assertEquals(10, item.itemSlot()); + assertEquals(Material.NETHERITE_SWORD, item.material()); + assertEquals(1, item.quantity()); assertEquals(lore, item.lore()); assertEquals(enchants, item.enchantments()); - assertEquals(123, item.customModelData()); - assertEquals(10, item.damage()); + assertEquals(customStack, item.customItemStack()); + assertEquals(1234, item.customModelData()); + assertEquals(50, item.damage()); } @Test - void testCustomItemStack() { - ItemStack stack = mock(ItemStack.class); - SlotItem item = new SlotItem( - null, - (short) 2, - null, - 0, - null, - null, - stack, - null, - null - ); + @DisplayName("Test builder with custom ItemStack only") + void testBuilderCustomItemStackOnly() { + ItemStack customStack = mock(ItemStack.class); + + SlotItem item = SlotItem.builder(5) + .customItemStack(customStack) + .quantity(64) + .build(); - assertEquals(stack, item.customItemStack()); + assertNull(item.itemName()); + assertEquals(5, item.itemSlot()); assertNull(item.material()); + assertEquals(64, item.quantity()); + assertNull(item.lore()); + assertNull(item.enchantments()); + assertEquals(customStack, item.customItemStack()); + assertNull(item.customModelData()); + assertNull(item.damage()); + } + + @Test + @DisplayName("Test addLore with single line") + void testAddLoreSingleLine() { + Component line = Component.text("A single lore line"); + + SlotItem item = SlotItem.builder(0) + .material(Material.STONE) + .addLore(line) + .build(); + + assertNotNull(item.lore()); + assertEquals(1, item.lore().size()); + assertEquals(line, item.lore().getFirst()); + } + + @Test + @DisplayName("Test addLore with multiple lines") + void testAddLoreMultipleLines() { + Component line1 = Component.text("First line"); + Component line2 = Component.text("Second line"); + Component line3 = Component.text("Third line"); + + SlotItem item = SlotItem.builder(0) + .material(Material.STONE) + .addLore(line1, line2, line3) + .build(); + + assertNotNull(item.lore()); + assertEquals(3, item.lore().size()); + assertEquals(line1, item.lore().get(0)); + assertEquals(line2, item.lore().get(1)); + assertEquals(line3, item.lore().get(2)); + } + + @Test + @DisplayName("Test addLore chaining with existing lore") + void testAddLoreChainingWithExistingLore() { + Component initialLore = Component.text("Initial lore"); + Component addedLine1 = Component.text("Added line 1"); + Component addedLine2 = Component.text("Added line 2"); + + SlotItem item = SlotItem.builder(0) + .material(Material.STONE) + .lore(List.of(initialLore)) + .addLore(addedLine1) + .addLore(addedLine2) + .build(); + + assertNotNull(item.lore()); + assertEquals(3, item.lore().size()); + assertEquals(initialLore, item.lore().get(0)); + assertEquals(addedLine1, item.lore().get(1)); + assertEquals(addedLine2, item.lore().get(2)); + } + + @Test + @DisplayName("Test addLore without initial lore") + void testAddLoreWithoutInitialLore() { + Component line = Component.text("First lore line"); + + SlotItem item = SlotItem.builder(0) + .material(Material.STONE) + .addLore(line) + .build(); + + assertNotNull(item.lore()); + assertEquals(1, item.lore().size()); + assertEquals(line, item.lore().getFirst()); + } + + @Test + @DisplayName("Test builder method chaining") + void testBuilderMethodChaining() { + Component name = Component.text("Chained Item"); + + SlotItem item = SlotItem.builder(15) + .itemName(name) + .material(Material.EMERALD) + .quantity(32) + .customModelData(5678) + .build(); + + assertEquals(name, item.itemName()); + assertEquals(15, item.itemSlot()); + assertEquals(Material.EMERALD, item.material()); + assertEquals(32, item.quantity()); + assertEquals(5678, item.customModelData()); + assertNull(item.lore()); + assertNull(item.enchantments()); + assertNull(item.customItemStack()); + assertNull(item.damage()); + } + + @Test + @DisplayName("Test builder with null values") + void testBuilderWithNullValues() { + SlotItem item = SlotItem.builder(20) + .itemName(null) + .material(Material.AIR) + .lore(null) + .enchantments(null) + .customItemStack(null) + .customModelData(null) + .damage(null) + .build(); + assertNull(item.itemName()); + assertEquals(20, item.itemSlot()); + assertEquals(Material.AIR, item.material()); + assertEquals(1, item.quantity()); // Default quantity + assertNull(item.lore()); + assertNull(item.enchantments()); + assertNull(item.customItemStack()); + assertNull(item.customModelData()); + assertNull(item.damage()); + } + + @Test + @DisplayName("Test legacy constructor still works") + void testLegacyConstructor() { + Component name = Component.text("Legacy Item"); + SlotItem item = new SlotItem(name, 5, Material.GOLD_INGOT, 16); + + assertEquals(name, item.itemName()); + assertEquals(5, item.itemSlot()); + assertEquals(Material.GOLD_INGOT, item.material()); + assertEquals(16, item.quantity()); + assertNull(item.lore()); + assertNull(item.enchantments()); + assertNull(item.customItemStack()); + assertNull(item.customModelData()); + assertNull(item.damage()); + } + + @Test + @DisplayName("Test builder produces immutable record") + void testBuilderProducesImmutableRecord() { + Component originalName = Component.text("Original"); + SlotItem item = SlotItem.builder(0) + .itemName(originalName) + .material(Material.DIAMOND) + .addLore(Component.text("Line 1")) + .build(); + + // Records are immutable, so these should work as expected + assertEquals(originalName, item.itemName()); + assertEquals(Material.DIAMOND, item.material()); + + // Verify that the lore list is immutable if created from builder + assertThrows(UnsupportedOperationException.class, () -> { + if (item.lore() != null) { + item.lore().add(Component.text("This should fail")); + } + }); + } + + @Test + @DisplayName("Test builder with enchantments") + void testBuilderWithEnchantments() { + Map enchants = Map.of( + Enchantment.PROTECTION, 4, + Enchantment.FEATHER_FALLING, 4 + ); + + SlotItem item = SlotItem.builder(8) + .material(Material.DIAMOND_CHESTPLATE) + .enchantments(enchants) + .build(); + + assertEquals(enchants, item.enchantments()); + assertEquals(Material.DIAMOND_CHESTPLATE, item.material()); + assertEquals(8, item.itemSlot()); + } + + @Test + @DisplayName("Test builder with damage value") + void testBuilderWithDamageValue() { + SlotItem item = SlotItem.builder(12) + .material(Material.IRON_SWORD) + .damage(100) + .build(); + + assertEquals(100, item.damage()); + assertEquals(Material.IRON_SWORD, item.material()); + assertEquals(12, item.itemSlot()); } } \ No newline at end of file