Jelajahi Sumber

First Minions, Enchanted Items with recipe, reading minion data from file, Util classes

Jan 3 minggu lalu
induk
melakukan
5db4a7f48a
64 mengubah file dengan 2888 tambahan dan 65 penghapusan
  1. 1 1
      pom.xml
  2. 12 0
      src/main/java/me/lethunderhawk/bazaarflux/util/CustomHeadCreator.java
  3. 6 0
      src/main/java/me/lethunderhawk/bazaarflux/util/command/CommandNode.java
  4. 28 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/GoBackItem.java
  5. 15 6
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryGUI.java
  6. 46 8
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryManager.java
  7. 6 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/PlayerHeadListGUI.java
  8. 25 0
      src/main/java/me/lethunderhawk/bazaarflux/util/lang/RomanNumbers.java
  9. 12 4
      src/main/java/me/lethunderhawk/clans/ClanManager.java
  10. 1 1
      src/main/java/me/lethunderhawk/clans/ClanModule.java
  11. 14 0
      src/main/java/me/lethunderhawk/clans/claim/Claim.java
  12. 4 0
      src/main/java/me/lethunderhawk/clans/gui/ClanGUI.java
  13. 3 1
      src/main/java/me/lethunderhawk/clans/placeholder/ClanPlaceHolder.java
  14. 1 1
      src/main/java/me/lethunderhawk/custom/item/CustomItemModule.java
  15. 2 2
      src/main/java/me/lethunderhawk/custom/item/command/CustomItemCommand.java
  16. 3 2
      src/main/java/me/lethunderhawk/economy/EconomyModule.java
  17. 0 4
      src/main/java/me/lethunderhawk/economy/api/EconomyAPI.java
  18. 0 2
      src/main/java/me/lethunderhawk/economy/api/EconomyPlaceholder.java
  19. 8 5
      src/main/java/me/lethunderhawk/main/Main.java
  20. 0 10
      src/main/java/me/lethunderhawk/main/util/ItalicDeco.java
  21. 36 0
      src/main/java/me/lethunderhawk/main/util/UnItalic.java
  22. 137 0
      src/main/java/me/lethunderhawk/minion/MinionModule.java
  23. 11 0
      src/main/java/me/lethunderhawk/minion/api/MinionBehavior.java
  24. 94 0
      src/main/java/me/lethunderhawk/minion/api/MinionLevel.java
  25. 16 0
      src/main/java/me/lethunderhawk/minion/api/MinionLevelData.java
  26. 26 0
      src/main/java/me/lethunderhawk/minion/api/MinionLevelTable.java
  27. 23 0
      src/main/java/me/lethunderhawk/minion/api/MinionType.java
  28. 6 0
      src/main/java/me/lethunderhawk/minion/api/NextActionState.java
  29. 175 0
      src/main/java/me/lethunderhawk/minion/command/MinionCommand.java
  30. 30 0
      src/main/java/me/lethunderhawk/minion/enchantedVariant/item/EnchantedCobblestone.java
  31. 30 0
      src/main/java/me/lethunderhawk/minion/enchantedVariant/item/EnchantedRedstone.java
  32. 18 0
      src/main/java/me/lethunderhawk/minion/enchantedVariant/item/abstraction/DisableEnchantedItemPlacingListener.java
  33. 106 0
      src/main/java/me/lethunderhawk/minion/enchantedVariant/item/abstraction/EnchantedItem.java
  34. 36 0
      src/main/java/me/lethunderhawk/minion/enchantedVariant/item/abstraction/EnchantedItemRegistry.java
  35. 166 0
      src/main/java/me/lethunderhawk/minion/enchantedVariant/recipe/EnchantedItemRecipe.java
  36. 150 0
      src/main/java/me/lethunderhawk/minion/enchantedVariant/recipe/RecipeManager.java
  37. 37 0
      src/main/java/me/lethunderhawk/minion/enchantedVariant/recipe/StackSizeIngredient.java
  38. 54 0
      src/main/java/me/lethunderhawk/minion/factory/MinionFactory.java
  39. 34 0
      src/main/java/me/lethunderhawk/minion/impl/behavior/CobbleMinionBehavior.java
  40. 23 0
      src/main/java/me/lethunderhawk/minion/impl/behavior/RedstoneMinionBehavior.java
  41. 54 0
      src/main/java/me/lethunderhawk/minion/impl/type/CobbleMinionType.java
  42. 52 0
      src/main/java/me/lethunderhawk/minion/impl/type/RedstoneMinionType.java
  43. 67 0
      src/main/java/me/lethunderhawk/minion/item/MinionItem.java
  44. 119 0
      src/main/java/me/lethunderhawk/minion/listener/MinionListener.java
  45. 63 0
      src/main/java/me/lethunderhawk/minion/manager/MinionManager.java
  46. 56 0
      src/main/java/me/lethunderhawk/minion/persistence/MinionSerializer.java
  47. 92 0
      src/main/java/me/lethunderhawk/minion/persistence/MinionStorage.java
  48. 27 0
      src/main/java/me/lethunderhawk/minion/registry/MinionRegistry.java
  49. 198 0
      src/main/java/me/lethunderhawk/minion/runtime/MinionInventory.java
  50. 14 0
      src/main/java/me/lethunderhawk/minion/runtime/MinionState.java
  51. 24 0
      src/main/java/me/lethunderhawk/minion/runtime/MinionTask.java
  52. 160 0
      src/main/java/me/lethunderhawk/minion/runtime/PlacedMinion.java
  53. 335 0
      src/main/java/me/lethunderhawk/minion/ui/MinionMenu.java
  54. 10 0
      src/main/java/me/lethunderhawk/minion/ui/MinionMenuFactory.java
  55. 47 0
      src/main/java/me/lethunderhawk/minion/ui/minionList/MinionLevelListMenu.java
  56. 46 0
      src/main/java/me/lethunderhawk/minion/ui/minionList/MinionListMenu.java
  57. 6 1
      src/main/java/me/lethunderhawk/tradeplugin/TradeModule.java
  58. 1 1
      src/main/java/me/lethunderhawk/tradeplugin/api/TradePlaceholder.java
  59. 8 7
      src/main/java/me/lethunderhawk/tradeplugin/input/player/NumberInputGUI.java
  60. 3 3
      src/main/java/me/lethunderhawk/tradeplugin/trade/TradeInventory.java
  61. 6 6
      src/main/java/me/lethunderhawk/tradeplugin/trade/TradeSession.java
  62. 49 0
      src/main/resources/minions/cobblestone.yml
  63. 49 0
      src/main/resources/minions/redstone.yml
  64. 7 0
      src/main/resources/plugin.yml

+ 1 - 1
pom.xml

@@ -6,7 +6,7 @@
 
     <groupId>me.lethunderhawk</groupId>
     <artifactId>bazaar-flux</artifactId>
-    <version>1.0-SNAPSHOT</version>
+    <version>1.2-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <name>BazaarFlux</name>

+ 12 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/CustomHeadCreator.java

@@ -25,6 +25,18 @@ public class CustomHeadCreator {
 
         return playerHead; // Return the custom head ItemStack
     }
+    public static ItemStack createCustomHead(String textureValue, Component displayName, List<Component> loreText) {
+        // Create the player head ItemStack
+        ItemStack playerHead = createCustomHead(textureValue);
+        SkullMeta meta = (SkullMeta) playerHead.getItemMeta();
+
+        meta.displayName(displayName);
+        meta.lore(loreText);
+
+        playerHead.setItemMeta(meta);
+
+        return playerHead; // Return the custom head ItemStack
+    }
 
     public static ItemStack createCustomHead(String textureValue) {
         ItemStack head = new ItemStack(Material.PLAYER_HEAD);

+ 6 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/command/CommandNode.java

@@ -31,6 +31,12 @@ public class CommandNode {
         subCommand.parent = this;
         subCommands.put(subCommand.getName().toLowerCase(), subCommand);
     }
+    public void addSubCommands(CommandNode... subCommandNodes) {
+        for(CommandNode subCommand : subCommandNodes) {
+            subCommand.parent = this;
+            subCommands.put(subCommand.getName().toLowerCase(), subCommand);
+        }
+    }
 
     public CommandNode getSubCommand(String name) {
         return subCommands.get(name.toLowerCase());

+ 28 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/gui/GoBackItem.java

@@ -0,0 +1,28 @@
+package me.lethunderhawk.bazaarflux.util.gui;
+
+import me.lethunderhawk.main.util.UnItalic;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.List;
+
+public class GoBackItem extends ItemStack {
+    public GoBackItem() {
+        super(Material.ARROW);
+
+        setItemMeta(buildItemMeta());
+    }
+
+    private ItemMeta buildItemMeta() {
+        ItemMeta meta = getItemMeta();
+        meta.displayName(Component.text("Go back", NamedTextColor.RED));
+        meta.lore(List.of(
+                Component.text("Go back to the previous menu!", NamedTextColor.GRAY)
+        ));
+        UnItalic.removeItalicFromMeta(meta);
+        return meta;
+    }
+}

+ 15 - 6
src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryGUI.java

@@ -18,7 +18,7 @@ import java.util.function.BiConsumer;
 /**
  * Generic, reusable Inventory GUI.
  */
-public class InventoryGUI implements InventoryHolder {
+public abstract class InventoryGUI implements InventoryHolder {
 
     private final String title;
     private final int size;
@@ -81,27 +81,36 @@ public class InventoryGUI implements InventoryHolder {
      * Opens another GUI and remembers this one for navigation.
      */
     public void openNext(Player player, InventoryGUI nextGui) {
+        // Push current GUI onto the next GUI's stack
         nextGui.previousGuis.push(this);
-        nextGui.open(player);
+        // Actually open the next GUI
+        InventoryManager.openFor(player, nextGui);
     }
 
+
     /**
      * Opens the previous GUI if available.
      */
     public void openPrevious(Player player) {
         if (!previousGuis.isEmpty()) {
-            previousGuis.pop().open(player);
+            InventoryGUI previousGui = previousGuis.pop();
+            // Don't use InventoryManager.close() here as it triggers close events
+            // Instead, directly open the previous GUI
+            InventoryManager.openFor(player, previousGui);
         }
     }
 
+    // Also need to update the overloaded version:
+    public void openPrevious(Player player, ClickType type) {
+        openPrevious(player);
+    }
+
     @Override
     public @NotNull Inventory getInventory() {
         return inventory;
     }
 
-    public void openPrevious(Player player, ClickType type) {
-        openPrevious(player);
-    }
+    public abstract void update();
 
     public interface AutoCloseHandler {
         void onClosedByPlayer(Player player);

+ 46 - 8
src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryManager.java

@@ -1,14 +1,17 @@
 package me.lethunderhawk.bazaarflux.util.gui;
 
+import me.lethunderhawk.minion.ui.MinionMenu;
 import org.bukkit.Bukkit;
 import org.bukkit.entity.Player;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
-import org.bukkit.event.inventory.InventoryCloseEvent;
 import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
 import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Map;
+import java.util.Objects;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -34,6 +37,15 @@ public final class InventoryManager implements Listener {
             Bukkit.getScheduler().runTask(providerPlugin, () -> openFor(player, gui));
             return;
         }
+
+        // Close current inventory if open
+        InventoryGUI current = openGuis.get(player.getUniqueId());
+        if (current != null) {
+            player.closeInventory();
+            openGuis.remove(player.getUniqueId());
+        }
+
+        // Open new GUI
         openGuis.put(player.getUniqueId(), gui);
         gui.open(player);
     }
@@ -42,8 +54,22 @@ public final class InventoryManager implements Listener {
         return openGuis.get(playerId);
     }
 
-    public static void close(UUID playerId) {
-        openGuis.remove(playerId);
+    public static void close(@NotNull UUID playerId) {
+        InventoryGUI gui = openGuis.remove(playerId);
+        if (gui != null) {
+            Player p = Bukkit.getPlayer(playerId);
+            if(p!= null) p.closeInventory();
+        }
+    }
+
+    public static void refreshMinionMenuForPlayer(UUID ownerId, UUID minionId) {
+        if(openGuis.containsKey(ownerId)) {
+            InventoryGUI gui = openGuis.get(ownerId);
+            if(!(gui instanceof MinionMenu minionMenu)) return;
+            if(!minionMenu.getMinionId().equals(minionId)) return;
+            minionMenu.update();
+            Objects.requireNonNull(Bukkit.getPlayer(ownerId)).updateInventory();
+        }
     }
 
     @EventHandler
@@ -67,12 +93,24 @@ public final class InventoryManager implements Listener {
         InventoryGUI gui = openGuis.get(player.getUniqueId());
         if (gui == null) return;
 
-        // If the closed inventory is the tracked GUI and not due to opening other inventories, treat as accidental close
-        if (event.getView().getTopInventory().getHolder() == gui && event.getReason() != InventoryCloseEvent.Reason.OPEN_NEW) {
-            if (gui instanceof InventoryGUI.AutoCloseHandler handler) {
-                handler.onClosedByPlayer(player);
+        // If the closed inventory is the tracked GUI
+        if (event.getView().getTopInventory().getHolder() == gui) {
+            // Check if this is an intentional navigation (opening a new GUI)
+            // If the player is opening a new GUI, it will be handled by openFor
+            // We should only treat it as accidental if reason is not OPEN_NEW
+            // and the player doesn't have another GUI queued up
+            if (event.getReason() != InventoryCloseEvent.Reason.OPEN_NEW) {
+                // Small delay to check if another GUI is being opened
+                Bukkit.getScheduler().runTask(providerPlugin, () -> {
+                    // If still the same GUI (no new GUI was opened), treat as accidental close
+                    if (openGuis.get(player.getUniqueId()) == gui) {
+                        if (gui instanceof InventoryGUI.AutoCloseHandler handler) {
+                            handler.onClosedByPlayer(player);
+                        }
+                        openGuis.remove(player.getUniqueId());
+                    }
+                });
             }
-            openGuis.remove(player.getUniqueId());
         }
     }
 }

+ 6 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/gui/PlayerHeadListGUI.java

@@ -38,6 +38,7 @@ public class PlayerHeadListGUI extends InventoryGUI {
         fillBackground(Material.GRAY_STAINED_GLASS_PANE, " ");
     }
 
+
     private ItemStack createPlayerHead(Player player) {
         ItemStack head = new ItemStack(Material.PLAYER_HEAD);
         SkullMeta meta = (SkullMeta) head.getItemMeta();
@@ -46,4 +47,9 @@ public class PlayerHeadListGUI extends InventoryGUI {
         head.setItemMeta(meta);
         return head;
     }
+
+    @Override
+    public void update() {
+
+    }
 }

+ 25 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/lang/RomanNumbers.java

@@ -0,0 +1,25 @@
+package me.lethunderhawk.bazaarflux.util.lang;
+
+public class RomanNumbers {
+    public static String intToRoman(int num) {
+        // Arrays to hold the Roman numeral symbols and their corresponding integer values
+        String[] romanSymbols = {
+                "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"
+        };
+        int[] romanValues = {
+                1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1
+        };
+
+        StringBuilder result = new StringBuilder();
+
+        // Loop through the values to create the Roman numeral
+        for (int i = 0; i < romanValues.length; i++) {
+            while (num >= romanValues[i]) {
+                result.append(romanSymbols[i]);
+                num -= romanValues[i];
+            }
+        }
+
+        return result.toString();
+    }
+}

+ 12 - 4
src/main/java/me/lethunderhawk/clans/ClanManager.java

@@ -1,9 +1,10 @@
 package me.lethunderhawk.clans;
 
 import me.lethunderhawk.clans.claim.Claim;
+import me.lethunderhawk.clans.claim.ClaimManager;
 import me.lethunderhawk.clans.rules.Rule;
-import me.lethunderhawk.main.Main;
 import org.bukkit.Bukkit;
+import org.bukkit.Location;
 import org.bukkit.configuration.ConfigurationSection;
 import org.bukkit.configuration.file.FileConfiguration;
 import org.bukkit.configuration.file.YamlConfiguration;
@@ -21,10 +22,11 @@ public class ClanManager {
     private final HashMap<UUID, Clan> clans = new HashMap<>();
 
     private final File clansFile;
+    private final ClaimManager claimManager;
     private FileConfiguration clansConfig;
 
-    public ClanManager(JavaPlugin plugin) {
-
+    public ClanManager(JavaPlugin plugin, ClaimManager claimManager) {
+        this.claimManager = claimManager;
         this.clansFile = new File(plugin.getDataFolder(), "clans.yml");
         this.clansConfig = YamlConfiguration.loadConfiguration(clansFile);
         loadFile(plugin);
@@ -89,7 +91,7 @@ public class ClanManager {
                 Claim claim = Claim.fromMap(clan.getId(), map);
                 if (claim != null){
                     clan.addClaim(claim);
-                    Main.getInstance().getClanModule().getClaimManager().registerClaim(claim);
+                    claimManager.registerClaim(claim);
                 }else{
                     Bukkit.getLogger().warning("Clan " + id + " has invalid Claim");
                 }
@@ -191,4 +193,10 @@ public class ClanManager {
         if(clan == null || getMyClan(player) != null) return false;
         return clan.joinRequest(player);
     }
+
+    public String getClaimNameByLocation(Location location) {
+        Claim claim = claimManager.getClaimAt(location);
+        if(claim == null) return "Unknown";
+        return claim.getName();
+    }
 }

+ 1 - 1
src/main/java/me/lethunderhawk/clans/ClanModule.java

@@ -20,7 +20,7 @@ public class ClanModule {
     public void onEnable(){
 
         claimManager = new ClaimManager();
-        clanManager = new ClanManager(Main.getInstance());
+        clanManager = new ClanManager(Main.getInstance(), claimManager);
 
         Main.getInstance().getCommand("clan").setExecutor(new ClanCommand(clanManager));
         Main.getInstance().getCommand("clan").setTabCompleter(new ClanCommand(clanManager));

+ 14 - 0
src/main/java/me/lethunderhawk/clans/claim/Claim.java

@@ -1,5 +1,7 @@
 package me.lethunderhawk.clans.claim;
 
+import me.lethunderhawk.clans.Clan;
+import me.lethunderhawk.main.Main;
 import org.bukkit.Location;
 
 import java.util.Map;
@@ -12,6 +14,7 @@ public class Claim {
 
     private final int minX, maxX;
     private final int minZ, maxZ;
+    private String name;
 
     public Claim(UUID clanId, String world, int x1, int x2, int z1, int z2) {
         this.clanId = clanId;
@@ -72,4 +75,15 @@ public class Claim {
     public int getMaxZ() {
         return maxZ;
     }
+    public void setName(String name) {
+        this.name = name;
+    }
+    public String getName() {
+        if(name == null || name.isEmpty()) {
+            Clan clan = Main.getInstance().getClanModule().getClanManager().getClanById(clanId);
+            if(clan == null) return "Unknown";
+            else return clan.getName();
+        }
+        return name;
+    }
 }

+ 4 - 0
src/main/java/me/lethunderhawk/clans/gui/ClanGUI.java

@@ -45,4 +45,8 @@ public class ClanGUI extends InventoryGUI {
         open(player);
     }
 
+    @Override
+    public void update() {
+
+    }
 }

+ 3 - 1
src/main/java/me/lethunderhawk/clans/placeholder/ClanPlaceHolder.java

@@ -26,7 +26,7 @@ public class ClanPlaceHolder extends PlaceholderExpansion {
 
     @Override
     public @NotNull String getVersion() {
-        return "1.0";
+        return "1.1";
     }
     @Override
     public String onPlaceholderRequest(Player p, String identifier) {
@@ -34,6 +34,8 @@ public class ClanPlaceHolder extends PlaceholderExpansion {
             Clan clan = manager.getMyClan(p.getUniqueId());
             if(clan == null) return "/";
             return manager.getMyClan(p.getUniqueId()).getName();
+        }else if (identifier.equalsIgnoreCase("region") && p != null) {
+            return manager.getClaimNameByLocation(p.getLocation());
         }
         return null;
     }

+ 1 - 1
src/main/java/me/lethunderhawk/custom/item/CustomItemModule.java

@@ -28,7 +28,7 @@ public class CustomItemModule implements BazaarFluxModule {
     }
 
     @Override
-    public void onEnable() {
+    public void onEnable(){
         itemManager = new CustomItemManager(plugin);
         registerCustomItems();
         CustomItemCommand customItemCommand = new CustomItemCommand(itemManager);

+ 2 - 2
src/main/java/me/lethunderhawk/custom/item/command/CustomItemCommand.java

@@ -68,7 +68,7 @@ public class CustomItemCommand implements CommandExecutor, TabCompleter {
         }
 
         if (targetNode == null) {
-            EconomyModule.sendText(sender,"§cUnknown command. Use /eco help for available commands.");
+            EconomyModule.sendText(sender,"§cUnknown command. Use /customItem help for available commands.");
             return true;
         }
 
@@ -86,7 +86,7 @@ public class CustomItemCommand implements CommandExecutor, TabCompleter {
     private void sendHelp(CommandSender sender) {
         sender.sendMessage("§6=== Custom Item Commands ===");
         for (CommandNode cmd : rootCommand.getSubCommands()) {
-            sender.sendMessage("§e/eco " + cmd.getName() + " §7- " + cmd.getDescription());
+            sender.sendMessage("§e/customItem " + cmd.getName() + " §7- " + cmd.getDescription());
         }
     }
 

+ 3 - 2
src/main/java/me/lethunderhawk/economy/EconomyModule.java

@@ -20,9 +20,10 @@ public class EconomyModule {
     private EconomyAPI economyAPI;
     private ScoreboardManager scoreboardManager;
     private JavaPlugin plugin;
-
-    public void onEnable(JavaPlugin plugin) {
+    public EconomyModule(JavaPlugin plugin) {
         this.plugin = plugin;
+    }
+    public void onEnable() {
         instance = this;
 
         plugin.saveDefaultConfig();

+ 0 - 4
src/main/java/me/lethunderhawk/economy/api/EconomyAPI.java

@@ -2,7 +2,6 @@ package me.lethunderhawk.economy.api;
 
 import me.lethunderhawk.economy.currency.EconomyManager;
 import me.lethunderhawk.economy.util.DayTime;
-import org.jetbrains.annotations.NotNull;
 
 import java.util.UUID;
 
@@ -37,7 +36,4 @@ public class EconomyAPI {
         return DayTime.formatMcTime(eco.getTime());
     }
 
-    public String getRegion(@NotNull UUID uniqueId) {
-        return eco.getRegion();
-    }
 }

+ 0 - 2
src/main/java/me/lethunderhawk/economy/api/EconomyPlaceholder.java

@@ -32,8 +32,6 @@ public class EconomyPlaceholder extends PlaceholderExpansion {
             return String.valueOf(api.getMoney(p.getUniqueId()));
         }else if (identifier.equalsIgnoreCase("formattedTime") && p != null) {
             return String.valueOf(api.getFormattedTime());
-        }else if (identifier.equalsIgnoreCase("region") && p != null) {
-            return String.valueOf(api.getRegion(p.getUniqueId()));
         }
         return null;
     }

+ 8 - 5
src/main/java/me/lethunderhawk/main/Main.java

@@ -5,12 +5,14 @@ import me.lethunderhawk.clans.ClanModule;
 import me.lethunderhawk.custom.item.CustomItemModule;
 import me.lethunderhawk.custom.item.manager.CustomItemManager;
 import me.lethunderhawk.economy.EconomyModule;
+import me.lethunderhawk.minion.MinionModule;
 import me.lethunderhawk.tradeplugin.TradeModule;
 import org.bukkit.plugin.java.JavaPlugin;
 import org.jetbrains.annotations.NotNull;
 
 public class Main extends JavaPlugin {
     private static Main instance;
+    public boolean isEnchantedItemCraftingEventRegistered;
     private ClanModule clanModule;
     private CustomItemModule customItemModule;
 
@@ -26,16 +28,17 @@ public class Main extends JavaPlugin {
         customItemModule = new CustomItemModule(this);
         customItemModule.onEnable();
 
-        TradeModule tradeModule = new TradeModule();
-        tradeModule.onEnable(this);
+        TradeModule tradeModule = new TradeModule(this);
+        tradeModule.onEnable();
 
         clanModule = new ClanModule();
         clanModule.onEnable();
 
-        EconomyModule economyModule = new EconomyModule();
-        economyModule.onEnable(this);
-
+        EconomyModule economyModule = new EconomyModule(this);
+        economyModule.onEnable();
 
+        MinionModule minionModule = new MinionModule(this);
+        minionModule.onEnable();
     }
 
 

+ 0 - 10
src/main/java/me/lethunderhawk/main/util/ItalicDeco.java

@@ -1,10 +0,0 @@
-package me.lethunderhawk.main.util;
-
-import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.format.TextDecoration;
-
-public class ItalicDeco {
-    public static Component remove(Component component){
-        return component.decoration(TextDecoration.ITALIC, false);
-    }
-}

+ 36 - 0
src/main/java/me/lethunderhawk/main/util/UnItalic.java

@@ -0,0 +1,36 @@
+package me.lethunderhawk.main.util;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class UnItalic {
+    public static Component removeItalic(Component component){
+        return component.decoration(TextDecoration.ITALIC, false);
+    }
+    public static List<Component> removeItalic(List<Component> components){
+        List<Component> newComponents = new ArrayList<>();
+        for(Component component : components){
+            newComponents.add(component.decoration(TextDecoration.ITALIC, false));
+        }
+        return newComponents;
+    }
+    public static Component text(String text){
+        return removeItalic(Component.text(text, NamedTextColor.GRAY));
+    }
+
+    /**
+     *
+     * @param itemMeta The {@link ItemMeta} you want to remove the Italic from
+     * @return The {@link ItemMeta} with Italic style removed from lore and displayName
+     */
+    public static ItemMeta removeItalicFromMeta(ItemMeta itemMeta) {
+        if(itemMeta.hasLore()) itemMeta.lore(removeItalic(itemMeta.lore()));
+        if(itemMeta.hasDisplayName()) itemMeta.displayName(removeItalic(itemMeta.displayName()));
+        return itemMeta;
+    }
+}

+ 137 - 0
src/main/java/me/lethunderhawk/minion/MinionModule.java

@@ -0,0 +1,137 @@
+package me.lethunderhawk.minion;
+
+import me.lethunderhawk.bazaarflux.util.interfaces.BazaarFluxModule;
+import me.lethunderhawk.main.Main;
+import me.lethunderhawk.minion.command.MinionCommand;
+import me.lethunderhawk.minion.enchantedVariant.item.EnchantedCobblestone;
+import me.lethunderhawk.minion.enchantedVariant.item.EnchantedRedstone;
+import me.lethunderhawk.minion.enchantedVariant.item.abstraction.DisableEnchantedItemPlacingListener;
+import me.lethunderhawk.minion.enchantedVariant.item.abstraction.EnchantedItem;
+import me.lethunderhawk.minion.enchantedVariant.item.abstraction.EnchantedItemRegistry;
+import me.lethunderhawk.minion.enchantedVariant.recipe.RecipeManager;
+import me.lethunderhawk.minion.factory.MinionFactory;
+import me.lethunderhawk.minion.impl.type.CobbleMinionType;
+import me.lethunderhawk.minion.impl.type.RedstoneMinionType;
+import me.lethunderhawk.minion.listener.MinionListener;
+import me.lethunderhawk.minion.manager.MinionManager;
+import me.lethunderhawk.minion.persistence.MinionStorage;
+import me.lethunderhawk.minion.registry.MinionRegistry;
+import me.lethunderhawk.minion.runtime.PlacedMinion;
+import org.bukkit.Material;
+import org.bukkit.event.HandlerList;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MinionModule implements BazaarFluxModule {
+
+    private final Main plugin;
+    private List<ItemStack> allMinions;
+    private MinionListener minionListener;
+    private MinionFactory factory;
+    private MinionStorage storage;
+    private RecipeManager recipeManager;
+
+    public MinionModule(Main plugin) {
+        this.plugin = plugin;
+    }
+
+    @Override
+    public String getPrefix() {
+        return "[Minions]";
+    }
+
+    @Override
+    public void onEnable() {
+        allMinions = new ArrayList<>();
+        customRecipes();
+
+        MinionCommand cmd = new MinionCommand(this);
+        plugin.getServer().getPluginCommand("minion").setExecutor(cmd);
+        plugin.getServer().getPluginCommand("minion").setTabCompleter(cmd);
+        this.factory = new MinionFactory(plugin);
+        registerMinions();
+
+        loadAllMinions();
+
+        minionListener = new MinionListener(plugin, factory.getMinionItem());
+        plugin.getServer().getPluginManager().registerEvents(minionListener, plugin);
+    }
+
+    private void customRecipes() {
+
+        this.recipeManager = new RecipeManager(Main.getInstance());
+        plugin.getServer().getPluginManager().registerEvents(new DisableEnchantedItemPlacingListener(), plugin);
+
+        registerRecipes();
+        clearRecipes();
+    }
+    private void registerRecipes() {
+        EnchantedItem enchantedCobblestone = new EnchantedCobblestone();
+        recipeManager.registerRecipe(
+                Material.COBBLESTONE,
+                enchantedCobblestone.getItemStack()
+        );
+        EnchantedItemRegistry.registerNoPlaceItem(enchantedCobblestone.getItemIdKey());
+        EnchantedItem enchantedRedstone = new EnchantedRedstone();
+        recipeManager.registerRecipe(
+                Material.REDSTONE,
+                enchantedRedstone.getItemStack()
+        );
+        EnchantedItemRegistry.registerNoPlaceItem(enchantedRedstone.getItemIdKey());
+    }
+    public void clearRecipes(){
+
+    }
+    public void loadAllMinions(){
+        storage = new MinionStorage(plugin);
+        List<PlacedMinion> minions = storage.loadAll();
+        MinionManager.startAndRegisterAll(plugin, minions);
+    }
+
+
+    public List<ItemStack> getAllMinions(){
+        return allMinions;
+    }
+    public void registerMinions(){
+        registerCobbleStoneMinions();
+        registerRedstoneMinions();
+    }
+
+    private void registerRedstoneMinions() {
+        MinionRegistry.register(new RedstoneMinionType());
+        List<ItemStack> heads = factory.createItems("redstone");
+        allMinions.addAll(heads);
+    }
+
+    public void registerCobbleStoneMinions(){
+        MinionRegistry.register(new CobbleMinionType());
+        List<ItemStack> heads = factory.createItems("cobblestone");
+        allMinions.addAll(heads);
+    }
+
+
+    public void reload(){
+        plugin.getLogger().info("Reloading Minions");
+        onDisable();
+        onEnable();
+    }
+
+    @Override
+    public void onDisable() {
+        clearRecipes();
+        unregisterListeners();
+        saveAllMinions();
+
+    }
+
+    private void saveAllMinions() {
+        storage.saveAllMinionsToStorage();
+    }
+
+    public void unregisterListeners() {
+        HandlerList.unregisterAll(minionListener);
+        minionListener = null;
+    }
+}

+ 11 - 0
src/main/java/me/lethunderhawk/minion/api/MinionBehavior.java

@@ -0,0 +1,11 @@
+package me.lethunderhawk.minion.api;
+
+import me.lethunderhawk.minion.runtime.PlacedMinion;
+
+public interface MinionBehavior {
+
+    /**
+     * Called every action cycle (based on level speed).
+     */
+    void performAction(PlacedMinion minion);
+}

+ 94 - 0
src/main/java/me/lethunderhawk/minion/api/MinionLevel.java

@@ -0,0 +1,94 @@
+package me.lethunderhawk.minion.api;
+
+import me.lethunderhawk.main.Main;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MinionLevel {
+    private MinionLevelTable cachedTable;
+    private final String fileName;
+
+    public MinionLevel(String fileName){
+        this.fileName = fileName;
+    }
+
+    public MinionLevelTable create() {
+
+        JavaPlugin plugin = Main.getInstance();
+        // Return cached version if available
+
+        Map<Integer, MinionLevelData> levelDataMap = new HashMap<>();
+
+        // Get or create config file
+        File configFile = new File(plugin.getDataFolder(), "minions/" + fileName);
+
+        // If config doesn't exist, save default from resources
+        if (!configFile.exists()) {
+            plugin.saveResource("minions/" + fileName, false);
+        }
+
+        // Load configuration
+        FileConfiguration config = YamlConfiguration.loadConfiguration(configFile);
+
+        // Load all levels
+        for (String key : config.getKeys(false)) {
+            if (!config.isConfigurationSection(key)) {
+                continue; // Skip if it's not a section
+            }
+
+            try {
+                int level = Integer.parseInt(key);
+
+                // Get values with defaults
+                double actionTime = config.getDouble(key + ".actionTime");
+                int storageSize = config.getInt(key + ".storageSize");
+                String customHeadValue = config.getString(key + ".customHeadValue");
+
+                // Validate data
+                if (actionTime <= 0) {
+                    plugin.getLogger().warning("Invalid actionTime for level " + level + ": " + actionTime);
+                    continue;
+                }
+
+                if (storageSize <= 0) {
+                    plugin.getLogger().warning("Invalid storageSize for level " + level + ": " + storageSize);
+                    continue;
+                }
+
+                if (customHeadValue == null || customHeadValue.isEmpty()) {
+                    plugin.getLogger().warning("Missing customHeadValue for level " + level);
+                    continue;
+                }
+
+                // Convert to ticks
+                long actionIntervalTicks = (long) (actionTime * 20L);
+
+                // Create MinionLevelData
+                MinionLevelData data = new MinionLevelData(
+                        level,
+                        actionIntervalTicks,
+                        storageSize,
+                        customHeadValue
+                );
+
+                levelDataMap.put(level, data);
+
+            } catch (NumberFormatException e) {
+                plugin.getLogger().warning("Invalid level in config: " + key);
+            }
+        }
+
+        cachedTable = new MinionLevelTable(levelDataMap);
+        return cachedTable;
+    }
+
+    public void reload() {
+        cachedTable = null;
+        create();
+    }
+}

+ 16 - 0
src/main/java/me/lethunderhawk/minion/api/MinionLevelData.java

@@ -0,0 +1,16 @@
+package me.lethunderhawk.minion.api;
+
+public record MinionLevelData(
+        int level,
+        long actionIntervalTicks,
+        int storageSize,
+        String headTexture
+) {
+    public String getTimeBetweenActions(){
+        return actionIntervalTicks/20 + "s";
+    }
+
+    public int getMaxStorageItems() {
+        return storageSize*64;
+    }
+}

+ 26 - 0
src/main/java/me/lethunderhawk/minion/api/MinionLevelTable.java

@@ -0,0 +1,26 @@
+package me.lethunderhawk.minion.api;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class MinionLevelTable {
+
+    private final Map<Integer, MinionLevelData> levels;
+
+    public MinionLevelTable(Map<Integer, MinionLevelData> levels) {
+        this.levels = levels;
+    }
+
+    public MinionLevelData get(int level) {
+        return levels.get(level);
+    }
+
+    public List<MinionLevelData> getAllLevels() {
+        return new ArrayList<>(levels.values());
+    }
+
+    public int getMaxLevel() {
+        return levels.keySet().stream().max(Integer::compareTo).orElse(1);
+    }
+}

+ 23 - 0
src/main/java/me/lethunderhawk/minion/api/MinionType.java

@@ -0,0 +1,23 @@
+package me.lethunderhawk.minion.api;
+
+import org.bukkit.Color;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.List;
+
+public interface MinionType {
+
+    String getId();
+
+    String getName();
+
+    ItemStack getProducedItem();
+
+    MinionBehavior getBehavior();
+
+    MinionLevelTable getLevelTable();
+
+    List<MinionLevelTable> getAllLevelTables();
+
+    Color getArmorColor();
+}

+ 6 - 0
src/main/java/me/lethunderhawk/minion/api/NextActionState.java

@@ -0,0 +1,6 @@
+package me.lethunderhawk.minion.api;
+
+public enum NextActionState {
+    PLACING,
+    BREAKING
+}

+ 175 - 0
src/main/java/me/lethunderhawk/minion/command/MinionCommand.java

@@ -0,0 +1,175 @@
+package me.lethunderhawk.minion.command;
+
+import me.lethunderhawk.bazaarflux.util.command.CommandNode;
+import me.lethunderhawk.bazaarflux.util.gui.InventoryManager;
+import me.lethunderhawk.clans.ClanModule;
+import me.lethunderhawk.minion.MinionModule;
+import me.lethunderhawk.minion.manager.MinionManager;
+import me.lethunderhawk.minion.ui.minionList.MinionLevelListMenu;
+import me.lethunderhawk.minion.ui.minionList.MinionListMenu;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+public class MinionCommand implements CommandExecutor, TabCompleter {
+
+    private final CommandNode rootCommand;
+    private final MinionModule module;
+
+    public MinionCommand(MinionModule minionModule) {
+        this.module = minionModule;
+        this.rootCommand = new CommandNode("clan", "Main clan command", null);
+        setupDefaultCommands();
+    }
+
+    private void setupDefaultCommands() {
+        registerCommand("all", "get all minions", this::getAll);
+        registerCommand("reload", "reload", this::reload);
+        registerCommand("removeAll", "remove all minions", this::removeAll);
+
+        CommandNode menu = registerCommand("menu", "See all minion types in a GUI", this::showMenu);
+            CommandNode cobble = new CommandNode("cobblestone", "See all cobblestone minions", this::showCobbleMenu);
+
+        menu.addSubCommands(cobble);
+    }
+
+    private void showCobbleMenu(CommandSender sender, String[] strings) {
+        if(sender instanceof Player p) {
+            InventoryManager.openFor(p, new MinionLevelListMenu("cobblestone"));
+        }
+    }
+
+    private void showMenu(CommandSender sender, String[] strings) {
+        if(strings.length == 0 && sender instanceof Player p) {
+            InventoryManager.openFor(p, new MinionListMenu());
+        }
+    }
+
+    private void removeAll(CommandSender sender, String[] strings) {
+        MinionManager.stopAndUnregisterAll();
+    }
+
+    private void reload(CommandSender sender, String[] strings) {
+        module.reload();
+    }
+
+    private void getAll(CommandSender sender, String[] strings) {
+        if(!(sender instanceof Player p)) return;
+        for(ItemStack minion : module.getAllMinions()){
+            p.getInventory().addItem(minion);
+        }
+    }
+
+    // Helper method to easily register commands
+    public CommandNode registerCommand(String name, String description, BiConsumer<CommandSender, String[]> executor) {
+        return rootCommand.registerSubCommand(name, description, executor);
+    }
+
+    @Override
+    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+        if (args.length == 0) {
+            sendHelp(sender);
+            return true;
+        }
+
+        List<String> argList = new ArrayList<>(Arrays.asList(args));
+        CommandNode currentNode = rootCommand;
+        CommandNode targetNode = null;
+
+        // Traverse the command tree
+        while (!argList.isEmpty()) {
+            String nextArg = argList.get(0);
+            CommandNode nextNode = currentNode.getSubCommand(nextArg);
+
+            if (nextNode == null) {
+                // No matching subcommand found, use current node if it has an executor
+                targetNode = currentNode.getExecutor() != null ? currentNode : null;
+                break;
+            }
+
+            currentNode = nextNode;
+            argList.remove(0);
+
+            // If this is the last argument or node has no further subcommands
+            if (argList.isEmpty() || nextNode.getSubCommands().isEmpty()) {
+                targetNode = nextNode;
+                break;
+            }
+        }
+
+        if (targetNode == null) {
+            ClanModule.sendText(sender,"§cUnknown command. Use /minion help for available commands.");
+            return true;
+        }
+
+        if (targetNode.getExecutor() == null) {
+            ClanModule.sendText(sender,"§cThis command requires additional arguments.");
+            sendSubCommands(sender, targetNode);
+            return true;
+        }
+
+        // Execute the command with remaining arguments
+        String[] remainingArgs = argList.toArray(new String[0]);
+        targetNode.getExecutor().accept(sender, remainingArgs);
+        return true;
+    }
+
+    @Override
+    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
+        List<String> suggestions = new ArrayList<>();
+
+        if (args.length == 0) {
+            return suggestions;
+        }
+
+        // Start at root and traverse
+        CommandNode currentNode = rootCommand;
+        List<String> argList = new ArrayList<>(Arrays.asList(args));
+        String lastArg = args[args.length - 1].toLowerCase();
+
+        // Try to traverse as far as possible
+        for (int i = 0; i < argList.size() - 1; i++) {
+            String arg = argList.get(i);
+            CommandNode nextNode = currentNode.getSubCommand(arg);
+            if (nextNode == null) {
+                break;
+            }
+            currentNode = nextNode;
+        }
+
+        // Get suggestions from current node
+        if (currentNode.getTabCompleter() != null) {
+            suggestions.addAll(currentNode.getTabCompleter().apply(sender, args));
+        } else {
+            // Suggest subcommands
+            suggestions.addAll(currentNode.getSubCommandNames().stream()
+                    .filter(name -> name.toLowerCase().startsWith(lastArg))
+                    .collect(Collectors.toList()));
+        }
+
+        return suggestions;
+    }
+
+    private void sendHelp(CommandSender sender) {
+        sender.sendMessage("§6=== Minion Commands ===");
+        for (CommandNode cmd : rootCommand.getSubCommands()) {
+            sender.sendMessage("§e/minion " + cmd.getName() + " §7- " + cmd.getDescription());
+        }
+    }
+
+    private void sendSubCommands(CommandSender sender, CommandNode node) {
+        sender.sendMessage("§6Available subcommands:");
+        for (CommandNode subCmd : node.getSubCommands()) {
+            sender.sendMessage("§e" + subCmd.getName() + " §7- " + subCmd.getDescription());
+        }
+    }
+}

+ 30 - 0
src/main/java/me/lethunderhawk/minion/enchantedVariant/item/EnchantedCobblestone.java

@@ -0,0 +1,30 @@
+package me.lethunderhawk.minion.enchantedVariant.item;
+
+import me.lethunderhawk.minion.enchantedVariant.item.abstraction.EnchantedItem;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.List;
+
+public class EnchantedCobblestone extends EnchantedItem {
+
+    public EnchantedCobblestone() {
+        super("enchanted_cobblestone", Component.text("Enchanted Cobblestone", NamedTextColor.GREEN),
+                List.of(
+                        Component.text("Collection Item", NamedTextColor.GRAY),
+                        Component.text(""),
+                        Component.text("UNCOMMON BLOCK", NamedTextColor.GREEN)
+                ),
+                Material.COBBLESTONE);
+    }
+
+    @Override
+    protected void applyCustomizations(ItemStack item) {
+        ItemMeta meta = item.getItemMeta();
+        meta.setEnchantmentGlintOverride(true);
+        item.setItemMeta(meta);
+    }
+}

+ 30 - 0
src/main/java/me/lethunderhawk/minion/enchantedVariant/item/EnchantedRedstone.java

@@ -0,0 +1,30 @@
+package me.lethunderhawk.minion.enchantedVariant.item;
+
+import me.lethunderhawk.minion.enchantedVariant.item.abstraction.EnchantedItem;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.List;
+
+public class EnchantedRedstone extends EnchantedItem {
+
+    public EnchantedRedstone() {
+        super("enchanted_redstone", Component.text("Enchanted Redstone", NamedTextColor.GREEN),
+                List.of(
+                        Component.text("Collection Item", NamedTextColor.GRAY),
+                        Component.text(""),
+                        Component.text("UNCOMMON BLOCK", NamedTextColor.GREEN)
+                ),
+                Material.REDSTONE);
+    }
+
+    @Override
+    protected void applyCustomizations(ItemStack item) {
+        ItemMeta meta = item.getItemMeta();
+        meta.setEnchantmentGlintOverride(true);
+        item.setItemMeta(meta);
+    }
+}

+ 18 - 0
src/main/java/me/lethunderhawk/minion/enchantedVariant/item/abstraction/DisableEnchantedItemPlacingListener.java

@@ -0,0 +1,18 @@
+package me.lethunderhawk.minion.enchantedVariant.item.abstraction;
+
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockPlaceEvent;
+import org.bukkit.inventory.ItemStack;
+
+public final class DisableEnchantedItemPlacingListener implements Listener {
+
+    @EventHandler(ignoreCancelled = true)
+    public void onBlockPlace(BlockPlaceEvent event) {
+        ItemStack item = event.getItemInHand();
+
+        if (EnchantedItemRegistry.isNoPlaceItem(item)) {
+            event.setCancelled(true);
+        }
+    }
+}

+ 106 - 0
src/main/java/me/lethunderhawk/minion/enchantedVariant/item/abstraction/EnchantedItem.java

@@ -0,0 +1,106 @@
+package me.lethunderhawk.minion.enchantedVariant.item.abstraction;
+
+import me.lethunderhawk.main.Main;
+import me.lethunderhawk.main.util.UnItalic;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataType;
+
+import java.util.List;
+
+/**
+ * Abstract base class for all enchanted items
+ */
+public abstract class EnchantedItem {
+    protected final Component displayName;
+    protected final List<Component> lore;
+    protected final Material baseMaterial;
+
+    // Persistent Data Container key for storing item ID
+    protected final NamespacedKey itemIdKey;
+    private final String itemId;
+    private final ItemStack itemStack;
+
+    public EnchantedItem(String itemId, Component displayName, List<Component> lore, Material baseMaterial) {
+        this.displayName = displayName;
+        this.lore = lore;
+        this.baseMaterial = baseMaterial;
+        this.itemId = itemId;
+        this.itemIdKey = new NamespacedKey(Main.getInstance(), itemId);
+        this.itemStack = createItem();
+    }
+
+    public ItemStack getItemStack() {
+        return itemStack;
+    }
+    /**
+     * Create the ItemStack with embedded metadata
+     */
+    private ItemStack createItem() {
+        ItemStack item = new ItemStack(baseMaterial);
+        ItemMeta meta = item.getItemMeta();
+
+        // Set visible properties
+        meta.displayName(displayName);
+        if (lore != null && !lore.isEmpty()) {
+            meta.lore(lore);
+        }
+
+        // Embed the unique identifier in Persistent Data Container
+        meta.getPersistentDataContainer().set(
+                itemIdKey,
+                PersistentDataType.STRING,
+                itemId
+        );
+        //meta.setHideTooltip(true);
+        meta.addItemFlags(ItemFlag.HIDE_PLACED_ON);
+        item.setItemMeta(UnItalic.removeItalicFromMeta(meta));
+        // Apply any additional customizations
+        applyCustomizations(item);
+        return item;
+    }
+
+    /**
+     * Check if an ItemStack is an instance of this custom item
+     */
+    public boolean isEnchantedItem(ItemStack item) {
+        if (item == null || !item.hasItemMeta()) return false;
+
+        ItemMeta meta = item.getItemMeta();
+        String storedId = meta.getPersistentDataContainer().get(itemIdKey, PersistentDataType.STRING);
+
+        return itemId.equals(storedId);
+    }
+
+
+    /**
+     * Get the custom item ID from an ItemStack
+     */
+    public static String getEnchantedItemId(ItemStack item, NamespacedKey key) {
+        if (item == null || !item.hasItemMeta()) return null;
+
+        ItemMeta meta = item.getItemMeta();
+        return meta.getPersistentDataContainer().get(key, PersistentDataType.STRING);
+    }
+
+    /**
+     * Template method for subclasses to add additional customizations
+     */
+    protected void applyCustomizations(ItemStack item) {
+
+    }
+
+    // Getters
+    public String getItemId() { return itemId; }
+    public Component getDisplayName() { return displayName; }
+    public Material getBaseMaterial() { return baseMaterial; }
+    public NamespacedKey getItemIdKey() { return itemIdKey; }
+
+    public String getName() {
+        return  displayName.toString();
+    }
+}

+ 36 - 0
src/main/java/me/lethunderhawk/minion/enchantedVariant/item/abstraction/EnchantedItemRegistry.java

@@ -0,0 +1,36 @@
+package me.lethunderhawk.minion.enchantedVariant.item.abstraction;
+
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public final class EnchantedItemRegistry {
+
+    private EnchantedItemRegistry() {}
+
+    // Items that are NOT allowed to be placed
+    private static final Set<NamespacedKey> NO_PLACE_ITEMS = new HashSet<>();
+
+    public static void registerNoPlaceItem(NamespacedKey key) {
+        NO_PLACE_ITEMS.add(key);
+    }
+
+    public static boolean isNoPlaceItem(ItemStack item) {
+        if (item == null || !item.hasItemMeta()) return false;
+
+        ItemMeta meta = item.getItemMeta();
+        PersistentDataContainer pdc = meta.getPersistentDataContainer();
+
+        for (NamespacedKey key : NO_PLACE_ITEMS) {
+            if (pdc.has(key, PersistentDataType.STRING)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

+ 166 - 0
src/main/java/me/lethunderhawk/minion/enchantedVariant/recipe/EnchantedItemRecipe.java

@@ -0,0 +1,166 @@
+package me.lethunderhawk.minion.enchantedVariant.recipe;
+
+import me.lethunderhawk.main.Main;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.Recipe;
+import org.bukkit.inventory.RecipeChoice;
+import org.bukkit.inventory.ShapedRecipe;
+import org.bukkit.plugin.Plugin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class EnchantedItemRecipe implements Recipe {
+    private final NamespacedKey key;
+    private final ItemStack result;
+    private final Material ingredient;
+    private final Map<Character, RecipeChoice> ingredients = new HashMap<>();
+    private final String[] shape = {
+            "ABC",
+            "DE ",
+            "   ",
+    };
+
+    public EnchantedItemRecipe(NamespacedKey key, Material ingredient, ItemStack result) {
+        this.key = key;
+        this.ingredient = ingredient;
+        this.result = result.clone();
+        setupIngredients();
+    }
+
+    private void setupIngredients() {
+        // Set up the first 5 slots with full stack requirement
+        ingredients.put('A', new StackSizeIngredient(ingredient, 64));
+        ingredients.put('B', new StackSizeIngredient(ingredient, 64));
+        ingredients.put('C', new StackSizeIngredient(ingredient, 64));
+        ingredients.put('D', new StackSizeIngredient(ingredient, 64));
+        ingredients.put('E', new StackSizeIngredient(ingredient, 64));
+
+        // Empty slots (space character)
+        //ingredients.put(' ', new RecipeChoice.MaterialChoice(Material.AIR));
+    }
+
+    @Override
+    public ItemStack getResult() {
+        return result.clone();
+    }
+
+    public ItemStack getResult(int amount) {
+        ItemStack resultCopy = result.clone();
+        resultCopy.setAmount(amount);
+        return resultCopy;
+    }
+
+    public RecipeChoice getInputChoice() {
+        return new RecipeChoice.MaterialChoice(ingredient);
+    }
+
+    public boolean matches(ItemStack[] craftingMatrix) {
+        // Check first 5 slots (0-4)
+        for (int i = 0; i < 5; i++) {
+            ItemStack item = craftingMatrix[i];
+            if (item == null || item.getType() != ingredient) {
+                return false;
+            }
+        }
+
+        // Check remaining slots are empty (5-8)
+        for (int i = 5; i < 9; i++) {
+            if (craftingMatrix[i] != null && craftingMatrix[i].getType() != Material.AIR) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public int getOutputAmount(ItemStack[] craftingMatrix) {
+        // Check if all stacks are full (64)
+        boolean allFullStacks = true;
+        boolean allHalfStacks = true;
+
+        for (int i = 0; i < 5; i++) {
+            ItemStack item = craftingMatrix[i];
+            if (item == null) {
+                allFullStacks = false;
+                allHalfStacks = false;
+                break;
+            }
+
+            if (item.getAmount() != 64) {
+                allFullStacks = false;
+            }
+
+            if (item.getAmount() != 32) {
+                allHalfStacks = false;
+            }
+        }
+
+        if (allFullStacks) return 2;
+        if (allHalfStacks) return 1;
+        return 0;
+    }
+
+    public static void register(Material ingredient, ItemStack result) {
+        Plugin plugin = Main.getInstance(); // Replace with your plugin name
+
+        NamespacedKey key = new NamespacedKey(plugin,
+                "enchanted_" + ingredient.name().toLowerCase());
+
+        EnchantedItemRecipe recipe = new EnchantedItemRecipe(key, ingredient, result);
+
+        // Register both full stack and half stack recipes
+        registerFullStackRecipe(plugin, ingredient, result);
+        registerHalfStackRecipe(plugin, ingredient, result);
+    }
+
+    private static void registerFullStackRecipe(Plugin plugin, Material ingredient, ItemStack result) {
+        NamespacedKey fullKey = new NamespacedKey(plugin,
+                "enchanted_" + ingredient.name().toLowerCase() + "_full");
+
+        ShapedRecipe fullRecipe = new ShapedRecipe(fullKey, result);
+        fullRecipe.shape("ABC",
+                "DE ",
+                "   ");
+
+        // Full stack requirements (64)
+        fullRecipe.setIngredient('A', new StackSizeIngredient(ingredient, 64));
+        fullRecipe.setIngredient('B', new StackSizeIngredient(ingredient, 64));
+        fullRecipe.setIngredient('C', new StackSizeIngredient(ingredient, 64));
+        fullRecipe.setIngredient('D', new StackSizeIngredient(ingredient, 64));
+        fullRecipe.setIngredient('E', new StackSizeIngredient(ingredient, 64));
+
+        Bukkit.addRecipe(fullRecipe);
+    }
+
+    private static void registerHalfStackRecipe(Plugin plugin, Material ingredient, ItemStack result) {
+        ItemStack halfResult = result.clone();
+        halfResult.setAmount(1);
+
+        NamespacedKey halfKey = new NamespacedKey(plugin,
+                "enchanted_" + ingredient.name().toLowerCase() + "_half");
+
+        ShapedRecipe halfRecipe = new ShapedRecipe(halfKey, halfResult);
+        halfRecipe.shape("ABC", "DE ", "   ");
+
+        // Half stack requirements (32)
+        halfRecipe.setIngredient('A', new StackSizeIngredient(ingredient, 32));
+        halfRecipe.setIngredient('B', new StackSizeIngredient(ingredient, 32));
+        halfRecipe.setIngredient('C', new StackSizeIngredient(ingredient, 32));
+        halfRecipe.setIngredient('D', new StackSizeIngredient(ingredient, 32));
+        halfRecipe.setIngredient('E', new StackSizeIngredient(ingredient, 32));
+
+        Bukkit.addRecipe(halfRecipe);
+    }
+
+    public NamespacedKey getKey() {
+        return key;
+    }
+
+    public Material getIngredient() {
+        return ingredient;
+    }
+}

+ 150 - 0
src/main/java/me/lethunderhawk/minion/enchantedVariant/recipe/RecipeManager.java

@@ -0,0 +1,150 @@
+package me.lethunderhawk.minion.enchantedVariant.recipe;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.CraftItemEvent;
+import org.bukkit.event.inventory.PrepareItemCraftEvent;
+import org.bukkit.inventory.CraftingInventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.plugin.Plugin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class RecipeManager implements Listener {
+    private final Plugin plugin;
+    private final Map<Material, ItemStack> registeredRecipes = new HashMap<>();
+
+    public RecipeManager(Plugin plugin) {
+        this.plugin = plugin;
+        Bukkit.getPluginManager().registerEvents(this, plugin);
+    }
+
+    public void registerRecipe(Material ingredient, ItemStack result) {
+        registeredRecipes.put(ingredient, result);
+        EnchantedItemRecipe.register(ingredient, result);
+    }
+
+    @EventHandler
+    public void onCraftItem(PrepareItemCraftEvent event) {
+        CraftingInventory inventory = event.getInventory();
+        ItemStack[] matrix = inventory.getMatrix();
+
+        // Check if recipe matches any registered ingredient
+        for (Map.Entry<Material, ItemStack> entry : registeredRecipes.entrySet()) {
+            Material ingredient = entry.getKey();
+            ItemStack result = entry.getValue();
+
+            EnchantedItemRecipe recipe = new EnchantedItemRecipe(
+                    new NamespacedKey(plugin, "temp_" + ingredient.name()),
+                    ingredient,
+                    result
+            );
+
+            if (recipe.matches(matrix)) {
+                int outputAmount = recipe.getOutputAmount(matrix);
+
+                if (outputAmount > 0) {
+                    ItemStack finalResult = result.clone();
+                    finalResult.setAmount(outputAmount);
+                    inventory.setResult(finalResult);
+                } else {
+                    inventory.setResult(null);
+                }
+                return;
+            }
+        }
+
+        // If no custom recipe matches, check if it's a registered custom recipe but doesn't meet stack requirements
+        // This prevents the vanilla recipe from working with our custom ingredients
+        for (Map.Entry<Material, ItemStack> entry : registeredRecipes.entrySet()) {
+            Material ingredient = entry.getKey();
+
+            // Check if first 5 slots have our ingredient (any amount)
+            boolean hasOurIngredient = true;
+            for (int i = 0; i < 5; i++) {
+                ItemStack item = matrix[i];
+                if (item == null || item.getType() != ingredient) {
+                    hasOurIngredient = false;
+                    break;
+                }
+            }
+
+            // If our ingredient is present but doesn't meet stack requirements, cancel the result
+            if (hasOurIngredient) {
+                inventory.setResult(null);
+                return;
+            }
+        }
+    }
+
+    @EventHandler
+    public void onCraftComplete(CraftItemEvent event) {
+        if (event.isCancelled()) return;
+
+        CraftingInventory inventory = event.getInventory();
+        ItemStack[] matrix = inventory.getMatrix();
+
+        // Find matching recipe
+        for (Map.Entry<Material, ItemStack> entry : registeredRecipes.entrySet()) {
+            Material ingredient = entry.getKey();
+            ItemStack result = entry.getValue();
+
+            EnchantedItemRecipe recipe = new EnchantedItemRecipe(
+                    new NamespacedKey(plugin, "temp_" + ingredient.name()),
+                    ingredient,
+                    result
+            );
+
+            if (recipe.matches(matrix)) {
+                int outputAmount = recipe.getOutputAmount(matrix);
+
+                if (outputAmount > 0) {
+                    // Cancel the default behavior
+                    event.setCancelled(true);
+
+                    // Create the result item with correct amount
+                    ItemStack craftedResult = result.clone();
+                    craftedResult.setAmount(outputAmount);
+
+                    // Remove items from crafting grid based on stack size
+                    for (int i = 0; i < 5; i++) {
+                        ItemStack item = matrix[i];
+                        if (item != null) {
+                            int requiredAmount = (outputAmount == 2) ? 64 : 32;
+
+                            if (item.getAmount() > requiredAmount) {
+                                item.setAmount(item.getAmount() - requiredAmount);
+                                inventory.setItem(i + 1, item);
+                            } else {
+                                inventory.setItem(i + 1, new ItemStack(Material.AIR));
+                            }
+                        }
+                    }
+
+                    // Update the matrix
+                    inventory.setMatrix(new ItemStack[9]);
+
+                    // Give the result to the player
+                    Player player = (Player) event.getWhoClicked();
+                    HashMap<Integer, ItemStack> leftover = player.getInventory().addItem(craftedResult);
+
+                    // Drop leftovers if inventory is full
+                    if (!leftover.isEmpty()) {
+                        for (ItemStack left : leftover.values()) {
+                            player.getWorld().dropItem(player.getLocation(), left);
+                        }
+                    }
+
+                    // Update inventory
+                    player.updateInventory();
+                }
+                break;
+            }
+        }
+    }
+}

+ 37 - 0
src/main/java/me/lethunderhawk/minion/enchantedVariant/recipe/StackSizeIngredient.java

@@ -0,0 +1,37 @@
+package me.lethunderhawk.minion.enchantedVariant.recipe;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.RecipeChoice;
+
+public class StackSizeIngredient extends RecipeChoice.MaterialChoice {
+    private final int requiredAmount;
+    private final Material material;
+
+    public StackSizeIngredient(Material material, int requiredAmount) {
+        super(material);
+        this.material = material;
+        this.requiredAmount = requiredAmount;
+    }
+
+    @Override
+    public boolean test(ItemStack item) {
+        return super.test(item) && item.getAmount() >= requiredAmount;
+    }
+
+    @Override
+    public ItemStack getItemStack() {
+        ItemStack item = super.getItemStack();
+        item.setAmount(requiredAmount);
+        return item;
+    }
+
+    public int getRequiredAmount() {
+        return requiredAmount;
+    }
+
+    @Override
+    public StackSizeIngredient clone() {
+        return new StackSizeIngredient(material, requiredAmount);
+    }
+}

+ 54 - 0
src/main/java/me/lethunderhawk/minion/factory/MinionFactory.java

@@ -0,0 +1,54 @@
+package me.lethunderhawk.minion.factory;
+
+import me.lethunderhawk.minion.api.MinionLevelData;
+import me.lethunderhawk.minion.api.MinionType;
+import me.lethunderhawk.minion.item.MinionItem;
+import me.lethunderhawk.minion.registry.MinionRegistry;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MinionFactory {
+
+    private final MinionItem item;
+
+    public MinionFactory(JavaPlugin plugin) {
+        this.item = new MinionItem(plugin);
+    }
+
+    public ItemStack createItem(String typeId, int level) {
+        MinionType type = MinionRegistry.get(typeId);
+        if (type == null) throw new IllegalArgumentException("Unknown minion type");
+
+        var levelData = type.getLevelTable().get(level);
+        if (levelData == null) throw new IllegalArgumentException("Invalid level");
+
+        return item.create(typeId, level, levelData.headTexture());
+    }
+
+    public ItemStack createItem(MinionType type, int level) {
+        if (type == null) return null;
+
+        var levelData = type.getLevelTable().get(level);
+        if (levelData == null) return null;
+
+        return item.create(type, level, levelData.headTexture());
+    }
+    public MinionItem getMinionItem() {
+        return item;
+    }
+
+    public List<ItemStack> createItems(String typeId) {
+        MinionType type = MinionRegistry.get(typeId);
+        if (type == null) throw new IllegalArgumentException("Unknown minion type");
+
+        List<ItemStack> items = new ArrayList<>();
+        for(MinionLevelData levelData : type.getLevelTable().getAllLevels()){
+            if (levelData == null) throw new IllegalArgumentException("Invalid level");
+            items.add(item.create(typeId, levelData.level(), levelData.headTexture()));
+        }
+        return items;
+    }
+}

+ 34 - 0
src/main/java/me/lethunderhawk/minion/impl/behavior/CobbleMinionBehavior.java

@@ -0,0 +1,34 @@
+package me.lethunderhawk.minion.impl.behavior;
+
+import me.lethunderhawk.bazaarflux.util.gui.InventoryManager;
+import me.lethunderhawk.minion.api.NextActionState;
+import me.lethunderhawk.minion.api.MinionBehavior;
+import me.lethunderhawk.minion.runtime.PlacedMinion;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.Material;
+
+public class CobbleMinionBehavior implements MinionBehavior {
+    private NextActionState actionstate = NextActionState.PLACING;
+    @Override
+    public void performAction(PlacedMinion minion) {
+        ItemStack cobble = new ItemStack(Material.COBBLESTONE, 1);
+        minion.addItem(cobble);
+        InventoryManager.refreshMinionMenuForPlayer(minion.getOwnerId(), minion.getMinionId());
+    }
+    private void breakBlock(){
+        // choose a random cobblestone block
+        // look at it slowly
+        // break it
+        // add it to inv
+    }
+
+    private void place(){
+        // if all blocks around are full, go into break mode
+        // else
+        // choose random block
+        // look at it slowly
+        // place it
+
+        // optional: test for the same material when looking around
+    }
+}

+ 23 - 0
src/main/java/me/lethunderhawk/minion/impl/behavior/RedstoneMinionBehavior.java

@@ -0,0 +1,23 @@
+package me.lethunderhawk.minion.impl.behavior;
+
+import me.lethunderhawk.bazaarflux.util.gui.InventoryManager;
+import me.lethunderhawk.minion.api.MinionBehavior;
+import me.lethunderhawk.minion.runtime.PlacedMinion;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+
+public class RedstoneMinionBehavior implements MinionBehavior {
+    @Override
+    public void performAction(PlacedMinion minion) {
+        ItemStack redstone = new ItemStack(Material.REDSTONE, 1);
+        minion.addItem(redstone);
+        InventoryManager.refreshMinionMenuForPlayer(minion.getOwnerId(), minion.getMinionId());
+    }
+    private void breakBlock(){
+
+    }
+
+    private void place(){
+
+    }
+}

+ 54 - 0
src/main/java/me/lethunderhawk/minion/impl/type/CobbleMinionType.java

@@ -0,0 +1,54 @@
+package me.lethunderhawk.minion.impl.type;
+
+import me.lethunderhawk.minion.api.MinionBehavior;
+import me.lethunderhawk.minion.api.MinionLevel;
+import me.lethunderhawk.minion.api.MinionLevelTable;
+import me.lethunderhawk.minion.api.MinionType;
+import me.lethunderhawk.minion.impl.behavior.CobbleMinionBehavior;
+import org.bukkit.Color;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.List;
+
+public class CobbleMinionType implements MinionType {
+
+    private final MinionBehavior behavior = new CobbleMinionBehavior();
+    private final MinionLevelTable levelTable = new MinionLevel("cobblestone.yml").create();
+
+    @Override
+    public String getId() {
+        return "cobblestone";
+    }
+
+    @Override
+    public String getName() {
+        return "Cobblestone Minion";
+    }
+
+    @Override
+    public ItemStack getProducedItem() {
+        return new ItemStack(Material.COBBLESTONE);
+    }
+
+    @Override
+    public MinionBehavior getBehavior() {
+        return behavior;
+    }
+
+    @Override
+    public MinionLevelTable getLevelTable() {
+        return levelTable;
+    }
+
+    @Override
+    public List<MinionLevelTable> getAllLevelTables() {
+        return List.of();
+    }
+
+    @Override
+    public Color getArmorColor() {
+        int brightness = 60;
+        return Color.fromRGB(brightness,brightness,brightness);
+    }
+}

+ 52 - 0
src/main/java/me/lethunderhawk/minion/impl/type/RedstoneMinionType.java

@@ -0,0 +1,52 @@
+package me.lethunderhawk.minion.impl.type;
+
+import me.lethunderhawk.minion.api.MinionBehavior;
+import me.lethunderhawk.minion.api.MinionLevel;
+import me.lethunderhawk.minion.api.MinionLevelTable;
+import me.lethunderhawk.minion.api.MinionType;
+import me.lethunderhawk.minion.impl.behavior.RedstoneMinionBehavior;
+import org.bukkit.Color;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.List;
+
+public class RedstoneMinionType implements MinionType {
+    private final MinionBehavior behavior = new RedstoneMinionBehavior();
+    private final MinionLevelTable levelTable = new MinionLevel("redstone.yml").create();
+
+    @Override
+    public String getId() {
+        return "redstone";
+    }
+
+    @Override
+    public String getName() {
+        return "Redstone Minion";
+    }
+
+    @Override
+    public ItemStack getProducedItem() {
+        return new ItemStack(Material.REDSTONE);
+    }
+
+    @Override
+    public MinionBehavior getBehavior() {
+        return behavior;
+    }
+
+    @Override
+    public MinionLevelTable getLevelTable() {
+        return levelTable;
+    }
+
+    @Override
+    public List<MinionLevelTable> getAllLevelTables() {
+        return List.of();
+    }
+
+    @Override
+    public Color getArmorColor() {
+        return Color.fromRGB(255,25,25);
+    }
+}

+ 67 - 0
src/main/java/me/lethunderhawk/minion/item/MinionItem.java

@@ -0,0 +1,67 @@
+package me.lethunderhawk.minion.item;
+
+import me.lethunderhawk.bazaarflux.util.CustomHeadCreator;
+import me.lethunderhawk.bazaarflux.util.lang.RomanNumbers;
+import me.lethunderhawk.main.util.UnItalic;
+import me.lethunderhawk.minion.api.MinionType;
+import me.lethunderhawk.minion.registry.MinionRegistry;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataType;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MinionItem {
+
+    private final NamespacedKey typeKey;
+    private final NamespacedKey levelKey;
+
+    public MinionItem(JavaPlugin plugin) {
+        this.typeKey = new NamespacedKey(plugin, "minion_type");
+        this.levelKey = new NamespacedKey(plugin, "minion_level");
+    }
+
+    public ItemStack create(String typeId, int level, String texture) {
+        MinionType type = MinionRegistry.get(typeId);
+        return create(type, level, texture);
+    }
+    public ItemStack create(MinionType type, int level, String texture) {
+        Component minionName = UnItalic.removeItalic(Component.text(type.getName() + " " + RomanNumbers.intToRoman(level), NamedTextColor.BLUE));
+        List<Component> lore = new ArrayList<>();
+        lore.add(UnItalic.removeItalic(Component.text("Place this minion and it will start ", NamedTextColor.GRAY)));
+        lore.add(UnItalic.text("generating and mining " + type.getName() +  "!"));
+        lore.add(UnItalic.text("Requires an open area."));
+        lore.add(UnItalic.text("Minions also work when you are offline!"));
+        lore.add(Component.text(""));
+
+
+        lore.add(UnItalic.removeItalic(Component.text("Time Between Actions: ", NamedTextColor.GRAY)
+                .append(Component.text(type.getLevelTable().get(level).getTimeBetweenActions(), NamedTextColor.GREEN))));
+        lore.add(UnItalic.removeItalic(Component.text("Max Storage: ", NamedTextColor.GRAY)
+                .append(Component.text(type.getLevelTable().get(level).getMaxStorageItems(), NamedTextColor.YELLOW))));
+        //lore.add(UnItalic.removeItalic(Component.text("Generated Resources: ", NamedTextColor.GRAY).append(Component.text(type.getLevelTable().get(level).getMaxStorageItems(), NamedTextColor.YELLOW))));
+        ItemStack head = CustomHeadCreator.createCustomHead(texture, minionName, lore);
+        ItemMeta meta = head.getItemMeta();
+
+        meta.getPersistentDataContainer().set(typeKey, PersistentDataType.STRING, type.getId());
+        meta.getPersistentDataContainer().set(levelKey, PersistentDataType.INTEGER, level);
+
+        head.setItemMeta(meta);
+        return head;
+    }
+
+    public String getTypeId(ItemStack item) {
+        return item.getItemMeta().getPersistentDataContainer()
+                .get(typeKey, PersistentDataType.STRING);
+    }
+
+    public int getLevel(ItemStack item) {
+        return item.getItemMeta().getPersistentDataContainer()
+                .get(levelKey, PersistentDataType.INTEGER);
+    }
+}

+ 119 - 0
src/main/java/me/lethunderhawk/minion/listener/MinionListener.java

@@ -0,0 +1,119 @@
+package me.lethunderhawk.minion.listener;
+
+import me.lethunderhawk.bazaarflux.util.gui.InventoryManager;
+import me.lethunderhawk.minion.api.MinionType;
+import me.lethunderhawk.minion.item.MinionItem;
+import me.lethunderhawk.minion.manager.MinionManager;
+import me.lethunderhawk.minion.registry.MinionRegistry;
+import me.lethunderhawk.minion.runtime.PlacedMinion;
+import me.lethunderhawk.minion.ui.MinionMenu;
+import me.lethunderhawk.minion.ui.MinionMenuFactory;
+import org.bukkit.Location;
+import org.bukkit.entity.ArmorStand;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.Action;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+import org.bukkit.event.player.PlayerInteractAtEntityEvent;
+import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.UUID;
+
+public class MinionListener implements Listener {
+
+    private final JavaPlugin plugin;
+    private final MinionItem minionItem;
+
+    public MinionListener(JavaPlugin plugin, MinionItem item) {
+        this.plugin = plugin;
+        this.minionItem = item;
+    }
+    @EventHandler
+    public void onHit(EntityDamageByEntityEvent event) {}
+    @EventHandler
+    public void onUse(PlayerInteractEvent event) {
+        // Only main hand
+        if (event.getHand() != EquipmentSlot.HAND) return;
+
+        // Only right / left click
+        Action action = event.getAction();
+        if (action != Action.RIGHT_CLICK_BLOCK && action != Action.LEFT_CLICK_BLOCK) return;
+
+        ItemStack item = event.getItem();
+        if (item == null || !item.hasItemMeta()) return;
+
+        // Check MinionItem data
+        String typeId;
+        int level;
+        try {
+            typeId = minionItem.getTypeId(item);
+            level = minionItem.getLevel(item);
+        } catch (Exception ignored) {
+            return;
+        }
+
+        if (typeId == null) return;
+        MinionType type = MinionRegistry.get(typeId);
+        if (type == null) return;
+
+        event.setCancelled(true);
+
+        Player player = event.getPlayer();
+
+        // Placement location (top of clicked block)
+        Location placeLocation = event.getClickedBlock()
+                .getLocation()
+                .add(0.5, 1.0, 0.5);
+        if(MinionManager.isMinionAtLocation(placeLocation)){
+
+            return;
+        }
+
+
+        // Create runtime minion
+        PlacedMinion minion = new PlacedMinion(
+                UUID.randomUUID(),
+                player.getUniqueId(),
+                type,
+                level,
+                placeLocation,
+                item,
+                type.getArmorColor()
+        );
+
+        MinionManager.startAndRegister(minion, plugin);
+
+        // Consume item
+        item.setAmount(item.getAmount() - 1);
+
+        // OPTIONAL: feedback
+        player.sendMessage("§aMinion placed.");
+    }
+
+    @EventHandler
+    public void onInteractAtEntity(PlayerInteractAtEntityEvent event) {
+        if (!(event.getRightClicked() instanceof ArmorStand armorStand)) return;
+
+        // Resolve PlacedMinion
+        PlacedMinion minion = MinionManager.getByEntity(armorStand.getUniqueId());
+        if (minion == null) return; // Not a minion
+
+        event.setCancelled(true);
+
+        Player player = event.getPlayer();
+
+        // Owner check
+        if (!minion.getOwnerId().equals(player.getUniqueId())) {
+            player.sendMessage("§cYou are not the owner of this minion.");
+            return;
+        }
+
+        // Open UI
+        MinionMenu menu = MinionMenuFactory.create(minion);
+        InventoryManager.openFor(player, menu);
+    }
+}

+ 63 - 0
src/main/java/me/lethunderhawk/minion/manager/MinionManager.java

@@ -0,0 +1,63 @@
+package me.lethunderhawk.minion.manager;
+
+import me.lethunderhawk.main.Main;
+import me.lethunderhawk.minion.runtime.PlacedMinion;
+import org.bukkit.Location;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class MinionManager {
+
+    private static final Map<UUID, PlacedMinion> BY_ENTITY = new ConcurrentHashMap<>();
+
+    public static void register(PlacedMinion minion) {
+        BY_ENTITY.put(minion.getArmorStand().getUniqueId(), minion);
+    }
+
+    public static PlacedMinion getByEntity(UUID entityId) {
+        return BY_ENTITY.get(entityId);
+    }
+
+    public static void unregister(PlacedMinion minion) {
+        BY_ENTITY.remove(minion.getArmorStand().getUniqueId());
+    }
+
+    public static void stopAndUnregister(PlacedMinion minion) {
+        minion.stop();
+        unregister(minion);
+    }
+
+    public static void startAndRegister(PlacedMinion minion, JavaPlugin plugin) {
+        minion.start(plugin);
+        register(minion);
+    }
+
+    public static List<PlacedMinion> stopAndUnregisterAll() {
+        List<PlacedMinion> minions = new ArrayList<>();
+        BY_ENTITY.forEach((key, minion) -> {
+            minions.add(minion);
+            stopAndUnregister(minion);
+        });
+        return minions;
+    }
+
+    public static boolean isMinionAtLocation(Location placeLocation) {
+        for(PlacedMinion minion : BY_ENTITY.values()){
+            if(minion.getLocation().equals(placeLocation)){
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static void startAndRegisterAll(Main plugin, List<PlacedMinion> minions) {
+        for(PlacedMinion minion : minions){
+            startAndRegister(minion, plugin);
+        }
+    }
+}

+ 56 - 0
src/main/java/me/lethunderhawk/minion/persistence/MinionSerializer.java

@@ -0,0 +1,56 @@
+package me.lethunderhawk.minion.persistence;
+
+import me.lethunderhawk.minion.api.MinionType;
+import me.lethunderhawk.minion.registry.MinionRegistry;
+import me.lethunderhawk.minion.runtime.MinionInventory;
+import me.lethunderhawk.minion.runtime.PlacedMinion;
+import org.bukkit.Location;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.UUID;
+
+public class MinionSerializer {
+
+    public static void serialize(PlacedMinion minion, YamlConfiguration cfg) {
+        cfg.set("uuid", minion.getMinionId().toString());
+        cfg.set("owner", minion.getOwnerId().toString());
+        cfg.set("type", minion.getType().getId());
+        cfg.set("level", minion.getLevel());
+        cfg.set("location", minion.getLocation());
+        cfg.set("helmet", minion.getHelmetItem());
+        cfg.set("state.lastAction", minion.getState().getLastActionTimestamp());
+
+        ConfigurationSection invSec = cfg.createSection("inventory");
+        minion.getInventory().serialize(invSec);
+    }
+
+    public static PlacedMinion deserialize(YamlConfiguration cfg) {
+        UUID minionId = UUID.fromString(cfg.getString("uuid"));
+        UUID ownerId = UUID.fromString(cfg.getString("owner"));
+
+        MinionType type = MinionRegistry.get(cfg.getString("type"));
+        int level = cfg.getInt("level");
+
+        Location location = cfg.getLocation("location");
+        ItemStack helmet = cfg.getItemStack("helmet");
+
+        PlacedMinion minion = new PlacedMinion(
+                minionId,
+                ownerId,
+                type,
+                level,
+                location,
+                helmet,
+                type.getArmorColor()
+        );
+
+        ConfigurationSection invSec = cfg.getConfigurationSection("inventory");
+        if (invSec != null) {
+            minion.setInventory(MinionInventory.deserialize(invSec));
+        }
+
+        return minion;
+    }
+}

+ 92 - 0
src/main/java/me/lethunderhawk/minion/persistence/MinionStorage.java

@@ -0,0 +1,92 @@
+package me.lethunderhawk.minion.persistence;
+
+import me.lethunderhawk.minion.manager.MinionManager;
+import me.lethunderhawk.minion.runtime.PlacedMinion;
+import org.bukkit.Bukkit;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public class MinionStorage {
+
+    private final File folder;
+
+    public MinionStorage(JavaPlugin plugin) {
+        this.folder = new File(plugin.getDataFolder(), "minions/placedMinions");
+        folder.mkdirs();
+    }
+
+    public void save(PlacedMinion minion) {
+        File file = getFile(minion.getMinionId().toString());
+        YamlConfiguration cfg = new YamlConfiguration();
+
+        MinionSerializer.serialize(minion, cfg);
+
+        try {
+            cfg.save(file);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+    public boolean delete(UUID minionId) {
+        File file = getFile(minionId.toString());
+        return file.exists() && file.delete();
+    }
+
+    public PlacedMinion load(UUID uuid) {
+        File file = getFile(uuid.toString());
+        if (!file.exists()) return null;
+
+        YamlConfiguration cfg = YamlConfiguration.loadConfiguration(file);
+        return MinionSerializer.deserialize(cfg);
+    }
+
+    public File getFile(String uuid) {
+        return new File(folder, uuid + ".yml");
+    }
+
+    public List<PlacedMinion> loadAll() {
+        List<PlacedMinion> minions = new ArrayList<>();
+
+        File[] files = folder.listFiles((dir, name) -> name.endsWith(".yml"));
+        if (files == null) return minions;
+
+        for (File file : files) {
+            try {
+                YamlConfiguration cfg = YamlConfiguration.loadConfiguration(file);
+                PlacedMinion minion = MinionSerializer.deserialize(cfg);
+
+                if (minion != null) {
+                    minions.add(minion);
+                }
+            } catch (Exception e) {
+                Bukkit.getLogger().warning("[Minions] Failed to load minion file: " + file.getName());
+                e.printStackTrace();
+            }
+        }
+
+        return minions;
+    }
+    public void clear() {
+        File[] files = folder.listFiles((dir, name) -> name.endsWith(".yml"));
+        if (files == null) return;
+
+        for (File file : files) {
+            if (!file.delete()) {
+                Bukkit.getLogger().warning("Failed to delete minion file: " + file.getName());
+            }
+        }
+    }
+    public void saveAllMinionsToStorage() {
+        clear();
+        List<PlacedMinion> minions = MinionManager.stopAndUnregisterAll();
+        for(PlacedMinion minion : minions) {
+            save(minion);
+        }
+    }
+}

+ 27 - 0
src/main/java/me/lethunderhawk/minion/registry/MinionRegistry.java

@@ -0,0 +1,27 @@
+package me.lethunderhawk.minion.registry;
+
+import me.lethunderhawk.minion.api.MinionType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class MinionRegistry {
+
+    private static final Map<String, MinionType> TYPES = new HashMap<>();
+
+    public static void register(MinionType type) {
+        TYPES.putIfAbsent(type.getId(), type);
+    }
+
+    public static MinionType get(String id) {
+        return TYPES.get(id);
+    }
+
+    public static boolean exists(String id) {
+        return TYPES.containsKey(id);
+    }
+
+    public static Map<String, MinionType> getAllRegisteredTypes() {
+        return TYPES;
+    }
+}

+ 198 - 0
src/main/java/me/lethunderhawk/minion/runtime/MinionInventory.java

@@ -0,0 +1,198 @@
+package me.lethunderhawk.minion.runtime;
+
+import org.bukkit.Material;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class MinionInventory {
+    private ItemStack[] items;
+    private int maxSize;
+
+    public MinionInventory() {
+        this(9);
+    }
+
+    public MinionInventory(int maxSize) {
+        this.maxSize = maxSize;
+        this.items = new ItemStack[maxSize];
+    }
+
+    public void resize(int newSize) {
+        if (newSize < 0) throw new IllegalArgumentException("Invalid size: " + newSize);
+
+        if (newSize == maxSize) return;
+
+        ItemStack[] newItems = new ItemStack[newSize];
+        int copyLimit = Math.min(maxSize, newSize);
+
+        // Copy existing items
+        System.arraycopy(items, 0, newItems, 0, copyLimit);
+
+        // Handle overflow if shrinking
+        if (newSize < maxSize) {
+            handleOverflowFromArray(items, copyLimit, maxSize);
+        }
+
+        this.items = newItems;
+        this.maxSize = newSize;
+    }
+
+
+
+    /**
+     *
+     * @param itemsToAdd The ItemStacks you want to add to this minion
+     * @return The leftovers if there isn't enough space
+     */
+    public Map<Integer, ItemStack> addItem(ItemStack... itemsToAdd) {
+        Map<Integer, ItemStack> leftover = new HashMap<>();
+
+        if (itemsToAdd == null || itemsToAdd.length == 0) {
+            return leftover;
+        }
+
+        for (int i = 0; i < itemsToAdd.length; i++) {
+            ItemStack item = itemsToAdd[i];
+
+            if (item == null || item.getType() == Material.AIR || item.getAmount() <= 0) {
+                continue;
+            }
+
+            int maxStackSize = item.getType().getMaxStackSize();
+            int remainingAmount = item.getAmount();
+
+            // Try to merge with existing similar items first
+            for (int slot = 0; slot < items.length && remainingAmount > 0; slot++) {
+                ItemStack existingItem = items[slot];
+
+                if (existingItem != null && existingItem.isSimilar(item)) {
+                    int existingAmount = existingItem.getAmount();
+                    int spaceAvailable = maxStackSize - existingAmount;
+
+                    if (spaceAvailable > 0) {
+                        int amountToAdd = Math.min(remainingAmount, spaceAvailable);
+                        existingItem.setAmount(existingAmount + amountToAdd);
+                        remainingAmount -= amountToAdd;
+                    }
+                }
+            }
+
+            // If still have items remaining, find empty slots
+            while (remainingAmount > 0) {
+                int emptySlot = findEmptySlot();
+                if (emptySlot == -1) { // No more space
+                    ItemStack leftoverItem = item.clone();
+                    leftoverItem.setAmount(remainingAmount);
+                    leftover.put(i, leftoverItem);
+                    break;
+                }
+
+                int amountForNewStack = Math.min(remainingAmount, maxStackSize);
+                ItemStack newStack = item.clone();
+                newStack.setAmount(amountForNewStack);
+                items[emptySlot] = newStack;
+                remainingAmount -= amountForNewStack;
+            }
+        }
+
+        return leftover;
+    }
+
+    private int findEmptySlot() {
+        for (int i = 0; i < items.length; i++) {
+            ItemStack item = items[i];
+            if (item == null || item.getType() == Material.AIR) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    public ItemStack removeItem(ItemStack item) {
+        if (item == null || item.getType() == Material.AIR || item.getAmount() <= 0) {
+            return null;
+        }
+
+        // If amount is 0 or negative, return null
+        if (item.getAmount() <= 0) {
+            return null;
+        }
+
+        int remainingToRemove = item.getAmount();
+        ItemStack removedItem = item.clone();
+        removedItem.setAmount(0);
+
+        for (int i = 0; i < maxSize && remainingToRemove > 0; i++) {
+            ItemStack slotItem = items[i];
+
+            if (slotItem != null && slotItem.isSimilar(item)) {
+                int slotAmount = slotItem.getAmount();
+
+                if (slotAmount <= remainingToRemove) {
+                    // Remove entire stack
+                    removedItem.setAmount(removedItem.getAmount() + slotAmount);
+                    remainingToRemove -= slotAmount;
+                    items[i] = null; // Clear the slot
+                } else {
+                    // Remove part of the stack
+                    removedItem.setAmount(removedItem.getAmount() + remainingToRemove);
+                    slotItem.setAmount(slotAmount - remainingToRemove);
+                    remainingToRemove = 0;
+                }
+            }
+        }
+
+        // Return null if nothing was removed
+        if (removedItem.getAmount() <= 0) {
+            return null;
+        }
+
+        return removedItem;
+    }
+    private void handleOverflowFromArray(ItemStack[] items, int copyLimit, int maxSize) {
+        // do nothing for now.
+    }
+
+    public ItemStack[] getAllItems() {
+        return items;
+    }
+
+
+    public ItemStack getItemAtIndex(int i) {
+        return items[i];
+    }
+
+    public int getMaxStorageItems() {
+        return maxSize * 64;
+    }
+    public void serialize(ConfigurationSection section) {
+        section.set("size", maxSize);
+
+        for (int i = 0; i < items.length; i++) {
+            if (items[i] != null && items[i].getType() != Material.AIR) {
+                section.set("items." + i, items[i]);
+            }
+        }
+    }
+
+    public static MinionInventory deserialize(ConfigurationSection section) {
+        int size = section.getInt("size", 9);
+        MinionInventory inv = new MinionInventory(size);
+
+        ConfigurationSection itemsSec = section.getConfigurationSection("items");
+        if (itemsSec != null) {
+            for (String key : itemsSec.getKeys(false)) {
+                int slot = Integer.parseInt(key);
+                ItemStack item = itemsSec.getItemStack(key);
+                if (slot >= 0 && slot < size) {
+                    inv.items[slot] = item;
+                }
+            }
+        }
+
+        return inv;
+    }
+}

+ 14 - 0
src/main/java/me/lethunderhawk/minion/runtime/MinionState.java

@@ -0,0 +1,14 @@
+package me.lethunderhawk.minion.runtime;
+
+public class MinionState {
+
+    private long lastActionTimestamp;
+
+    public long getLastActionTimestamp() {
+        return lastActionTimestamp;
+    }
+
+    public void setLastActionTimestamp(long lastActionTimestamp) {
+        this.lastActionTimestamp = lastActionTimestamp;
+    }
+}

+ 24 - 0
src/main/java/me/lethunderhawk/minion/runtime/MinionTask.java

@@ -0,0 +1,24 @@
+package me.lethunderhawk.minion.runtime;
+
+import me.lethunderhawk.minion.api.MinionLevelData;
+import org.bukkit.scheduler.BukkitRunnable;
+
+public class MinionTask extends BukkitRunnable {
+
+    private final PlacedMinion minion;
+    private final MinionLevelData levelData;
+
+    public MinionTask(PlacedMinion minion, MinionLevelData levelData) {
+        this.minion = minion;
+        this.levelData = levelData;
+    }
+
+    @Override
+    public void run() {
+        minion.performAction();
+    }
+
+    public long getInterval() {
+        return levelData.actionIntervalTicks();
+    }
+}

+ 160 - 0
src/main/java/me/lethunderhawk/minion/runtime/PlacedMinion.java

@@ -0,0 +1,160 @@
+package me.lethunderhawk.minion.runtime;
+
+import me.lethunderhawk.bazaarflux.util.lang.RomanNumbers;
+import me.lethunderhawk.minion.api.MinionType;
+import me.lethunderhawk.minion.factory.MinionFactory;
+import org.bukkit.Bukkit;
+import org.bukkit.Color;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.entity.ArmorStand;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.LeatherArmorMeta;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.Objects;
+import java.util.UUID;
+
+public class PlacedMinion {
+
+    private final UUID minionId;
+    private final UUID ownerId;
+    private final MinionType type;
+    private final int level;
+    private final ArmorStand armorStand;
+    private final MinionState state = new MinionState();
+    private final Location placedLocation;
+    private final ItemStack helmetItem;
+    private final Color armorColor;
+    private MinionTask task;
+    private MinionInventory inventory;
+
+    public PlacedMinion(
+            UUID minionId,
+            UUID ownerId,
+            MinionType type,
+            int level,
+            Location placedLocation,
+            ItemStack helmetItem,
+            Color armorColor) {
+        this.minionId = minionId;
+        this.ownerId = ownerId;
+        this.type = type;
+        this.level = level;
+        this.placedLocation = placedLocation;
+        this.helmetItem = helmetItem.clone();
+        this.armorColor = armorColor;
+        this.armorStand = createArmorStand();
+        this.inventory = new MinionInventory(type.getLevelTable().get(level).storageSize());
+    }
+
+    private ArmorStand createArmorStand() {
+        ItemStack chestplate = new ItemStack(Material.LEATHER_CHESTPLATE);
+        LeatherArmorMeta chest_meta = (LeatherArmorMeta) chestplate.getItemMeta();
+        chest_meta.setColor(armorColor);
+        chestplate.setItemMeta(chest_meta);
+
+        ItemStack leggings = new ItemStack(Material.LEATHER_LEGGINGS);
+        LeatherArmorMeta leggings_meta = (LeatherArmorMeta) leggings.getItemMeta();
+        leggings_meta.setColor(armorColor);
+        leggings.setItemMeta(leggings_meta);
+
+        ItemStack boots = new ItemStack(Material.LEATHER_BOOTS);
+        LeatherArmorMeta boots_meta = (LeatherArmorMeta) boots.getItemMeta();
+        boots_meta.setColor(armorColor);
+        boots.setItemMeta(boots_meta);
+
+        return placedLocation.getWorld().spawn(placedLocation, ArmorStand.class, as -> {
+            as.setInvulnerable(true);
+            as.setGravity(false);
+            as.setSmall(true);
+            as.setCustomNameVisible(false);
+            as.getEquipment().setArmorContents(new ItemStack[]{boots, leggings, chestplate, helmetItem });
+            as.setBasePlate(false);
+            as.setArms(true);
+            as.getEquipment().setItemInMainHand(new ItemStack(Material.IRON_PICKAXE));
+        });
+    }
+
+    public void start(JavaPlugin plugin) {
+        var levelData = type.getLevelTable().get(level);
+        this.task = new MinionTask(this, levelData);
+        this.task.runTaskTimer(plugin, 0L, task.getInterval());
+    }
+
+    public void stop() {
+        if (task != null) task.cancel();
+        if(armorStand != null) armorStand.remove();
+    }
+
+    public void performAction() {
+        type.getBehavior().performAction(this);
+        state.setLastActionTimestamp(System.currentTimeMillis());
+    }
+
+    public UUID getMinionId() { return minionId; }
+    public UUID getOwnerId() { return ownerId; }
+    public MinionType getType() { return type; }
+    public int getLevel() { return level; }
+    public ArmorStand getArmorStand() { return armorStand; }
+    public Location getLocation() { return armorStand.getLocation(); }
+    public MinionState getState() { return state; }
+    public ItemStack getHelmetItem() { return helmetItem; }
+
+    public String getRomanLevel() {
+        return RomanNumbers.intToRoman(level);
+    }
+
+    public long getSpeed() {
+        return type.getLevelTable().get(level).actionIntervalTicks();
+    }
+
+    public String getSpeedAsString() {
+        return type.getLevelTable().get(level).actionIntervalTicks() / 20L + "s";
+    }
+
+    public ItemStack[] getCollectedResources() {
+        return inventory.getAllItems();
+    }
+
+    public void pickupItem(int i) {
+        ItemStack itemStack = inventory.removeItem(inventory.getItemAtIndex(i));
+        if(itemStack != null) {
+            Objects.requireNonNull(Bukkit.getPlayer(ownerId)).getInventory().addItem(itemStack);
+        }
+    }
+
+    public void pickupAllItems() {
+        for(ItemStack item : inventory.getAllItems()){
+            if(item == null) continue;
+            Objects.requireNonNull(Bukkit.getPlayer(ownerId)).getInventory().addItem(item);
+            inventory.removeItem(item);
+        }
+    }
+
+    public void addItem(ItemStack item) {
+        inventory.addItem(item);
+    }
+
+    public PlacedMinion getUpgrade(JavaPlugin plugin) {
+        int newLevel = this.level + 1;
+        ItemStack newHelmet = new MinionFactory(plugin).createItem(this.type, newLevel);
+        if(newHelmet == null) return null;
+        PlacedMinion newMinion = new PlacedMinion(this.minionId, this.ownerId, this.type, newLevel, this.placedLocation, newHelmet, this.armorColor);
+        inventory.resize(this.type.getLevelTable().get(newLevel).storageSize());
+        newMinion.setInventory(inventory);
+        return newMinion;
+    }
+
+    public int getMaxStorageItems() {
+        return inventory.getMaxStorageItems();
+    }
+
+    public MinionInventory getInventory() {
+        return inventory;
+    }
+
+    public void setInventory(MinionInventory inventory) {
+        this.inventory = inventory;
+    }
+}

+ 335 - 0
src/main/java/me/lethunderhawk/minion/ui/MinionMenu.java

@@ -0,0 +1,335 @@
+package me.lethunderhawk.minion.ui;
+
+import me.lethunderhawk.bazaarflux.util.gui.InventoryGUI;
+import me.lethunderhawk.bazaarflux.util.gui.InventoryManager;
+import me.lethunderhawk.main.Main;
+import me.lethunderhawk.main.util.UnItalic;
+import me.lethunderhawk.minion.api.MinionLevelData;
+import me.lethunderhawk.minion.manager.MinionManager;
+import me.lethunderhawk.minion.registry.MinionRegistry;
+import me.lethunderhawk.minion.runtime.PlacedMinion;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public class MinionMenu extends InventoryGUI implements InventoryGUI.AutoCloseHandler {
+    private final PlacedMinion minion;
+
+    public MinionMenu(PlacedMinion minion) {
+        super(minion.getType().getName() + " " + minion.getRomanLevel(), 54);
+        this.minion = minion;
+        buildContents();
+    }
+
+    private void buildContents() {
+        // Fill background with stained glass panes
+        fillBackground(Material.GRAY_STAINED_GLASS_PANE, " ");
+
+        // Minion head/icon in the center (slot 13)
+        ItemStack minionItem = minion.getHelmetItem().clone();
+        ItemMeta minionMeta = minionItem.getItemMeta();
+        minionMeta.displayName(UnItalic.text(minion.getType().getName() + " " + minion.getRomanLevel()).color(NamedTextColor.BLUE));
+
+        List<Component> minionLore = minion.getHelmetItem().lore();
+        minionMeta.lore(minionLore);
+        minionItem.setItemMeta(minionMeta);
+        setItem(4, minionItem);
+
+        // Next tier info
+        ItemStack nextTier = buildNextTierItem();
+        setItemWithClickAction(5, nextTier, (p, type) -> {
+            p.sendMessage("not implemented yet");
+        });
+
+        // Collect resources button
+        ItemStack collectItem = buildCollectResourcesItem();
+        setItemWithClickAction(48, collectItem, (p, type) -> {
+            minion.pickupAllItems();
+            InventoryManager.refreshMinionMenuForPlayer(p.getUniqueId(), minion.getMinionId());
+        });
+
+        // Quick upgrade button
+        ItemStack upgradeItem = buildQuickUpgradeItem();
+        setItemWithClickAction(50, upgradeItem, (p, type) -> {
+
+            JavaPlugin plugin = Main.getInstance();
+            PlacedMinion newMinion = minion.getUpgrade(plugin);
+            if(newMinion != null) {
+                MinionManager.stopAndUnregister(minion);
+                MinionManager.startAndRegister(newMinion, plugin);
+                InventoryManager.close(p.getUniqueId());
+                InventoryManager.openFor(p, new MinionMenu(newMinion));
+            }else{
+                p.sendMessage("§cAlready at max tier.");
+            }
+
+        });
+
+        // Fuel slot
+        ItemStack fuelItem = buildFuelSlot();
+        setItem(19, fuelItem);
+
+
+        ItemStack settingsItem = buildSettingsItem();
+        setItemWithClickAction(3, settingsItem, (p, type) -> {
+            new MinionSettingsMenu(minion).open(p);
+        });
+
+
+        ItemStack pickupMinionItem = buildPickupMinionItem();
+        setItemWithClickAction(53, pickupMinionItem, (p, type) -> {
+            InventoryManager.close(p.getUniqueId());
+            p.getInventory().addItem(minion.getHelmetItem());
+            minion.pickupAllItems();
+            MinionManager.stopAndUnregister(minion);
+            p.sendMessage(Component.text("Removed Minion!", NamedTextColor.RED));
+        });
+
+        // Display collected resources in border slots
+        int[] displaySlots = {21, 22, 23, 24, 25,
+                              30, 31, 32, 33, 34,
+                              39, 40, 41, 42, 43};
+
+        ItemStack[] resources = minion.getCollectedResources();
+        ItemStack lockedStorage = buildLockedSlot();
+        for (int i = 0; i < displaySlots.length; i++) {
+            if (resources == null || i >= resources.length) {
+                setItem(displaySlots[i], lockedStorage);
+            } else {
+                int finalI = i;
+                setItemWithClickAction(displaySlots[i], resources[i], (p, type) -> {
+                    minion.pickupItem(finalI);
+                    updateDisplay(p);
+                });
+            }
+        }
+    }
+
+    private ItemStack buildLockedSlot() {
+        ItemStack lockedStorage = new ItemStack(Material.WHITE_STAINED_GLASS_PANE);
+        ItemMeta lockedStorageMeta = lockedStorage.getItemMeta();
+        lockedStorageMeta.displayName(Component.text("Locked Storage", NamedTextColor.RED));
+        lockedStorage.setItemMeta(UnItalic.removeItalicFromMeta(lockedStorageMeta));
+        return lockedStorage;
+    }
+
+    private ItemStack buildSettingsItem() {
+        ItemStack settingsItem = new ItemStack(Material.REDSTONE_TORCH);
+        ItemMeta settingsMeta = settingsItem.getItemMeta();
+        settingsMeta.displayName(Component.text("§aMinion Settings"));
+        List<Component> settingsLore = new ArrayList<>();
+        settingsLore.add(Component.text("§7Configure how this"));
+        settingsLore.add(Component.text("§7minion behaves!"));
+        settingsMeta.lore(settingsLore);
+        settingsItem.setItemMeta(settingsMeta);
+        return settingsItem;
+    }
+
+    private ItemStack buildFuelSlot() {
+        ItemStack fuelItem = new ItemStack(Material.ORANGE_STAINED_GLASS_PANE);
+        ItemMeta fuelMeta = fuelItem.getItemMeta();
+        fuelMeta.displayName(Component.text("§aFuel"));
+        List<Component> fuelLore = new ArrayList<>();
+        fuelLore.add(Component.text("§7Increase the speed of your"));
+        fuelLore.add(Component.text("§7minion by adding minion fuel"));
+        fuelLore.add(Component.text("§7items here."));
+        fuelLore.add(Component.text(""));
+        fuelLore.add(Component.text("§cNote: §7You cant take fuel "));
+        fuelLore.add(Component.text("§7back out after you place it "));
+        fuelLore.add(Component.text("§7here!"));
+        fuelLore.add(Component.text("§7Current Fuel: §eNone"));
+        fuelMeta.lore(fuelLore);
+        fuelItem.setItemMeta(fuelMeta);
+        return fuelItem;
+    }
+
+    private ItemStack buildQuickUpgradeItem() {
+        ItemStack upgradeItem = new ItemStack(Material.DIAMOND);
+        ItemMeta upgradeMeta = upgradeItem.getItemMeta();
+        upgradeMeta.displayName(Component.text("Quick-Upgrade your Minion", NamedTextColor.GREEN));
+        List<Component> upgradeLore = new ArrayList<>();
+        upgradeLore.add(Component.text("Click here to upgrade your", NamedTextColor.GRAY));
+        upgradeLore.add(Component.text("minion to the next tier.", NamedTextColor.GRAY));
+        upgradeLore.add(Component.text(""));
+        MinionLevelData nextLevel = getNextLevelData();
+
+        if(nextLevel == null || nextLevel.getTimeBetweenActions().equals(minion.getSpeedAsString())){
+            upgradeLore.add(Component.text("Time Between Actions: ", NamedTextColor.GRAY).append(Component.text(minion.getSpeedAsString(), NamedTextColor.GREEN)));
+        }else{
+            upgradeLore.add(Component.text("Time Between Actions: ", NamedTextColor.GRAY)
+                    .append(Component.text(minion.getSpeedAsString() + " ➜ ", NamedTextColor.DARK_GRAY))
+                    .append(Component.text(nextLevel.getTimeBetweenActions(), NamedTextColor.GREEN)));
+        }
+        if(nextLevel == null || nextLevel.getMaxStorageItems() == minion.getMaxStorageItems()){
+            upgradeLore.add(Component.text("Max Storage: ", NamedTextColor.GRAY)
+                    .append(Component.text( minion.getMaxStorageItems(), NamedTextColor.YELLOW)));
+        }else{
+            upgradeLore.add(Component.text("Max Storage: ", NamedTextColor.GRAY)
+                    .append(Component.text(minion.getMaxStorageItems() + " ➜ ", NamedTextColor.DARK_GRAY))
+                    .append(Component.text( nextLevel.getMaxStorageItems(), NamedTextColor.YELLOW)));
+        }
+        upgradeMeta.lore(upgradeLore);
+        upgradeItem.setItemMeta(UnItalic.removeItalicFromMeta(upgradeMeta));
+        return upgradeItem;
+    }
+
+    private ItemStack buildPickupMinionItem() {
+        ItemStack pickupItem = new ItemStack(Material.BEDROCK);
+        ItemMeta pickupMeta = pickupItem.getItemMeta();
+        pickupMeta.displayName(Component.text("§cPickup Minion"));
+        List<Component> pickupLore = new ArrayList<>();
+        pickupLore.add(Component.text("§7Click to pickup this"));
+        pickupLore.add(Component.text("§7minion and all its"));
+        pickupLore.add(Component.text("§7upgrades!"));
+        pickupLore.add(Component.text(""));
+        pickupLore.add(Component.text("§cWarning: This will"));
+        pickupLore.add(Component.text("§cstop the minion!"));
+        pickupMeta.lore(pickupLore);
+        pickupItem.setItemMeta(pickupMeta);
+        return  pickupItem;
+    }
+
+    private ItemStack buildCollectResourcesItem() {
+        ItemStack collectItem = new ItemStack(Material.CHEST);
+        ItemMeta collectMeta = collectItem.getItemMeta();
+        collectMeta.displayName(Component.text("§aCollect All"));
+        List<Component> collectLore = new ArrayList<>();
+        collectLore.add(Component.text("Click to collect all", NamedTextColor.GRAY));
+        collectLore.add(Component.text("resources from this", NamedTextColor.GRAY));
+        collectLore.add(Component.text("minion!", NamedTextColor.GRAY));
+        collectMeta.lore(collectLore);
+        collectItem.setItemMeta(UnItalic.removeItalicFromMeta(collectMeta));
+        return collectItem;
+    }
+
+    private ItemStack buildNextTierItem(){
+        ItemStack nextTier = new ItemStack(Material.GOLD_INGOT);
+        ItemMeta nextTierItemMeta = nextTier.getItemMeta();
+        nextTierItemMeta.displayName(Component.text("Next tier info", NamedTextColor.GREEN));
+        List<Component> nextTierLore = new ArrayList<>();
+        nextTierLore.add(Component.text("View the items required to", NamedTextColor.GRAY));
+        nextTierLore.add(Component.text("upgrade this minion to the next", NamedTextColor.GRAY));
+        nextTierLore.add(Component.text("tier.", NamedTextColor.GRAY));
+        nextTierLore.add(Component.text(""));
+
+        MinionLevelData nextLevel = getNextLevelData();
+
+        if(nextLevel == null || nextLevel.getTimeBetweenActions().equals(minion.getSpeedAsString())){
+            nextTierLore.add(Component.text("Time Between Actions: ", NamedTextColor.GRAY).append(Component.text(minion.getSpeedAsString(), NamedTextColor.GREEN)));
+        }else{
+            nextTierLore.add(Component.text("Time Between Actions: ", NamedTextColor.GRAY)
+                    .append(Component.text(minion.getSpeedAsString() + " ➜ ", NamedTextColor.DARK_GRAY))
+                    .append(Component.text(nextLevel.getTimeBetweenActions(), NamedTextColor.GREEN)));
+        }
+        if(nextLevel == null || nextLevel.getMaxStorageItems() == minion.getMaxStorageItems()){
+            nextTierLore.add(Component.text("Max Storage: ", NamedTextColor.GRAY)
+                    .append(Component.text( minion.getMaxStorageItems(), NamedTextColor.YELLOW)));
+        }else{
+            nextTierLore.add(Component.text("Max Storage: ", NamedTextColor.GRAY)
+                    .append(Component.text(minion.getMaxStorageItems() + " ➜ ", NamedTextColor.DARK_GRAY))
+                    .append(Component.text( nextLevel.getMaxStorageItems(), NamedTextColor.YELLOW)));
+        }
+
+        nextTierItemMeta.lore(nextTierLore);
+
+        nextTier.setItemMeta(UnItalic.removeItalicFromMeta(nextTierItemMeta));
+        return nextTier;
+    }
+
+
+    private MinionLevelData getNextLevelData(){
+        return MinionRegistry.get(minion.getType().getId()).getLevelTable().get(minion.getLevel() + 1);
+    }
+
+    private void updateDisplay(Player player) {
+        buildContents();
+        player.updateInventory();
+    }
+
+    @Override
+    public void onClosedByPlayer(Player player) {
+        // Save minion data when menu is closed
+        //minion.saveData();
+    }
+
+    @Override
+    public void update() {
+        buildContents();
+    }
+
+    public UUID getMinionId() {
+        return minion.getMinionId();
+    }
+
+    // Helper class for storage menu
+    private static class MinionStorageMenu extends InventoryGUI {
+        private final PlacedMinion minion;
+
+        public MinionStorageMenu(PlacedMinion minion) {
+            super(minion.getType().getName() + " Storage", 54);
+            this.minion = minion;
+            buildContents();
+        }
+
+        private void buildContents() {
+            fillBackground(Material.GRAY_STAINED_GLASS_PANE, " ");
+
+//            List<ItemStack> storage = minion.getStorageContents();
+//            for (int i = 0; i < Math.min(storage.size(), 36); i++) {
+//                setItem(i, storage.get(i));
+//            }
+
+            // Back button
+            ItemStack backItem = new ItemStack(Material.ARROW);
+            ItemMeta backMeta = backItem.getItemMeta();
+            backMeta.setDisplayName("§aGo Back");
+            backItem.setItemMeta(backMeta);
+            setItemWithClickAction(49, backItem, (p, type) -> {
+                new MinionMenu(minion).open(p);
+            });
+        }
+
+        @Override
+        public void update() {
+
+        }
+    }
+
+    // Helper class for settings menu
+    private static class MinionSettingsMenu extends InventoryGUI {
+        private final PlacedMinion minion;
+
+        public MinionSettingsMenu(PlacedMinion minion) {
+            super(minion.getType().getName() + " Settings", 27);
+            this.minion = minion;
+            buildContents();
+        }
+
+        private void buildContents() {
+            fillBackground(Material.GRAY_STAINED_GLASS_PANE, " ");
+
+
+            // Back button
+            ItemStack backItem = new ItemStack(Material.ARROW);
+            ItemMeta backMeta = backItem.getItemMeta();
+            backMeta.setDisplayName("§aGo Back");
+            backItem.setItemMeta(backMeta);
+            setItemWithClickAction(22, backItem, (p, type) -> {
+                new MinionMenu(minion).open(p);
+            });
+        }
+
+        @Override
+        public void update() {
+
+        }
+    }
+}

+ 10 - 0
src/main/java/me/lethunderhawk/minion/ui/MinionMenuFactory.java

@@ -0,0 +1,10 @@
+package me.lethunderhawk.minion.ui;
+
+import me.lethunderhawk.minion.runtime.PlacedMinion;
+
+public class MinionMenuFactory {
+
+    public static MinionMenu create(PlacedMinion minion) {
+        return new MinionMenu(minion);
+    }
+}

+ 47 - 0
src/main/java/me/lethunderhawk/minion/ui/minionList/MinionLevelListMenu.java

@@ -0,0 +1,47 @@
+package me.lethunderhawk.minion.ui.minionList;
+
+import me.lethunderhawk.bazaarflux.util.gui.GoBackItem;
+import me.lethunderhawk.bazaarflux.util.gui.InventoryGUI;
+import me.lethunderhawk.main.Main;
+import me.lethunderhawk.minion.api.MinionLevelData;
+import me.lethunderhawk.minion.api.MinionLevelTable;
+import me.lethunderhawk.minion.factory.MinionFactory;
+import me.lethunderhawk.minion.registry.MinionRegistry;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+public class MinionLevelListMenu extends InventoryGUI {
+    private final String typeId;
+
+    public MinionLevelListMenu(String typeId) {
+        super(typeId + " Minions", 27);
+        this.typeId = typeId;
+        buildLevelList(typeId);
+    }
+
+    private void buildLevelList(String typeId) {
+        fillBackground(Material.GRAY_STAINED_GLASS_PANE, " ");
+        MinionLevelTable table = MinionRegistry.get(typeId).getLevelTable();
+        int slot = 0;
+        for(MinionLevelData data : table.getAllLevels()){
+            ItemStack item = new MinionFactory(Main.getInstance()).createItem(typeId, data.level());
+            setItemWithClickAction(slot, item, (player, clickType) -> {
+                getMinionItem(player, item);
+            });
+            slot++;
+        }
+        // Use the correct method signature for the lambda
+        setItemWithClickAction(getInventory().getSize()-1, new GoBackItem(),
+                (player, clickType) -> openPrevious(player));
+    }
+
+    private void getMinionItem(Player player, ItemStack minionItem) {
+        player.getInventory().addItem(minionItem);
+    }
+
+    @Override
+    public void update() {
+        buildLevelList(this.typeId);
+    }
+}

+ 46 - 0
src/main/java/me/lethunderhawk/minion/ui/minionList/MinionListMenu.java

@@ -0,0 +1,46 @@
+package me.lethunderhawk.minion.ui.minionList;
+
+import me.lethunderhawk.bazaarflux.util.gui.InventoryGUI;
+import me.lethunderhawk.main.Main;
+import me.lethunderhawk.minion.api.MinionType;
+import me.lethunderhawk.minion.factory.MinionFactory;
+import me.lethunderhawk.minion.registry.MinionRegistry;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.Map;
+
+public class MinionListMenu extends InventoryGUI {
+
+
+    public MinionListMenu() {
+        super("Minion List", 27);
+        buildMenu();
+    }
+
+    private void buildMenu() {
+        fillBackground(Material.GRAY_STAINED_GLASS_PANE, " ");
+        Map<String, MinionType> types = MinionRegistry.getAllRegisteredTypes();
+        int slot = 0;
+        for(String typeId : types.keySet()){
+            ItemStack item = new MinionFactory(Main.getInstance()).createItem(typeId, 1);
+
+            setItemWithClickAction(slot, item, (player, clickType) -> {
+                openBiggerMinionListMenu(player, typeId);
+            });
+            slot++;
+        }
+    }
+
+    private void openBiggerMinionListMenu(Player player, String typeId) {
+        MinionLevelListMenu menu = new MinionLevelListMenu(typeId);
+        openNext(player, menu);
+    }
+
+
+    @Override
+    public void update() {
+
+    }
+}

+ 6 - 1
src/main/java/me/lethunderhawk/tradeplugin/TradeModule.java

@@ -15,8 +15,13 @@ public class TradeModule {
 
     private static TradeManager tradeManager;
     private static TradeRequestManager requestManager;
+    private final JavaPlugin plugin;
 
-    public void onEnable(JavaPlugin plugin) {
+    public TradeModule(JavaPlugin plugin) {
+        this.plugin = plugin;
+    }
+
+    public void onEnable() {
         tradeManager = new TradeManager(plugin);
         requestManager = new TradeRequestManager();
 

+ 1 - 1
src/main/java/me/lethunderhawk/tradeplugin/api/TradePlaceholder.java

@@ -23,7 +23,7 @@ public class TradePlaceholder extends PlaceholderExpansion {
 
     @Override
     public @NotNull String getVersion() {
-        return "";
+        return "1.0";
     }
     @Override
     public String onPlaceholderRequest(Player p, String identifier) {

+ 8 - 7
src/main/java/me/lethunderhawk/tradeplugin/input/player/NumberInputGUI.java

@@ -44,8 +44,7 @@ public class NumberInputGUI extends InventoryGUI implements InventoryGUI.AutoClo
     }
 
     private void buildContents() {
-        // center display
-        updateDisplay();
+        update();
 
         // confirm button
         ItemStack confirm = make(Material.GREEN_DYE, "§a§lBestätigen");
@@ -70,7 +69,7 @@ public class NumberInputGUI extends InventoryGUI implements InventoryGUI.AutoClo
             }else if(type.isRightClick()){
                 currentValue = Math.max(minValue, currentValue - (type.isShiftClick() ? 10 : 1));
             }
-            updateDisplay();
+            update();
             // ensure client sees update
             p.updateInventory();
         });
@@ -90,10 +89,6 @@ public class NumberInputGUI extends InventoryGUI implements InventoryGUI.AutoClo
         return it;
     }
 
-    private void updateDisplay() {
-        ItemStack valueItem = buildPaperWithContents();
-        setItem(PAPER_SLOT, valueItem);
-    }
     private ItemStack buildPaperWithContents() {
         ItemStack valueItem = new ItemStack(Material.PAPER);
         ItemMeta meta = valueItem.getItemMeta();
@@ -138,4 +133,10 @@ public class NumberInputGUI extends InventoryGUI implements InventoryGUI.AutoClo
         // accidental close -> null callback (original behaviour)
         callback.accept(p, null);
     }
+
+    @Override
+    public void update() {
+        ItemStack valueItem = buildPaperWithContents();
+        setItem(PAPER_SLOT, valueItem);
+    }
 }

+ 3 - 3
src/main/java/me/lethunderhawk/tradeplugin/trade/TradeInventory.java

@@ -1,7 +1,7 @@
 package me.lethunderhawk.tradeplugin.trade;
 
 import me.lethunderhawk.bazaarflux.util.CustomHeadCreator;
-import me.lethunderhawk.main.util.ItalicDeco;
+import me.lethunderhawk.main.util.UnItalic;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.format.NamedTextColor;
 import net.kyori.adventure.text.format.Style;
@@ -35,7 +35,7 @@ public class TradeInventory {
         this.invP1 = Bukkit.createInventory(null, TRADE_INV_SIZE, "Trade mit " + session.getP2().getName());
         this.invP2 = Bukkit.createInventory(null, TRADE_INV_SIZE, "Trade mit " + session.getP1().getName());
         setupMaterials();
-        fluxItemStack = CustomHeadCreator.createCustomHead(fluxHeadValue, ItalicDeco.remove(Component.text("Trade Flux", NamedTextColor.GOLD)), ItalicDeco.remove(Component.text("Click to set a reasonable amount", NamedTextColor.GRAY)));
+        fluxItemStack = CustomHeadCreator.createCustomHead(fluxHeadValue, UnItalic.removeItalic(Component.text("Trade Flux", NamedTextColor.GOLD)), UnItalic.removeItalic(Component.text("Click to set a reasonable amount", NamedTextColor.GRAY)));
         setupIcons();
         setup(invP1);
         setup(invP2);
@@ -90,7 +90,7 @@ public class TradeInventory {
     private ItemStack button(Material m, String name, TextColor color) {
         ItemStack item = new ItemStack(m);
         ItemMeta meta = item.getItemMeta();
-        meta.displayName(ItalicDeco.remove(Component.text(name).style(Style.style(color))));
+        meta.displayName(UnItalic.removeItalic(Component.text(name).style(Style.style(color))));
         item.setItemMeta(meta);
         return item;
     }

+ 6 - 6
src/main/java/me/lethunderhawk/tradeplugin/trade/TradeSession.java

@@ -2,7 +2,7 @@ package me.lethunderhawk.tradeplugin.trade;
 
 import me.lethunderhawk.economy.EconomyModule;
 import me.lethunderhawk.main.Main;
-import me.lethunderhawk.main.util.ItalicDeco;
+import me.lethunderhawk.main.util.UnItalic;
 import me.lethunderhawk.tradeplugin.TradeModule;
 import net.kyori.adventure.audience.Audience;
 import net.kyori.adventure.text.Component;
@@ -262,7 +262,7 @@ public class TradeSession {
         ItemStack item = TradeInventory.getFluxItemStack();
         SkullMeta meta = (SkullMeta) item.getItemMeta();
 
-        meta.displayName(ItalicDeco.remove(Component.text(value + " Flux", NamedTextColor.GOLD)));
+        meta.displayName(UnItalic.removeItalic(Component.text(value + " Flux", NamedTextColor.GOLD)));
 
         meta.getPersistentDataContainer().set(FLUX_KEY, PersistentDataType.INTEGER, 1);
         meta.getPersistentDataContainer().set(OWNER_KEY, PersistentDataType.STRING, owner.getUniqueId().toString());
@@ -273,10 +273,10 @@ public class TradeSession {
                 .getMoneyFor(owner);
 
         meta.lore(List.of(
-                ItalicDeco.remove(Component.text("Lup-sum amount", NamedTextColor.GRAY)),
-                ItalicDeco.remove(Component.text("", NamedTextColor.GRAY)),
-                ItalicDeco.remove(Component.text("Total Flux Offered: ", NamedTextColor.GOLD)),
-                ItalicDeco.remove(Component.text(String.valueOf(total), NamedTextColor.GRAY))
+                UnItalic.removeItalic(Component.text("Lup-sum amount", NamedTextColor.GRAY)),
+                UnItalic.removeItalic(Component.text("", NamedTextColor.GRAY)),
+                UnItalic.removeItalic(Component.text("Total Flux Offered: ", NamedTextColor.GOLD)),
+                UnItalic.removeItalic(Component.text(String.valueOf(total), NamedTextColor.GRAY))
         ));
 
         item.setItemMeta(meta);

+ 49 - 0
src/main/resources/minions/cobblestone.yml

@@ -0,0 +1,49 @@
+1:
+  actionTime: 14.0
+  storageSize: 1
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjljMzhmZTRmYzk4YTI0ODA3OWNkMDRjNjViNmJmZjliNDUwMTdmMTY0NjBkYWIzYzM0YzE3YmZjM2VlMWQyZiJ9fX0="
+
+2:
+  actionTime: 14.0
+  storageSize: 3
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZGZjMjYzYmIzZGVmM2VhOGVhZjFjMjBhN2ViODRiNTQyOGMzMTQ1NmI0MzNmMGEyMTllYWM1YjQyZTJhYmExMSJ9fX0="
+
+3:
+  actionTime: 12.0
+  storageSize: 3
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMjk3OGZkZDlhODI1ZTc1NzQ3N2U5M2Q1ZmM1ZGMyMmU2MjdlYzI2YzEwZTU4ZDk5ZDhkMTRmOTA0NTFmODI2YiJ9fX0="
+
+4:
+  actionTime: 12.0
+  storageSize: 6
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODc0NTNmMjg0YTViYTlmMGRlOWRhMWIxNzUzYWM0NWE2Mzk1ZDFmOTFlMzhjY2I5ZGI4N2Q2OGZlNzhjZTc3ZiJ9fX0="
+
+5:
+  actionTime: 10.0
+  storageSize: 6
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDJmOTE2MDY1MzA5OTg2OWMwOGM1Mzc3NGNlZjYxYTRiZTNiOGJmODQ1ZjAzYzUyYjVlM2VlMzUxY2VlNGViYSJ9fX0="
+
+6:
+  actionTime: 10.0
+  storageSize: 9
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjY1MjdiOWM4MDM3OGJjY2IwMzBjODc2YjE5ZjY1NDdhYTc2YmY0NjU4MzNmNDcxYTM1MTE0YjdkODFlNzg4ZiJ9fX0="
+
+7:
+  actionTime: 9.0
+  storageSize: 9
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjkxNDkyNzk2MTk1ZmVjM2Q1NmJlMGJjZTNlYzQwZjYzMWJmOWI1NDA1NDE3OGY0NTgyYmI3OTUyY2YxNzc1MiJ9fX0="
+
+8:
+  actionTime: 9.0
+  storageSize: 12
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjFlYjg2ZTFkNWNkYjNiYjdkZDhlZmZkOWU1MDY0MTYwYmU5ZjgzNDBkNDgwYjQ5NDk4MzQ5MGVlYjU0YzhhYSJ9fX0="
+
+9:
+  actionTime: 8.0
+  storageSize: 12
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzRjNWE0NjMzMjgzNzQzYjgxZWUwYzFkYWUwYmFiZDNmOGFhYjRiODQ4ZTAzNGIzYzdmZGQwNzJlM2U2MWQwOCJ9fX0="
+
+10:
+  actionTime: 8.0
+  storageSize: 15
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZmNiOTQ2ZDQxNWU2MTBmYzU0MWMxZTc3YmViMTM3Nzc2YzcwZDI5ODA5Yzk0MjU5OWYzNWQ2Mjk1M2E4YmNkNiJ9fX0="

+ 49 - 0
src/main/resources/minions/redstone.yml

@@ -0,0 +1,49 @@
+1:
+  actionTime: 29.0
+  storageSize: 1
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMWUyMWY4Y2ZlOGNjMGUwODk5MGQ1MjE2NWU3OGU2ZjgxNmY0ZjhlMWNlN2NiMGYwYTZjOGViZTFkYTg1ZTQyYSJ9fX0="
+
+2:
+  actionTime: 29.0
+  storageSize: 3
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzgzM2MyZDQ3NTdjYTFjMGIxZmZiZTc4NThjOGFlMzYzYTdmNGQxYjJiMGJiYjEwYjFmYjFkNGI1NzMyOWQwIn19fQ=="
+
+3:
+  actionTime: 27.0
+  storageSize: 3
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTgwNzllZjU0Mzk1YTljZWE2NGQ5OGUxYTgxNjVkYzI0MDBiNzNiMjQwMGJkMTY1NjdhNWFjNDBmNjI5ZTVjNyJ9fX0="
+
+4:
+  actionTime: 27.0
+  storageSize: 6
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTEyYWU0ZTJkNmYwMWNlODdlNmE0NGFmN2Y3Y2Y3NGU1Zjk0ZGE5NzYzOWNhOThkYTBlYjY1MDlhMzE2MmU1YiJ9fX0="
+
+5:
+  actionTime: 25.0
+  storageSize: 6
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDI2NWRkNjJhOWVhMTIwYjA0YmY2Y2E0ZDA3NjhiMjA5ZmQyY2FmZTc5OWIyMmNlZWQyMTk4ZjA4NmY0N2Q5MSJ9fX0="
+
+6:
+  actionTime: 25.0
+  storageSize: 9
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZWM3YzdiZTFhZTY0MjBiOTg2Mzk5ZjI0YTM2MTY1NWQ0YWI4YzJiZDdiODQxYzA0NzBlMmJkMzJlYzgzMzczNyJ9fX0="
+
+7:
+  actionTime: 23.0
+  storageSize: 9
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMjBkOWMyMjgyMmRmMGExYjI5MzUzNTRjN2Q5YmMzZjhjNjI0NWI1OGJmZGFhN2EyOTc5ZjE5ZGNlODQ1OTA3MiJ9fX0="
+
+8:
+  actionTime: 23.0
+  storageSize: 12
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvN2M4MTVkYmI2MTViNTFjYmY1NWEzYzk2Nzk2MmMxOThkZjFlY2MxNzAwODBlOTIwZDUzOWRjZWJiYjZkY2JkYSJ9fX0="
+
+9:
+  actionTime: 21.0
+  storageSize: 12
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjNiMmE0NjQwZTU2NWU4MjE4ZTQxOTlhZjJkYjMwZDA4ZjU4NDczZDhmYjAxN2ViOGMzMTY1YjU0M2VmNzM5NSJ9fX0="
+
+10:
+  actionTime: 21.0
+  storageSize: 15
+  customHeadValue: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTRhNjU4YzdjYjdiMjE5MzU3ZmY4OGQxNmRkZTIzZGFlNGQwYmM1ZDc5YWFlODg3MzNhNjQzNGFkYWMxYmI3NCJ9fX0="

+ 7 - 0
src/main/resources/plugin.yml

@@ -19,8 +19,15 @@ commands:
     description: Custom Item management command
     usage: /customItem
     permission: customItem.commands
+  minion:
+    description: Minion management command
+    usage: /minion
+    permission: minion.commands
 
 permissions:
+  minion.commands:
+    description: Allows use of Minion commands
+    default: op
   customItem.commands:
     description: Allows use of custom Items commands
     default: op