Просмотр исходного кода

Huge Custom Items revamp, custom abilities on Items, Items loaded from yml and only abilities have to be coded, starting of dungeons, loading dungeons from files, added Dice and Hyperion

Jan 6 дней назад
Родитель
Сommit
6782281c16
61 измененных файлов с 3191 добавлено и 625 удалено
  1. 5 1
      src/main/java/me/lethunderhawk/bazaarflux/service/Services.java
  2. 53 0
      src/main/java/me/lethunderhawk/bazaarflux/util/animation/Animation.java
  3. 254 0
      src/main/java/me/lethunderhawk/bazaarflux/util/itemdesign/LoreDesigner.java
  4. 142 0
      src/main/java/me/lethunderhawk/bazaarflux/util/loottables/RiggedChanceGenerator.java
  5. 5 4
      src/main/java/me/lethunderhawk/clans/Clan.java
  6. 2 1
      src/main/java/me/lethunderhawk/clans/claim/ClaimListener.java
  7. 4 3
      src/main/java/me/lethunderhawk/clans/command/ClanCommand.java
  8. 51 22
      src/main/java/me/lethunderhawk/custom/item/CustomItemModule.java
  9. 0 109
      src/main/java/me/lethunderhawk/custom/item/abstraction/CustomItem.java
  10. 11 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/ability/AbilityContext.java
  11. 65 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/ability/AbilityDefinition.java
  12. 47 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/ability/AbilityTrigger.java
  13. 8 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/definition/DefaultItemDefinitionValidator.java
  14. 52 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/definition/ItemDefinition.java
  15. 209 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/definition/ItemDefinitionLoader.java
  16. 5 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/definition/ItemDefinitionValidator.java
  17. 47 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/definition/ItemVisualDefinition.java
  18. 185 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/definition/parser/ItemYamlParser.java
  19. 11 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/definition/raw/RawAbilityData.java
  20. 22 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/definition/raw/RawItemData.java
  21. 11 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/handling/AbilityHandler.java
  22. 143 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/handling/ResolvedParams.java
  23. 51 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/instance/ItemInstance.java
  24. 43 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/instance/ItemInstanceReader.java
  25. 35 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/instance/ItemPersistentData.java
  26. 12 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/migration/ItemMigration.java
  27. 27 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/migration/MigrationService.java
  28. 30 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/registry/AbilityRegistry.java
  29. 30 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/registry/CustomItemRegistry.java
  30. 67 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/runtime/AbilityDispatchService.java
  31. 26 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/util/JsonUtil.java
  32. 57 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/visual/ItemRenderer.java
  33. 13 20
      src/main/java/me/lethunderhawk/custom/item/command/CustomItemCommand.java
  34. 0 97
      src/main/java/me/lethunderhawk/custom/item/concrete/ClaimTool.java
  35. 113 0
      src/main/java/me/lethunderhawk/custom/item/concrete/ability/ClaimToolAbility.java
  36. 192 0
      src/main/java/me/lethunderhawk/custom/item/concrete/ability/HyperionAbility.java
  37. 91 0
      src/main/java/me/lethunderhawk/custom/item/concrete/ability/RollingDiceAbility.java
  38. 12 0
      src/main/java/me/lethunderhawk/custom/item/concrete/ability/TestAbilityHandler.java
  39. 11 0
      src/main/java/me/lethunderhawk/custom/item/concrete/dice/DiceReward.java
  40. 83 0
      src/main/java/me/lethunderhawk/custom/item/concrete/dice/RollingDiceAnimation.java
  41. 15 51
      src/main/java/me/lethunderhawk/custom/item/listener/CustomItemListener.java
  42. 0 88
      src/main/java/me/lethunderhawk/custom/item/manager/CustomItemManager.java
  43. 1 1
      src/main/java/me/lethunderhawk/dungeon/DungeonModule.java
  44. 5 2
      src/main/java/me/lethunderhawk/dungeon/command/DungeonCommand.java
  45. 9 0
      src/main/java/me/lethunderhawk/dungeon/generation/ConnectionType.java
  46. 673 198
      src/main/java/me/lethunderhawk/dungeon/generation/DungeonGridGenerator.java
  47. 22 6
      src/main/java/me/lethunderhawk/dungeon/generation/DungeonWorld.java
  48. 7 6
      src/main/java/me/lethunderhawk/dungeon/manager/DungeonManager.java
  49. 7 0
      src/main/java/me/lethunderhawk/dungeon/placement/RoomMetadata.java
  50. 24 0
      src/main/java/me/lethunderhawk/dungeon/placement/RotatableBoundingBox.java
  51. 20 0
      src/main/java/me/lethunderhawk/dungeon/placement/Rotation.java
  52. 142 0
      src/main/java/me/lethunderhawk/dungeon/placement/SchematicRoomPlacer.java
  53. 3 0
      src/main/java/me/lethunderhawk/dungeon/placement/Vector3i.java
  54. 1 1
      src/main/java/me/lethunderhawk/economy/EconomyModule.java
  55. 15 2
      src/main/java/me/lethunderhawk/economy/currency/EconomyManager.java
  56. 3 3
      src/main/java/me/lethunderhawk/tradeplugin/listener/TradeInventoryListener.java
  57. 6 6
      src/main/java/me/lethunderhawk/tradeplugin/trade/TradeRequestManager.java
  58. 3 3
      src/main/java/me/lethunderhawk/tradeplugin/trade/TradeSession.java
  59. 1 1
      src/main/resources/plugin.yml
  60. 9 0
      src/main/resources/rooms/start_room.json
  61. BIN
      src/main/resources/rooms/start_room.schem

+ 5 - 1
src/main/java/me/lethunderhawk/bazaarflux/service/Services.java

@@ -19,7 +19,11 @@ public final class Services {
 
     @SuppressWarnings("unchecked")
     public static <T> T get(Class<T> type) {
-        return (T) services.get(type);
+        T service = (T) services.get(type);
+        if(service == null) {
+            throw new RuntimeException("No service registered for " + type);
+        }
+        return service;
     }
     @SuppressWarnings("unchecked")
     public static <T> T unregister(Class<T> type) {

+ 53 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/animation/Animation.java

@@ -0,0 +1,53 @@
+package me.lethunderhawk.bazaarflux.util.animation;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.main.Main;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitTask;
+
+public abstract class Animation {
+
+    protected final JavaPlugin plugin;
+    protected final World world;
+    protected int tick = 0;
+    protected int duration;
+
+    private BukkitTask task;
+    private Runnable onComplete;
+
+    protected Animation(World world, int duration) {
+        this.plugin = Services.get(Main.class);
+        this.world = world;
+        this.duration = duration;
+    }
+
+    public final void start() {
+        onStart();
+        task = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
+            if (tick >= duration) {
+                stop(true);
+                return;
+            }
+            onTick(tick);
+            tick++;
+        }, 0L, 1L);
+    }
+
+    public final void stop(boolean completed) {
+        if (task != null) task.cancel();
+        onEnd();
+        if (completed && onComplete != null) {
+            onComplete.run();
+        }
+    }
+
+    public void onComplete(Runnable runnable) {
+        this.onComplete = runnable;
+    }
+
+    protected abstract void onStart();
+    protected abstract void onTick(int tick);
+    protected abstract void onEnd();
+}

+ 254 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/itemdesign/LoreDesigner.java

@@ -0,0 +1,254 @@
+package me.lethunderhawk.bazaarflux.util.itemdesign;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.ComponentBuilder;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+public final class LoreDesigner {
+
+    private LoreDesigner() {}
+    public static Component createSingle(String text) {
+        if (text == null || text.isBlank()) {
+            return Component.empty();
+        }
+
+        StyleState style = new StyleState();
+        ComponentBuilder<TextComponent, TextComponent.Builder> builder = Component.text();
+
+        int i = 0;
+        while (i < text.length()) {
+            if (text.charAt(i) == '<') {
+                int end = text.indexOf('>', i);
+                if (end != -1) {
+                    String token = text.substring(i + 1, end).toLowerCase();
+                    applyToken(style, token);
+                    i = end + 1;
+                    continue;
+                }
+            }
+
+            char c = text.charAt(i);
+            builder.append(Component.text(String.valueOf(c), style.toStyle()));
+            i++;
+        }
+
+        return builder.build();
+    }
+    public static List<Component> createLore(String text, String widthReference) {
+        int maxLength = widthReference.length();
+        if (text == null || text.isBlank() || maxLength <= 0) {
+            return List.of();
+        }
+
+        List<Component> lore = new ArrayList<>();
+
+        StyleState style = new StyleState();
+        ComponentBuilder<TextComponent, TextComponent.Builder> lineBuilder = Component.text();
+        int currentLength = 0;
+
+        String[] lines = text.split("<br>", -1);
+
+        for (int i = 0; i < lines.length; i++) {
+
+            String[] wrapParts = lines[i].split(" ");
+
+            for (int p = 0; p < wrapParts.length; p++) {
+
+                ParsedWord parsed = parseWord(wrapParts[p], style);
+                appendWord(
+                        lore,
+                        parsed,
+                        maxLength,
+                        style,
+                        Holder.of(lineBuilder, currentLength)
+                );
+
+                currentLength = Holder.length;
+                lineBuilder = Holder.builder;
+
+                // add space between parts
+                if (p < wrapParts.length - 1) {
+                    lineBuilder.append(Component.space());
+                    currentLength++;
+                }
+            }
+
+            currentLength = Holder.length;
+            lineBuilder = Holder.builder;
+
+            // force line break if <br> occurred
+            if (i < lines.length - 1) {
+                lore.add(lineBuilder.build());
+                lineBuilder = Component.text().style(style.toStyle());
+                currentLength = 0;
+            }
+        }
+
+        if (currentLength > 0) {
+            lore.add(lineBuilder.build());
+        }
+
+        return lore;
+    }
+
+    // -------------------- Append Logic --------------------
+
+    private static void appendWord(
+            List<Component> lore,
+            ParsedWord parsed,
+            int maxLength,
+            StyleState style,
+            Holder holder
+    ) {
+        int wordLength = parsed.visibleLength();
+
+        if (holder.length > 0 && holder.length + 1 + wordLength > maxLength) {
+            lore.add(holder.builder.build());
+            holder.builder = Component.text().style(style.toStyle());
+            holder.length = 0;
+        }
+
+        holder.builder.append(parsed.component());
+        holder.length += wordLength;
+    }
+
+    // -------------------- Parsing --------------------
+
+    private static ParsedWord parseWord(String raw, StyleState style) {
+        ComponentBuilder<TextComponent, TextComponent.Builder> builder = Component.text();
+        int visibleLength = 0;
+
+        int i = 0;
+        while (i < raw.length()) {
+            if (raw.charAt(i) == '<') {
+                int end = raw.indexOf('>', i);
+                if (end != -1) {
+                    String token = raw.substring(i + 1, end).toLowerCase();
+                    applyToken(style, token);
+                    i = end + 1;
+                    continue;
+                }
+            }
+
+            char c = raw.charAt(i);
+            builder.append(Component.text(String.valueOf(c), style.toStyle()));
+            visibleLength++;
+            i++;
+        }
+
+        return new ParsedWord(builder.build(), visibleLength);
+    }
+
+    private static void applyToken(StyleState style, String token) {
+
+        if (token.startsWith("/")) {
+            String closing = token.substring(1);
+
+            switch (closing) {
+                case "bold" -> style.undecorate(TextDecoration.BOLD);
+                case "italic" -> style.undecorate(TextDecoration.ITALIC);
+                case "underlined" -> style.undecorate(TextDecoration.UNDERLINED);
+                case "strikethrough" -> style.undecorate(TextDecoration.STRIKETHROUGH);
+                case "obfuscated" -> style.undecorate(TextDecoration.OBFUSCATED);
+                default -> {
+                    // closing color tag like </red>
+                    if (NAMED_COLORS.containsKey(closing)) {
+                        style.color(style.defaultColor);
+                    }
+                }
+            }
+            return;
+        }
+
+        switch (token) {
+            case "reset" -> style.reset();
+
+            case "bold" -> style.decorate(TextDecoration.BOLD);
+            case "italic" -> style.decorate(TextDecoration.ITALIC);
+            case "underlined" -> style.decorate(TextDecoration.UNDERLINED);
+            case "strikethrough" -> style.decorate(TextDecoration.STRIKETHROUGH);
+            case "obfuscated" -> style.decorate(TextDecoration.OBFUSCATED);
+
+            default -> {
+                NamedTextColor color = NAMED_COLORS.get(token);
+                if (color != null) {
+                    style.color(color);
+                }
+            }
+        }
+    }
+
+    // -------------------- Helpers --------------------
+
+    private static final Map<String, NamedTextColor> NAMED_COLORS = Map.ofEntries(
+            Map.entry("black", NamedTextColor.BLACK),
+            Map.entry("dark_blue", NamedTextColor.DARK_BLUE),
+            Map.entry("dark_green", NamedTextColor.DARK_GREEN),
+            Map.entry("dark_aqua", NamedTextColor.DARK_AQUA),
+            Map.entry("dark_red", NamedTextColor.DARK_RED),
+            Map.entry("dark_purple", NamedTextColor.DARK_PURPLE),
+            Map.entry("gold", NamedTextColor.GOLD),
+            Map.entry("gray", NamedTextColor.GRAY),
+            Map.entry("dark_gray", NamedTextColor.DARK_GRAY),
+            Map.entry("blue", NamedTextColor.BLUE),
+            Map.entry("green", NamedTextColor.GREEN),
+            Map.entry("aqua", NamedTextColor.AQUA),
+            Map.entry("red", NamedTextColor.RED),
+            Map.entry("light_purple", NamedTextColor.LIGHT_PURPLE),
+            Map.entry("yellow", NamedTextColor.YELLOW),
+            Map.entry("white", NamedTextColor.WHITE)
+    );
+
+    private record ParsedWord(Component component, int visibleLength) {}
+
+    private static final class StyleState {
+        private final NamedTextColor defaultColor = NamedTextColor.GRAY;
+
+        private NamedTextColor color = defaultColor;
+        private final EnumSet<TextDecoration> decorations = EnumSet.noneOf(TextDecoration.class);
+
+        void color(NamedTextColor color) {
+            this.color = color;
+        }
+        void undecorate(TextDecoration decoration) {
+            decorations.remove(decoration);
+        }
+        void decorate(TextDecoration decoration) {
+            decorations.add(decoration);
+        }
+
+        void reset() {
+            color = defaultColor;
+            decorations.clear();
+        }
+
+        net.kyori.adventure.text.format.Style toStyle() {
+            net.kyori.adventure.text.format.Style.Builder style =
+                    net.kyori.adventure.text.format.Style.style();
+            if (color != null) style.color(color);
+            for (TextDecoration d : decorations) style.decorate(d);
+            return style.build();
+        }
+    }
+
+    /**
+     * Mutable holder to avoid excessive object creation
+     */
+    private static final class Holder {
+        static ComponentBuilder builder;
+        static int length;
+
+        static Holder of(ComponentBuilder b, int l) {
+            builder = b;
+            length = l;
+            return null;
+        }
+    }
+}

+ 142 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/loottables/RiggedChanceGenerator.java

@@ -0,0 +1,142 @@
+package me.lethunderhawk.bazaarflux.util.loottables;
+
+import java.util.*;
+import java.util.function.Supplier;
+
+public class RiggedChanceGenerator<T> {
+
+    private static final Random RANDOM = new Random();
+
+    // Sequential rigged steps
+    private final List<ChanceStep<T>> steps = new ArrayList<>();
+
+    // Final weighted pool
+    private final List<WeightedChance<T>> finalChances = new ArrayList<>();
+
+    private Supplier<T> fallback;
+
+    /* =======================
+       PRE-STEPS (RIGGED)
+       ======================= */
+
+    public RiggedChanceGenerator<T> addStep(int numerator, int denominator, T result) {
+        steps.add(new ChanceStep<>(numerator, denominator, () -> result));
+        return this;
+    }
+
+    public RiggedChanceGenerator<T> addStep(int numerator, int denominator, Supplier<T> resultSupplier) {
+        steps.add(new ChanceStep<>(numerator, denominator, resultSupplier));
+        return this;
+    }
+
+    /* =======================
+       FINAL POOL (DICE)
+       ======================= */
+
+    /**
+     * Adds a weighted chance to the final roll.
+     * Weights do NOT need to sum to 100.
+     */
+    public RiggedChanceGenerator<T> addChance(int weight, T result) {
+        finalChances.add(new WeightedChance<>(weight, () -> result));
+        return this;
+    }
+
+    public RiggedChanceGenerator<T> addChance(int weight, Supplier<T> resultSupplier) {
+        finalChances.add(new WeightedChance<>(weight, resultSupplier));
+        return this;
+    }
+
+    /* =======================
+       FALLBACK
+       ======================= */
+
+    public RiggedChanceGenerator<T> fallback(T fallbackResult) {
+        this.fallback = () -> fallbackResult;
+        return this;
+    }
+
+    public RiggedChanceGenerator<T> fallback(Supplier<T> fallbackSupplier) {
+        this.fallback = fallbackSupplier;
+        return this;
+    }
+
+    /* =======================
+       EXECUTION
+       ======================= */
+
+    public T roll() {
+
+        // 1. Rigged pre-steps
+        for (ChanceStep<T> step : steps) {
+            if (step.roll()) {
+                return step.result();
+            }
+        }
+
+        // 2. Final weighted dice
+        if (!finalChances.isEmpty()) {
+            int totalWeight = finalChances.stream().mapToInt(c -> c.weight).sum();
+            int roll = RANDOM.nextInt(totalWeight);
+
+            int cumulative = 0;
+            for (WeightedChance<T> chance : finalChances) {
+                cumulative += chance.weight;
+                if (roll < cumulative) {
+                    return chance.result();
+                }
+            }
+        }
+
+        // 3. Fallback
+        if (fallback == null) {
+            throw new IllegalStateException("No fallback defined");
+        }
+        return fallback.get();
+    }
+
+    /* =======================
+       INTERNAL CLASSES
+       ======================= */
+
+    private static final class ChanceStep<T> {
+        private final int numerator;
+        private final int denominator;
+        private final Supplier<T> result;
+
+        private ChanceStep(int numerator, int denominator, Supplier<T> result) {
+            if (numerator <= 0 || denominator <= 0 || numerator > denominator) {
+                throw new IllegalArgumentException("Invalid chance: " + numerator + "/" + denominator);
+            }
+            this.numerator = numerator;
+            this.denominator = denominator;
+            this.result = result;
+        }
+
+        private boolean roll() {
+            return RANDOM.nextInt(denominator) < numerator;
+        }
+
+        private T result() {
+            return result.get();
+        }
+    }
+
+    private static final class WeightedChance<T> {
+        private final int weight;
+        private final Supplier<T> result;
+
+        private WeightedChance(int weight, Supplier<T> result) {
+            if (weight <= 0) {
+                throw new IllegalArgumentException("Weight must be > 0");
+            }
+            this.weight = weight;
+            this.result = result;
+        }
+
+        private T result() {
+            return result.get();
+        }
+    }
+}
+

+ 5 - 4
src/main/java/me/lethunderhawk/clans/Clan.java

@@ -1,5 +1,6 @@
 package me.lethunderhawk.clans;
 
+import me.lethunderhawk.bazaarflux.service.Services;
 import me.lethunderhawk.clans.claim.Claim;
 import me.lethunderhawk.clans.settings.ClanSetting;
 import me.lethunderhawk.clans.settings.ClanSettings;
@@ -71,10 +72,10 @@ public class Clan {
         Player owner = Bukkit.getPlayer(getOwnerUUID());
         Player player = Bukkit.getPlayer(playerUUID);
         if(owner == null || player == null) return;
-
-        ClanModule.sendText(owner, Component.text("=== Request to join ===", NamedTextColor.GOLD));
-        ClanModule.sendText(owner, Component.text("The player " + player.getName() + " wants to join your clan!", NamedTextColor.GRAY));
-        ClanModule.sendText(owner, Component.text("[Decline] ", NamedTextColor.RED).clickEvent(ClickEvent.runCommand("/clan declineRequest " + player.getName()))
+        ClanModule module = Services.get(ClanModule.class);
+        module.sendText(owner, Component.text("=== Request to join ===", NamedTextColor.GOLD));
+        module.sendText(owner, Component.text("The player " + player.getName() + " wants to join your clan!", NamedTextColor.GRAY));
+        module.sendText(owner, Component.text("[Decline] ", NamedTextColor.RED).clickEvent(ClickEvent.runCommand("/clan declineRequest " + player.getName()))
                 .append(Component.text("[Accept]", NamedTextColor.GREEN).clickEvent(ClickEvent.runCommand("/clan acceptRequest " + player.getName()))));
     }
 

+ 2 - 1
src/main/java/me/lethunderhawk/clans/claim/ClaimListener.java

@@ -13,6 +13,7 @@ import org.bukkit.block.Container;
 import org.bukkit.block.data.BlockData;
 import org.bukkit.block.data.Openable;
 import org.bukkit.block.data.Powerable;
+import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.Player;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
@@ -91,7 +92,7 @@ public class ClaimListener implements Listener {
         if(e.getEntity() instanceof Player && preventForAll(e.getEntity().getLocation(), ClanSetting.ALLOW_HITTING_PLAYERS)){
             e.setCancelled(true);
             e.getDamager().sendMessage(Component.text("You cant damage other Players in this region!", NamedTextColor.RED));
-        }else if(!(e.getEntity() instanceof Player) && preventForAll(e.getEntity().getLocation(), ClanSetting.ALLOW_HITTING_MOBS)){
+        }else if(e.getEntity() instanceof LivingEntity && preventForAll(e.getEntity().getLocation(), ClanSetting.ALLOW_HITTING_MOBS)){
             e.setCancelled(true);
             e.getDamager().sendMessage(Component.text("You cant damage Mobs in this region!", NamedTextColor.RED));
         }

+ 4 - 3
src/main/java/me/lethunderhawk/clans/command/ClanCommand.java

@@ -10,8 +10,8 @@ import me.lethunderhawk.clans.ClanModule;
 import me.lethunderhawk.clans.claim.Claim;
 import me.lethunderhawk.clans.claim.ClaimManager;
 import me.lethunderhawk.clans.gui.ClanMenu;
-import me.lethunderhawk.custom.item.concrete.ClaimTool;
-import me.lethunderhawk.main.Main;
+import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
+import me.lethunderhawk.custom.item.abstraction.registry.CustomItemRegistry;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.format.NamedTextColor;
 import net.kyori.adventure.text.format.TextDecoration;
@@ -59,7 +59,8 @@ public class ClanCommand extends CustomCommand{
 
     private void getClaimTool(CommandSender sender, String[] strings) {
         if(!(sender instanceof Player p)) return;
-        p.getInventory().addItem(new ClaimTool(Services.get(Main.class)).createItem());
+        CustomItemRegistry registry = Services.get(CustomItemRegistry.class);
+        p.getInventory().addItem(new ItemInstance(registry.get("claim_tool")).buildItemStack());
         module.sendText(p, Component.text("The tool magically appears in your inventory! How convenient!"));
     }
 

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

@@ -2,23 +2,31 @@ package me.lethunderhawk.custom.item;
 
 import me.lethunderhawk.bazaarflux.service.Services;
 import me.lethunderhawk.bazaarflux.util.interfaces.BazaarFluxModule;
+import me.lethunderhawk.custom.item.abstraction.definition.DefaultItemDefinitionValidator;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinitionLoader;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinitionValidator;
+import me.lethunderhawk.custom.item.abstraction.registry.AbilityRegistry;
+import me.lethunderhawk.custom.item.abstraction.registry.CustomItemRegistry;
+import me.lethunderhawk.custom.item.abstraction.runtime.AbilityDispatchService;
+import me.lethunderhawk.custom.item.concrete.ability.TestAbilityHandler;
 import me.lethunderhawk.custom.item.command.CustomItemCommand;
-import me.lethunderhawk.custom.item.concrete.ClaimTool;
+import me.lethunderhawk.custom.item.concrete.ability.ClaimToolAbility;
+import me.lethunderhawk.custom.item.concrete.ability.HyperionAbility;
+import me.lethunderhawk.custom.item.concrete.ability.RollingDiceAbility;
 import me.lethunderhawk.custom.item.listener.CustomItemListener;
-import me.lethunderhawk.custom.item.manager.CustomItemManager;
 import me.lethunderhawk.main.Main;
 import org.bukkit.command.CommandSender;
 import org.bukkit.event.HandlerList;
 
+import java.io.File;
+import java.util.Map;
+
 public class CustomItemModule extends BazaarFluxModule{
-    private CustomItemManager itemManager;
     private CustomItemListener listener;
 
     public CustomItemModule() {
     }
-    private void registerCustomItems(){
-        itemManager.registerItem(new ClaimTool(plugin));
-    }
 
     @Override
     public String getPrefix() {
@@ -27,32 +35,53 @@ public class CustomItemModule extends BazaarFluxModule{
 
     @Override
     public void onEnable(){
-        itemManager = new CustomItemManager(plugin);
-        registerCustomItems();
-        registerCommands();
+        AbilityRegistry abilityRegistry = new AbilityRegistry();
+        registerAbilities(abilityRegistry);
+
+        ItemDefinitionValidator validator = new DefaultItemDefinitionValidator();
+        ItemDefinitionLoader loader = new ItemDefinitionLoader(validator);
+
+        Main main = Services.get(Main.class);
+        File itemsDir = new File(main.getDataFolder(), "custom/items");
+
+        // --- Load definitions from disk ---
+        Map<String, ItemDefinition> definitions = loader.loadAll(itemsDir);
+
+        CustomItemRegistry itemRegistry = new CustomItemRegistry()
+                .fromRegistry(definitions);
 
+        // --- Reload listener ---
 
-        listener = new CustomItemListener(itemManager);
-        plugin.getServer().getPluginManager().registerEvents(
-                listener,
-                plugin
+        listener = new CustomItemListener(
+                new AbilityDispatchService(itemRegistry, abilityRegistry)
         );
+        main.getServer().getPluginManager().registerEvents(listener, main);
+
+        // --- Services ---
+        Services.register(CustomItemRegistry.class, itemRegistry);
+        Services.register(ItemDefinitionLoader.class, loader);
+        Services.register(ItemDefinitionValidator.class, validator);
+        Services.register(AbilityRegistry.class, abilityRegistry);
+        registerCommands();
+    }
+    private static void registerAbilities(AbilityRegistry abilityRegistry) {
+        abilityRegistry.register("test_handler", new TestAbilityHandler());
+        abilityRegistry.register("rolling_dice", new RollingDiceAbility());
+        abilityRegistry.register("claim_tool", new ClaimToolAbility());
+        abilityRegistry.register("hyperion", new HyperionAbility());
     }
 
     private void registerCommands() {
-        CustomItemCommand customItemCommand = new CustomItemCommand(itemManager, this);
-        Services.get(Main.class).getCommand("customItem").setExecutor(customItemCommand);
-        Services.get(Main.class).getCommand("customItem").setTabCompleter(customItemCommand);
+        CustomItemCommand customItemCommand = new CustomItemCommand(this);
+        Services.get(Main.class).getCommand("customItems").setExecutor(customItemCommand);
+        Services.get(Main.class).getCommand("customItems").setTabCompleter(customItemCommand);
     }
 
     @Override
     public void onDisable() {
-        HandlerList.unregisterAll(listener);
-        itemManager = null;
-    }
-
-    public CustomItemManager getManager() {
-        return itemManager;
+        if (listener != null) {
+            HandlerList.unregisterAll(listener);
+        }
     }
 
     public void reload(CommandSender sender, String[] strings) {

+ 0 - 109
src/main/java/me/lethunderhawk/custom/item/abstraction/CustomItem.java

@@ -1,109 +0,0 @@
-package me.lethunderhawk.custom.item.abstraction;
-
-import net.kyori.adventure.text.Component;
-import org.bukkit.Material;
-import org.bukkit.NamespacedKey;
-import org.bukkit.event.player.PlayerInteractEvent;
-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.List;
-
-/**
- * Abstract base class for all custom items
- */
-public abstract class CustomItem {
-    protected final JavaPlugin plugin;
-    protected final String itemId;
-    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;
-
-    public CustomItem(JavaPlugin plugin, String itemId, Component displayName, List<Component> lore, Material baseMaterial) {
-        this.plugin = plugin;
-        this.itemId = itemId;
-        this.displayName = displayName;
-        this.lore = lore;
-        this.baseMaterial = baseMaterial;
-        this.itemIdKey = new NamespacedKey(plugin, "custom_item_id");
-    }
-
-
-    /**
-     * Create the ItemStack with embedded metadata
-     */
-    public ItemStack createItem() {
-        ItemStack item = new ItemStack(baseMaterial);
-        ItemMeta meta = item.getItemMeta();
-
-        // Set visible properties
-        meta.displayName(displayName);
-        if (lore != null && !lore.isEmpty() && meta.hasLore()) {
-            meta.lore(lore);
-        }
-
-        // Embed the unique identifier in Persistent Data Container
-        meta.getPersistentDataContainer().set(
-                itemIdKey,
-                PersistentDataType.STRING,
-                itemId
-        );
-
-        item.setItemMeta(meta);
-
-        // Apply any additional customizations
-        applyCustomizations(item);
-
-        return item;
-    }
-
-    /**
-     * Check if an ItemStack is an instance of this custom item
-     */
-    public boolean isCustomItem(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 getCustomItemId(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) {
-        // Can be overridden by subclasses
-    }
-
-    /**
-     * Called when a player right-clicks with this item
-     */
-    public abstract void onRightClick(PlayerInteractEvent event);
-
-    /**
-     * Called when a player left-clicks with this item
-     */
-    public abstract void onLeftClick(PlayerInteractEvent event);
-
-    // Getters
-    public String getItemId() { return itemId; }
-    public Component getDisplayName() { return displayName; }
-    public Material getBaseMaterial() { return baseMaterial; }
-    public NamespacedKey getItemIdKey() { return itemIdKey; }
-}

+ 11 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/ability/AbilityContext.java

@@ -0,0 +1,11 @@
+package me.lethunderhawk.custom.item.abstraction.ability;
+
+import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
+
+public record AbilityContext(
+        Player player,
+        ItemInstance itemInstance,
+        Event event
+) {}

+ 65 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/ability/AbilityDefinition.java

@@ -0,0 +1,65 @@
+package me.lethunderhawk.custom.item.abstraction.ability;
+
+import me.lethunderhawk.bazaarflux.util.itemdesign.LoreDesigner;
+import net.kyori.adventure.text.Component;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+public class AbilityDefinition implements Serializable {
+
+    private final AbilityTrigger trigger;
+    private final String handlerId;
+    private final Map<String, String> params;
+    private final String name;
+    private final String description;
+
+    public AbilityDefinition(
+            AbilityTrigger trigger,
+            String handlerId,
+            String name,
+            String description,
+            Map<String, String> params
+    ) {
+        this.trigger = trigger;
+        this.handlerId = handlerId;
+        this.description = description;
+        this.params = Map.copyOf(params);
+        this.name = name;
+    }
+    public String getName(){
+        return this.name;
+    }
+    public AbilityTrigger trigger() {
+        return trigger;
+    }
+
+    public List<Component> getLore(String firstLine) {
+        String replacedDescription = getDescriptionWithParams();
+        return LoreDesigner.createLore(replacedDescription, firstLine);
+    }
+    public String handlerId() {
+        return handlerId;
+    }
+
+    public Map<String, String> getParams() {
+        return params;
+    }
+    public String getDescriptionWithParams() {
+        String result = description;
+        for (Map.Entry<String, String> entry : params.entrySet()) {
+            result = result.replace("{" + entry.getKey() + "}", entry.getValue());
+        }
+        return result;
+    }
+    public long getCooldownMillis() {
+        String cd = params.get("cooldownMillis");
+        if (cd == null) return 0L;
+        try {
+            return Long.parseLong(cd);
+        } catch (NumberFormatException e) {
+            return 0L;
+        }
+    }
+}

+ 47 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/ability/AbilityTrigger.java

@@ -0,0 +1,47 @@
+package me.lethunderhawk.custom.item.abstraction.ability;
+
+public enum AbilityTrigger {
+    LEFT_CLICK("LEFT CLICK", null), // top-level
+    LEFT_CLICK_BLOCK("LEFT CLICK", LEFT_CLICK),
+    LEFT_CLICK_AIR("LEFT CLICK", LEFT_CLICK),
+
+    RIGHT_CLICK("RIGHT CLICK", null),
+    RIGHT_CLICK_BLOCK("RIGHT CLICK", RIGHT_CLICK),
+    RIGHT_CLICK_AIR("RIGHT CLICK", RIGHT_CLICK),
+
+    SNEAK_LEFT_CLICK("SNEAK LEFT CLICK", null),
+    SNEAK_RIGHT_CLICK("SNEAK RIGHT CLICK", null),
+
+    ON_HIT("ON HIT", null),
+    ON_INTERACT("ON INTERACT", null),
+    ON_SNEAK("ON SNEAK", null);
+
+    private final String displayName;
+    private final AbilityTrigger parent;
+
+    AbilityTrigger(String displayName, AbilityTrigger parent) {
+        this.displayName = displayName;
+        this.parent = parent;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public AbilityTrigger getParent() {
+        return parent;
+    }
+
+    /**
+     * Checks if this trigger is equivalent to or a subtype of the given other trigger.
+     * E.g. LEFT_CLICK_BLOCK.isSubTypeOf(LEFT_CLICK) == true
+     */
+    public boolean isSubTypeOf(AbilityTrigger other) {
+        AbilityTrigger current = this;
+        while (current != null) {
+            if (current == other) return true;
+            current = current.getParent();
+        }
+        return false;
+    }
+}

+ 8 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/definition/DefaultItemDefinitionValidator.java

@@ -0,0 +1,8 @@
+package me.lethunderhawk.custom.item.abstraction.definition;
+
+public class DefaultItemDefinitionValidator implements ItemDefinitionValidator {
+    @Override
+    public void validate(ItemDefinition definition) {
+
+    }
+}

+ 52 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/definition/ItemDefinition.java

@@ -0,0 +1,52 @@
+package me.lethunderhawk.custom.item.abstraction.definition;
+
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityDefinition;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public final class ItemDefinition implements Serializable {
+
+    private final String id;
+    private final int version;
+
+    private final ItemVisualDefinition visual;
+    private final Map<String, Double> stats;
+    private final List<AbilityDefinition> abilities;
+
+    public ItemDefinition(
+            String id,
+            int version,
+            ItemVisualDefinition visual,
+            Map<String, Double> stats,
+            List<AbilityDefinition> abilities
+    ) {
+        this.id = Objects.requireNonNull(id);
+        this.version = version;
+        this.visual = Objects.requireNonNull(visual);
+        this.stats = Map.copyOf(stats);
+        this.abilities = List.copyOf(abilities);
+    }
+
+    public String id() {
+        return id;
+    }
+
+    public int version() {
+        return version;
+    }
+
+    public ItemVisualDefinition visual() {
+        return visual;
+    }
+
+    public Map<String, Double> stats() {
+        return stats;
+    }
+
+    public List<AbilityDefinition> abilities() {
+        return abilities;
+    }
+}

+ 209 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/definition/ItemDefinitionLoader.java

@@ -0,0 +1,209 @@
+package me.lethunderhawk.custom.item.abstraction.definition;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.bazaarflux.util.itemdesign.LoreDesigner;
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityDefinition;
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityTrigger;
+import me.lethunderhawk.main.Main;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Material;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.YamlConfiguration;
+
+import java.io.File;
+import java.util.*;
+
+public final class ItemDefinitionLoader {
+
+    private final ItemDefinitionValidator validator;
+
+    public ItemDefinitionLoader(ItemDefinitionValidator validator) {
+        this.validator = validator;
+    }
+
+
+    /**
+     * @param directory to load the files from
+     * @return A {@link Map} of the item_id and the corresponding ItemDefinition
+     */
+
+
+    public Map<String, ItemDefinition> loadAll(File directory) {
+        Map<String, ItemDefinition> definitions = new HashMap<>();
+
+        for (File file : Objects.requireNonNull(directory.listFiles())) {
+            ItemDefinition def = load(file);
+            if (definitions.containsKey(def.id())) {
+                throw new IllegalStateException("Duplicate item id: " + def.id());
+            }
+            definitions.put(def.id(), def);
+        }
+        return Map.copyOf(definitions);
+    }
+
+    private ItemDefinition load(File file) {
+        YamlConfiguration cfg = YamlConfiguration.loadConfiguration(file);
+
+        // ---------- BASIC METADATA ----------
+
+        String id = cfg.getString("id");
+        if (id == null) {
+            throw new IllegalStateException("Missing 'id' in " + file.getName());
+        }
+
+        int version = cfg.getInt("version", -1);
+        if (version < 1) {
+            throw new IllegalStateException(
+                    "Invalid or missing 'version' in " + file.getName()
+            );
+        }
+
+        // ---------- VISUAL DEFINITION ----------
+
+        String material = cfg.getString("material");
+        if (material == null) {
+            throw new IllegalStateException(
+                    "Missing 'material' in item " + id
+            );
+        }
+
+        String name = cfg.getString("name");
+        if (name == null) {
+            throw new IllegalStateException(
+                    "Missing 'name' in item " + id
+            );
+        }
+        Material realMaterial = Material.getMaterial(material);
+        List<Component> lore =
+                LoreDesigner.createLore(cfg.getString("lore"), "This is a reasonable lore");
+
+        String headTexture =
+                cfg.getString("head.texture", null);
+
+        ItemVisualDefinition visual = new ItemVisualDefinition(
+                realMaterial,
+                name,
+                lore,
+                headTexture
+        );
+
+        // ---------- STATS ----------
+
+        Map<String, Double> stats = new HashMap<>();
+
+        ConfigurationSection statsSection =
+                cfg.getConfigurationSection("stats");
+
+        if (statsSection != null) {
+            for (String key : statsSection.getKeys(false)) {
+                Object raw = statsSection.get(key);
+
+                if (!(raw instanceof Number)) {
+                    throw new IllegalStateException(
+                            "Stat '" + key + "' in item " + id + " must be numeric"
+                    );
+                }
+
+                stats.put(key, ((Number) raw).doubleValue());
+            }
+        }
+
+        // ---------- ABILITIES ----------
+
+        List<AbilityDefinition> abilities = new ArrayList<>();
+
+        ConfigurationSection abilitiesSection =
+                cfg.getConfigurationSection("abilities");
+
+        if (abilitiesSection != null) {
+            for (String abilityKey : abilitiesSection.getKeys(false)) {
+
+                ConfigurationSection abilityCfg =
+                        abilitiesSection.getConfigurationSection(abilityKey);
+
+                if (abilityCfg == null) {
+                    throw new IllegalStateException(
+                            "Invalid ability entry '" + abilityKey + "' in item " + id
+                    );
+                }
+
+                String triggerRaw =
+                        abilityCfg.getString("trigger");
+
+                if (triggerRaw == null) {
+                    throw new IllegalStateException(
+                            "Missing ability trigger in item " + id
+                    );
+                }
+
+                AbilityTrigger trigger;
+                try {
+                    trigger = AbilityTrigger.valueOf(triggerRaw.toUpperCase());
+                } catch (IllegalArgumentException ex) {
+                    throw new IllegalStateException(
+                            "Invalid ability trigger '" + triggerRaw +
+                                    "' in item " + id
+                    );
+                }
+
+                String handler_id =
+                        abilityCfg.getString("handler_id");
+                String abilityName =
+                        abilityCfg.getString("name");
+                String abilityDescription =
+                        abilityCfg.getString("description");
+
+                if (handler_id == null) {
+                    throw new IllegalStateException(
+                            "Missing ability handler id in item " + id
+                    );
+                }
+
+                if (abilityName == null) {
+                    Services.get(Main.class).getLogger().info("Missing ability Name in item " + id + ". Is this wanted? Using handler ID: " + handler_id + " as default.");
+                    abilityName = handler_id;
+                }
+
+                if (abilityDescription == null) {
+                    Services.get(Main.class).getLogger().info("Missing ability Description in item " + id + ". Is this wanted?");
+                }
+
+                Map<String, String> params = new HashMap<>();
+
+                ConfigurationSection paramsSection =
+                        abilityCfg.getConfigurationSection("params");
+
+                if (paramsSection != null) {
+                    for (String paramKey : paramsSection.getKeys(false)) {
+                        Object raw = paramsSection.get(paramKey);
+                        params.put(paramKey, String.valueOf(raw));
+                    }
+                }
+
+                abilities.add(new AbilityDefinition(
+                        trigger,
+                        handler_id,
+                        abilityName,
+                        abilityDescription,
+                        params
+                ));
+            }
+        }
+
+        // ---------- BUILD DEFINITION ----------
+
+        ItemDefinition definition = new ItemDefinition(
+                id,
+                version,
+                visual,
+                stats,
+                abilities
+        );
+
+        // ---------- VALIDATE ----------
+
+        validator.validate(definition);
+
+        return definition;
+    }
+}

+ 5 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/definition/ItemDefinitionValidator.java

@@ -0,0 +1,5 @@
+package me.lethunderhawk.custom.item.abstraction.definition;
+
+public interface ItemDefinitionValidator {
+    void validate(ItemDefinition definition);
+}

+ 47 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/definition/ItemVisualDefinition.java

@@ -0,0 +1,47 @@
+package me.lethunderhawk.custom.item.abstraction.definition;
+
+import me.lethunderhawk.bazaarflux.util.itemdesign.LoreDesigner;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Material;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Optional;
+
+public final class ItemVisualDefinition implements Serializable {
+
+    private final Material material;
+    private final Optional<String> headTexture;
+
+    private final String displayName;
+    private final List<Component> loreTemplate;
+
+    public ItemVisualDefinition(
+            Material material,
+            String displayName,
+            List<Component> loreTemplate,
+            String headTexture
+    ) {
+        this.material = material;
+        this.displayName = displayName;
+        this.loreTemplate = List.copyOf(loreTemplate);
+        this.headTexture = Optional.ofNullable(headTexture);
+    }
+
+    public Material material() {
+        return material;
+    }
+
+    public Optional<String> headTexture() {
+        return headTexture;
+    }
+
+    public Component displayName() {
+        return LoreDesigner.createSingle(displayName);
+    }
+
+    public List<Component> loreTemplate() {
+        return loreTemplate;
+    }
+}
+

+ 185 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/definition/parser/ItemYamlParser.java

@@ -0,0 +1,185 @@
+package me.lethunderhawk.custom.item.abstraction.definition.parser;
+
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityTrigger;
+import me.lethunderhawk.custom.item.abstraction.definition.raw.RawAbilityData;
+import me.lethunderhawk.custom.item.abstraction.definition.raw.RawItemData;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.YamlConfiguration;
+
+import java.util.*;
+
+public final class ItemYamlParser {
+
+    private ItemYamlParser() {}
+
+    public static RawItemData parse(YamlConfiguration cfg) {
+
+        String id = requireString(cfg, "id");
+        int version = requireInt(cfg, "version", v -> v >= 1);
+
+        String material = requireString(cfg, "material");
+        String name = requireString(cfg, "name");
+
+        String headTexture = cfg.getString("head.texture");
+
+        List<String> lore = cfg.getStringList("lore");
+
+        Map<String, Double> stats = parseStats(cfg);
+
+        List<RawAbilityData> abilities = parseAbilities(cfg);
+
+        return new RawItemData(
+                id,
+                version,
+                material,
+                headTexture,
+                name,
+                List.copyOf(lore),
+                Map.copyOf(stats),
+                List.copyOf(abilities)
+        );
+    }
+
+    // -------------------------------
+    // Stats
+    // -------------------------------
+
+    private static Map<String, Double> parseStats(YamlConfiguration cfg) {
+        ConfigurationSection section = cfg.getConfigurationSection("stats");
+        if (section == null) return Map.of();
+
+        Map<String, Double> stats = new HashMap<>();
+
+        for (String key : section.getKeys(false)) {
+            Object raw = section.get(key);
+
+            if (!(raw instanceof Number number)) {
+                error("stats." + key, "must be numeric");
+            }
+
+            stats.put(key, ((Number)raw).doubleValue());
+        }
+
+        return stats;
+    }
+
+    // -------------------------------
+    // Abilities
+    // -------------------------------
+
+    private static List<RawAbilityData> parseAbilities(YamlConfiguration cfg) {
+        ConfigurationSection section = cfg.getConfigurationSection("abilities");
+        if (section == null) return List.of();
+
+        List<RawAbilityData> abilities = new ArrayList<>();
+
+        int index = 0;
+        for (Map<?, ?> map : section.getMapList("")) {
+            index++;
+
+            String basePath = "abilities[" + index + "]";
+
+            AbilityTrigger trigger = parseTrigger(map, basePath);
+            int order = parseOrder(map, basePath);
+            String handler = require(map, basePath, "handler");
+
+            Map<String, String> params = parseParams(map, basePath);
+
+            abilities.add(new RawAbilityData(
+                    trigger,
+                    order,
+                    handler,
+                    Map.copyOf(params)
+            ));
+        }
+
+        abilities.sort(Comparator.comparingInt(RawAbilityData::order));
+        return abilities;
+    }
+
+    private static AbilityTrigger parseTrigger(Map<?, ?> map, String basePath) {
+        Object raw = map.get("trigger");
+        if (raw == null) {
+            error(basePath + ".trigger", "is required");
+        }
+
+        try {
+            return AbilityTrigger.valueOf(raw.toString().toUpperCase());
+        } catch (IllegalArgumentException ex) {
+            error(basePath + ".trigger", "invalid trigger: " + raw);
+            return null; // unreachable
+        }
+    }
+
+    private static int parseOrder(Map<?, ?> map, String basePath) {
+        Object raw = map.getOrDefault("order", null);
+
+        if (!(raw instanceof Number)) {
+            error(basePath + ".order", "must be a number");
+        }
+
+        return ((Number) raw).intValue();
+    }
+
+    private static Map<String, String> parseParams(Map<?, ?> map, String basePath) {
+        Object raw = map.get("params");
+        if (raw == null) return Map.of();
+        if (!(raw instanceof Map<?, ?>)) {
+            error(basePath + ".params", "must be a map");
+        }
+        Map<?,?> paramMap = (Map<?,?>) raw;
+
+        Map<String, String> params = new HashMap<>();
+
+        for (Map.Entry<?, ?> entry : paramMap.entrySet()) {
+            params.put(
+                    entry.getKey().toString(),
+                    String.valueOf(entry.getValue())
+            );
+        }
+
+        return params;
+    }
+
+    // -------------------------------
+    // Helpers
+    // -------------------------------
+
+    private static String requireString(YamlConfiguration cfg, String path) {
+        String value = cfg.getString(path);
+        if (value == null) {
+            error(path, "is required");
+        }
+        return value;
+    }
+
+    private static int requireInt(
+            YamlConfiguration cfg,
+            String path,
+            java.util.function.IntPredicate validator
+    ) {
+        if (!cfg.contains(path)) {
+            error(path, "is required");
+        }
+
+        int value = cfg.getInt(path, Integer.MIN_VALUE);
+
+        if (!validator.test(value)) {
+            error(path, "has invalid value: " + value);
+        }
+
+        return value;
+    }
+
+    private static String require(Map<?, ?> map, String basePath, String key) {
+        Object value = map.get(key);
+        if (value == null) {
+            error(basePath + "." + key, "is required");
+        }
+        return value.toString();
+    }
+
+    private static void error(String path, String message) {
+        throw new IllegalStateException("YAML error at '" + path + "': " + message);
+    }
+}

+ 11 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/definition/raw/RawAbilityData.java

@@ -0,0 +1,11 @@
+package me.lethunderhawk.custom.item.abstraction.definition.raw;
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityTrigger;
+
+import java.util.Map;
+
+public record RawAbilityData(
+        AbilityTrigger trigger,
+        int order,
+        String handler,
+        Map<String, String> params
+) {}

+ 22 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/definition/raw/RawItemData.java

@@ -0,0 +1,22 @@
+package me.lethunderhawk.custom.item.abstraction.definition.raw;
+
+import java.util.List;
+import java.util.Map;
+
+public record RawItemData(
+
+        // meta
+        String id,
+        int version,
+
+        // visuals
+        String material,
+        String headTexture,
+        String name,
+        List<String> lore,
+
+        // gameplay
+        Map<String, Double> stats,
+        List<RawAbilityData> abilities
+
+) {}

+ 11 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/handling/AbilityHandler.java

@@ -0,0 +1,11 @@
+package me.lethunderhawk.custom.item.abstraction.handling;
+
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityContext;
+
+public interface AbilityHandler {
+
+    void execute(
+            AbilityContext context,
+            ResolvedParams params
+    );
+}

+ 143 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/handling/ResolvedParams.java

@@ -0,0 +1,143 @@
+package me.lethunderhawk.custom.item.abstraction.handling;
+
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityDefinition;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
+import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public final class ResolvedParams {
+
+    private final Map<String, Object> values;
+
+    public ResolvedParams(Map<String, Object> values) {
+        this.values = Map.copyOf(values);
+    }
+
+    public static ResolvedParams resolve(
+            AbilityDefinition ability,
+            ItemDefinition def,
+            ItemInstance instance) {
+        Map<String, Object> resolved = new HashMap<>();
+
+        for (Map.Entry<String, String> entry : ability.getParams().entrySet()) {
+            String key = entry.getKey();
+            String rawValue = entry.getValue();
+
+            Object value;
+
+            if (rawValue.startsWith("{") && rawValue.endsWith("}")) {
+                value = resolvePlaceholder(rawValue, def, instance);
+            } else {
+                value = parseLiteral(rawValue);
+            }
+
+            if (value == null) {
+                throw new IllegalStateException(
+                        "Unable to resolve parameter '" + key + "' with value '" + rawValue +
+                                "' for ability handler '" + ability.handlerId() + "'"
+                );
+            }
+
+            resolved.put(key, value);
+        }
+
+        return new ResolvedParams(resolved);
+    }
+    private static Object resolvePlaceholder(
+            String placeholder,
+            ItemDefinition def,
+            ItemInstance instance
+    ) {
+        // Strip { and }
+        String content = placeholder.substring(1, placeholder.length() - 1);
+        String[] parts = content.split("\\.", 2);
+
+        if (parts.length != 2) {
+            throw new IllegalArgumentException("Invalid placeholder format: " + placeholder);
+        }
+
+        String scope = parts[0];
+        String key = parts[1];
+
+        return switch (scope) {
+            case "stat" -> def.stats().get(key);
+            case "instance" -> instance.data().get(key);
+            default -> throw new IllegalArgumentException(
+                    "Unknown placeholder scope '" + scope + "' in " + placeholder
+            );
+        };
+    }
+    private static Object parseLiteral(String raw) {
+        // Boolean
+        if (raw.equalsIgnoreCase("true") || raw.equalsIgnoreCase("false")) {
+            return Boolean.parseBoolean(raw);
+        }
+
+        // Integer
+        try {
+            if (!raw.contains(".")) {
+                return Integer.parseInt(raw);
+            }
+        } catch (NumberFormatException ignored) {}
+
+        // Double
+        try {
+            return Double.parseDouble(raw);
+        } catch (NumberFormatException ignored) {}
+
+        // Fallback: string
+        return raw;
+    }
+
+    public double getDouble(String key) {
+        Object val = values.get(key);
+        if (val instanceof Number number) {
+            return number.doubleValue();
+        }
+        throw new IllegalArgumentException("Value for key '" + key + "' is not a number");
+    }
+
+    public double getDoubleOrDefault(String key, double defaultValue) {
+        Object val = values.get(key);
+        if (val instanceof Number number) {
+            return number.doubleValue();
+        }
+        return defaultValue;
+    }
+
+    public int getInt(String key) {
+        Object val = values.get(key);
+        if (val instanceof Number number) {
+            return number.intValue();
+        }
+        throw new IllegalArgumentException("Value for key '" + key + "' is not a number");
+    }
+
+    public int getIntOrDefault(String key, int defaultValue) {
+        Object val = values.get(key);
+        if (val instanceof Number number) {
+            return number.intValue();
+        }
+        return defaultValue;
+    }
+
+    public boolean getBoolean(String key) {
+        Object val = values.get(key);
+        if (val instanceof Boolean bool) {
+            return bool;
+        }
+        throw new IllegalArgumentException("Value for key '" + key + "' is not a boolean");
+    }
+
+    public boolean getBooleanOrDefault(String key, boolean defaultValue) {
+        Object val = values.get(key);
+        if (val instanceof Boolean bool) {
+            return bool;
+        }
+        return defaultValue;
+    }
+
+}
+

+ 51 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/instance/ItemInstance.java

@@ -0,0 +1,51 @@
+package me.lethunderhawk.custom.item.abstraction.instance;
+
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
+import me.lethunderhawk.custom.item.abstraction.visual.ItemRenderer;
+import org.bukkit.inventory.ItemStack;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+public final class ItemInstance implements Serializable {
+
+    private final String itemId;
+    private final ItemDefinition definition;
+    private int version;
+
+    // per-instance mutable data (cooldowns, rolls, XP, charges, etc.)
+    private final Map<String, Object> data = new HashMap<>();
+
+    public ItemInstance(ItemDefinition definition) {
+        this.definition = Objects.requireNonNull(definition);
+        this.itemId = definition.id();
+        this.version = definition.version();
+    }
+
+    public String itemId() {
+        return itemId;
+    }
+
+    public ItemDefinition definition() {
+        return definition;
+    }
+
+    public int version() {
+        return version;
+    }
+
+    public void setVersion(int version) {
+        this.version = version;
+    }
+
+    public Map<String, Object> data() {
+        return data;
+    }
+
+    public ItemStack buildItemStack() {
+        return ItemRenderer.render(this);
+    }
+}
+

+ 43 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/instance/ItemInstanceReader.java

@@ -0,0 +1,43 @@
+package me.lethunderhawk.custom.item.abstraction.instance;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
+import me.lethunderhawk.custom.item.abstraction.registry.CustomItemRegistry;
+import me.lethunderhawk.custom.item.abstraction.util.JsonUtil;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+
+import java.util.Map;
+
+public final class ItemInstanceReader {
+
+    private ItemInstanceReader() {}
+
+    public static ItemInstance read(ItemStack stack) {
+        if (stack == null || !stack.hasItemMeta()) return null;
+
+        ItemMeta meta = stack.getItemMeta();
+        PersistentDataContainer pdc = meta.getPersistentDataContainer();
+
+        String itemId = pdc.get(ItemPersistentData.ITEM_ID, PersistentDataType.STRING);
+        Integer version = pdc.get(ItemPersistentData.VERSION, PersistentDataType.INTEGER);
+
+        if (itemId == null || version == null) return null;
+        CustomItemRegistry registry = Services.get(CustomItemRegistry.class);
+        ItemDefinition definition = registry.get(itemId);
+        if (definition == null) return null;
+
+        ItemInstance instance = new ItemInstance(definition);
+
+        String json = pdc.get(ItemPersistentData.INSTANCE_DATA, PersistentDataType.STRING);
+        if (json != null) {
+            Map<String, Object> data = JsonUtil.fromJsonToMap(json);
+            instance.data().putAll(data);
+        }
+
+        return instance;
+    }
+}
+

+ 35 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/instance/ItemPersistentData.java

@@ -0,0 +1,35 @@
+package me.lethunderhawk.custom.item.abstraction.instance;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.custom.item.abstraction.util.JsonUtil;
+import me.lethunderhawk.main.Main;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+
+public final class ItemPersistentData {
+    private static final Main main = Services.get(Main.class);
+    static final NamespacedKey ITEM_ID =
+            new NamespacedKey(main, "item_id");
+
+    static final NamespacedKey VERSION =
+            new NamespacedKey(main, "item_version");
+
+    static final NamespacedKey INSTANCE_DATA =
+            new NamespacedKey(main, "item_data");
+
+    private ItemPersistentData() {}
+
+    public static void write(ItemMeta meta, ItemInstance instance) {
+        PersistentDataContainer pdc = meta.getPersistentDataContainer();
+
+        pdc.set(ITEM_ID, PersistentDataType.STRING, instance.itemId());
+        pdc.set(VERSION, PersistentDataType.INTEGER, instance.version());
+
+        // serialize instance.data() → JSON
+        String json = JsonUtil.toJson(instance.data());
+        pdc.set(INSTANCE_DATA, PersistentDataType.STRING, json);
+    }
+}
+

+ 12 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/migration/ItemMigration.java

@@ -0,0 +1,12 @@
+package me.lethunderhawk.custom.item.abstraction.migration;
+
+import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
+
+public interface ItemMigration {
+
+    int fromVersion();
+    int toVersion();
+
+    void migrate(ItemInstance instance);
+}
+

+ 27 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/migration/MigrationService.java

@@ -0,0 +1,27 @@
+package me.lethunderhawk.custom.item.abstraction.migration;
+
+import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
+
+import java.util.List;
+
+public final class MigrationService {
+
+    private final List<ItemMigration> migrations;
+
+    public MigrationService(List<ItemMigration> migrations) {
+        this.migrations = migrations;
+    }
+
+    public void migrate(ItemInstance instance, int targetVersion) {
+        for (ItemMigration migration : migrations) {
+            if (instance.version() == migration.fromVersion()) {
+                migration.migrate(instance);
+                instance.setVersion(migration.toVersion());
+            }
+        }
+
+        if (instance.version() != targetVersion) {
+            throw new IllegalStateException("Migration incomplete for " + instance.itemId());
+        }
+    }
+}

+ 30 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/registry/AbilityRegistry.java

@@ -0,0 +1,30 @@
+package me.lethunderhawk.custom.item.abstraction.registry;
+
+import me.lethunderhawk.custom.item.abstraction.handling.AbilityHandler;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public final class AbilityRegistry {
+
+    private final Map<String, AbilityHandler> handlers = new HashMap<>();
+
+    public void register(String id, AbilityHandler handler) {
+        if (exists(id)) {
+            throw new IllegalStateException("Duplicate ability handler: " + id);
+        }
+        handlers.put(id, handler);
+    }
+
+    public AbilityHandler get(String id) {
+        AbilityHandler handler = handlers.get(id);
+        if (handler == null) {
+            throw new IllegalStateException("Missing ability handler: " + id);
+        }
+        return handler;
+    }
+
+    public boolean exists(String id) {
+        return handlers.containsKey(id);
+    }
+}

+ 30 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/registry/CustomItemRegistry.java

@@ -0,0 +1,30 @@
+package me.lethunderhawk.custom.item.abstraction.registry;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
+import me.lethunderhawk.main.Main;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CustomItemRegistry {
+    private Map<String, ItemDefinition> definitions = new HashMap<>();
+
+    public CustomItemRegistry fromRegistry(Map<String, ItemDefinition> definitionsMap) {
+        this.definitions = new HashMap<>(definitionsMap);
+        return this;
+    }
+    public void add(ItemDefinition definition) {
+        definitions.put(definition.id(), definition);
+    }
+
+    public ItemDefinition get(String itemId) {
+        ItemDefinition definition = definitions.get(itemId);
+        if(definition == null) Services.get(Main.class).getLogger().warning("Item " + itemId + " not found");
+        return definition;
+    }
+
+    public Iterable<? extends ItemDefinition> getAll() {
+        return definitions.values();
+    }
+}

+ 67 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/runtime/AbilityDispatchService.java

@@ -0,0 +1,67 @@
+package me.lethunderhawk.custom.item.abstraction.runtime;
+
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityContext;
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityDefinition;
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityTrigger;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
+import me.lethunderhawk.custom.item.abstraction.handling.AbilityHandler;
+import me.lethunderhawk.custom.item.abstraction.handling.ResolvedParams;
+import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
+import me.lethunderhawk.custom.item.abstraction.instance.ItemInstanceReader;
+import me.lethunderhawk.custom.item.abstraction.registry.AbilityRegistry;
+import me.lethunderhawk.custom.item.abstraction.registry.CustomItemRegistry;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public final class AbilityDispatchService {
+
+    private final CustomItemRegistry itemRegistry;
+    private final AbilityRegistry abilityRegistry;
+    private final Map<String, Long> cooldowns = new HashMap<>();
+    public AbilityDispatchService(CustomItemRegistry itemRegistry, AbilityRegistry abilityRegistry) {
+        this.itemRegistry = itemRegistry;
+        this.abilityRegistry = abilityRegistry;
+    }
+
+    public void dispatch(
+            Player player,
+            ItemStack stack,
+            AbilityTrigger trigger,
+            Event event
+    ) {
+        ItemInstance instance = ItemInstanceReader.read(stack);
+        if (instance == null) return;
+
+        ItemDefinition def = itemRegistry.get(instance.itemId());
+
+        for (AbilityDefinition ability : def.abilities()) {
+            if (trigger.isSubTypeOf(ability.trigger())) {
+                String cooldownKey = player.getUniqueId() + ":" + ability.handlerId();
+
+                long lastUsed = cooldowns.getOrDefault(cooldownKey, 0L);
+                long cooldown = ability.getCooldownMillis();
+                long now = System.currentTimeMillis();
+
+                if (cooldown > 0 && (now - lastUsed) < cooldown) {
+                    long remaining = (cooldown - (now - lastUsed)) / 1000;
+                    player.sendMessage("§cAbility is on cooldown! Please wait " + (remaining + 1) + " seconds.");
+                    continue;
+                }
+
+                AbilityHandler handler = abilityRegistry.get(ability.handlerId());
+                handler.execute(
+                        new AbilityContext(player, instance, event),
+                        ResolvedParams.resolve(ability, def, instance)
+                );
+
+                if (cooldown > 0) {
+                    cooldowns.put(cooldownKey, now);
+                }
+            }
+        }
+    }
+}

+ 26 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/util/JsonUtil.java

@@ -0,0 +1,26 @@
+package me.lethunderhawk.custom.item.abstraction.util;
+
+import com.google.gson.Gson;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class JsonUtil {
+
+    private static final Gson GSON = new Gson();
+
+    private static final Type STRING_OBJECT_MAP =
+            new com.google.gson.reflect.TypeToken<Map<String, Object>>() {}.getType();
+
+    private JsonUtil() {}
+
+    public static String toJson(Map<String, Object> map) {
+        return GSON.toJson(map);
+    }
+
+    public static Map<String, Object> fromJsonToMap(String json) {
+        if (json == null || json.isEmpty()) return new HashMap<>();
+        return GSON.fromJson(json, STRING_OBJECT_MAP);
+    }
+}

+ 57 - 0
src/main/java/me/lethunderhawk/custom/item/abstraction/visual/ItemRenderer.java

@@ -0,0 +1,57 @@
+package me.lethunderhawk.custom.item.abstraction.visual;
+
+import me.lethunderhawk.bazaarflux.util.CustomHeadCreator;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemVisualDefinition;
+import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
+import me.lethunderhawk.custom.item.abstraction.instance.ItemPersistentData;
+import me.lethunderhawk.main.util.UnItalic;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class ItemRenderer {
+
+    private ItemRenderer() {}
+
+    public static ItemStack render(ItemInstance instance) {
+        ItemDefinition def = instance.definition();
+        ItemVisualDefinition visual = def.visual();
+
+        ItemStack stack = visual.headTexture().isPresent()
+                ? CustomHeadCreator.createCustomHead(visual.headTexture().get())
+                : new ItemStack(visual.material());
+
+        ItemMeta meta = stack.getItemMeta();
+
+        meta.displayName(visual.displayName());
+
+        List<Component> lore = new ArrayList<>();
+
+        def.stats().forEach((key, value) ->
+
+                lore.add(Component.text(key + ": " + (value >= 0 ? "+" : "") + value, NamedTextColor.RED))
+        );
+
+        def.abilities().forEach(ability -> {
+                    String abilityName = "Ability: " + ability.getName() + " [" + ability.trigger().getDisplayName() + "]";
+                    lore.add(Component.text(abilityName, NamedTextColor.GOLD));
+                    lore.addAll(ability.getLore(abilityName));
+                });
+
+        lore.addAll(visual.loreTemplate());
+        meta.lore(lore);
+
+        meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES);
+
+        ItemPersistentData.write(meta, instance);
+
+        stack.setItemMeta(UnItalic.removeItalicFromMeta(meta));
+        return stack;
+    }
+}

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

@@ -1,43 +1,36 @@
 package me.lethunderhawk.custom.item.command;
 
+import me.lethunderhawk.bazaarflux.service.Services;
 import me.lethunderhawk.bazaarflux.util.command.CommandNode;
 import me.lethunderhawk.bazaarflux.util.command.CustomCommand;
 import me.lethunderhawk.custom.item.CustomItemModule;
-import me.lethunderhawk.custom.item.abstraction.CustomItem;
-import me.lethunderhawk.custom.item.manager.CustomItemManager;
-import org.bukkit.command.Command;
+import me.lethunderhawk.custom.item.abstraction.definition.ItemDefinition;
+import me.lethunderhawk.custom.item.abstraction.instance.ItemInstance;
+import me.lethunderhawk.custom.item.abstraction.registry.CustomItemRegistry;
 import org.bukkit.command.CommandSender;
 import org.bukkit.entity.Player;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.List;
 
 public class CustomItemCommand extends CustomCommand {
-    private final CustomItemManager manager;
 
-    public CustomItemCommand(CustomItemManager manager, CustomItemModule customItemModule) {
+    public CustomItemCommand(CustomItemModule customItemModule) {
         super(new CommandNode("customItems", "Main Custom Item Command", null), customItemModule);
-        this.manager = manager;
     }
 
     private void getAllItems(CommandSender sender, String[] strings) {
         if(!(sender instanceof Player p)) return;
         if(!p.hasPermission("custom.items")) return;
-
-        for(CustomItem item : manager.getRegisteredItems().values()) {
-            p.getInventory().addItem(item.createItem());
-        }
+        getAllItems(p);
     }
 
     @Override
     public void createCommands() {
-        //rootCommand.registerSubCommand("getAll", "Get all custom items", this::getAllItems);
-        //rootCommand.registerSubCommand("reload", "Reload", module::reload);
+        rootCommand.registerSubCommand("getAll", "Get all custom items", this::getAllItems);
+        rootCommand.registerSubCommand("reload", "Reload", module::reload);
     }
-
-    @Override
-    public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String @NotNull [] args) {
-        return List.of();
+    private void getAllItems(Player player) {
+        for (ItemDefinition definition : Services.get(CustomItemRegistry.class).getAll()) {
+            ItemInstance instance = new ItemInstance(definition);
+            player.getInventory().addItem(instance.buildItemStack());
+        }
     }
 }

+ 0 - 97
src/main/java/me/lethunderhawk/custom/item/concrete/ClaimTool.java

@@ -1,97 +0,0 @@
-package me.lethunderhawk.custom.item.concrete;
-
-import me.lethunderhawk.bazaarflux.service.Services;
-import me.lethunderhawk.clans.Clan;
-import me.lethunderhawk.clans.ClanManager;
-import me.lethunderhawk.clans.ClanModule;
-import me.lethunderhawk.clans.claim.Claim;
-import me.lethunderhawk.clans.claim.ClaimManager;
-import me.lethunderhawk.custom.item.abstraction.CustomItem;
-import me.lethunderhawk.main.Main;
-import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.format.NamedTextColor;
-import net.kyori.adventure.text.format.TextDecoration;
-import org.bukkit.Location;
-import org.bukkit.Material;
-import org.bukkit.block.Block;
-import org.bukkit.entity.Player;
-import org.bukkit.event.player.PlayerInteractEvent;
-import org.bukkit.plugin.java.JavaPlugin;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-
-public class ClaimTool extends CustomItem {
-    Map<UUID, Location> firstCorner = new HashMap<>();
-
-    public ClaimTool(JavaPlugin plugin) {
-        super(
-                plugin,
-                "claim_tool",
-                Component.text("Claim Tool", NamedTextColor.BLUE).decoration(TextDecoration.ITALIC, false),
-                Arrays.asList(
-                        Component.text("Left-click to set corners of your claim").decoration(TextDecoration.ITALIC, false),
-                        Component.text("")
-                ),
-                Material.GOLDEN_SHOVEL
-        );
-    }
-
-    @Override
-    public void onRightClick(PlayerInteractEvent event) {
-        event.setCancelled(true);
-    }
-
-    @Override
-    public void onLeftClick(PlayerInteractEvent e) {
-        Player p = e.getPlayer();
-        Block block = e.getClickedBlock();
-        if(block == null) return;
-        Location second = block.getLocation();
-        ClanModule module = Services.get(ClanModule.class);
-        UUID uuid = p.getUniqueId();
-        Clan clan = Services.get(ClanManager.class).getMyClan(uuid);
-        if(clan == null){
-            e.setCancelled(true);
-            module.sendText(p,Component.text("Join a clan first!", NamedTextColor.RED));
-            return;
-        }
-        if (!firstCorner.containsKey(uuid)) {
-            firstCorner.put(uuid, second);
-            module.sendText(p,Component.text("First corner of your claim set.", NamedTextColor.GREEN));
-        } else {
-            Location first = firstCorner.remove(uuid);
-
-            // create claim
-            Claim claim = new Claim(clan.getId(), first.getWorld().getName(), first.getBlockX(), second.getBlockX(), first.getBlockZ(), second.getBlockZ());
-
-            int maxBlocks = Services.get(Main.class).getConfig().getInt("claims.max-blocks");
-
-            if (clan.getUsedBlocks() + claim.getVolume() > maxBlocks) {
-                p.sendMessage("§cClaim too large. Current use of space: " + clan.getUsedBlocks() + "/" + maxBlocks);
-                return;
-            }
-            Claim overlapping = Services.get(ClaimManager.class).overlaps(claim);
-
-            if(overlapping != null) {
-                Clan overlappingClan = Services.get(ClanManager.class).getClanById(overlapping.getClanId());
-
-                if (overlappingClan != null) {
-                    module.sendText(p,Component.text("This area is overlapping with already claimed land from Clan " + overlappingClan.getName() + ".", NamedTextColor.RED));
-                    e.setCancelled(true);
-                    return;
-                }
-            }
-
-
-            Services.get(ClaimManager.class).registerClaim(claim);
-            clan.addClaim(claim);
-            module.sendText(p,Component.text("Second corner set.", NamedTextColor.GREEN));
-            module.sendText(p,Component.text("Claim created.", NamedTextColor.GREEN));
-        }
-
-        e.setCancelled(true);
-    }
-}

+ 113 - 0
src/main/java/me/lethunderhawk/custom/item/concrete/ability/ClaimToolAbility.java

@@ -0,0 +1,113 @@
+package me.lethunderhawk.custom.item.concrete.ability;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.clans.Clan;
+import me.lethunderhawk.clans.ClanManager;
+import me.lethunderhawk.clans.ClanModule;
+import me.lethunderhawk.clans.claim.Claim;
+import me.lethunderhawk.clans.claim.ClaimManager;
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityContext;
+import me.lethunderhawk.custom.item.abstraction.handling.AbilityHandler;
+import me.lethunderhawk.custom.item.abstraction.handling.ResolvedParams;
+import me.lethunderhawk.main.Main;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Location;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Player;
+import org.bukkit.event.player.PlayerInteractEvent;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public class ClaimToolAbility implements AbilityHandler {
+
+    private final Map<UUID, Location> firstCorner = new HashMap<>();
+
+    private Claim createClaim(Clan clan, Location first, Location second) {
+        return new Claim(
+                clan.getId(),
+                first.getWorld().getName(),
+                first.getBlockX(),
+                second.getBlockX(),
+                first.getBlockZ(),
+                second.getBlockZ()
+        );
+    }
+
+    private boolean hasEnoughClaimSpace(Player player, Clan clan, Claim claim) {
+        int maxBlocks = Services.get(Main.class).getConfig().getInt("claims.max-blocks");
+        int usedAfterClaim = clan.getUsedBlocks() + claim.getVolume();
+
+        if (usedAfterClaim > maxBlocks) {
+            player.sendMessage("§cClaim too large. Current use of space: "
+                    + clan.getUsedBlocks() + "/" + maxBlocks);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean isOverlapping(
+            Player player,
+            ClanModule module,
+            ClanManager clanManager,
+            ClaimManager claimManager,
+            Claim claim
+    ) {
+        Claim overlapping = claimManager.overlaps(claim);
+        if (overlapping == null) return false;
+
+        Clan otherClan = clanManager.getClanById(overlapping.getClanId());
+        if (otherClan != null) {
+            module.sendText(
+                    player,
+                    Component.text(
+                            "This area overlaps with land claimed by Clan " + otherClan.getName() + ".",
+                            NamedTextColor.RED
+                    )
+            );
+        }
+        return true;
+    }
+
+    @Override
+    public void execute(AbilityContext context, ResolvedParams params) {
+        PlayerInteractEvent event = ((PlayerInteractEvent) context.event());
+        event.setCancelled(true);
+        Block block = event.getClickedBlock();
+        if (block == null) return;
+
+        Player player = event.getPlayer();
+        UUID playerId = player.getUniqueId();
+        Location clickedLocation = block.getLocation();
+
+        ClanModule clanModule = Services.get(ClanModule.class);
+        ClanManager clanManager = Services.get(ClanManager.class);
+        ClaimManager claimManager = Services.get(ClaimManager.class);
+
+        Clan clan = clanManager.getMyClan(playerId);
+        if (clan == null) {
+            clanModule.sendText(player, Component.text("Join a clan first!", NamedTextColor.RED));
+            return;
+        }
+
+        if (!firstCorner.containsKey(playerId)) {
+            firstCorner.put(playerId, clickedLocation);
+            clanModule.sendText(player, Component.text("First corner of your claim set.", NamedTextColor.GREEN));
+            return;
+        }
+
+        Location first = firstCorner.remove(playerId);
+        Claim claim = createClaim(clan, first, clickedLocation);
+
+        if (!hasEnoughClaimSpace(player, clan, claim)) return;
+        if (isOverlapping(player, clanModule, clanManager, claimManager, claim)) return;
+
+        claimManager.registerClaim(claim);
+        clan.addClaim(claim);
+
+        clanModule.sendText(player, Component.text("Second corner set.", NamedTextColor.GREEN));
+        clanModule.sendText(player, Component.text("Claim created.", NamedTextColor.GREEN));
+    }
+}

+ 192 - 0
src/main/java/me/lethunderhawk/custom/item/concrete/ability/HyperionAbility.java

@@ -0,0 +1,192 @@
+package me.lethunderhawk.custom.item.concrete.ability;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityContext;
+import me.lethunderhawk.custom.item.abstraction.handling.AbilityHandler;
+import me.lethunderhawk.custom.item.abstraction.handling.ResolvedParams;
+import me.lethunderhawk.main.Main;
+import org.bukkit.Location;
+import org.bukkit.Particle;
+import org.bukkit.Sound;
+import org.bukkit.World;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitRunnable;
+import org.bukkit.util.Vector;
+
+public class HyperionAbility implements AbilityHandler {
+
+    // Configuration constants
+    private static final double DEFAULT_DAMAGE = 5.0;
+    private static final double DEFAULT_RANGE = 8.0;
+    private static final double DEFAULT_DAMAGE_RADIUS = 5.0;
+    private static final int DASH_STEPS = 4; // More steps for smoother animation
+    private static final double STEP_INTERVAL = 1.0; // Ticks between steps (20ms)
+
+    @Override
+    public void execute(AbilityContext context, ResolvedParams params) {
+        double damage = params.getDoubleOrDefault("damage", DEFAULT_DAMAGE);
+        double range = params.getDoubleOrDefault("range", DEFAULT_RANGE);
+        double damageRadius = params.getDoubleOrDefault("damageRadius", DEFAULT_DAMAGE_RADIUS);
+
+        Player player = context.player();
+
+        Location start = player.getLocation().clone().add(0, player.getEyeHeight(true),0);
+        Vector direction = start.getDirection().normalize();
+        World world = player.getWorld();
+
+        // Find the teleport destination - ALWAYS teleport forward
+        Location target = findTeleportDestination(start, direction, range);
+
+        // Preserve player's original orientation
+        target.setYaw(start.getYaw());
+        target.setPitch(start.getPitch());
+
+        // Perform smooth dash animation
+        smoothDashTeleport(player, start, target, () -> {
+            playExplosionEffect(world, target);
+            damageNearbyEntities(player, target, damageRadius, damage);
+        });
+    }
+
+    private Location findTeleportDestination(Location start, Vector direction, double maxRange) {
+        // First, try to find a solid block in front of the player
+        Location hitLocation = findSolidBlockHit(start, direction, maxRange);
+
+        if (hitLocation != null) {
+            // We hit a block, teleport to just in front of it
+            return getPositionInFrontOfBlock(hitLocation, direction);
+        } else {
+            // No block hit, teleport to max range
+            return start.clone().add(direction.clone().multiply(maxRange));
+        }
+    }
+    private Location findSolidBlockHit(Location start, Vector direction, double maxRange) {
+        final double STEP = 0.2;
+
+        for (double distance = STEP; distance <= maxRange; distance += STEP) {
+            Location check = start.clone().add(direction.clone().multiply(distance));
+
+            // Check if this location contains a solid block
+            if (check.getBlock().getType().isSolid()) {
+                return check;
+            }
+        }
+
+        return null; // No solid block found
+    }
+
+    private Location getPositionInFrontOfBlock(Location blockHit, Vector direction) {
+        // Move back slightly from the hit block to be in front of it
+        // We use a small offset to ensure we're not inside the block
+        double offset = 0.3;
+
+        // Reverse the direction slightly
+        Vector reverseDir = direction.clone().multiply(-offset);
+
+        // Return position just in front of the block
+        return blockHit.clone().add(reverseDir);
+    }
+
+    private void smoothDashTeleport(Player player, Location start, Location target, Runnable onComplete) {
+        Vector startVec = start.toVector();
+        Vector targetVec = target.toVector();
+        Vector totalMovement = targetVec.clone().subtract(startVec);
+
+        // Calculate movement per step
+        Vector stepMovement = totalMovement.clone().multiply(1.0 / DASH_STEPS);
+
+        // Store player's original look direction
+        float originalYaw = start.getYaw();
+        float originalPitch = start.getPitch();
+
+        new BukkitRunnable() {
+            int currentStep = 0;
+            Location currentLocation = start.clone();
+
+            @Override
+            public void run() {
+                if (currentStep > DASH_STEPS) {
+                    // Final teleport to exact target
+                    Location finalLocation = target.clone();
+                    finalLocation.setYaw(originalYaw);
+                    finalLocation.setPitch(originalPitch);
+
+                    player.teleport(finalLocation);
+                    onComplete.run();
+                    cancel();
+                    return;
+                }
+
+                // Calculate next position
+                Vector nextVec = startVec.clone().add(stepMovement.clone().multiply(currentStep));
+                Location nextLocation = new Location(
+                        start.getWorld(),
+                        nextVec.getX(),
+                        nextVec.getY(),
+                        nextVec.getZ(),
+                        originalYaw,
+                        originalPitch
+                );
+
+                // Teleport to next position
+                player.teleport(nextLocation);
+                currentLocation = nextLocation.clone();
+
+                // Visual and sound effects during dash
+                player.getWorld().spawnParticle(
+                        Particle.ELECTRIC_SPARK,
+                        nextLocation,
+                        3, 0.1, 0.1, 0.1, 0
+                );
+
+                if (currentStep % 2 == 0) {
+                    player.getWorld().playSound(
+                            nextLocation,
+                            Sound.BLOCK_BEACON_AMBIENT,
+                            0.3f, 2.0f
+                    );
+                }
+
+                currentStep++;
+            }
+        }.runTaskTimer(Services.get(Main.class), 0L, (long) STEP_INTERVAL);
+    }
+
+    private void playExplosionEffect(World world, Location loc) {
+        world.spawnParticle(Particle.EXPLOSION_EMITTER, loc, 1);
+        world.spawnParticle(Particle.EXPLOSION, loc, 30, 0.5, 0.5, 0.5, 0.05);
+        world.playSound(loc, Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 1.0f);
+        //world.spawnParticle(Particle.FLASH, loc, 1);
+
+        // Create a ring of smoke particles
+        for (int i = 0; i < 12; i++) {
+            double angle = 2 * Math.PI * i / 12;
+            double x = Math.cos(angle) * 0.5;
+            double z = Math.sin(angle) * 0.5;
+            world.spawnParticle(Particle.SMOKE,
+                    loc.getX(), loc.getY(), loc.getZ(),
+                    0, x, 0.2, z, 0.1);
+        }
+    }
+
+    private void damageNearbyEntities(Player source, Location center, double radius, double damage) {
+        double radiusSquared = radius * radius;
+
+        for (LivingEntity entity : center.getWorld().getNearbyLivingEntities(center, radius, radius, radius)) {
+            if (entity.equals(source)) continue;
+            if (entity.isDead()) continue;
+
+            if (entity.getLocation().distanceSquared(center) <= radiusSquared) {
+                entity.damage(damage, source);
+
+                // Add a small knockback effect
+                Vector knockback = entity.getLocation().toVector()
+                        .subtract(center.toVector())
+                        .normalize()
+                        .multiply(0.5);
+                entity.setVelocity(knockback);
+            }
+        }
+    }
+}

+ 91 - 0
src/main/java/me/lethunderhawk/custom/item/concrete/ability/RollingDiceAbility.java

@@ -0,0 +1,91 @@
+package me.lethunderhawk.custom.item.concrete.ability;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.bazaarflux.util.loottables.RiggedChanceGenerator;
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityContext;
+import me.lethunderhawk.custom.item.abstraction.handling.AbilityHandler;
+import me.lethunderhawk.custom.item.abstraction.handling.ResolvedParams;
+import me.lethunderhawk.custom.item.concrete.dice.DiceReward;
+import me.lethunderhawk.custom.item.concrete.dice.RollingDiceAnimation;
+import me.lethunderhawk.economy.api.EconomyAPI;
+import org.bukkit.attribute.Attribute;
+import org.bukkit.attribute.AttributeInstance;
+import org.bukkit.entity.Player;
+
+import java.util.UUID;
+
+public class RollingDiceAbility implements AbilityHandler {
+    private int abilityReward;
+    private int abilityCost;
+    private int abilityRewardHealth;
+    private static final String headValue = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmFlMTU0YTU3NzE5N2M1NWVkYzVlNTk3NjlmZTg4ZmRhMzY5M2U3ZWM0MDc4ZWQ0M2FjNjk5OGE0ZGY2NGJiOSJ9fX0=";
+    @Override
+    public void execute(AbilityContext context, ResolvedParams params) {
+        abilityReward = params.getInt("reward");
+        abilityCost =  params.getInt("cost");
+        abilityRewardHealth = params.getInt("rewardHealth");
+
+        Player player = context.player();
+
+        if (!withdrawCost(player)) {
+            player.sendMessage("§cYou don't have enough money!");
+            return;
+        }
+
+        RollingDiceAnimation animation = new RollingDiceAnimation(player, headValue);
+        animation.onComplete(() -> rollAndReward(player));
+        animation.start();
+    }
+
+
+    private boolean withdrawCost(Player player) {
+        EconomyAPI economy = Services.get(EconomyAPI.class);
+        UUID uuid = player.getUniqueId();
+
+        long balance = economy.getMoney(uuid);
+        if (balance < abilityCost) return false;
+
+        economy.removeMoney(uuid, abilityCost);
+        return true;
+    }
+
+    private void rollAndReward(Player player) {
+        DiceReward result = rollDice();
+        EconomyAPI economy = Services.get(EconomyAPI.class);
+
+        switch (result) {
+            case SEVEN -> {
+                economy.addMoney(player.getUniqueId(), abilityReward * 10L);
+                setMaxHealth(player, abilityRewardHealth);
+                player.sendMessage("§eYou rolled a §a" + result.name().toLowerCase()
+                        + "§e! You well deserve a large amount of money!");
+            }
+            case SIX -> {
+                economy.addMoney(player.getUniqueId(), abilityReward);
+                player.sendMessage("§eYou rolled a §a" + result.name().toLowerCase()
+                        + "§e! §6Nice job!");
+            }
+            default ->
+                    player.sendMessage("§eYou rolled a §a" + result.name().toLowerCase() + "§e!");
+        }
+    }
+
+    private void setMaxHealth(Player player, double value) {
+        AttributeInstance attr = player.getAttribute(Attribute.MAX_HEALTH);
+        if (attr != null) {
+            attr.setBaseValue(value);
+        }
+    }
+
+    private DiceReward rollDice() {
+        return new RiggedChanceGenerator<DiceReward>()
+                .addStep(1, 3_333, DiceReward.SEVEN)
+                .addStep(1, 24, DiceReward.SIX)
+                .addChance(1, DiceReward.ONE)
+                .addChance(1, DiceReward.TWO)
+                .addChance(1, DiceReward.THREE)
+                .addChance(1, DiceReward.FOUR)
+                .addChance(1, DiceReward.FIVE)
+                .roll();
+    }
+}

+ 12 - 0
src/main/java/me/lethunderhawk/custom/item/concrete/ability/TestAbilityHandler.java

@@ -0,0 +1,12 @@
+package me.lethunderhawk.custom.item.concrete.ability;
+
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityContext;
+import me.lethunderhawk.custom.item.abstraction.handling.AbilityHandler;
+import me.lethunderhawk.custom.item.abstraction.handling.ResolvedParams;
+
+public class TestAbilityHandler implements AbilityHandler {
+    @Override
+    public void execute(AbilityContext context, ResolvedParams params) {
+        context.player().sendMessage("§cAbility of Test item used.");
+    }
+}

+ 11 - 0
src/main/java/me/lethunderhawk/custom/item/concrete/dice/DiceReward.java

@@ -0,0 +1,11 @@
+package me.lethunderhawk.custom.item.concrete.dice;
+
+public enum DiceReward {
+    ONE,
+    TWO,
+    THREE,
+    FOUR,
+    FIVE,
+    SIX,
+    SEVEN,
+}

+ 83 - 0
src/main/java/me/lethunderhawk/custom/item/concrete/dice/RollingDiceAnimation.java

@@ -0,0 +1,83 @@
+package me.lethunderhawk.custom.item.concrete.dice;
+
+import me.lethunderhawk.bazaarflux.util.CustomHeadCreator;
+import me.lethunderhawk.bazaarflux.util.animation.Animation;
+import org.bukkit.Location;
+import org.bukkit.entity.Display;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.ItemDisplay;
+import org.bukkit.entity.Player;
+import org.bukkit.util.Transformation;
+import org.bukkit.util.Vector;
+import org.joml.Quaternionf;
+
+import java.util.Random;
+
+
+public class RollingDiceAnimation extends Animation {
+    private static final int duration = 40;
+    private final Player player;
+    private final String headValue;
+    private ItemDisplay display;
+
+    private Location start;
+    private Vector direction;
+    private Quaternionf currentRotation = new Quaternionf();
+    private Random random = new Random();
+    public RollingDiceAnimation(Player player, String headValue) {
+        super(player.getWorld(), duration); // 2 seconds
+        this.player = player;
+        this.headValue = headValue;
+    }
+
+    public static int getDuration() {
+        return duration;
+    }
+
+    public static long getDurationInMilliSeconds() {
+        return duration/20 * 1000L;
+    }
+
+    @Override
+    protected void onStart() {
+        start = player.getEyeLocation().add(player.getLocation().getDirection().normalize().multiply(1.0));
+        direction = player.getLocation().getDirection().normalize();
+
+        display = (ItemDisplay) world.spawnEntity(start, EntityType.ITEM_DISPLAY);
+        display.setItemStack(CustomHeadCreator.createCustomHead(headValue));
+        display.setBillboard(Display.Billboard.FIXED);
+
+        display.setInterpolationDuration(1);
+        display.setInterpolationDelay(0);
+    }
+
+    @Override
+    protected void onTick(int tick) {
+        double progress = tick / (double) duration;
+
+        // Move forward and slightly down (dice settles)
+        Vector offset = direction.clone().multiply(progress * 3.0);
+        offset.setY(offset.getY() - progress * 0.8);
+        Location loc = start.clone().add(offset);
+        display.teleport(loc);
+
+        // Rotation (rolling effect)
+        Transformation t = display.getTransformation();
+//        float randomAngleX = random.nextFloat(0.4f, 0.6f);
+//        float randomAngleY = random.nextFloat(0.1f, 0.3f);
+//        float randomAngleZ = random.nextFloat(0.1f, 0.3f);
+        currentRotation.rotateXYZ(
+                0.55f,
+                0.15f,
+                0.25f
+        );
+
+        t.getLeftRotation().set(currentRotation);
+        display.setTransformation(t);
+    }
+
+    @Override
+    protected void onEnd() {
+        display.remove();
+    }
+}

+ 15 - 51
src/main/java/me/lethunderhawk/custom/item/listener/CustomItemListener.java

@@ -1,67 +1,31 @@
 package me.lethunderhawk.custom.item.listener;
 
-import me.lethunderhawk.custom.item.manager.CustomItemManager;
+import me.lethunderhawk.custom.item.abstraction.ability.AbilityTrigger;
+import me.lethunderhawk.custom.item.abstraction.runtime.AbilityDispatchService;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
-import org.bukkit.event.block.Action;
 import org.bukkit.event.player.PlayerInteractEvent;
-import org.bukkit.event.player.PlayerItemHeldEvent;
-import org.bukkit.event.entity.EntityDamageByEntityEvent;
-import org.bukkit.inventory.ItemStack;
 
 /**
  * Handles events for custom items
  */
 public class CustomItemListener implements Listener {
-    private final CustomItemManager itemManager;
+    private final AbilityDispatchService dispatchService;
 
-    public CustomItemListener(CustomItemManager itemManager) {
-        this.itemManager = itemManager;
+    public CustomItemListener(AbilityDispatchService dispatchService) {
+        this.dispatchService = dispatchService;
     }
 
     @EventHandler
-    public void onPlayerInteract(PlayerInteractEvent event) {
-        ItemStack item = event.getItem();
-
-        if (item == null || !itemManager.isCustomItem(item)) return;
-
-        itemManager.getItemFromStack(item).ifPresent(customItem -> {
-            event.setCancelled(true); // Cancel default behavior
-
-            if (event.getAction() == Action.RIGHT_CLICK_AIR ||
-                    event.getAction() == Action.RIGHT_CLICK_BLOCK) {
-                customItem.onRightClick(event);
-            }
-            else if (event.getAction() == Action.LEFT_CLICK_AIR ||
-                    event.getAction() == Action.LEFT_CLICK_BLOCK) {
-                customItem.onLeftClick(event);
-            }
-        });
-    }
-
-    @EventHandler
-    public void onEntityDamage(EntityDamageByEntityEvent event) {
-        if (event.getDamager() instanceof org.bukkit.entity.Player) {
-            org.bukkit.entity.Player player = (org.bukkit.entity.Player) event.getDamager();
-            ItemStack item = player.getInventory().getItemInMainHand();
-
-            if (itemManager.isCustomItem(item)) {
-                itemManager.getItemFromStack(item).ifPresent(customItem -> {
-                    // You could add specific damage event handling here
-                    // For now, we'll just cancel to prevent default damage
-                    event.setCancelled(true);
-                });
-            }
-        }
-    }
-
-    @EventHandler
-    public void onItemHeldChange(PlayerItemHeldEvent event) {
-        // You could add effects when switching to custom items
-        ItemStack newItem = event.getPlayer().getInventory().getItem(event.getNewSlot());
-
-        if (itemManager.isCustomItem(newItem)) {
-            // Optional: Play sound or show message when switching to custom item
-        }
+    public void onAction(PlayerInteractEvent event) {
+        AbilityTrigger triggered = switch (event.getAction()) {
+            case LEFT_CLICK_AIR -> AbilityTrigger.LEFT_CLICK_AIR;
+            case LEFT_CLICK_BLOCK -> AbilityTrigger.LEFT_CLICK_BLOCK;
+            case RIGHT_CLICK_AIR -> AbilityTrigger.RIGHT_CLICK_AIR;
+            case RIGHT_CLICK_BLOCK -> AbilityTrigger.RIGHT_CLICK_BLOCK;
+            default -> null;
+        };
+
+        dispatchService.dispatch(event.getPlayer(), event.getItem(), triggered, event);
     }
 }

+ 0 - 88
src/main/java/me/lethunderhawk/custom/item/manager/CustomItemManager.java

@@ -1,88 +0,0 @@
-package me.lethunderhawk.custom.item.manager;
-
-import me.lethunderhawk.custom.item.abstraction.CustomItem;
-import org.bukkit.NamespacedKey;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.plugin.java.JavaPlugin;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * Manages registration and retrieval of custom items
- */
-public class CustomItemManager {
-    private final JavaPlugin plugin;
-    private final Map<String, CustomItem> registeredItems;
-    private final NamespacedKey itemIdKey;
-
-    public CustomItemManager(JavaPlugin plugin) {
-        this.plugin = plugin;
-        this.registeredItems = new HashMap<>();
-        this.itemIdKey = new NamespacedKey(plugin, "custom_item_id");
-    }
-
-    /**
-     * Register a custom item
-     */
-    public void registerItem(CustomItem item) {
-        registeredItems.put(item.getItemId(), item);
-        plugin.getLogger().info("Registered custom item: " + item.getItemId());
-    }
-
-    /**
-     * Unregister a custom item
-     */
-    public void unregisterItem(String itemId) {
-        registeredItems.remove(itemId);
-    }
-
-    /**
-     * Get a custom item by its ID
-     */
-    public Optional<CustomItem> getItemById(String itemId) {
-        return Optional.ofNullable(registeredItems.get(itemId));
-    }
-
-    /**
-     * Check if an ItemStack is any custom item
-     */
-    public boolean isCustomItem(ItemStack item) {
-        if (item == null || !item.hasItemMeta()) return false;
-
-        String itemId = getItemIdFromStack(item);
-        return itemId != null && registeredItems.containsKey(itemId);
-    }
-
-    /**
-     * Get the custom item from an ItemStack
-     */
-    public Optional<CustomItem> getItemFromStack(ItemStack item) {
-        String itemId = getItemIdFromStack(item);
-        if (itemId == null) return Optional.empty();
-
-        return getItemById(itemId);
-    }
-
-    /**
-     * Extract the custom item ID from an ItemStack
-     */
-    public String getItemIdFromStack(ItemStack item) {
-        if (item == null || !item.hasItemMeta()) return null;
-
-        return item.getItemMeta().getPersistentDataContainer()
-                .get(itemIdKey, org.bukkit.persistence.PersistentDataType.STRING);
-    }
-
-    /**
-     * Get all registered custom items
-     */
-    public Map<String, CustomItem> getRegisteredItems() {
-        return new HashMap<>(registeredItems);
-    }
-
-    public NamespacedKey getItemIdKey() {
-        return itemIdKey;
-    }
-}

+ 1 - 1
src/main/java/me/lethunderhawk/dungeon/DungeonModule.java

@@ -17,7 +17,7 @@ public class DungeonModule extends BazaarFluxModule {
     public void onEnable() {
 
         Main mainPlugin = Services.get(Main.class);
-        Services.register(DungeonManager.class, new DungeonManager(mainPlugin));
+        Services.register(DungeonManager.class, new DungeonManager());
 
         mainPlugin.getCommand("dungeon").setExecutor(new DungeonCommand());
         mainPlugin.getCommand("dungeon").setTabCompleter(new DungeonCommand());

+ 5 - 2
src/main/java/me/lethunderhawk/dungeon/command/DungeonCommand.java

@@ -46,8 +46,11 @@ public class DungeonCommand extends CustomCommand {
 
     private void testDungeon(CommandSender sender, String[] args) {
         if (!(sender instanceof Player player)) return;
-        backFromDungeon(sender, args);
-        DungeonWorld dungeon = Services.get(DungeonManager.class).createDungeon();
+        DungeonManager manager = Services.get(DungeonManager.class);
+        if(manager.isDungeonWorld(player.getWorld())) {
+            backFromDungeon(sender, args);
+        }
+        DungeonWorld dungeon = manager.createDungeon();
         dungeon.sendPlayerToDungeon(player);
     }
 

+ 9 - 0
src/main/java/me/lethunderhawk/dungeon/generation/ConnectionType.java

@@ -0,0 +1,9 @@
+package me.lethunderhawk.dungeon.generation;
+
+public enum ConnectionType {
+    STRAIGHT,
+    TURN_LEFT,
+    T,
+    SINGLE,
+    HUB,
+}

Разница между файлами не показана из-за своего большого размера
+ 673 - 198
src/main/java/me/lethunderhawk/dungeon/generation/DungeonGridGenerator.java


+ 22 - 6
src/main/java/me/lethunderhawk/dungeon/generation/DungeonWorld.java

@@ -1,6 +1,7 @@
 package me.lethunderhawk.dungeon.generation;
 
 import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.dungeon.manager.DungeonManager;
 import me.lethunderhawk.main.Main;
 import org.bukkit.*;
 import org.bukkit.entity.Player;
@@ -52,19 +53,31 @@ public class DungeonWorld implements Listener {
         world.setGameRule(GameRule.KEEP_INVENTORY, true);
         world.setGameRule(GameRule.DO_MOB_SPAWNING, false);
         world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
-        world.setTime(18000);
+        world.setTime(1000);
 
         // Hook: start dungeon generation async/sync later
         startDungeonGeneration();
     }
 
     private void startDungeonGeneration() {
+        Map<DungeonRoomType, DungeonGridGenerator.RoomTypeConfig> config = new HashMap<>();
+        config.put(DungeonRoomType.REGULAR, new DungeonGridGenerator.RoomTypeConfig(41, 4));// ensure enough regular rooms are possible to place
+
+        config.put(DungeonRoomType.START, new DungeonGridGenerator.RoomTypeConfig(1, 1));
+        config.put(DungeonRoomType.BLOOD, new DungeonGridGenerator.RoomTypeConfig(1, 1));
+        config.put(DungeonRoomType.FAIRY, new DungeonGridGenerator.RoomTypeConfig(1, 4)); // Can have up to 4 exits
+
+        config.put(DungeonRoomType.PUZZLE, new DungeonGridGenerator.RoomTypeConfig(3, 1));
+        config.put(DungeonRoomType.MINIBOSS, new DungeonGridGenerator.RoomTypeConfig(1, 1));
+        config.put(DungeonRoomType.TRAP, new DungeonGridGenerator.RoomTypeConfig(1, 1));
+
         DungeonGridGenerator gen = new DungeonGridGenerator(
                 world,
-                5,     // grid width (e.g. 4 to 6)
-                5,     // grid height
-                13,     // room size (odd recommended)
-                13      // room height
+                7,     // grid width (e.g. 4 to 6)
+                7,     // grid height
+                21,     // room size (odd recommended)
+                21,      // room height
+                config
         );
         gen.generate();
     }
@@ -107,7 +120,10 @@ public class DungeonWorld implements Listener {
         if (!isInDungeon(player)) return;
 
         // Delay teleport to avoid respawn conflicts
-        Bukkit.getScheduler().runTask(plugin, () -> returnPlayer(player));
+        Bukkit.getScheduler().runTask(plugin, () -> {
+            returnPlayer(player);
+            Services.get(DungeonManager.class).deleteDungeon(this.uuid);
+        });
     }
 
     public UUID getUUID() {

+ 7 - 6
src/main/java/me/lethunderhawk/dungeon/manager/DungeonManager.java

@@ -4,21 +4,17 @@ import me.lethunderhawk.dungeon.generation.DungeonWorld;
 import org.bukkit.Bukkit;
 import org.bukkit.World;
 import org.bukkit.entity.Player;
-import org.bukkit.plugin.java.JavaPlugin;
 
 import java.io.File;
 import java.util.*;
 
 public class DungeonManager {
 
-    private final JavaPlugin plugin;
 
     // dungeonUUID -> DungeonWorld
     private final Map<UUID, DungeonWorld> activeDungeons = new HashMap<>();
 
-    public DungeonManager(JavaPlugin plugin) {
-        this.plugin = plugin;
-    }
+    public DungeonManager() {}
 
     /* =========================
        CREATION
@@ -80,7 +76,12 @@ public class DungeonManager {
 
     public boolean isDungeonWorld(World world) {
         if (world == null) return false;
-        return activeDungeons.containsKey(UUID.fromString(world.getName()));
+        try{
+            UUID uuid = UUID.fromString(world.getName());
+            return activeDungeons.containsKey(uuid);
+        }catch(Exception e){
+            return false;
+        }
     }
     public DungeonWorld getDungeonByWorld(World world) {
         if (world == null) return null;

+ 7 - 0
src/main/java/me/lethunderhawk/dungeon/placement/RoomMetadata.java

@@ -0,0 +1,7 @@
+package me.lethunderhawk.dungeon.placement;
+
+public class RoomMetadata {
+    public String roomType;
+    public Vector3i anchor;
+    public RotatableBoundingBox boundingBox;
+}

+ 24 - 0
src/main/java/me/lethunderhawk/dungeon/placement/RotatableBoundingBox.java

@@ -0,0 +1,24 @@
+package me.lethunderhawk.dungeon.placement;
+import org.bukkit.util.BoundingBox;
+
+public class RotatableBoundingBox {
+
+    public int width;
+    public int height;
+    public int depth;
+
+    public BoundingBox rotated(Rotation rotation) {
+        return switch (rotation) {
+            case CLOCKWISE_90, CLOCKWISE_270 ->
+                    BoundingBox.of(
+                            new org.bukkit.util.Vector(0, 0, 0),
+                            depth, height, width
+                    );
+            default ->
+                    BoundingBox.of(
+                            new org.bukkit.util.Vector(0, 0, 0),
+                            width, height, depth
+                    );
+        };
+    }
+}

+ 20 - 0
src/main/java/me/lethunderhawk/dungeon/placement/Rotation.java

@@ -0,0 +1,20 @@
+package me.lethunderhawk.dungeon.placement;
+
+import com.sk89q.worldedit.math.transform.AffineTransform;
+
+public enum Rotation {
+    NONE(0),
+    CLOCKWISE_90(270),
+    CLOCKWISE_180(180),
+    CLOCKWISE_270(90);
+
+    private final int degrees;
+
+    Rotation(int degrees) {
+        this.degrees = degrees;
+    }
+
+    public AffineTransform toTransform() {
+        return new AffineTransform().rotateY(degrees);
+    }
+}

+ 142 - 0
src/main/java/me/lethunderhawk/dungeon/placement/SchematicRoomPlacer.java

@@ -0,0 +1,142 @@
+package me.lethunderhawk.dungeon.placement;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParseException;
+import com.sk89q.worldedit.EditSession;
+import com.sk89q.worldedit.WorldEdit;
+import com.sk89q.worldedit.extent.clipboard.Clipboard;
+import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
+import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats;
+import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader;
+import com.sk89q.worldedit.function.operation.Operation;
+import com.sk89q.worldedit.function.operation.Operations;
+import com.sk89q.worldedit.math.BlockVector3;
+import com.sk89q.worldedit.session.ClipboardHolder;
+import com.sk89q.worldedit.bukkit.BukkitAdapter;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.util.BoundingBox;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+
+public final class SchematicRoomPlacer {
+
+    private static final Gson GSON = new Gson();
+
+    private SchematicRoomPlacer() {}
+
+    /**
+     * Places a room schematic at a given anchor.
+     *
+     * @param schematicFile .schem file
+     * @param world Bukkit world
+     * @param anchor anchor location (world-space origin)
+     * @param rotation rotation (NONE, CLOCKWISE_90, CLOCKWISE_180, CLOCKWISE_270)
+     * @return true if successfully validated and placed
+     */
+    public static boolean placeRoom(
+            File schematicFile,
+            World world,
+            Location anchor,
+            Rotation rotation
+    ) {
+        if(schematicFile == null) return false;
+        if (!schematicFile.exists()) return false;
+
+        RoomMetadata meta = loadMetadata(schematicFile);
+        if (meta == null) return false;
+
+        Clipboard clipboard = loadClipboard(schematicFile);
+        if (clipboard == null) return false;
+
+        BoundingBox box = meta.boundingBox.rotated(rotation).shift(anchor.toVector());
+        //if (!isAreaClear(world, box)) return false;
+
+        return pasteClipboard(clipboard, world, anchor, meta.anchor, rotation);
+    }
+
+    // ---------- Internal ----------
+
+    private static Clipboard loadClipboard(File file) {
+        ClipboardFormat format = ClipboardFormats.findByPath(file.getAbsoluteFile().toPath());
+        if (format == null) return null;
+
+        try (ClipboardReader reader = format.getReader(new java.io.FileInputStream(file))) {
+            return reader.read();
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    private static RoomMetadata loadMetadata(File schematic) {
+        File metaFile = new File(schematic.getParent(), schematic.getName().replace(".schem", ".json"));
+        if (!metaFile.exists()) return null;
+
+        try (FileReader reader = new FileReader(metaFile)) {
+            return GSON.fromJson(reader, RoomMetadata.class);
+        } catch (IOException | JsonParseException e) {
+            return null;
+        }
+    }
+
+    private static boolean isAreaClear(World world, BoundingBox box) {
+        int minX = (int) box.getMinX();
+        int minY = (int) box.getMinY();
+        int minZ = (int) box.getMinZ();
+        int maxX = (int) box.getMaxX();
+        int maxY = (int) box.getMaxY();
+        int maxZ = (int) box.getMaxZ();
+
+        for (int x = minX; x <= maxX; x++) {
+            for (int y = minY; y <= maxY; y++) {
+                for (int z = minZ; z <= maxZ; z++) {
+
+                    if (!world.getBlockAt(x, y, z).isEmpty()) {
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+    private static boolean pasteClipboard(
+            Clipboard clipboard,
+            World world,
+            Location anchor,
+            Vector3i schematicAnchor,
+            Rotation rotation
+    ) {
+        com.sk89q.worldedit.world.World weWorld = BukkitAdapter.adapt(world);
+
+        BlockVector3 pastePos = BlockVector3.at(
+                anchor.getBlockX() - schematicAnchor.x(),
+                anchor.getBlockY() - schematicAnchor.y(),
+                anchor.getBlockZ() - schematicAnchor.z()
+        );
+
+        try (EditSession session = WorldEdit.getInstance()
+                .newEditSessionBuilder()
+                .world(weWorld)
+                .build()) {
+
+            ClipboardHolder holder = new ClipboardHolder(clipboard);
+            holder.setTransform(holder.getTransform().combine(rotation.toTransform()));
+
+            Operation op = holder
+                    .createPaste(session)
+                    .to(pastePos)
+                    .ignoreAirBlocks(true)
+                    .build();
+
+            Operations.complete(op);
+            session.close();
+            return true;
+
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}

+ 3 - 0
src/main/java/me/lethunderhawk/dungeon/placement/Vector3i.java

@@ -0,0 +1,3 @@
+package me.lethunderhawk.dungeon.placement;
+
+public record Vector3i(int x, int y, int z) {}

+ 1 - 1
src/main/java/me/lethunderhawk/economy/EconomyModule.java

@@ -20,7 +20,7 @@ public class EconomyModule extends BazaarFluxModule {
 
     @Override
     public String getPrefix() {
-        return "[Economy]";
+        return "[Bank]";
     }
 
     public void onEnable() {

+ 15 - 2
src/main/java/me/lethunderhawk/economy/currency/EconomyManager.java

@@ -26,11 +26,24 @@ public class EconomyManager {
     }
 
     public void addMoney(UUID uuid, long amount) {
+        /*Player player = Bukkit.getPlayer(uuid);
+        if(player != null) {
+            player.sendMessage("§c" +EconomyUtil.stringFromNumber(amount) + "§e has been transferred into your account!");
+        }*/
         setMoney(uuid, getMoney(uuid) + amount);
     }
 
-    public void removeMoney(UUID uuid, long amount) {
-        setMoney(uuid, Math.max(0, getMoney(uuid) - amount));
+    public boolean removeMoney(UUID uuid, long amount) {
+        /*Player player = Bukkit.getPlayer(uuid);
+        if(player != null) {
+            player.sendMessage("§c" +EconomyUtil.stringFromNumber(amount) + "§e has been taken from your account!");
+        }*/
+        long newAmount = getMoney(uuid) - amount;
+        if(newAmount < 0){
+            return false;
+        }
+        setMoney(uuid, Math.max(0, newAmount));
+        return true;
     }
 
     public long getTime() {

+ 3 - 3
src/main/java/me/lethunderhawk/tradeplugin/listener/TradeInventoryListener.java

@@ -117,17 +117,17 @@ public class TradeInventoryListener implements Listener {
                             if(originalSession.addFluxValue(player, value)){
                                 player.openInventory(originalSession.getTradeInventory().getInventoryFor(player));
                             }else{
-                                TradeModule.sendText(player, Component.text("No free space in menu!"));
+                                Services.get(TradeModule.class).sendText(player, Component.text("No free space in menu!"));
                                 return false;
                             }
                         }else{
-                            TradeModule.sendText(player,Component.text("You don't have enough money!", NamedTextColor.RED));
+                            Services.get(TradeModule.class).sendText(player,Component.text("You don't have enough money!", NamedTextColor.RED));
                             return false;
                         }
                     } else if(value == 0) {
                         return true;
                     }else{
-                        TradeModule.sendText(player,Component.text("You cant pay negative amounts!!", NamedTextColor.RED));
+                        Services.get(TradeModule.class).sendText(player,Component.text("You cant pay negative amounts!!", NamedTextColor.RED));
                         player.openInventory(originalSession.getTradeInventory().getInventoryFor(player));
                         return false;
                     }

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

@@ -35,8 +35,8 @@ public class TradeRequestManager {
                     ).append(
                             Component.text(" angenommen.").color(NamedTextColor.YELLOW)
                     );
-            TradeModule.sendText(sender, sendermsg);
-            TradeModule.sendText(target, targetmsg);
+            Services.get(TradeModule.class).sendText(sender, sendermsg);
+            Services.get(TradeModule.class).sendText(target, targetmsg);
 
             TradeModule.getTradeManager().startTrade(sender, target);
 
@@ -63,8 +63,8 @@ public class TradeRequestManager {
                     ).append(
                             Component.text(" gesendet").color(NamedTextColor.YELLOW)
                     );
-            TradeModule.sendText(sender, sendermsg);
-            TradeModule.sendText(target, targetmsg);
+            Services.get(TradeModule.class).sendText(sender, sendermsg);
+            Services.get(TradeModule.class).sendText(target, targetmsg);
 
             target.playSound(
                     Sound.sound()
@@ -81,8 +81,8 @@ public class TradeRequestManager {
                     () -> {
                         if (isPending(target, sender)) {
                             requests.remove(target.getUniqueId());
-                            TradeModule.sendText(sender, Component.text("Deine Handelsanfrage an " + target.getName() + " ist abgelaufen.", NamedTextColor.RED));
-                            TradeModule.sendText(target, Component.text("Die Handelsanfrage von " + sender.getName() + " ist abgelaufen.", NamedTextColor.RED));
+                            Services.get(TradeModule.class).sendText(sender, Component.text("Deine Handelsanfrage an " + target.getName() + " ist abgelaufen.", NamedTextColor.RED));
+                            Services.get(TradeModule.class).sendText(target, Component.text("Die Handelsanfrage von " + sender.getName() + " ist abgelaufen.", NamedTextColor.RED));
                         }
                     },
                     20L * 60 // 1 Minute

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

@@ -132,12 +132,12 @@ public class TradeSession {
 
     }
     private void sendReceivingMsg(Audience receiver, Component itemName, int amount){
-        TradeModule.sendText(receiver, Component.text("+ ", NamedTextColor.GREEN )
+        Services.get(TradeModule.class).sendText(receiver, Component.text("+ ", NamedTextColor.GREEN )
                 .append(itemName.color(NamedTextColor.GRAY))
                 .append(Component.text((amount> 1 ? " x" + amount : ""), NamedTextColor.GRAY)));
     }
     private void sendGivingMsg(Audience receiver, Component itemName, int amount){
-        TradeModule.sendText(receiver, Component.text("- ", NamedTextColor.RED )
+        Services.get(TradeModule.class).sendText(receiver, Component.text("- ", NamedTextColor.RED )
                 .append(itemName.color(NamedTextColor.GRAY))
                 .append(Component.text((amount> 1 ? " x" + amount : ""), NamedTextColor.GRAY)));
     }
@@ -154,7 +154,7 @@ public class TradeSession {
     }
 
     private void sendTradeCompletedLine(Player receiver, Player other) {
-        TradeModule.sendText(receiver,
+        Services.get(TradeModule.class).sendText(receiver,
                 Component.text("Handel mit ", NamedTextColor.GOLD)
                         .append(other.displayName().color(NamedTextColor.GRAY))
                         .append(Component.text(" abgeschlossen!", NamedTextColor.GOLD))

+ 1 - 1
src/main/resources/plugin.yml

@@ -15,7 +15,7 @@ commands:
   clan:
     description: Clan management command
     usage: /clan <join|rule>
-  customItem:
+  customItems:
     description: Custom Item management command
     usage: /customItem
     permission: customItem.commands

+ 9 - 0
src/main/resources/rooms/start_room.json

@@ -0,0 +1,9 @@
+{
+  "roomType": "START",
+  "anchor": { "x": 0, "y": 0, "z": 0 },
+  "boundingBox": {
+    "width": 21,
+    "height": 3,
+    "depth": 21
+  }
+}

BIN
src/main/resources/rooms/start_room.schem


Некоторые файлы не были показаны из-за большого количества измененных файлов