Bladeren bron

First commit. Trade, Economy, Clans, Claims, all working, might need bugfix

Jan 1 maand geleden
commit
3726cb1e55
54 gewijzigde bestanden met toevoegingen van 4166 en 0 verwijderingen
  1. 85 0
      pom.xml
  2. 42 0
      src/main/java/me/lethunderhawk/bazaarflux/util/CustomHeadCreator.java
  3. 12 0
      src/main/java/me/lethunderhawk/bazaarflux/util/MessageSender.java
  4. 65 0
      src/main/java/me/lethunderhawk/bazaarflux/util/command/CommandNode.java
  5. 109 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryGUI.java
  6. 78 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryManager.java
  7. 49 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/PlayerHeadListGUI.java
  8. 14 0
      src/main/java/me/lethunderhawk/bazaarflux/util/interfaces/BazaarFluxModule.java
  9. 149 0
      src/main/java/me/lethunderhawk/clans/Clan.java
  10. 194 0
      src/main/java/me/lethunderhawk/clans/ClanManager.java
  11. 53 0
      src/main/java/me/lethunderhawk/clans/ClanModule.java
  12. 75 0
      src/main/java/me/lethunderhawk/clans/claim/Claim.java
  13. 47 0
      src/main/java/me/lethunderhawk/clans/claim/ClaimListener.java
  14. 48 0
      src/main/java/me/lethunderhawk/clans/claim/ClaimManager.java
  15. 378 0
      src/main/java/me/lethunderhawk/clans/command/ClanCommand.java
  16. 48 0
      src/main/java/me/lethunderhawk/clans/gui/ClanGUI.java
  17. 40 0
      src/main/java/me/lethunderhawk/clans/placeholder/ClanPlaceHolder.java
  18. 23 0
      src/main/java/me/lethunderhawk/clans/rules/Rule.java
  19. 53 0
      src/main/java/me/lethunderhawk/custom/item/CustomItemModule.java
  20. 112 0
      src/main/java/me/lethunderhawk/custom/item/abstraction/CustomItem.java
  21. 104 0
      src/main/java/me/lethunderhawk/custom/item/command/CustomItemCommand.java
  22. 86 0
      src/main/java/me/lethunderhawk/custom/item/concrete/ClaimTool.java
  23. 71 0
      src/main/java/me/lethunderhawk/custom/item/concrete/TeleportSword.java
  24. 67 0
      src/main/java/me/lethunderhawk/custom/item/listener/CustomItemListener.java
  25. 88 0
      src/main/java/me/lethunderhawk/custom/item/manager/CustomItemManager.java
  26. 70 0
      src/main/java/me/lethunderhawk/economy/EconomyModule.java
  27. 43 0
      src/main/java/me/lethunderhawk/economy/api/EconomyAPI.java
  28. 40 0
      src/main/java/me/lethunderhawk/economy/api/EconomyPlaceholder.java
  29. 251 0
      src/main/java/me/lethunderhawk/economy/command/EcoCommand.java
  30. 49 0
      src/main/java/me/lethunderhawk/economy/currency/EconomyManager.java
  31. 20 0
      src/main/java/me/lethunderhawk/economy/listener/PlayerJoinListener.java
  32. 60 0
      src/main/java/me/lethunderhawk/economy/scoreboard/ScoreboardManager.java
  33. 70 0
      src/main/java/me/lethunderhawk/economy/scoreboard/ScoreboardTemplate.java
  34. 19 0
      src/main/java/me/lethunderhawk/economy/util/DayTime.java
  35. 55 0
      src/main/java/me/lethunderhawk/main/Main.java
  36. 10 0
      src/main/java/me/lethunderhawk/main/util/ItalicDeco.java
  37. 41 0
      src/main/java/me/lethunderhawk/tradeplugin/TradeModule.java
  38. 37 0
      src/main/java/me/lethunderhawk/tradeplugin/api/TradePlaceholder.java
  39. 46 0
      src/main/java/me/lethunderhawk/tradeplugin/command/TradeAcceptCommand.java
  40. 52 0
      src/main/java/me/lethunderhawk/tradeplugin/command/TradeCommand.java
  41. 141 0
      src/main/java/me/lethunderhawk/tradeplugin/input/player/NumberInputGUI.java
  42. 2 0
      src/main/java/me/lethunderhawk/tradeplugin/input/player/SignInputGUI.java
  43. 93 0
      src/main/java/me/lethunderhawk/tradeplugin/input/player/SignInputManager.java
  44. 155 0
      src/main/java/me/lethunderhawk/tradeplugin/listener/InventoryListener.java
  45. 31 0
      src/main/java/me/lethunderhawk/tradeplugin/listener/PlayerInteractListener.java
  46. 241 0
      src/main/java/me/lethunderhawk/tradeplugin/trade/TradeInventory.java
  47. 46 0
      src/main/java/me/lethunderhawk/tradeplugin/trade/TradeManager.java
  48. 105 0
      src/main/java/me/lethunderhawk/tradeplugin/trade/TradeRequestManager.java
  49. 348 0
      src/main/java/me/lethunderhawk/tradeplugin/trade/TradeSession.java
  50. 10 0
      src/main/java/me/lethunderhawk/tradeplugin/trade/TradeState.java
  51. 0 0
      src/main/resources/clans.yml
  52. 0 0
      src/main/resources/config.yml
  53. 33 0
      src/main/resources/plugin.yml
  54. 8 0
      src/main/resources/scoreboards/default.yml

+ 85 - 0
pom.xml

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>me.lethunderhawk</groupId>
+    <artifactId>bazaar-flux</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <name>BazaarFlux</name>
+
+    <properties>
+        <java.version>21</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>21</source>
+                    <target>21</target>
+                    <release>21</release>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.2.4</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <createDependencyReducedPom>false</createDependencyReducedPom>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+    </build>
+
+    <repositories>
+        <repository>
+            <id>papermc</id>
+            <url>https://repo.papermc.io/repository/maven-public/</url>
+        </repository>
+        <repository>
+            <id>sonatype</id>
+            <url>https://oss.sonatype.org/content/groups/public/</url>
+        </repository>
+        <repository>
+            <id>placeholderapi</id>
+            <url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
+        </repository>
+    </repositories>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.papermc.paper</groupId>
+            <artifactId>paper-api</artifactId>
+            <version>1.21.10-R0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>me.clip</groupId>
+            <artifactId>placeholderapi</artifactId>
+            <version>2.11.7</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>

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

@@ -0,0 +1,42 @@
+package me.lethunderhawk.bazaarflux.util;
+
+import com.destroystokyo.paper.profile.PlayerProfile;
+import com.destroystokyo.paper.profile.ProfileProperty;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.SkullMeta;
+
+import java.util.List;
+import java.util.UUID;
+
+public class CustomHeadCreator {
+
+    public static ItemStack createCustomHead(String textureValue, Component displayName, Component loreText) {
+        // Create the player head ItemStack
+        ItemStack playerHead = createCustomHead(textureValue);
+        SkullMeta meta = (SkullMeta) playerHead.getItemMeta();
+
+        meta.displayName(displayName);
+        meta.lore(List.of(loreText));
+
+        playerHead.setItemMeta(meta);
+
+        return playerHead; // Return the custom head ItemStack
+    }
+
+    public static ItemStack createCustomHead(String textureValue) {
+        ItemStack head = new ItemStack(Material.PLAYER_HEAD);
+        SkullMeta meta = (SkullMeta) head.getItemMeta();
+
+        if (meta != null) {
+            PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID(), null);
+            profile.setProperty(new ProfileProperty("textures", textureValue));
+            meta.setPlayerProfile(profile);
+            head.setItemMeta(meta);
+        }
+
+        return head;
+    }
+}

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

@@ -0,0 +1,12 @@
+package me.lethunderhawk.bazaarflux.util;
+
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+
+public class MessageSender {
+    public static void sendText(Audience receiver, Component message, String prefix) {
+        receiver.sendMessage(Component.text(prefix + " ", NamedTextColor.GOLD)
+                .append(message));
+    }
+}

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

@@ -0,0 +1,65 @@
+package me.lethunderhawk.bazaarflux.util.command;
+
+import org.bukkit.command.CommandSender;
+
+import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+
+public class CommandNode {
+    private final String name;
+    private final String description;
+    private BiConsumer<CommandSender, String[]> executor;
+    private BiFunction<CommandSender, String[], List<String>> tabCompleter;
+    private Map<String, CommandNode> subCommands = new HashMap<>();
+    private CommandNode parent;
+
+    public CommandNode(String name, String description, BiConsumer<CommandSender, String[]> executor) {
+        this.name = name;
+        this.description = description;
+        this.executor = executor;
+    }
+
+    public CommandNode registerSubCommand(String name, String description, BiConsumer<CommandSender, String[]> executor) {
+        CommandNode subCommand = new CommandNode(name, description, executor);
+        subCommand.parent = this;
+        subCommands.put(name.toLowerCase(), subCommand);
+        return subCommand;
+    }
+
+    public void addSubCommand(CommandNode subCommand) {
+        subCommand.parent = this;
+        subCommands.put(subCommand.getName().toLowerCase(), subCommand);
+    }
+
+    public CommandNode getSubCommand(String name) {
+        return subCommands.get(name.toLowerCase());
+    }
+
+    public boolean hasSubCommand(String name) {
+        return subCommands.containsKey(name.toLowerCase());
+    }
+
+    public Collection<CommandNode> getSubCommands() {
+        return subCommands.values();
+    }
+
+    public List<String> getSubCommandNames() {
+        return new ArrayList<>(subCommands.keySet());
+    }
+
+    public void setTabCompleter(BiFunction<CommandSender, String[], List<String>> tabCompleter) {
+        this.tabCompleter = tabCompleter;
+    }
+
+    // Getters
+    public String getName() { return name; }
+    public String getDescription() { return description; }
+    public BiConsumer<CommandSender, String[]> getExecutor() { return executor; }
+    public BiFunction<CommandSender, String[], List<String>> getTabCompleter() { return tabCompleter; }
+    public CommandNode getParent() { return parent; }
+
+    public void setExecutor(BiConsumer<CommandSender, String[]> executor) {
+        this.executor = executor;
+    }
+}

+ 109 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryGUI.java

@@ -0,0 +1,109 @@
+package me.lethunderhawk.bazaarflux.util.gui;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryHolder;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Stack;
+import java.util.function.BiConsumer;
+
+/**
+ * Generic, reusable Inventory GUI.
+ */
+public class InventoryGUI implements InventoryHolder {
+
+    private final String title;
+    private final int size;
+    private final Inventory inventory;
+
+    private final Map<Integer, BiConsumer<Player, ClickType>> slotActions = new HashMap<>();
+    private final Stack<InventoryGUI> previousGuis = new Stack<>();
+
+    public InventoryGUI(String title, int size) {
+        this.title = title;
+        this.size = size;
+        this.inventory = Bukkit.createInventory(this, size, title);
+    }
+
+    public void handleClick(Player player, int slot, ClickType type) {
+        BiConsumer<Player, ClickType> action = slotActions.get(slot);
+        if (action != null) {
+            action.accept(player, type);
+        }
+    }
+    /**
+     * Sets an item in a specific slot.
+     */
+    public void setItem(int slot, ItemStack item) {
+        inventory.setItem(slot, item);
+    }
+
+    /**
+     * Sets an item and assigns a click action for that slot.
+     */
+    public void setItemWithClickAction(int slot, ItemStack item, BiConsumer<Player, ClickType> action) {
+        inventory.setItem(slot, item);
+        slotActions.put(slot, action);
+    }
+
+    /**
+     * Fills all empty slots with a background item.
+     */
+    public void fillBackground(Material material, String displayName) {
+        ItemStack background = new ItemStack(material);
+        ItemMeta meta = background.getItemMeta();
+        meta.setDisplayName(displayName);
+        background.setItemMeta(meta);
+
+        for (int i = 0; i < size; i++) {
+            if (inventory.getItem(i) == null) {
+                inventory.setItem(i, background);
+            }
+        }
+    }
+
+    /**
+     * Opens this GUI for a player.
+     */
+    public void open(Player player) {
+        player.openInventory(inventory);
+    }
+
+    /**
+     * Opens another GUI and remembers this one for navigation.
+     */
+    public void openNext(Player player, InventoryGUI nextGui) {
+        nextGui.previousGuis.push(this);
+        nextGui.open(player);
+    }
+
+    /**
+     * Opens the previous GUI if available.
+     */
+    public void openPrevious(Player player) {
+        if (!previousGuis.isEmpty()) {
+            previousGuis.pop().open(player);
+        }
+    }
+
+    @Override
+    public @NotNull Inventory getInventory() {
+        return inventory;
+    }
+
+    public void openPrevious(Player player, ClickType type) {
+        openPrevious(player);
+    }
+
+    public interface AutoCloseHandler {
+        void onClosedByPlayer(Player player);
+    }
+}

+ 78 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryManager.java

@@ -0,0 +1,78 @@
+package me.lethunderhawk.bazaarflux.util.gui;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.plugin.Plugin;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Central registry & listener for open InventoryGUI instances.
+ * Register once: InventoryManager.register(plugin);
+ */
+public final class InventoryManager implements Listener {
+    private static final Map<UUID, InventoryGUI> openGuis = new ConcurrentHashMap<>();
+    private static volatile boolean registered = false;
+    private static Plugin providerPlugin;
+
+    public static void register(Plugin plugin) {
+        if (registered) return;
+        providerPlugin = plugin;
+        Bukkit.getPluginManager().registerEvents(new InventoryManager(), plugin);
+        registered = true;
+    }
+
+    public static void openFor(Player player, InventoryGUI gui) {
+        // ensure main thread
+        if (!Bukkit.isPrimaryThread()) {
+            Bukkit.getScheduler().runTask(providerPlugin, () -> openFor(player, gui));
+            return;
+        }
+        openGuis.put(player.getUniqueId(), gui);
+        gui.open(player);
+    }
+
+    public static InventoryGUI getOpen(UUID playerId) {
+        return openGuis.get(playerId);
+    }
+
+    public static void close(UUID playerId) {
+        openGuis.remove(playerId);
+    }
+
+    @EventHandler
+    public void onInventoryClick(InventoryClickEvent event) {
+        if (!(event.getWhoClicked() instanceof Player player)) return;
+        if (!(event.getView().getTopInventory().getHolder() instanceof InventoryGUI gui)) return;
+
+        // ignore clicks outside top inventory
+        if (event.getRawSlot() >= event.getView().getTopInventory().getSize()) return;
+
+        InventoryGUI open = openGuis.get(player.getUniqueId());
+        if (open == null || open != gui) return; // ensure it's the tracked instance
+
+        event.setCancelled(true);
+        gui.handleClick(player, event.getRawSlot(), event.getClick());
+    }
+
+    @EventHandler
+    public void onInventoryClose(InventoryCloseEvent event) {
+        if (!(event.getPlayer() instanceof Player player)) return;
+        InventoryGUI gui = openGuis.get(player.getUniqueId());
+        if (gui == null) return;
+
+        // If the closed inventory is the tracked GUI and not due to opening other inventories, treat as accidental close
+        if (event.getView().getTopInventory().getHolder() == gui && event.getReason() != InventoryCloseEvent.Reason.OPEN_NEW) {
+            if (gui instanceof InventoryGUI.AutoCloseHandler handler) {
+                handler.onClosedByPlayer(player);
+            }
+            openGuis.remove(player.getUniqueId());
+        }
+    }
+}

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

@@ -0,0 +1,49 @@
+package me.lethunderhawk.bazaarflux.util.gui;
+
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.SkullMeta;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+
+/**
+ * GUI displaying a list of players as clickable heads.
+ */
+public class PlayerHeadListGUI extends InventoryGUI {
+
+    public PlayerHeadListGUI(
+            String title,
+            int size,
+            List<Player> players,
+            BiConsumer<Player, Player> onHeadClick
+    ) {
+        super(title, size);
+
+        int slot = 0;
+        for (Player target : players) {
+            if (slot >= size) break;
+
+            ItemStack head = createPlayerHead(target);
+            int finalSlot = slot;
+
+            setItemWithClickAction(finalSlot, head, (clickingPlayer, type) ->
+                    onHeadClick.accept(clickingPlayer, target)
+            );
+
+            slot++;
+        }
+
+        fillBackground(Material.GRAY_STAINED_GLASS_PANE, " ");
+    }
+
+    private ItemStack createPlayerHead(Player player) {
+        ItemStack head = new ItemStack(Material.PLAYER_HEAD);
+        SkullMeta meta = (SkullMeta) head.getItemMeta();
+        meta.setOwningPlayer(player);
+        meta.setDisplayName(player.getName());
+        head.setItemMeta(meta);
+        return head;
+    }
+}

+ 14 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/interfaces/BazaarFluxModule.java

@@ -0,0 +1,14 @@
+package me.lethunderhawk.bazaarflux.util.interfaces;
+
+import me.lethunderhawk.bazaarflux.util.MessageSender;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+
+public interface BazaarFluxModule {
+    String getPrefix();
+    void onEnable();
+    void onDisable();
+    default void sendText(Audience receiver, Component infoText){
+        MessageSender.sendText(receiver, infoText, getPrefix());
+    }
+}

+ 149 - 0
src/main/java/me/lethunderhawk/clans/Clan.java

@@ -0,0 +1,149 @@
+package me.lethunderhawk.clans;
+
+import me.lethunderhawk.clans.claim.Claim;
+import me.lethunderhawk.clans.rules.Rule;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.entity.Player;
+
+import java.util.*;
+
+public class Clan {
+    private final UUID owner;
+    private final Set<UUID> members;
+    private final Map<String, UUID> pendingRequests = new HashMap<>(); //name of player, Player UUID
+    private final String name;
+    private Set<Rule> ruleSet;
+    private final List<Claim> claims = new ArrayList<>();
+    private final UUID id;
+
+    public Clan(String name, UUID owner, Set<Rule> ruleSet, Set<UUID> members){
+        this.name = name;
+        this.owner = owner;
+        this.ruleSet = ruleSet;
+        this.members = members;
+        id = UUID.randomUUID();
+    }
+
+    public Clan(String name, UUID owner, UUID id, Set<UUID> members){
+        this.name = name;
+        this.owner = owner;
+        this.members = members;
+        this.id = id;
+        ruleSet = new HashSet<>();
+    }
+    public String toTabComplete() {
+        return name.toLowerCase().trim();
+    }
+
+    public Set<Rule> getRules() {
+        return ruleSet;
+    }
+
+    public boolean isInClan(UUID player) {
+        return owner.equals(player) || members.contains(player);
+    }
+    /**
+     *
+     * @param player The player who wants to leave
+     * @return If the player can leave this clan
+     */
+    public boolean leave(UUID player) {
+        if (members.remove(player)) {
+            return true;
+        } else return owner.equals(player) && members.isEmpty();
+    }
+
+    public void acceptRequest(UUID owner, String player){
+        if(!owner.equals(this.owner)) return;
+        if(!pendingRequests.containsKey(player)) return;
+        UUID joining = pendingRequests.remove(player);
+        members.add(joining);
+    }
+
+    public void denyRequest(UUID owner, String player){
+        if(!owner.equals(this.owner)) return;
+        pendingRequests.remove(player);
+    }
+
+    public void sendRequestToOwner(UUID playerUUID){
+        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()))
+                .append(Component.text("[Accept]", NamedTextColor.GREEN).clickEvent(ClickEvent.runCommand("/clan acceptRequest " + player.getName()))));
+    }
+
+    public boolean joinRequest(UUID player) {
+        pendingRequests.put(Bukkit.getPlayer(player).getName(), player);
+        sendRequestToOwner(player);
+        return true;
+    }
+
+    public String getName() {
+        return name;
+    }
+    public Set<UUID> getAllMemberUUIDs() {
+        Set<UUID> allMembers = new HashSet<>(members);
+        allMembers.add(owner);
+        return allMembers;
+    }
+    public Set<UUID> getMembers() {
+        return members;
+    }
+
+    public Player getOwner() {
+        return Bukkit.getPlayer(owner);
+    }
+
+    public Set<String> getRequests() {
+        return pendingRequests.keySet();
+    }
+    public int getUsedBlocks() {
+        return claims.stream().mapToInt(Claim::getVolume).sum();
+    }
+
+    public void addClaim(Claim claim) {
+        claims.add(claim);
+    }
+
+
+    public boolean isInsideClaim(Location loc) {
+        return claims.stream().anyMatch(c -> c.contains(loc));
+    }
+
+    public List<Claim> getClaims() {
+        return claims;
+    }
+
+    public UUID getId() {
+        return id;
+    }
+
+    public UUID getOwnerUUID() {
+        return owner;
+    }
+
+    public ArrayList<Player> getAllMembers() {
+        ArrayList<Player> playerList = new ArrayList<>();
+        playerList.add(Bukkit.getPlayer(owner));
+        for (UUID uuid : members) {
+            Player player = Bukkit.getPlayer(uuid);
+            if (player != null) {
+                playerList.add(player);
+            }
+        }
+
+        return playerList;
+    }
+
+    public void setRules(Set<Rule> rules) {
+        this.ruleSet = rules;
+    }
+}

+ 194 - 0
src/main/java/me/lethunderhawk/clans/ClanManager.java

@@ -0,0 +1,194 @@
+package me.lethunderhawk.clans;
+
+import me.lethunderhawk.clans.claim.Claim;
+import me.lethunderhawk.clans.rules.Rule;
+import me.lethunderhawk.main.Main;
+import org.bukkit.Bukkit;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class ClanManager {
+    private final Set<Rule> basicRules = new HashSet<>();
+    private final HashMap<UUID, Clan> clans = new HashMap<>();
+
+    private final File clansFile;
+    private FileConfiguration clansConfig;
+
+    public ClanManager(JavaPlugin plugin) {
+
+        this.clansFile = new File(plugin.getDataFolder(), "clans.yml");
+        this.clansConfig = YamlConfiguration.loadConfiguration(clansFile);
+        loadFile(plugin);
+        loadClans();
+    }
+
+    private void loadFile(JavaPlugin plugin) {
+        if (!clansFile.exists()) {
+            plugin.getDataFolder().mkdirs();
+            plugin.saveResource("clans.yml", false);
+        }
+        clansConfig = YamlConfiguration.loadConfiguration(clansFile);
+    }
+
+    private void loadClans() {
+        ConfigurationSection section = clansConfig.getConfigurationSection("clans");
+        if (section == null) return;
+
+        for (String idStr : section.getKeys(false)) {
+            UUID id;
+            try {
+                id = UUID.fromString(idStr);
+            } catch (IllegalArgumentException e) {
+                Bukkit.getLogger().warning("Invalid clan UUID: " + idStr);
+                continue;
+            }
+
+            String base = "clans." + idStr;
+
+            String name = clansConfig.getString(base + ".name", "Unknown");
+            String ownerStr = clansConfig.getString(base + ".owner");
+
+            if (ownerStr == null) {
+                Bukkit.getLogger().warning("Clan " + id + " has no owner, skipping");
+                continue;
+            }
+
+            UUID owner;
+            try {
+                owner = UUID.fromString(ownerStr);
+            } catch (Exception e) {
+                Bukkit.getLogger().warning("Clan " + id + " has invalid owner UUID");
+                continue;
+            }
+
+            Set<UUID> members = clansConfig.getStringList(base + ".members")
+                    .stream()
+                    .map(UUID::fromString)
+                    .collect(Collectors.toSet());
+
+            Set<Rule> rules = clansConfig.getStringList(base + ".rules")
+                    .stream()
+                    .map(Rule::fromString)
+                    .collect(Collectors.toSet());
+
+            Clan clan = new Clan(name, owner, id, members);
+            clan.setRules(rules);
+
+            // ---- Claims laden ----
+            List<Map<?, ?>> claimData = clansConfig.getMapList(base + ".claims");
+            for (Map<?, ?> map : claimData) {
+                Claim claim = Claim.fromMap(clan.getId(), map);
+                if (claim != null){
+                    clan.addClaim(claim);
+                    Main.getInstance().getClanModule().getClaimManager().registerClaim(claim);
+                }else{
+                    Bukkit.getLogger().warning("Clan " + id + " has invalid Claim");
+                }
+            }
+
+            clans.put(id, clan);
+        }
+    }
+    public void saveClans() {
+        clansConfig.set("clans", null);
+
+        for (Clan clan : clans.values()) {
+            String path = "clans." + clan.getId();
+
+            clansConfig.set(path + ".name", clan.getName());
+            clansConfig.set(path + ".owner", clan.getOwnerUUID().toString());
+            clansConfig.set(path + ".members",
+                    clan.getMembers().stream().map(UUID::toString).toList());
+
+            List<Map<String, Object>> claimList = new ArrayList<>();
+            for (Claim c : clan.getClaims()) {
+                Map<String, Object> map = new HashMap<>();
+                map.put("world", c.getWorld());
+                map.put("minX", c.getMinX());
+                map.put("maxX", c.getMaxX());
+                map.put("minZ", c.getMinZ());
+                map.put("maxZ", c.getMaxZ());
+                claimList.add(map);
+            }
+
+            clansConfig.set(path + ".claims", claimList);
+        }
+
+        try {
+            clansConfig.save(clansFile);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    public ArrayList<Clan> getClans() {
+        return new ArrayList<>(clans.values());
+    }
+
+    public Clan getMyClan(UUID player) {
+        Clan found = null;
+        for(Clan clan : clans.values()){
+            if(clan.isInClan(player)){
+                found = clan;
+                break;
+            }
+        }
+        return found;
+    }
+    /**
+     *
+     * @param player The player that creates the Clan / the owner
+     * @param name The name of the clan
+     * @return if the clan was succesfully created or not
+     */
+    public boolean createClan(Player player, String name) {
+        if(name.isEmpty() || isThisNameTaken(name)) return false;
+        UUID random = UUID.randomUUID();
+        clans.put(random, new Clan(name, player.getUniqueId(), random, new HashSet<>()));
+        return true;
+    }
+
+    private boolean isThisNameTaken(String name) {
+        return getClanByName(name) != null;
+    }
+    @Nullable
+    public Clan getClanByName(String name){
+        for(Clan clan : clans.values()){
+            if(clan.getName().equals(name)) return clan;
+        }
+        return null;
+    }
+    @Nullable
+    public Clan getClanById(UUID uuid){
+        for(Clan clan : clans.values()){
+            if(clan.getId().equals(uuid)) return clan;
+        }
+        return null;
+    }
+
+    public void removeClan(String name){
+        clans.values().removeIf(clan -> clan.getName().equals(name));
+    }
+
+    public boolean leaveClan(UUID player) {
+        Clan clan = getMyClan(player);
+        if(clan != null) return clan.leave(player);
+        return false;
+    }
+
+    public boolean joinClan(UUID player, String arg) {
+        Clan clan = getClanByName(arg);
+        if(clan == null || getMyClan(player) != null) return false;
+        return clan.joinRequest(player);
+    }
+}

+ 53 - 0
src/main/java/me/lethunderhawk/clans/ClanModule.java

@@ -0,0 +1,53 @@
+package me.lethunderhawk.clans;
+
+import me.lethunderhawk.bazaarflux.util.MessageSender;
+import me.lethunderhawk.clans.claim.ClaimListener;
+import me.lethunderhawk.clans.claim.ClaimManager;
+import me.lethunderhawk.clans.command.ClanCommand;
+import me.lethunderhawk.clans.placeholder.ClanPlaceHolder;
+import me.lethunderhawk.main.Main;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+
+public class ClanModule {
+    private ClanManager clanManager;
+    private ClaimManager claimManager;
+
+    public String getPrefix(){
+        return "[Clan]";
+    }
+
+    public void onEnable(){
+
+        claimManager = new ClaimManager();
+        clanManager = new ClanManager(Main.getInstance());
+
+        Main.getInstance().getCommand("clan").setExecutor(new ClanCommand(clanManager));
+        Main.getInstance().getCommand("clan").setTabCompleter(new ClanCommand(clanManager));
+
+        Main.getInstance().getServer().getPluginManager().registerEvents(new ClaimListener(claimManager), Main.getInstance());
+        //Main.getInstance().getServer().getPluginManager().registerEvents(new ToolListener(), Main.getInstance());
+        if (Main.getInstance().getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {
+            new ClanPlaceHolder(clanManager).register();
+        }
+
+    }
+    public static void sendText(Audience receiver, String infoText){
+        MessageSender.sendText(receiver, Component.text(infoText), "[Clan]");
+    }
+    public static void sendText(Audience receiver, Component infoText){
+        MessageSender.sendText(receiver, infoText, "[Clan]");
+    }
+
+    public ClanManager getClanManager() {
+        return clanManager;
+    }
+
+    public ClaimManager getClaimManager() {
+        return claimManager;
+    }
+
+    public void onDisable(){
+        clanManager.saveClans();
+    }
+}

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

@@ -0,0 +1,75 @@
+package me.lethunderhawk.clans.claim;
+
+import org.bukkit.Location;
+
+import java.util.Map;
+import java.util.UUID;
+
+public class Claim {
+
+    private final UUID clanId; // or clan name if you don’t have IDs
+    private final String world;
+
+    private final int minX, maxX;
+    private final int minZ, maxZ;
+
+    public Claim(UUID clanId, String world, int x1, int x2, int z1, int z2) {
+        this.clanId = clanId;
+        this.world = world;
+
+        this.minX = Math.min(x1, x2);
+        this.maxX = Math.max(x1, x2);
+
+        this.minZ = Math.min(z1, z2);
+        this.maxZ = Math.max(z1, z2);
+    }
+
+    public static Claim fromMap(UUID clanId, Map<?, ?> map) {
+        String world = (String) map.get("world");
+        if (world == null) return null;
+
+        int minX = ((Number) map.get("minX")).intValue();
+        int maxX = ((Number) map.get("maxX")).intValue();
+        int minZ = ((Number) map.get("minZ")).intValue();
+        int maxZ = ((Number) map.get("maxZ")).intValue();
+
+        return new Claim(clanId, world, minX, maxX, minZ, maxZ);
+    }
+
+    public boolean contains(Location loc) {
+        if (!loc.getWorld().getName().equals(world)) return false;
+
+        int x = loc.getBlockX();
+        int z = loc.getBlockZ();
+
+        return x >= minX && x <= maxX
+                && z >= minZ && z <= maxZ;
+    }
+
+    public int getVolume() {
+        return (maxX - minX + 1)
+                * (maxZ - minZ + 1);
+    }
+    public UUID getClanId(){
+        return clanId;
+    }
+
+    public String getWorld() {
+        return world;
+    }
+
+    public int getMinX() {
+        return minX;
+    }
+    public int getMinZ() {
+        return minZ;
+    }
+
+    public int getMaxX() {
+        return maxX;
+    }
+
+    public int getMaxZ() {
+        return maxZ;
+    }
+}

+ 47 - 0
src/main/java/me/lethunderhawk/clans/claim/ClaimListener.java

@@ -0,0 +1,47 @@
+package me.lethunderhawk.clans.claim;
+
+import me.lethunderhawk.clans.Clan;
+import me.lethunderhawk.main.Main;
+import org.bukkit.Location;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+
+import java.util.UUID;
+
+public class ClaimListener implements Listener {
+    private final ClaimManager claimManager;
+
+    public ClaimListener(ClaimManager claimManager) {
+        this.claimManager = claimManager;
+    }
+
+    @EventHandler
+    public void onBreak(BlockBreakEvent e) {
+        if(preventChange(e.getPlayer().getUniqueId(), e.getBlock().getLocation())){
+            e.setCancelled(true);
+            e.getPlayer().sendMessage("§cThis land belongs to another clan!");
+        }
+    }
+
+    @EventHandler
+    public void onBlockPlace(BlockPlaceEvent e){
+        if(preventChange(e.getPlayer().getUniqueId(), e.getBlock().getLocation())){
+            e.setCancelled(true);
+            e.getPlayer().sendMessage("§cThis land belongs to another clan!");
+        }
+    }
+
+    private boolean preventChange(UUID playerUUID, Location location){
+        Claim claim = claimManager.getClaimAt(location);
+        if(claim != null){
+            Clan clan = Main.getInstance().getClanModule().getClanManager().getClanById(claim.getClanId());
+
+            if (clan != null) {
+                return !clan.isInClan(playerUUID);
+            }
+        }
+        return false;
+    }
+}

+ 48 - 0
src/main/java/me/lethunderhawk/clans/claim/ClaimManager.java

@@ -0,0 +1,48 @@
+package me.lethunderhawk.clans.claim;
+
+import org.bukkit.Location;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ClaimManager {
+
+    private List<Claim> allClaims = new ArrayList<>();
+
+    public Claim getClaimAt(Location loc) {
+        for (Claim c : allClaims) {
+            if (c.contains(loc)) return c;
+        }
+        return null;
+    }
+
+    public void registerClaim(Claim claim){
+        allClaims.add(claim);
+    }
+
+    public boolean overlaps(Claim a, Claim b) {
+        if (!a.getWorld().equals(b.getWorld())) return false;
+
+        return a.getMinX() <= b.getMaxX() && a.getMaxX() >= b.getMinX()
+                && a.getMinZ() <= b.getMaxZ() && a.getMaxZ() >= b.getMinZ();
+    }
+
+    /**
+     * @param newClaim The claim you want to check
+     * @return The Claim it overlaps with or null if it doesn't overlap with any claims
+     */
+    @Nullable
+    public Claim overlaps(Claim newClaim) {
+        for (Claim existing : allClaims) {
+            if (overlaps(newClaim, existing)) {
+                return existing;
+            }
+        }
+        return null;
+    }
+
+    public void removeAllClaims() {
+        allClaims = new ArrayList<>();
+    }
+}

+ 378 - 0
src/main/java/me/lethunderhawk/clans/command/ClanCommand.java

@@ -0,0 +1,378 @@
+package me.lethunderhawk.clans.command;
+
+import me.lethunderhawk.bazaarflux.util.command.CommandNode;
+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.gui.ClanGUI;
+import me.lethunderhawk.clans.rules.Rule;
+import me.lethunderhawk.main.Main;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+
+public class ClanCommand implements CommandExecutor, TabCompleter {
+    private ClanManager manager;
+    private CommandNode rootCommand;
+
+    public ClanCommand(ClanManager manager) {
+        this.manager = manager;
+        this.rootCommand = new CommandNode("clan", "Main clan command", null);
+        setupDefaultCommands();
+    }
+    private void setupDefaultCommands() {
+        // Register default commands
+
+        registerCommand("list", "List all clans", this::handleList);
+        registerCommand("help", "Gives information about the use of clan commands", this::handleHelp);
+        registerCommand("leave", "Leave your current clan", this::handleLeaveClan);
+        registerCommand("reload", "Reload the clan Plugin", this::handleReload);
+        registerCommand("members", "Show the members of your clan", this::handleShowMembers);
+        registerCommand("save", "Save all clans and Claims", this::handleSave);
+        //registerCommand("menu", "Shows a GUI to handle your clan if you are the owner", this::handleGUI);
+        CommandNode claimNode = registerCommand("claim", "Claim land for your clan", this::handleClaim);
+        claimNode.registerSubCommand("info", "Get information about your claims", this::infoClaims);
+        claimNode.registerSubCommand("removeAll", "Remove all current claims", this::removeAllClaims);
+
+        CommandNode acceptNode = registerCommand("acceptRequest", "Accept a pending request of a player", this::handleAcceptRequest);
+        acceptNode.setTabCompleter(this::completeRequests);
+
+        CommandNode declineNode = registerCommand("declineRequest", "Decline a pending request of a player", this::handleDeclineRequest);
+        declineNode.setTabCompleter(this::completeRequests);
+
+        CommandNode removeNode = registerCommand("remove", "Remove a Clan", this::removeClan);
+        removeNode.setTabCompleter(this::completeClans);
+
+        CommandNode joinNode = registerCommand("join", "Join a clan", this::handleJoin);
+        joinNode.setTabCompleter(this::completeClans);
+
+        CommandNode ruleNode = registerCommand("rule", "Manage clan rules", null);
+        ruleNode.registerSubCommand("list", "List all rules", this::handleRuleList);
+        ruleNode.registerSubCommand("set", "Set a rule value", this::handleRuleSet)
+                .setTabCompleter(this::completeRules);
+        ruleNode.registerSubCommand("add", "Add a new rule", this::handleRuleAdd);
+        ruleNode.registerSubCommand("remove", "Remove a rule", this::handleRuleRemove)
+                .setTabCompleter(this::completeRules);
+
+        registerCommand("create", "Create a new clan", this::handleCreate);
+    }
+
+    private void handleSave(CommandSender sender, String[] strings) {
+        if(!sender.isOp()) return;
+        Main.getInstance().getClanModule().getClanManager().saveClans();
+    }
+
+    private void removeAllClaims(CommandSender sender, String[] strings) {
+        if(!(sender instanceof Player player)) return;
+        Main.getInstance().getClanModule().getClaimManager().removeAllClaims();
+    }
+
+    private void infoClaims(CommandSender sender, String[] strings) {
+        if(!(sender instanceof Player player)) return;
+        UUID uuid = player.getUniqueId();
+        if(manager.getMyClan(uuid) == null) return;
+        for(Claim claim : manager.getMyClan(uuid).getClaims()){
+            ClanModule.sendText(player, "info about your claim");
+            ClanModule.sendText(player, claim.getVolume()+" is this volume");
+        }
+
+    }
+
+    private void handleClaim(CommandSender sender, String[] strings) {
+        sender.sendMessage("Nothing yet implemented! come back later!");
+    }
+
+    private void handleGUI(CommandSender sender, String[] strings) {
+        if(!(sender instanceof Player player)) return;
+        if(manager.getMyClan(player.getUniqueId()) == null) return;
+        new ClanGUI(manager, player).open();
+    }
+
+
+    private void handleDeclineRequest(CommandSender sender, String[] strings) {
+        if (!(sender instanceof Player player) || strings[0] == null || strings[0].isEmpty()) return;
+        UUID uuid = player.getUniqueId();
+        Clan clan = manager.getMyClan(uuid);
+        clan.denyRequest(uuid, strings[0]);
+        ClanModule.sendText(player, "You declined the request from " + strings[0] + ".");
+    }
+
+    private void handleAcceptRequest(CommandSender sender, String[] strings) {
+        if (!(sender instanceof Player player) || strings[0] == null || strings[0].isEmpty()) return;
+        UUID uuid = player.getUniqueId();
+        Clan clan = manager.getMyClan(uuid);
+        clan.acceptRequest(uuid, strings[0]);
+        ClanModule.sendText(player, "You accepted the request from " + strings[0] + ".");
+    }
+
+    private void handleShowMembers(CommandSender sender, String[] strings) {
+        if (!(sender instanceof Player player)) return;
+        UUID uuid = player.getUniqueId();
+        Clan clan = manager.getMyClan(uuid);
+        if(clan == null) return;
+        ClanModule.sendText(player, "§6=== Clan Members - " + clan.getName() + " ===");
+        Player owner = clan.getOwner();
+        ClanModule.sendText(player,
+                Component.text("Owner: ", NamedTextColor.RED)
+                .append(Component.text(owner.getName(), NamedTextColor.GRAY)));
+        for(UUID member : clan.getMembers()){
+            ClanModule.sendText(player, Bukkit.getPlayer(member).getName());
+        }
+    }
+
+    private void handleReload(CommandSender sender, String[] strings) {
+        if(sender.isOp()) {
+            reload();
+            ClanModule.sendText(sender, "Reloading...");
+        }
+    }
+
+    private void handleLeaveClan(CommandSender sender, String[] strings) {
+        if (!(sender instanceof Player player)) return;
+        UUID uuid = player.getUniqueId();
+        Clan clan = manager.getMyClan(uuid);
+        if(manager.leaveClan(uuid)){
+            ClanModule.sendText(player,"You left your Clan ");
+            if(clan.getOwner().equals(player)){
+                manager.removeClan(clan.getName());
+                ClanModule.sendText(player,"The clan was deleted since you are the only one left!");
+            }
+        }else{
+            ClanModule.sendText(player,"You cannot leave this clan! Either join a clan or if you are an owner, please remove all members first!");
+        }
+    }
+
+    private void handleHelp(CommandSender sender, String[] strings) {
+        sendHelp(sender);
+    }
+
+    private void removeClan(CommandSender commandSender, String[] strings) {
+        if(!commandSender.isOp()) return;
+        if(strings[0] != null && !strings[0].isEmpty()) manager.removeClan(strings[0]);
+    }
+
+    private void handleCreate(CommandSender commandSender, String[] strings) {
+        if (!(commandSender instanceof Player player)) return;
+
+        String name = String.join("-", strings);
+        if (manager.getMyClan(player.getUniqueId()) != null){
+            ClanModule.sendText(player,"You are already in a clan! Leave your clan first!");
+            return;
+        }
+        if(manager.createClan(player, name)) ClanModule.sendText(player,"Creating the Clan " + name + " ...");
+        else ClanModule.sendText(player,"The Name for the Clan " + name + " is already taken or you are already in a clan!");
+    }
+
+    private void handleRuleRemove(CommandSender commandSender, String[] strings) {
+
+    }
+
+    private void handleRuleAdd(CommandSender commandSender, String[] strings) {
+
+    }
+
+    private void handleRuleSet(CommandSender commandSender, String[] strings) {
+
+    }
+
+    private void handleRuleList(CommandSender commandSender, String[] strings) {
+
+    }
+
+    // Helper method to easily register commands
+    public CommandNode registerCommand(String name, String description, BiConsumer<CommandSender, String[]> executor) {
+        return rootCommand.registerSubCommand(name, description, executor);
+    }
+    @Override
+    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+        if (args.length == 0) {
+            sendHelp(sender);
+            return true;
+        }
+
+        List<String> argList = new ArrayList<>(Arrays.asList(args));
+        CommandNode currentNode = rootCommand;
+        CommandNode targetNode = null;
+
+        // Traverse the command tree
+        while (!argList.isEmpty()) {
+            String nextArg = argList.get(0);
+            CommandNode nextNode = currentNode.getSubCommand(nextArg);
+
+            if (nextNode == null) {
+                // No matching subcommand found, use current node if it has an executor
+                targetNode = currentNode.getExecutor() != null ? currentNode : null;
+                break;
+            }
+
+            currentNode = nextNode;
+            argList.remove(0);
+
+            // If this is the last argument or node has no further subcommands
+            if (argList.isEmpty() || nextNode.getSubCommands().isEmpty()) {
+                targetNode = nextNode;
+                break;
+            }
+        }
+
+        if (targetNode == null) {
+            ClanModule.sendText(sender,"§cUnknown command. Use /clan help for available commands.");
+            return true;
+        }
+
+        if (targetNode.getExecutor() == null) {
+            ClanModule.sendText(sender,"§cThis command requires additional arguments.");
+            sendSubCommands(sender, targetNode);
+            return true;
+        }
+
+        // Execute the command with remaining arguments
+        String[] remainingArgs = argList.toArray(new String[0]);
+        targetNode.getExecutor().accept(sender, remainingArgs);
+        return true;
+    }
+
+    @Override
+    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
+        List<String> suggestions = new ArrayList<>();
+
+        if (args.length == 0) {
+            return suggestions;
+        }
+
+        // Start at root and traverse
+        CommandNode currentNode = rootCommand;
+        List<String> argList = new ArrayList<>(Arrays.asList(args));
+        String lastArg = args[args.length - 1].toLowerCase();
+
+        // Try to traverse as far as possible
+        for (int i = 0; i < argList.size() - 1; i++) {
+            String arg = argList.get(i);
+            CommandNode nextNode = currentNode.getSubCommand(arg);
+            if (nextNode == null) {
+                break;
+            }
+            currentNode = nextNode;
+        }
+
+        // Get suggestions from current node
+        if (currentNode.getTabCompleter() != null) {
+            suggestions.addAll(currentNode.getTabCompleter().apply(sender, args));
+        } else {
+            // Suggest subcommands
+            suggestions.addAll(currentNode.getSubCommandNames().stream()
+                    .filter(name -> name.toLowerCase().startsWith(lastArg))
+                    .collect(Collectors.toList()));
+        }
+
+        return suggestions;
+    }
+
+    private void sendHelp(CommandSender sender) {
+        sender.sendMessage("§6=== Clan Commands ===");
+        for (CommandNode cmd : rootCommand.getSubCommands()) {
+            sender.sendMessage("§e/clan " + cmd.getName() + " §7- " + cmd.getDescription());
+        }
+    }
+
+    private void sendSubCommands(CommandSender sender, CommandNode node) {
+        sender.sendMessage("§6Available subcommands:");
+        for (CommandNode subCmd : node.getSubCommands()) {
+            sender.sendMessage("§e" + subCmd.getName() + " §7- " + subCmd.getDescription());
+        }
+    }
+
+    // Dynamic registration API
+    public void registerDynamicCommand(String path, String description,
+                                       BiConsumer<CommandSender, String[]> executor,
+                                       BiFunction<CommandSender, String[], List<String>> tabCompleter) {
+        String[] parts = path.split("\\.");
+        CommandNode currentNode = rootCommand;
+
+        for (String part : parts) {
+            CommandNode nextNode = currentNode.getSubCommand(part);
+            if (nextNode == null) {
+                nextNode = new CommandNode(part, "", null);
+                currentNode.addSubCommand(nextNode);
+            }
+            currentNode = nextNode;
+        }
+
+        currentNode.setExecutor(executor);
+        currentNode.setTabCompleter(tabCompleter);
+        if (!description.isEmpty()) {
+            // Can't directly set description as it's final, would need to modify CommandNode
+            // or use a different approach
+        }
+    }
+
+    // Example command handlers
+    private void handleList(CommandSender sender, String[] args) {
+        ClanModule.sendText(sender,"Aktuelle Liste an Clans: ");
+        for (Clan clan : manager.getClans()) {
+            ClanModule.sendText(sender,clan.getName());
+        }
+    }
+
+    private void handleJoin(CommandSender sender, String[] args) {
+        if (!(sender instanceof Player player)) return;
+        if (args.length < 1) {
+            ClanModule.sendText(sender,"Usage: /clan join <clan>");
+            return;
+        }
+        if(manager.joinClan(player.getUniqueId(), args[0])) ClanModule.sendText(player, "Sent a request to join the clan " + args[0]);
+    }
+
+    private List<String> completeClans(CommandSender sender, String[] args) {
+        if(!(sender instanceof Player player)) return List.of("Your not a player!");
+        String partial = args.length > 0 ? args[args.length - 1].toLowerCase() : "";
+        if(manager.getClans() == null) return List.of("No Clan found!");
+
+        return manager.getClans().stream()
+                .map(Clan::getName)
+                .filter(name -> name.toLowerCase().startsWith(partial))
+                .collect(Collectors.toList());
+    }
+
+    private List<String> completeRules(CommandSender sender, String[] args) {
+        if(!(sender instanceof Player player)) return List.of("Your not a player!");
+
+        Clan clan = manager.getMyClan(player.getUniqueId());
+        if (clan == null) return List.of("You are not in a clan!");
+        if (clan.getRules() == null) return List.of("No rules set yet!");
+
+        String partial = args.length > 0 ? args[args.length - 1].toLowerCase() : "";
+        return clan.getRules().stream()
+                .map(Rule::getName)
+                .filter(name -> name.toLowerCase().startsWith(partial))
+                .collect(Collectors.toList());
+    }
+    private List<String> completeRequests(CommandSender sender, String[] args) {
+        if(!(sender instanceof Player player)) return List.of("Your not a player!");
+
+        Clan clan = manager.getMyClan(player.getUniqueId());
+        if (clan == null) return List.of("You are not in a clan!");
+        String partial = args.length > 0 ? args[args.length - 1].toLowerCase() : "";
+        return clan.getRequests().stream()
+                .filter(name -> name.toLowerCase().startsWith(partial))
+                .collect(Collectors.toList());
+    }
+
+    private void reload() {
+        setupDefaultCommands();
+    }
+}

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

@@ -0,0 +1,48 @@
+package me.lethunderhawk.clans.gui;
+
+import me.lethunderhawk.bazaarflux.util.gui.InventoryGUI;
+import me.lethunderhawk.bazaarflux.util.gui.PlayerHeadListGUI;
+import me.lethunderhawk.clans.ClanManager;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+public class ClanGUI extends InventoryGUI {
+
+    private final ClanManager manager;
+    private final Player player;
+
+    public ClanGUI(ClanManager manager, Player player) {
+        super("Main Clan Menu", 54);
+        this.manager = manager;
+        this.player = player;
+        setup();
+    }
+    private void setup(){
+
+        ItemStack stick = new ItemStack(Material.STICK);
+        ItemStack arrow = new ItemStack(Material.ARROW);
+        setItemWithClickAction(0, stick, (p, type) ->
+                p.sendMessage("Hello")
+        );
+
+        fillBackground(Material.GRAY_STAINED_GLASS_PANE, " ");
+
+        PlayerHeadListGUI headGui = new PlayerHeadListGUI(
+                "Players",
+                54,
+                manager.getMyClan(player.getUniqueId()).getAllMembers()
+                ,
+                (clicker, target) -> clicker.sendMessage("Clicked: " + target.getName())
+        );
+
+        setItemWithClickAction(22, stick, (p, type) ->
+                openNext(p, headGui)
+        );
+        headGui.setItemWithClickAction(53, arrow, headGui::openPrevious);
+    }
+    public void open(){
+        open(player);
+    }
+
+}

+ 40 - 0
src/main/java/me/lethunderhawk/clans/placeholder/ClanPlaceHolder.java

@@ -0,0 +1,40 @@
+package me.lethunderhawk.clans.placeholder;
+
+import me.clip.placeholderapi.expansion.PlaceholderExpansion;
+import me.lethunderhawk.clans.Clan;
+import me.lethunderhawk.clans.ClanManager;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public class ClanPlaceHolder extends PlaceholderExpansion {
+
+    private final ClanManager manager;
+
+    public ClanPlaceHolder(ClanManager manager){
+        this.manager = manager;
+    }
+
+    @Override
+    public @NotNull String getIdentifier() {
+        return "clan";
+    }
+
+    @Override
+    public @NotNull String getAuthor() {
+        return "LeThunderHawk";
+    }
+
+    @Override
+    public @NotNull String getVersion() {
+        return "1.0";
+    }
+    @Override
+    public String onPlaceholderRequest(Player p, String identifier) {
+        if (identifier.equalsIgnoreCase("name") && p != null) {
+            Clan clan = manager.getMyClan(p.getUniqueId());
+            if(clan == null) return "/";
+            return manager.getMyClan(p.getUniqueId()).getName();
+        }
+        return null;
+    }
+}

+ 23 - 0
src/main/java/me/lethunderhawk/clans/rules/Rule.java

@@ -0,0 +1,23 @@
+package me.lethunderhawk.clans.rules;
+
+public class Rule {
+    private String name, description;
+
+    public Rule(String name, String description){
+
+    }
+    public String toTabComplete() {
+        return name.toLowerCase().trim();
+    }
+
+    public String getName() {
+        return name;
+    }
+    @Override
+    public String toString(){
+        return name;
+    }
+    public static Rule fromString(String s) {
+        return new Rule("custom-name", s);
+    }
+}

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

@@ -0,0 +1,53 @@
+package me.lethunderhawk.custom.item;
+
+import me.lethunderhawk.bazaarflux.util.interfaces.BazaarFluxModule;
+import me.lethunderhawk.custom.item.command.CustomItemCommand;
+import me.lethunderhawk.custom.item.concrete.ClaimTool;
+import me.lethunderhawk.custom.item.concrete.TeleportSword;
+import me.lethunderhawk.custom.item.listener.CustomItemListener;
+import me.lethunderhawk.custom.item.manager.CustomItemManager;
+import me.lethunderhawk.main.Main;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public class CustomItemModule implements BazaarFluxModule {
+    private CustomItemManager itemManager;
+    private final JavaPlugin plugin;
+
+    public CustomItemModule(JavaPlugin plugin) {
+        this.plugin = plugin;
+    }
+    private void registerCustomItems(){
+        itemManager.registerItem(new TeleportSword(plugin));
+        itemManager.registerItem(new ClaimTool(plugin));
+
+    }
+
+    @Override
+    public String getPrefix() {
+        return "[CustomItem]";
+    }
+
+    @Override
+    public void onEnable() {
+        itemManager = new CustomItemManager(plugin);
+        registerCustomItems();
+        CustomItemCommand customItemCommand = new CustomItemCommand(itemManager);
+        Main.getInstance().getCommand("customItem").setExecutor(customItemCommand);
+        Main.getInstance().getCommand("customItem").setTabCompleter(customItemCommand);
+
+
+        plugin.getServer().getPluginManager().registerEvents(
+                new CustomItemListener(itemManager),
+                plugin
+        );
+    }
+
+    @Override
+    public void onDisable() {
+
+    }
+
+    public CustomItemManager getManager() {
+        return itemManager;
+    }
+}

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

@@ -0,0 +1,112 @@
+package me.lethunderhawk.custom.item.abstraction;
+
+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.Arrays;
+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 String displayName;
+    protected final List<String> lore;
+    protected final Material baseMaterial;
+
+    // Persistent Data Container key for storing item ID
+    protected final NamespacedKey itemIdKey;
+
+    public CustomItem(JavaPlugin plugin, String itemId, String displayName, List<String> 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");
+    }
+
+    public CustomItem(JavaPlugin plugin, String itemId, String displayName, String[] lore, Material baseMaterial) {
+        this(plugin, itemId, displayName, Arrays.asList(lore), baseMaterial);
+    }
+
+    /**
+     * Create the ItemStack with embedded metadata
+     */
+    public ItemStack createItem() {
+        ItemStack item = new ItemStack(baseMaterial);
+        ItemMeta meta = item.getItemMeta();
+
+        // Set visible properties
+        meta.setDisplayName(displayName);
+        if (lore != null && !lore.isEmpty()) {
+            meta.setLore(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 String getDisplayName() { return displayName; }
+    public Material getBaseMaterial() { return baseMaterial; }
+    public NamespacedKey getItemIdKey() { return itemIdKey; }
+}

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

@@ -0,0 +1,104 @@
+package me.lethunderhawk.custom.item.command;
+
+import me.lethunderhawk.bazaarflux.util.command.CommandNode;
+import me.lethunderhawk.custom.item.abstraction.CustomItem;
+import me.lethunderhawk.custom.item.manager.CustomItemManager;
+import me.lethunderhawk.economy.EconomyModule;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class CustomItemCommand implements CommandExecutor, TabCompleter {
+    private final CustomItemManager manager;
+    private final CommandNode rootCommand;
+
+    public CustomItemCommand(CustomItemManager manager) {
+        this.manager = manager;
+        this.rootCommand = new CommandNode("customItems", "Main Custom Item Command", null);
+        CommandNode getAll = rootCommand.registerSubCommand("getAll", "Get all custom items", this::getAllItems);
+    }
+
+    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());
+        }
+    }
+
+    @Override
+    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String @NotNull [] args) {
+        if (args.length == 0) {
+            sendHelp(sender);
+            return true;
+        }
+
+        List<String> argList = new ArrayList<>(Arrays.asList(args));
+        CommandNode currentNode = rootCommand;
+        CommandNode targetNode = null;
+
+        // Traverse the command tree
+        while (!argList.isEmpty()) {
+            String nextArg = argList.get(0);
+            CommandNode nextNode = currentNode.getSubCommand(nextArg);
+
+            if (nextNode == null) {
+                // No matching subcommand found, use current node if it has an executor
+                targetNode = currentNode.getExecutor() != null ? currentNode : null;
+                break;
+            }
+
+            currentNode = nextNode;
+            argList.remove(0);
+
+            // If this is the last argument or node has no further subcommands
+            if (argList.isEmpty() || nextNode.getSubCommands().isEmpty()) {
+                targetNode = nextNode;
+                break;
+            }
+        }
+
+        if (targetNode == null) {
+            EconomyModule.sendText(sender,"§cUnknown command. Use /eco help for available commands.");
+            return true;
+        }
+
+        if (targetNode.getExecutor() == null) {
+            EconomyModule.sendText(sender,"§cThis command requires additional arguments.");
+            sendSubCommands(sender, targetNode);
+            return true;
+        }
+
+        // Execute the command with remaining arguments
+        String[] remainingArgs = argList.toArray(new String[0]);
+        targetNode.getExecutor().accept(sender, remainingArgs);
+        return true;
+    }
+    private void sendHelp(CommandSender sender) {
+        sender.sendMessage("§6=== Custom Item Commands ===");
+        for (CommandNode cmd : rootCommand.getSubCommands()) {
+            sender.sendMessage("§e/eco " + cmd.getName() + " §7- " + cmd.getDescription());
+        }
+    }
+
+    private void sendSubCommands(CommandSender sender, CommandNode node) {
+        sender.sendMessage("§6Available subcommands:");
+        for (CommandNode subCmd : node.getSubCommands()) {
+            sender.sendMessage("§e" + subCmd.getName() + " §7- " + subCmd.getDescription());
+        }
+    }
+
+    @Override
+    public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String @NotNull [] args) {
+        return List.of();
+    }
+}

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

@@ -0,0 +1,86 @@
+package me.lethunderhawk.custom.item.concrete;
+
+import me.lethunderhawk.clans.Clan;
+import me.lethunderhawk.clans.ClanModule;
+import me.lethunderhawk.clans.claim.Claim;
+import me.lethunderhawk.custom.item.abstraction.CustomItem;
+import me.lethunderhawk.main.Main;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Location;
+import org.bukkit.Material;
+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",
+                NamedTextColor.AQUA + "Claim Tool",
+                Arrays.asList(
+                        NamedTextColor.GRAY + "Left-click to set corners of your claim",
+                        ""
+                ),
+                Material.GOLDEN_SHOVEL
+        );
+    }
+
+    @Override
+    public void onRightClick(PlayerInteractEvent event) {
+
+    }
+
+    @Override
+    public void onLeftClick(PlayerInteractEvent e) {
+        Player p = e.getPlayer();
+
+        Location second = e.getClickedBlock().getLocation();
+        UUID uuid = p.getUniqueId();
+        ClanModule module = Main.getInstance().getClanModule();
+        Clan clan = module.getClanManager().getMyClan(uuid);
+        if(clan == null){
+            e.setCancelled(true);
+            ClanModule.sendText(p,"§cJoin a clan first!");
+            return;
+        }
+        if (!firstCorner.containsKey(uuid)) {
+            firstCorner.put(uuid, second);
+            ClanModule.sendText(p,"§aFirst corner of your claim set.");
+        } 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 = Main.getInstance().getConfig().getInt("claims.max-blocks");
+
+//            if (clan.getUsedBlocks() + claim.getVolume() > maxBlocks) {
+//                p.sendMessage("§cClaim too large.");
+//                return;
+//            }
+            Claim overlapping = module.getClaimManager().overlaps(claim);
+            Clan overlappingClan = module.getClanManager().getClanById(overlapping.getClanId());
+
+            if (overlappingClan != null) {
+                ClanModule.sendText(p,"§cThis area is overlapping with already claimed land from Clan " + overlappingClan.getName() + ".");
+                e.setCancelled(true);
+                return;
+            }
+
+            module.getClaimManager().registerClaim(claim);
+            clan.addClaim(claim);
+            ClanModule.sendText(p,"§aSecond corner set.");
+            ClanModule.sendText(p,"§aClaim created.");
+        }
+
+        e.setCancelled(true);
+    }
+}

+ 71 - 0
src/main/java/me/lethunderhawk/custom/item/concrete/TeleportSword.java

@@ -0,0 +1,71 @@
+package me.lethunderhawk.custom.item.concrete;
+
+import me.lethunderhawk.custom.item.abstraction.CustomItem;
+import org.bukkit.*;
+import org.bukkit.entity.Player;
+import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.Arrays;
+
+/**
+ * Teleport Sword - Right-click to teleport forward
+ */
+public class TeleportSword extends CustomItem {
+
+    public TeleportSword(JavaPlugin plugin) {
+        super(
+                plugin,
+                "teleport_sword",
+                ChatColor.AQUA + "Sword of Teleportation",
+                Arrays.asList(
+                        ChatColor.GRAY + "Right-click to teleport forward",
+                        ChatColor.GRAY + "Left-click to teleport upward",
+                        "",
+                        ChatColor.DARK_PURPLE + "Legendary Weapon"
+                ),
+                Material.DIAMOND_SWORD
+        );
+    }
+
+    @Override
+    protected void applyCustomizations(ItemStack item) {
+        // Add enchantment glint (without actual enchantment)
+        ItemMeta meta = item.getItemMeta();
+        meta.addEnchant(org.bukkit.enchantments.Enchantment.LURE, 1, true);
+        meta.addItemFlags(org.bukkit.inventory.ItemFlag.HIDE_ENCHANTS);
+        item.setItemMeta(meta);
+    }
+
+    @Override
+    public void onRightClick(PlayerInteractEvent event) {
+        Player player = event.getPlayer();
+        Location target = player.getTargetBlock(null, 30).getLocation().add(0.5, 1, 0.5);
+
+        // Visual effects
+        player.getWorld().playSound(player.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 1.0f);
+        player.getWorld().spawnParticle(Particle.PORTAL, player.getLocation(), 50);
+
+        // Teleport player
+        player.teleport(target);
+
+        // Post-teleport effects
+        player.getWorld().playSound(target, Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 1.0f);
+        player.getWorld().spawnParticle(Particle.DRAGON_BREATH, target, 30);
+
+        // Cooldown
+        player.setCooldown(getBaseMaterial(), 100); // 5 second cooldown (20 ticks = 1 second)
+    }
+
+    @Override
+    public void onLeftClick(PlayerInteractEvent event) {
+        Player player = event.getPlayer();
+        Location target = player.getLocation().add(0, 10, 0);
+
+        player.getWorld().playSound(player.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 1.0f);
+        player.teleport(target);
+        player.setCooldown(getBaseMaterial(), 60);
+    }
+}

+ 67 - 0
src/main/java/me/lethunderhawk/custom/item/listener/CustomItemListener.java

@@ -0,0 +1,67 @@
+package me.lethunderhawk.custom.item.listener;
+
+import me.lethunderhawk.custom.item.manager.CustomItemManager;
+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;
+
+    public CustomItemListener(CustomItemManager itemManager) {
+        this.itemManager = itemManager;
+    }
+
+    @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
+        }
+    }
+}

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

@@ -0,0 +1,88 @@
+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;
+    }
+}

+ 70 - 0
src/main/java/me/lethunderhawk/economy/EconomyModule.java

@@ -0,0 +1,70 @@
+package me.lethunderhawk.economy;
+
+import me.lethunderhawk.bazaarflux.util.MessageSender;
+import me.lethunderhawk.economy.api.EconomyAPI;
+import me.lethunderhawk.economy.api.EconomyPlaceholder;
+import me.lethunderhawk.economy.command.EcoCommand;
+import me.lethunderhawk.economy.currency.EconomyManager;
+import me.lethunderhawk.economy.listener.PlayerJoinListener;
+import me.lethunderhawk.economy.scoreboard.ScoreboardManager;
+import me.lethunderhawk.tradeplugin.api.TradePlaceholder;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public class EconomyModule {
+
+    private static EconomyModule instance;
+
+    private EconomyManager economyManager;
+    private EconomyAPI economyAPI;
+    private ScoreboardManager scoreboardManager;
+    private JavaPlugin plugin;
+
+    public void onEnable(JavaPlugin plugin) {
+        this.plugin = plugin;
+        instance = this;
+
+        plugin.saveDefaultConfig();
+        plugin.saveResource("scoreboards/default.yml", false);
+
+        economyManager = new EconomyManager(this);
+        economyAPI = new EconomyAPI(economyManager);
+        scoreboardManager = new ScoreboardManager(this);
+
+        plugin.getCommand("eco").setExecutor(new EcoCommand(this));
+        plugin.getCommand("eco").setTabCompleter(new EcoCommand(this));
+
+        if (plugin.getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {
+            new EconomyPlaceholder(economyAPI).register();
+            new TradePlaceholder().register();
+        }
+
+        plugin.getServer().getPluginManager().registerEvents(new PlayerJoinListener(scoreboardManager), plugin);
+    }
+    public static void sendText(Audience receiver, Component infoText){
+        MessageSender.sendText(receiver, infoText, "[Eco]");
+    }
+    public static void sendText(Audience receiver, String infoText){
+        MessageSender.sendText(receiver, Component.text(infoText), "[Eco]");
+    }
+    public void reload(){
+        scoreboardManager.reload();
+    }
+
+    public static EconomyModule getInstance() {
+        return instance;
+    }
+
+    public EconomyAPI getEconomyAPI() {
+        return economyAPI;
+    }
+
+    public ScoreboardManager getScoreboardManager() {
+        return scoreboardManager;
+    }
+
+    public JavaPlugin getPlugin() {
+        return plugin;
+    }
+}

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

@@ -0,0 +1,43 @@
+package me.lethunderhawk.economy.api;
+
+import me.lethunderhawk.economy.currency.EconomyManager;
+import me.lethunderhawk.economy.util.DayTime;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public class EconomyAPI {
+
+    private final EconomyManager eco;
+
+    public EconomyAPI(EconomyManager eco) {
+        this.eco = eco;
+    }
+    public void pay(UUID sender, UUID receiver, int amount){
+        eco.pay(sender, receiver, amount);
+    }
+
+    public int getMoney(UUID uuid) {
+        return eco.getMoney(uuid);
+    }
+
+    public void setMoney(UUID uuid, int amount) {
+        eco.setMoney(uuid, amount);
+    }
+
+    public void addMoney(UUID uuid, int amount) {
+        eco.addMoney(uuid, amount);
+    }
+
+    public void removeMoney(UUID uuid, int amount) {
+        eco.removeMoney(uuid, amount);
+    }
+
+    public String getFormattedTime() {
+        return DayTime.formatMcTime(eco.getTime());
+    }
+
+    public String getRegion(@NotNull UUID uniqueId) {
+        return eco.getRegion();
+    }
+}

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

@@ -0,0 +1,40 @@
+package me.lethunderhawk.economy.api;
+
+import me.clip.placeholderapi.expansion.PlaceholderExpansion;
+import org.bukkit.entity.Player;
+
+public class EconomyPlaceholder extends PlaceholderExpansion {
+
+    private final EconomyAPI api;
+
+    public EconomyPlaceholder(EconomyAPI api) {
+        this.api = api;
+    }
+
+    @Override
+    public String getIdentifier() {
+        return "flux";
+    }
+
+    @Override
+    public String getAuthor() {
+        return "LeThunderHawk";
+    }
+
+    @Override
+    public String getVersion() {
+        return "1.0";
+    }
+
+    @Override
+    public String onPlaceholderRequest(Player p, String identifier) {
+        if (identifier.equalsIgnoreCase("money") && p != null) {
+            return String.valueOf(api.getMoney(p.getUniqueId()));
+        }else if (identifier.equalsIgnoreCase("formattedTime") && p != null) {
+            return String.valueOf(api.getFormattedTime());
+        }else if (identifier.equalsIgnoreCase("region") && p != null) {
+            return String.valueOf(api.getRegion(p.getUniqueId()));
+        }
+        return null;
+    }
+}

+ 251 - 0
src/main/java/me/lethunderhawk/economy/command/EcoCommand.java

@@ -0,0 +1,251 @@
+package me.lethunderhawk.economy.command;
+
+import me.lethunderhawk.bazaarflux.util.command.CommandNode;
+import me.lethunderhawk.economy.EconomyModule;
+import me.lethunderhawk.economy.api.EconomyAPI;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+public class EcoCommand implements CommandExecutor, TabCompleter {
+    private static final List<String> AMOUNTS = List.of("1", "5", "10", "50", "100", "500", "1000");
+    private final EconomyModule module;
+    private final CommandNode rootCommand;
+
+    public EcoCommand(EconomyModule module) {
+        this.module = module;
+        this.rootCommand = new CommandNode("eco", "Main economy command", null);
+        setupDefaultCommands();
+    }
+
+    private void setupDefaultCommands() {
+        registerCommand("help", "Get a list of available commands", this::getBalance);
+        CommandNode set = registerCommand("set", "Set the balance of a player", this::setEco);
+        set.setTabCompleter(this::completeAllEcoCommands);
+
+        CommandNode add = registerCommand("add", "Add any amount to the balance of a player", this::addEco);
+        add.setTabCompleter(this::completeAllEcoCommands);
+
+        CommandNode remove = registerCommand("remove", "Remove any amount of the balance of a player", this::removeEco);
+        remove.setTabCompleter(this::completeAllEcoCommands);
+
+        CommandNode get = registerCommand("get", "Get the balance of a player", this::getBalance);
+        get.setTabCompleter(this::completePlayers);
+    }
+
+    private List<String> completePlayers(CommandSender sender, String[] args){
+        String partial = args[1].toLowerCase();
+        return Bukkit.getOnlinePlayers().stream()
+                .map(Player::getName)
+                .filter(name -> name.toLowerCase().startsWith(partial))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * @param args The full arguments list
+     * @return The completion in following format: /eco [action] [List of players] [amount]
+     */
+    private List<String> completeAllEcoCommands(CommandSender sender, String[] args){
+        // /eco <action> <player>
+        System.out.println(args.length);
+        if (args.length == 2) {
+            return completePlayers(sender, args);
+        }
+
+        // /eco <action> <player> <amount>
+        if (args.length == 3) {
+            String partial = args[2].toLowerCase();
+            return AMOUNTS.stream()
+                    .filter(s -> s.startsWith(partial))
+                    .collect(Collectors.toList());
+        }
+        return Collections.emptyList();
+    }
+
+    private void getBalance(CommandSender commandSender, String[] strings) {
+        Player target = Bukkit.getPlayer(strings[0]);
+        if (target == null) {
+            EconomyModule.sendText(commandSender, Component.text("Spieler nicht gefunden.",  NamedTextColor.RED));
+        }else{
+            EconomyModule.sendText(commandSender, Component.text("Spieler " + target.getName() + " hat " + getAPI().getMoney(target.getUniqueId()),  NamedTextColor.GRAY));
+        }
+    }
+
+    private void removeEco(CommandSender commandSender, String[] strings) {
+    }
+
+    private void addEco(CommandSender commandSender, String[] strings) {
+
+    }
+
+    private void setEco(CommandSender commandSender, String[] strings) {
+        Player target = Bukkit.getPlayer(strings[0]);
+        if (target == null) {
+            EconomyModule.sendText(commandSender, Component.text("Spieler nicht gefunden.",  NamedTextColor.RED));
+        }else{
+            getAPI().setMoney(target.getUniqueId(), Integer.parseInt(strings[1]));
+        }
+    }
+    private EconomyAPI getAPI(){
+        return module.getEconomyAPI();
+    }
+
+    public CommandNode registerCommand(String name, String description, BiConsumer<CommandSender, String[]> executor) {
+        return rootCommand.registerSubCommand(name, description, executor);
+    }
+
+    @Override
+    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+        if (args.length == 0) {
+            sendHelp(sender);
+            return true;
+        }
+
+        List<String> argList = new ArrayList<>(Arrays.asList(args));
+        CommandNode currentNode = rootCommand;
+        CommandNode targetNode = null;
+
+        // Traverse the command tree
+        while (!argList.isEmpty()) {
+            String nextArg = argList.get(0);
+            CommandNode nextNode = currentNode.getSubCommand(nextArg);
+
+            if (nextNode == null) {
+                // No matching subcommand found, use current node if it has an executor
+                targetNode = currentNode.getExecutor() != null ? currentNode : null;
+                break;
+            }
+
+            currentNode = nextNode;
+            argList.remove(0);
+
+            // If this is the last argument or node has no further subcommands
+            if (argList.isEmpty() || nextNode.getSubCommands().isEmpty()) {
+                targetNode = nextNode;
+                break;
+            }
+        }
+
+        if (targetNode == null) {
+            EconomyModule.sendText(sender,"§cUnknown command. Use /eco help for available commands.");
+            return true;
+        }
+
+        if (targetNode.getExecutor() == null) {
+            EconomyModule.sendText(sender,"§cThis command requires additional arguments.");
+            sendSubCommands(sender, targetNode);
+            return true;
+        }
+
+        // Execute the command with remaining arguments
+        String[] remainingArgs = argList.toArray(new String[0]);
+        targetNode.getExecutor().accept(sender, remainingArgs);
+        return true;
+    }
+    private void sendHelp(CommandSender sender) {
+        sender.sendMessage("§6=== Eco Commands ===");
+        for (CommandNode cmd : rootCommand.getSubCommands()) {
+            sender.sendMessage("§e/eco " + cmd.getName() + " §7- " + cmd.getDescription());
+        }
+    }
+
+    private void sendSubCommands(CommandSender sender, CommandNode node) {
+        sender.sendMessage("§6Available subcommands:");
+        for (CommandNode subCmd : node.getSubCommands()) {
+            sender.sendMessage("§e" + subCmd.getName() + " §7- " + subCmd.getDescription());
+        }
+    }/*
+    @Override
+    public boolean onCommand(CommandSender s, Command cmd, String label, String[] args) {
+
+        String mode = args[0].toLowerCase();
+        if (args.length < 3 && ! (mode.equals("get") || mode.equals("reload"))) {
+            s.sendMessage("/eco <set|add|remove|get> <player> <amount>");
+            return true;
+        }
+        if(mode.equals("reload")){
+            module.reload();
+            s.sendMessage("Reloading...");
+            return true;
+        }
+        Player target = Bukkit.getPlayer(args[1]);
+        if (target == null) {
+            s.sendMessage("Spieler nicht gefunden.");
+            return true;
+        }
+
+        EconomyAPI api = module.getEconomyAPI();
+
+        switch (mode) {
+            case "set":
+                api.setMoney(target.getUniqueId(), Integer.parseInt(args[2]));
+                s.sendMessage("Flux gesetzt: " + Integer.parseInt(args[2]));
+                break;
+
+            case "add":
+                api.addMoney(target.getUniqueId(), Integer.parseInt(args[2]));
+                s.sendMessage(Integer.parseInt(args[2]) + " Flux hinzugefügt: " + api.getMoney(target.getUniqueId()));
+                break;
+
+            case "remove":
+                api.removeMoney(target.getUniqueId(), Integer.parseInt(args[2]));
+                s.sendMessage(Integer.parseInt(args[2]) + " Flux entfernt: " + api.getMoney(target.getUniqueId()));
+                break;
+
+            case "get":
+                s.sendMessage("Flux: " + api.getMoney(target.getUniqueId()));
+                break;
+            default:
+                s.sendMessage("Ungültiger Modus.");
+        }
+
+        return true;
+    }*/
+    @Override
+    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
+        List<String> suggestions = new ArrayList<>();
+
+        if (args.length == 0) {
+            return suggestions;
+        }
+
+        // Start at root and traverse
+        CommandNode currentNode = rootCommand;
+        List<String> argList = new ArrayList<>(Arrays.asList(args));
+        String lastArg = args[args.length - 1].toLowerCase();
+
+        // Try to traverse as far as possible
+        for (int i = 0; i < argList.size() - 1; i++) {
+            String arg = argList.get(i);
+            CommandNode nextNode = currentNode.getSubCommand(arg);
+            if (nextNode == null) {
+                break;
+            }
+            currentNode = nextNode;
+        }
+
+        // Get suggestions from current node
+        if (currentNode.getTabCompleter() != null) {
+            suggestions.addAll(currentNode.getTabCompleter().apply(sender, args));
+        } else {
+            // Suggest subcommands
+            suggestions.addAll(currentNode.getSubCommandNames().stream()
+                    .filter(name -> name.toLowerCase().startsWith(lastArg))
+                    .collect(Collectors.toList()));
+        }
+
+        return suggestions;
+    }
+}

+ 49 - 0
src/main/java/me/lethunderhawk/economy/currency/EconomyManager.java

@@ -0,0 +1,49 @@
+package me.lethunderhawk.economy.currency;
+
+import me.lethunderhawk.economy.EconomyModule;
+import org.bukkit.configuration.file.FileConfiguration;
+
+import java.util.UUID;
+
+public class EconomyManager {
+
+    private final EconomyModule module;
+    private final FileConfiguration config;
+
+    public EconomyManager(EconomyModule module) {
+        this.module = module;
+        this.config = module.getPlugin().getConfig();
+    }
+
+    public int getMoney(UUID uuid) {
+        return config.getInt("players." + uuid + ".money", 0);
+    }
+
+    public void setMoney(UUID uuid, int amount) {
+        config.set("players." + uuid + ".money", amount);
+        module.getPlugin().saveConfig();
+    }
+
+    public void addMoney(UUID uuid, int amount) {
+        setMoney(uuid, getMoney(uuid) + amount);
+    }
+
+    public void removeMoney(UUID uuid, int amount) {
+        setMoney(uuid, Math.max(0, getMoney(uuid) - amount));
+    }
+
+    public long getTime() {
+        return module.getPlugin().getServer().getWorld("world").getTime();
+    }
+
+    public String getRegion() {
+        return "Bummsbach";
+    }
+
+    public void pay(UUID sender, UUID receiver, int amount) {
+        if(amount > 0 && getMoney(sender) >= amount){
+            removeMoney(sender, amount);
+            addMoney(receiver, amount);
+        }
+    }
+}

+ 20 - 0
src/main/java/me/lethunderhawk/economy/listener/PlayerJoinListener.java

@@ -0,0 +1,20 @@
+package me.lethunderhawk.economy.listener;
+
+import me.lethunderhawk.economy.scoreboard.ScoreboardManager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+
+public class PlayerJoinListener implements Listener {
+
+    private final ScoreboardManager manager;
+
+    public PlayerJoinListener(ScoreboardManager manager) {
+        this.manager = manager;
+    }
+
+    @EventHandler
+    public void onJoin(PlayerJoinEvent e) {
+        manager.apply(e.getPlayer(), "default");
+    }
+}

+ 60 - 0
src/main/java/me/lethunderhawk/economy/scoreboard/ScoreboardManager.java

@@ -0,0 +1,60 @@
+package me.lethunderhawk.economy.scoreboard;
+
+import me.lethunderhawk.economy.EconomyModule;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitTask;
+import org.bukkit.scoreboard.Scoreboard;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public class ScoreboardManager {
+
+    private final EconomyModule module;
+    private final Map<String, ScoreboardTemplate> templates = new HashMap<>();
+    private final Map<UUID, BukkitTask> tasks;
+
+    public ScoreboardManager(EconomyModule module) {
+        this.module = module;
+        loadTemplates();
+        tasks = new HashMap<>();
+    }
+
+    public void loadTemplates() {
+        File folder = new File(module.getPlugin().getDataFolder(), "scoreboards");
+        if (!folder.exists()) folder.mkdirs();
+
+        for (File f : folder.listFiles()) {
+            if (!f.getName().endsWith(".yml")) continue;
+            ScoreboardTemplate template = new ScoreboardTemplate(f);
+            templates.put(template.getName(), template);
+        }
+    }
+
+    public void apply(Player player, String templateName) {
+        ScoreboardTemplate t = templates.get(templateName);
+        if (t == null) return;
+        BukkitTask task = Bukkit.getScheduler().runTaskTimer(module.getPlugin(), () -> {
+            Scoreboard board = t.toScoreboard(player);
+            player.setScoreboard(board);
+        }, 20L, 20L);
+
+        if(tasks != null && tasks.get(player.getUniqueId()) != null){
+            tasks.get(player.getUniqueId()).cancel();
+        }
+
+        assert tasks != null;
+        tasks.put(player.getUniqueId(), task);
+    }
+
+    public void reload() {
+        templates.clear();
+        loadTemplates();
+        for(Player player : module.getPlugin().getServer().getOnlinePlayers()){
+            apply(player, "default");
+        }
+    }
+}

+ 70 - 0
src/main/java/me/lethunderhawk/economy/scoreboard/ScoreboardTemplate.java

@@ -0,0 +1,70 @@
+package me.lethunderhawk.economy.scoreboard;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import org.bukkit.Bukkit;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.entity.Player;
+import org.bukkit.scoreboard.*;
+
+import java.io.File;
+import java.util.List;
+
+public class ScoreboardTemplate {
+
+    private final String name;
+    private final String title;
+    private final List<String> lines;
+
+    public ScoreboardTemplate(File file) {
+        YamlConfiguration cfg = YamlConfiguration.loadConfiguration(file);
+        this.name = file.getName().replace(".yml", "");
+        this.title = cfg.getString("title", "Scoreboard");
+        this.lines = cfg.getStringList("lines");
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Scoreboard toScoreboard(Player p) {
+
+        Scoreboard board = Bukkit.getScoreboardManager().getNewScoreboard();
+        Objective obj = board.registerNewObjective(
+                "sb-" + name,
+                Criteria.DUMMY,
+                colorize(replacePlaceholders(title, p))
+        );
+        obj.setDisplaySlot(DisplaySlot.SIDEBAR);
+
+        int score = lines.size();
+
+        int index = 0;
+        for (String l : lines) {
+
+            String replaced = replacePlaceholders(l, p);
+            Component colored = colorize(replaced);
+
+            // Jedes Entry muss einzigartig sein
+            String entry = "§" + index;  // eindeutige Dummy-Line
+
+            Team team = board.registerNewTeam("line" + index);
+            team.addEntry(entry);
+            team.prefix(colored);
+
+            obj.getScore(entry).setScore(score--);
+            index++;
+        }
+
+        return board;
+    }
+    private Component colorize(String s) {
+        return LegacyComponentSerializer.legacyAmpersand().deserialize(s);
+    }
+    private String replacePlaceholders(String text, Player p) {
+        if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
+            return me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(p, text);
+        }
+        return text;
+    }
+}

+ 19 - 0
src/main/java/me/lethunderhawk/economy/util/DayTime.java

@@ -0,0 +1,19 @@
+package me.lethunderhawk.economy.util;
+
+public class DayTime {
+    public static String formatMcTime(long ticks) {
+        // Minecraft um 6:00 bei Tick 0 -> verschieben
+        long totalTicks = (ticks + 6000) % 24000;
+
+        int hours = (int) (totalTicks / 1000);
+        int minutes = (int) ((totalTicks % 1000) * 60 / 1000);
+
+        minutes = (minutes / 10) * 10;
+
+        if(hours >= 18){
+            return String.format("☽ %02d:%02d", hours, minutes);
+        }else{
+            return String.format("☀ %02d:%02d", hours, minutes);
+        }
+    }
+}

+ 55 - 0
src/main/java/me/lethunderhawk/main/Main.java

@@ -0,0 +1,55 @@
+package me.lethunderhawk.main;
+
+import me.lethunderhawk.bazaarflux.util.gui.InventoryManager;
+import me.lethunderhawk.clans.ClanModule;
+import me.lethunderhawk.custom.item.CustomItemModule;
+import me.lethunderhawk.custom.item.manager.CustomItemManager;
+import me.lethunderhawk.economy.EconomyModule;
+import me.lethunderhawk.tradeplugin.TradeModule;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+
+public class Main extends JavaPlugin {
+    private static Main instance;
+    private ClanModule clanModule;
+    private CustomItemModule customItemModule;
+
+    public static @NotNull Main getInstance() {
+        return instance;
+    }
+
+    @Override
+    public void onEnable() {
+        instance = this;
+        InventoryManager.register(this);
+
+        customItemModule = new CustomItemModule(this);
+        customItemModule.onEnable();
+
+        TradeModule tradeModule = new TradeModule();
+        tradeModule.onEnable(this);
+
+        clanModule = new ClanModule();
+        clanModule.onEnable();
+
+        EconomyModule economyModule = new EconomyModule();
+        economyModule.onEnable(this);
+
+
+    }
+
+
+
+    public ClanModule getClanModule(){
+        return clanModule;
+    }
+
+    @Override
+    public void onDisable(){
+        clanModule.onDisable();
+    }
+
+    public CustomItemManager getCustomItemManager() {
+        return customItemModule.getManager();
+    }
+}

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

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

+ 41 - 0
src/main/java/me/lethunderhawk/tradeplugin/TradeModule.java

@@ -0,0 +1,41 @@
+package me.lethunderhawk.tradeplugin;
+
+import me.lethunderhawk.bazaarflux.util.MessageSender;
+import me.lethunderhawk.tradeplugin.command.TradeAcceptCommand;
+import me.lethunderhawk.tradeplugin.command.TradeCommand;
+import me.lethunderhawk.tradeplugin.listener.InventoryListener;
+import me.lethunderhawk.tradeplugin.listener.PlayerInteractListener;
+import me.lethunderhawk.tradeplugin.trade.TradeManager;
+import me.lethunderhawk.tradeplugin.trade.TradeRequestManager;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public class TradeModule {
+
+    private static TradeManager tradeManager;
+    private static TradeRequestManager requestManager;
+
+    public void onEnable(JavaPlugin plugin) {
+        tradeManager = new TradeManager(plugin);
+        requestManager = new TradeRequestManager();
+
+        plugin.getServer().getPluginManager().registerEvents(new PlayerInteractListener(this), plugin);
+        plugin.getServer().getPluginManager().registerEvents(new InventoryListener(this), plugin);
+
+        plugin.getCommand("trade").setExecutor(new TradeCommand());
+        plugin.getCommand("tradeaccept").setExecutor(new TradeAcceptCommand());
+    }
+
+    public static void sendText(Audience receiver, Component infoText){
+        MessageSender.sendText(receiver, infoText, "[Trade]");
+    }
+
+    public static TradeManager getTradeManager() {
+        return tradeManager;
+    }
+
+    public static TradeRequestManager getRequestManager() {
+        return requestManager;
+    }
+}

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

@@ -0,0 +1,37 @@
+package me.lethunderhawk.tradeplugin.api;
+
+import me.clip.placeholderapi.expansion.PlaceholderExpansion;
+import me.lethunderhawk.tradeplugin.TradeModule;
+import me.lethunderhawk.tradeplugin.trade.TradeSession;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public class TradePlaceholder extends PlaceholderExpansion {
+
+    public TradePlaceholder() {
+    }
+
+    @Override
+    public @NotNull String getIdentifier() {
+        return "trade";
+    }
+
+    @Override
+    public @NotNull String getAuthor() {
+        return "LeThunderhawk";
+    }
+
+    @Override
+    public @NotNull String getVersion() {
+        return "";
+    }
+    @Override
+    public String onPlaceholderRequest(Player p, String identifier) {
+        if (identifier.equalsIgnoreCase("flux_total") && p != null) {
+            TradeSession session = TradeModule.getTradeManager().getSession(p);
+            if (session != null) return String.valueOf(session.getMoneyFor(p));
+            return "0";
+        }
+        return "";
+    }
+}

+ 46 - 0
src/main/java/me/lethunderhawk/tradeplugin/command/TradeAcceptCommand.java

@@ -0,0 +1,46 @@
+package me.lethunderhawk.tradeplugin.command;
+
+import me.lethunderhawk.tradeplugin.TradeModule;
+import me.lethunderhawk.tradeplugin.trade.TradeManager;
+import me.lethunderhawk.tradeplugin.trade.TradeRequestManager;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class TradeAcceptCommand implements CommandExecutor {
+
+    private final TradeRequestManager requestManager;
+    private final TradeManager tradeManager;
+
+    public TradeAcceptCommand() {
+        this.requestManager = TradeModule.getRequestManager();
+        this.tradeManager = TradeModule.getTradeManager();
+    }
+
+    @Override
+    public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
+        if (!(sender instanceof Player target)) return true;
+
+        if (args.length != 1) return true;
+
+        Player requester = Bukkit.getPlayerExact(args[0]);
+        if (requester == null) {
+            TradeModule.sendText(target, Component.text("Spieler ist nicht online.",  NamedTextColor.RED));
+            return true;
+        }
+
+        if (!requestManager.isPending(target, requester)) {
+            TradeModule.sendText(target, Component.text("Keine Anfrage von diesem Spieler offen.",  NamedTextColor.RED));
+            return true;
+        }
+
+        requestManager.accept(target, requester);
+
+        tradeManager.startTrade(requester, target);
+        return true;
+    }
+}

+ 52 - 0
src/main/java/me/lethunderhawk/tradeplugin/command/TradeCommand.java

@@ -0,0 +1,52 @@
+package me.lethunderhawk.tradeplugin.command;
+
+import me.lethunderhawk.tradeplugin.TradeModule;
+import me.lethunderhawk.tradeplugin.trade.TradeManager;
+import me.lethunderhawk.tradeplugin.trade.TradeRequestManager;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class TradeCommand implements CommandExecutor {
+
+    private final TradeManager tradeManager;
+    private final TradeRequestManager requestManager;
+
+    public TradeCommand() {
+        this.tradeManager = TradeModule.getTradeManager();
+        this.requestManager = TradeModule.getRequestManager();
+    }
+
+    @Override
+    public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
+        if (!(sender instanceof Player p)) return true;
+
+        if (args.length != 1) {
+            TradeModule.sendText(p, Component.text("Verwendung: /trade <Spieler>", NamedTextColor.RED));
+            return true;
+        }
+
+        Player target = Bukkit.getPlayerExact(args[0]);
+        if (target == null) {
+            TradeModule.sendText(p, Component.text("Spieler nicht gefunden.", NamedTextColor.RED));
+            return true;
+        }
+
+        if (p.equals(target)) {
+            TradeModule.sendText(p, Component.text("Du kannst nicht mit dir selbst handeln.", NamedTextColor.RED));
+            return true;
+        }
+
+        if (tradeManager.isInTrade(p) || tradeManager.isInTrade(target)) {
+            TradeModule.sendText(p, Component.text("Einer von euch handelt bereits.", NamedTextColor.RED));
+            return true;
+        }
+
+        requestManager.sendRequest(p, target);
+        return true;
+    }
+}

+ 141 - 0
src/main/java/me/lethunderhawk/tradeplugin/input/player/NumberInputGUI.java

@@ -0,0 +1,141 @@
+package me.lethunderhawk.tradeplugin.input.player;
+
+import me.lethunderhawk.bazaarflux.util.gui.InventoryGUI;
+import me.lethunderhawk.bazaarflux.util.gui.InventoryManager;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+/**
+ * Number input built on top of InventoryGUI.
+ * Open with: InventoryManager.openFor(player, new NumberInputGUI(plugin, player, title, min, max, def, callback));
+ */
+public class NumberInputGUI extends InventoryGUI implements InventoryGUI.AutoCloseHandler {
+    private final JavaPlugin plugin;
+    private final Player player;
+    private final int minValue;
+    private final int maxValue;
+
+    private final BiConsumer<Player, Integer> callback;
+    private int currentValue;
+
+    private static final int PAPER_SLOT = 13, CONFIRM_SLOT = 22, CANCEL_SLOT = 26;
+
+    public NumberInputGUI(JavaPlugin plugin, Player player, String title,
+                          int minValue, int maxValue, int defaultValue,
+                          BiConsumer<Player, Integer> callback) {
+        super(title, 27);
+        this.plugin = plugin;
+        this.player = player;
+        this.minValue = minValue;
+        this.maxValue = maxValue;
+        this.currentValue = Math.max(minValue, Math.min(maxValue, defaultValue));
+        this.callback = callback;
+
+        buildContents();
+    }
+
+    private void buildContents() {
+        // center display
+        updateDisplay();
+
+        // confirm button
+        ItemStack confirm = make(Material.GREEN_DYE, "§a§lBestätigen");
+        setItemWithClickAction(CONFIRM_SLOT, confirm, (p, t) -> {
+            // Confirm -> close and return value
+            finishAndClose(currentValue);
+        });
+
+        // cancel button
+        ItemStack cancel = make(Material.BARRIER, "§c§lAbbrechen");
+        setItemWithClickAction(CANCEL_SLOT, cancel, (p, t) -> {
+            finishAndClose(0); // original code returned 0 on cancel
+        });
+
+        // paper slot: we need to handle left/right and shift. InventoryGUI only provides slot action without click type,
+        // but GUIListener passes raw slot only. To handle click types we use the player's last click state via a small workaround:
+        // we rely on the player sneaking for "shift" and toggle behavior between increment/decrement using right-click sub-GUI is omitted.
+        // We'll implement: left-click (increment 1), sneak+left (increment 10).
+        setItemWithClickAction(PAPER_SLOT, buildPaperWithContents(), (p, type) -> {
+            if(type.isLeftClick()){
+                currentValue = Math.min(maxValue, currentValue + (type.isShiftClick() ? 10 : 1));
+            }else if(type.isRightClick()){
+                currentValue = Math.max(minValue, currentValue - (type.isShiftClick() ? 10 : 1));
+            }
+            updateDisplay();
+            // ensure client sees update
+            p.updateInventory();
+        });
+
+        // fill remaining slots with background
+        fillBackground(Material.GRAY_STAINED_GLASS_PANE, " ");
+        player.updateInventory();
+    }
+
+    private ItemStack make(Material mat, String name) {
+        ItemStack it = new ItemStack(mat);
+        ItemMeta m = it.getItemMeta();
+        if (m != null) {
+            m.itemName(Component.text(name));
+            it.setItemMeta(m);
+        }
+        return it;
+    }
+
+    private void updateDisplay() {
+        ItemStack valueItem = buildPaperWithContents();
+        setItem(PAPER_SLOT, valueItem);
+    }
+    private ItemStack buildPaperWithContents() {
+        ItemStack valueItem = new ItemStack(Material.PAPER);
+        ItemMeta meta = valueItem.getItemMeta();
+
+        meta.itemName(Component.text("§eAktueller Wert: §6" + currentValue));
+
+        List<Component> lore = new ArrayList<>();
+        lore.add(Component.text("§7Min: §f" + minValue));
+        lore.add(Component.text("§7Max: §f" + maxValue));
+        lore.add(Component.text(""));
+        lore.add(Component.text("§aLinksklick: §7+1"));
+        lore.add(Component.text("§aRechtsklick: §7-1"));
+        lore.add(Component.text("§aSchleichen + Klick: §7+/-10"));
+
+        meta.lore(lore);
+        valueItem.setItemMeta(meta);
+        return valueItem;
+    }
+
+    private void finishAndClose(Integer result) {
+        // ensure main-thread close & callback
+        if (!Bukkit.isPrimaryThread()) {
+            Bukkit.getScheduler().runTask(plugin, () -> finishAndClose(result));
+            return;
+        }
+        // remove tracking and close handled by InventoryManager on close event; manually remove and close here
+        InventoryManager.close(player.getUniqueId());
+        player.closeInventory();
+        callback.accept(player, result);
+    }
+
+    public void open() {
+        if (minValue >= maxValue) {
+            player.sendMessage("§cDu hast kein Geld zum Ausgeben!");
+            return;
+        }
+        InventoryManager.openFor(player, this);
+    }
+
+    @Override
+    public void onClosedByPlayer(Player p) {
+        // accidental close -> null callback (original behaviour)
+        callback.accept(p, null);
+    }
+}

+ 2 - 0
src/main/java/me/lethunderhawk/tradeplugin/input/player/SignInputGUI.java

@@ -0,0 +1,2 @@
+package me.lethunderhawk.tradeplugin.input.player;
+

+ 93 - 0
src/main/java/me/lethunderhawk/tradeplugin/input/player/SignInputManager.java

@@ -0,0 +1,93 @@
+package me.lethunderhawk.tradeplugin.input.player;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockState;
+import org.bukkit.block.Sign;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.block.sign.Side;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.SignChangeEvent;
+import org.bukkit.plugin.Plugin;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiConsumer;
+
+public class SignInputManager implements Listener {
+
+    private final Plugin plugin;
+
+    private record Session(
+            Location location,
+            BiConsumer<Player, String> callback,
+            Material oldType,
+            BlockData oldData
+    ) {}
+
+    private final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
+
+    public SignInputManager(Plugin plugin) {
+        this.plugin = plugin;
+        Bukkit.getPluginManager().registerEvents(this, plugin);
+    }
+
+    public void openSignInput(Player player, BiConsumer<Player, String> callback) {
+        Location loc = getSignLocation(player);
+        Block block = loc.getBlock();
+
+        Material oldType = block.getType();
+        BlockData oldData = block.getBlockData();
+
+        block.setBlockData(Bukkit.createBlockData(Material.OAK_SIGN), false);
+
+        BlockState state = block.getState();
+        if (!(state instanceof Sign sign)) {
+            restoreBlock(block, oldType, oldData);
+            return;
+        }
+
+        // preset lines (optional)
+        sign.getSide(Side.FRONT).line(0, Component.text(""));
+        sign.getSide(Side.FRONT).line(1, Component.text("^^^^^^^^^^"));
+        sign.getSide(Side.FRONT).line(2, Component.text("Wieviel Flux"));
+        sign.getSide(Side.FRONT).line(3, Component.text("willst du traden?"));
+        sign.update(true, false);
+
+        sessions.put(player.getUniqueId(),
+                new Session(loc, callback, oldType, oldData));
+
+        player.openSign(sign, Side.FRONT);
+
+    }
+
+    private Location getSignLocation(Player player) {
+        // high up so nobody sees it and no entities collide
+        return player.getWorld().getHighestBlockAt(player.getLocation()).getLocation().add(0,30,0);
+    }
+
+    @EventHandler
+    public void onSignChange(SignChangeEvent event) {
+        Player player = event.getPlayer();
+        Session session = sessions.remove(player.getUniqueId());
+        if (session == null) return;
+
+        // collect input
+        String input = event.getLine(0);
+
+        Block block = session.location().getBlock();
+        restoreBlock(block, session.oldType(), session.oldData());
+        session.callback().accept(player, input);
+    }
+
+    private void restoreBlock(Block block, Material type, BlockData data) {
+        block.setType(type, false);
+        block.setBlockData(data, false);
+    }
+}

+ 155 - 0
src/main/java/me/lethunderhawk/tradeplugin/listener/InventoryListener.java

@@ -0,0 +1,155 @@
+package me.lethunderhawk.tradeplugin.listener;
+
+import me.lethunderhawk.economy.EconomyModule;
+import me.lethunderhawk.main.Main;
+import me.lethunderhawk.tradeplugin.TradeModule;
+import me.lethunderhawk.tradeplugin.input.player.NumberInputGUI;
+import me.lethunderhawk.tradeplugin.trade.TradeInventory;
+import me.lethunderhawk.tradeplugin.trade.TradeManager;
+import me.lethunderhawk.tradeplugin.trade.TradeSession;
+import me.lethunderhawk.tradeplugin.trade.TradeState;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataType;
+
+import static me.lethunderhawk.tradeplugin.trade.TradeSession.getFluxKey;
+import static me.lethunderhawk.tradeplugin.trade.TradeSession.getValueKey;
+
+public class InventoryListener implements Listener {
+
+    private final TradeManager manager;
+
+    public InventoryListener(TradeModule plugin) {
+        this.manager = TradeModule.getTradeManager();
+    }
+
+    @EventHandler
+    public void onClick(InventoryClickEvent e) {
+
+        if (!(e.getWhoClicked() instanceof Player p)) return;
+        TradeSession session = manager.getSession(p);
+        if (session == null) return;
+        if(e.getInventory().getType() == InventoryType.ANVIL) return;
+
+
+        int slot = e.getRawSlot();
+        if (!session.getTradeInventory().isValidInventoryClick(slot)) {
+            e.setCancelled(true);
+            return;
+        }else if (session.getTradeInventory().isInsideOwnTradeMenu(slot)) {
+            ItemStack item = e.getCurrentItem();
+            e.setCancelled(true);
+
+            if (item == null) return;
+
+            ItemMeta meta = item.getItemMeta();
+            if (meta != null && meta.getPersistentDataContainer()
+                    .has(getFluxKey(), PersistentDataType.INTEGER)) {
+
+                // ---- Flux item clicked ----
+                Integer value = meta.getPersistentDataContainer()
+                        .get(getValueKey(), PersistentDataType.INTEGER);
+
+                if (value != null) {
+                    session.removeFlux(p, value);     // update numbers
+                }
+
+                // Remove the display item from trade UI
+                session.getTradeInventory().clearSlot(slot, session.isP1(p));
+
+                // Refresh both inventories so totals update
+                session.refreshAllFlux();
+
+                return;
+            }
+
+            // ---- Normal item clicked ----
+            session.removeItem(p, item, slot);
+        }
+
+        if (slot == TradeInventory.ACCEPT_SLOT) {
+            e.setCancelled(true);
+
+            if(session.accept(p)) {
+                session.undoAccept(p);
+            }
+
+
+            if (session.getTradeState() == TradeState.COMPLETED) {
+                manager.endTrade(session, true);
+                return;
+            }
+            return;
+        }
+
+        if (slot == TradeInventory.FLUX_SLOT) {
+            e.setCancelled(true);
+            new NumberInputGUI(
+                    Main.getInstance(),
+                    p,
+                    "§6Zahleneingabe",
+                    0,
+                    EconomyModule.getInstance().getEconomyAPI().getMoney(p.getUniqueId()) - manager.getSession(p).getMoneyFor(p),
+                    0,
+                    (player, value) -> {
+                        TradeSession originalSession = manager.getSession(player);
+                        if (value != null && value > 0) {
+                            if(EconomyModule.getInstance().getEconomyAPI().getMoney(player.getUniqueId()) >= value){
+                                if(originalSession.addFluxValue(player, value)){
+                                    TradeModule.sendText(player, Component.text("Added " + value + " Flux to the trade"));
+                                }else{
+                                    TradeModule.sendText(player, Component.text("No free space in menu!"));
+                                }
+                            }else{
+                                TradeModule.sendText(player,Component.text("You don't have enough money!", NamedTextColor.RED));
+                            }
+                        } else {
+                            TradeModule.sendText(player,Component.text("!", NamedTextColor.RED));
+                        }
+                        player.openInventory(originalSession.getTradeInventory().getInventoryFor(player));
+                    }
+            ).open();
+//            SignInputManager signInput = new SignInputManager(Main.getInstance());
+//            signInput.openSignInput(p, (player, text) -> {
+//                player.sendMessage("Du hast eingegeben: " + text);
+//                // parse ints, handle input …
+//            });
+            return;
+        }
+        if(slot >= TradeInventory.TRADE_INV_SIZE){
+            ItemStack clicked = e.getCurrentItem();
+            e.setCancelled(true);
+
+            if (clicked != null && clicked.getType() != Material.AIR) {
+                if (session.addItem(p, clicked)) {
+                    e.setCurrentItem(null);
+                }else{
+                    e.setCancelled(true);
+                }
+            }
+            return;
+        }
+
+    }
+
+    @EventHandler
+    public void onInventoryClose(InventoryCloseEvent e) {
+        Player p = (Player) e.getPlayer();
+        TradeSession session = manager.getSession(p);
+        if (session != null
+                && e.getReason() != InventoryCloseEvent.Reason.PLUGIN
+                && e.getReason() != InventoryCloseEvent.Reason.OPEN_NEW) {
+            manager.endTrade(session, false);
+        }
+//        p.sendMessage("Reason: " + e.getReason() + ", Type: " + e.getInventory().getType());
+    }
+}

+ 31 - 0
src/main/java/me/lethunderhawk/tradeplugin/listener/PlayerInteractListener.java

@@ -0,0 +1,31 @@
+package me.lethunderhawk.tradeplugin.listener;
+
+import me.lethunderhawk.tradeplugin.TradeModule;
+import me.lethunderhawk.tradeplugin.trade.TradeManager;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerInteractEntityEvent;
+
+public class PlayerInteractListener implements Listener {
+
+    private final TradeManager manager;
+
+    public PlayerInteractListener(TradeModule plugin) {
+        this.manager = TradeModule.getTradeManager();
+    }
+
+    @EventHandler
+    public void onInteract(PlayerInteractEntityEvent e) {
+        if (!(e.getRightClicked() instanceof Player target)) return;
+
+        Player p = e.getPlayer();
+        if (!p.isSneaking()) return;
+
+        if (manager.isInTrade(p) || manager.isInTrade(target)) {
+            return;
+        }
+
+        TradeModule.getRequestManager().sendRequest(p, target);
+    }
+}

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

@@ -0,0 +1,241 @@
+package me.lethunderhawk.tradeplugin.trade;
+
+import me.lethunderhawk.bazaarflux.util.CustomHeadCreator;
+import me.lethunderhawk.main.util.ItalicDeco;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.Style;
+import net.kyori.adventure.text.format.TextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TradeInventory {
+    public static final int ACCEPT_SLOT = 39, ACCEPT_INDICATOR = ACCEPT_SLOT + 5;
+    public static final int FLUX_SLOT = 38;
+    public static final int DECLINE_SLOT = 37;
+    public static final int TRADE_INV_SIZE = 45;
+    private static final String fluxHeadValue = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNmU1Y2Y3ZjJlMGY2YjE2N2IwYjZmZDBjNGFjMTZjYTcwZTRjNWM4MTFiOGQ1YWQwZWVkMmUzYWE2ZGQyYjcifX19";
+    private static ItemStack fluxItemStack;
+    private final TradeSession session;
+
+    private final Inventory invP1;
+    private final Inventory invP2;
+    private ItemStack acceptedIndicator, youAcceptedIndicator, pendingIndicator, declineButton, acceptButton;
+    private Material acceptMaterial, declineMaterial, acceptedIndicatorMaterial, pendingIndicatorMaterial, youAcceptedIndicatorMaterial;
+
+    public TradeInventory(TradeSession session) {
+        this.session = session;
+        this.invP1 = Bukkit.createInventory(null, TRADE_INV_SIZE, "Trade mit " + session.getP2().getName());
+        this.invP2 = Bukkit.createInventory(null, TRADE_INV_SIZE, "Trade mit " + session.getP1().getName());
+        setupMaterials();
+        fluxItemStack = CustomHeadCreator.createCustomHead(fluxHeadValue, ItalicDeco.remove(Component.text("Trade Flux", NamedTextColor.GOLD)), ItalicDeco.remove(Component.text("Click to set a reasonable amount", NamedTextColor.GRAY)));
+        setupIcons();
+        setup(invP1);
+        setup(invP2);
+    }
+
+    private void setupMaterials() {
+        acceptMaterial = Material.LIME_CONCRETE;
+        youAcceptedIndicatorMaterial = Material.GREEN_CONCRETE;
+        declineMaterial = Material.RED_CONCRETE;
+
+        acceptedIndicatorMaterial = Material.GREEN_DYE;
+        pendingIndicatorMaterial = Material.RED_DYE;
+
+    }
+    public static ItemStack getFluxItemStack(){
+        return CustomHeadCreator.createCustomHead(fluxHeadValue);
+    }
+
+
+    private void setupIcons(){
+        acceptedIndicator = button(acceptedIndicatorMaterial, "Angenommen", TextColor.color(0,255,0));
+
+        youAcceptedIndicator = button(youAcceptedIndicatorMaterial, "Angenommen", TextColor.color(0,255,0));
+        ItemMeta meta = youAcceptedIndicator.getItemMeta();
+        meta.setEnchantmentGlintOverride(true);
+        youAcceptedIndicator.setItemMeta(meta);
+
+        pendingIndicator = button(pendingIndicatorMaterial, "Warten auf anderen Spieler", TextColor.color(255,0,0));
+
+        acceptButton = button(acceptMaterial, "Annehmen", TextColor.color(0,255,0));
+        ItemMeta acceptMeta = acceptButton.getItemMeta();
+        acceptMeta.setEnchantmentGlintOverride(false);
+        acceptButton.setItemMeta(acceptMeta);
+
+        declineButton = button(declineMaterial, "Abbrechen", TextColor.color(255, 0, 0));
+    }
+
+    private void setup(Inventory inv) {
+        ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
+        ItemMeta meta = glass.getItemMeta();
+        meta.displayName(Component.text(" "));
+        glass.setItemMeta(meta);
+
+        for (int i = 4; i <= TRADE_INV_SIZE; i+=9) inv.setItem(i, glass);
+        for (int i = TRADE_INV_SIZE-9; i < TRADE_INV_SIZE; i++) inv.setItem(i, glass);
+        inv.setItem(ACCEPT_SLOT, acceptButton);
+        //inv.setItem(DECLINE_SLOT, declineButton);
+        inv.setItem(FLUX_SLOT, fluxItemStack);
+        inv.setItem(ACCEPT_INDICATOR, pendingIndicator);
+    }
+
+    private ItemStack button(Material m, String name, TextColor color) {
+        ItemStack item = new ItemStack(m);
+        ItemMeta meta = item.getItemMeta();
+        meta.displayName(ItalicDeco.remove(Component.text(name).style(Style.style(color))));
+        item.setItemMeta(meta);
+        return item;
+    }
+
+    public Inventory getInventoryFor(Player p) {
+        return p.equals(session.getP1()) ? invP1 : invP2;
+    }
+    public Inventory getInventoryForOther(Player p) {
+        return p.equals(session.getP1()) ? invP2 : invP1;
+    }
+
+    public boolean isValidInventoryClick(int slot) {
+        return (slot % 9 < 4 && slot < TRADE_INV_SIZE - 9) || slot > TRADE_INV_SIZE
+                //|| slot == DECLINE_SLOT
+                || slot == ACCEPT_SLOT
+                || slot == FLUX_SLOT;
+    }
+    /**
+     * @return If the action was successful
+     */
+    public boolean addItem(Player player, ItemStack item, boolean isMyTradeInventory) {
+        Inventory inv = getInventoryFor(player);
+        int emptySlot = getFirstEmptySlot(inv, isMyTradeInventory);
+        if(emptySlot != -1){
+            inv.setItem(emptySlot, item);
+            updateInventory(TradeState.WAITING);
+            return true;
+        }
+        return false;
+    }
+
+    public boolean removeItem(Player player, ItemStack item, int slot) {
+        getInventoryFor(player).setItem(slot, null);
+        getInventoryForOther(player).setItem(slot + 5, null);
+
+        player.getInventory().addItem(item);
+        updateInventory(TradeState.WAITING);
+        return true;
+    }
+
+    private int getFirstEmptySlot(Inventory inv, boolean isMyTradeInventory){
+        for(int slot = 0; slot < inv.getSize(); slot++){
+            if(isMyTradeInventory){
+                if((slot % 9 < 4 && slot < TRADE_INV_SIZE - 9)){
+                    ItemStack itemStack = inv.getItem(slot);
+                    if(itemStack == null) return slot;
+                }
+            }else{
+                if((slot % 9 > 4 && slot < TRADE_INV_SIZE - 9)){
+                    ItemStack itemStack = inv.getItem(slot);
+                    if(itemStack == null) return slot;
+                }
+            }
+        }
+        return -1;
+    }
+
+    public void updateInventory(TradeState state){
+        if(state == TradeState.ACCEPTED_P1){
+            invP2.setItem(ACCEPT_INDICATOR, acceptedIndicator);
+            invP1.setItem(ACCEPT_SLOT, youAcceptedIndicator);
+
+        }else if(state == TradeState.ACCEPTED_P2){
+            invP1.setItem(ACCEPT_INDICATOR, acceptedIndicator);
+            invP2.setItem(ACCEPT_SLOT, youAcceptedIndicator);
+        }else if(state == TradeState.WAITING){
+            // snapping back of items logic
+            snapItemsToFront();
+            // setting back of trade conditions
+            invP1.setItem(ACCEPT_INDICATOR, pendingIndicator);
+            invP1.setItem(ACCEPT_SLOT, acceptButton);
+
+            invP2.setItem(ACCEPT_INDICATOR, pendingIndicator);
+            invP2.setItem(ACCEPT_SLOT, acceptButton);
+        }
+        session.setTradeState(state);
+    }
+    private void snapItemsToFront() {
+        snapSide(invP1, true);     // left side of P1
+        snapSide(invP1, false);    // right side of P1
+
+        snapSide(invP2, true);     // left side of P2
+        snapSide(invP2, false);    // right side of P2
+    }
+    private void snapSide(Inventory inv, boolean leftSide) {
+        List<ItemStack> items = new ArrayList<>();
+
+        // collect
+        for (int slot = 0; slot < TRADE_INV_SIZE; slot++) {
+            if (slot >= TRADE_INV_SIZE - 9) continue; // ignore bottom row
+
+            int col = slot % 9;
+
+            if (leftSide) {
+                if (col >= 4) continue;
+            } else {
+                if (col <= 4) continue;
+            }
+
+            ItemStack it = inv.getItem(slot);
+            if (it != null) items.add(it);
+        }
+
+        // clear region
+        for (int slot = 0; slot < TRADE_INV_SIZE; slot++) {
+            if (slot >= TRADE_INV_SIZE - 9) continue;
+            int col = slot % 9;
+
+            if (leftSide && col >= 4) continue;
+            if (!leftSide && col <= 4) continue;
+
+            inv.setItem(slot, null);
+        }
+
+        // re-fill in scan order
+        int index = 0;
+        for (int slot = 0; slot < TRADE_INV_SIZE && index < items.size(); slot++) {
+            if (slot >= TRADE_INV_SIZE - 9) continue;
+            int col = slot % 9;
+
+            if (leftSide && col >= 4) continue;
+            if (!leftSide && col <= 4) continue;
+
+            inv.setItem(slot, items.get(index++));
+        }
+    }
+
+    /**
+     *
+     * @param slot The slot of the item (inside the players Inventory)
+     * @param isP1 If the player is the player1
+     */
+    public void clearSlot(int slot, boolean isP1) {
+        if(isP1){
+            invP1.setItem(slot, null);
+            invP2.setItem(slot + 5, null);
+        }else{
+            invP2.setItem(slot, null);
+            invP1.setItem(slot + 5, null);
+        }
+        updateInventory(TradeState.WAITING);
+    }
+
+    public boolean isInsideOwnTradeMenu(int slot) {
+        return (slot % 9 < 4 && slot < TRADE_INV_SIZE - 10);
+    }
+
+}

+ 46 - 0
src/main/java/me/lethunderhawk/tradeplugin/trade/TradeManager.java

@@ -0,0 +1,46 @@
+package me.lethunderhawk.tradeplugin.trade;
+
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.Plugin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class TradeManager {
+
+    private final Plugin plugin;
+
+    public TradeManager(Plugin plugin){
+        this.plugin = plugin;
+    }
+    
+    private final Map<Player, TradeSession> sessions = new HashMap<>();
+
+    public boolean isInTrade(Player p) {
+        return sessions.containsKey(p);
+    }
+
+    public void startTrade(Player p1, Player p2) {
+        TradeSession session = new TradeSession(p1, p2);
+        sessions.put(p1, session);
+        sessions.put(p2, session);
+    }
+
+    public void endTrade(TradeSession session, boolean accepted) {
+        session.getP1().closeInventory();
+        session.getP2().closeInventory();
+
+        if(accepted){
+            session.complete();
+        }else {
+            session.decline();
+        }
+
+        sessions.remove(session.getP1());
+        sessions.remove(session.getP2());
+    }
+
+    public TradeSession getSession(Player p) {
+        return sessions.get(p);
+    }
+}

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

@@ -0,0 +1,105 @@
+package me.lethunderhawk.tradeplugin.trade;
+
+import me.lethunderhawk.main.Main;
+import me.lethunderhawk.tradeplugin.TradeModule;
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.sound.Sound;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public class TradeRequestManager {
+
+    private final Map<UUID, UUID> requests = new HashMap<>(); // Sender ; Target
+    private final Map<UUID, Integer> timeoutTasks = new HashMap<>();
+
+    public void sendRequest(Player sender, Player target) {
+        if(isPending(sender, target)){
+
+            Component targetmsg = Component.text(sender.getName())
+                    .color(NamedTextColor.GREEN)
+                    .append(Component.text(" hat deine Handelsanfrage angenommen!")
+                            .color(NamedTextColor.YELLOW));
+            Component sendermsg = Component.text("Handelsanfrage von ")
+                    .color(NamedTextColor.YELLOW)
+                    .append(
+                            Component.text(target.getName())
+                                    .color(NamedTextColor.GREEN)
+                    ).append(
+                            Component.text(" angenommen.").color(NamedTextColor.YELLOW)
+                    );
+            TradeModule.sendText(sender, sendermsg);
+            TradeModule.sendText(target, targetmsg);
+
+            TradeModule.getTradeManager().startTrade(sender, target);
+
+            accept(sender, target);
+            accept(target, sender);
+        }else{
+            if(requests.get(target.getUniqueId()) != null) return;
+
+            requests.put(target.getUniqueId(), sender.getUniqueId());
+
+            Component targetmsg = Component.text(sender.getName() + " möchte mit dir handeln. ")
+                    .color(NamedTextColor.YELLOW)
+                    .append(
+                            Component.text("[Annehmen]")
+                                    .color(NamedTextColor.GREEN)
+                                    .clickEvent(ClickEvent.runCommand("/tradeaccept " + sender.getName()))
+                    );
+
+            Component sendermsg = Component.text("Handelsanfrage an ")
+                    .color(NamedTextColor.YELLOW)
+                    .append(
+                            Component.text(target.getName())
+                                    .color(NamedTextColor.GREEN)
+                    ).append(
+                            Component.text(" gesendet").color(NamedTextColor.YELLOW)
+                    );
+            TradeModule.sendText(sender, sendermsg);
+            TradeModule.sendText(target, targetmsg);
+
+            target.playSound(
+                    Sound.sound()
+                            .type(Key.key("minecraft:entity.villager.yes"))
+                            .source(Sound.Source.PLAYER)
+                            .volume(1f)
+                            .pitch(1f)
+                            .build(),
+                    Sound.Emitter.self()
+            );
+
+            int task = Bukkit.getScheduler().scheduleSyncDelayedTask(
+                    Main.getInstance(),
+                    () -> {
+                        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));
+                        }
+                    },
+                    20L * 60 // 1 Minute
+            );
+
+            timeoutTasks.put(target.getUniqueId(), task);
+        }
+    }
+
+    public boolean isPending(Player target, Player sender) {
+        return requests.getOrDefault(target.getUniqueId(), null) != null
+                && requests.get(target.getUniqueId()).equals(sender.getUniqueId());
+    }
+
+    public void accept(Player target, Player sender) {
+        requests.remove(target.getUniqueId());
+
+        Integer task = timeoutTasks.remove(target.getUniqueId());
+        if (task != null) Bukkit.getScheduler().cancelTask(task);
+    }
+}

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

@@ -0,0 +1,348 @@
+package me.lethunderhawk.tradeplugin.trade;
+
+import me.lethunderhawk.economy.EconomyModule;
+import me.lethunderhawk.main.Main;
+import me.lethunderhawk.main.util.ItalicDeco;
+import me.lethunderhawk.tradeplugin.TradeModule;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.inventory.meta.SkullMeta;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public class TradeSession {
+
+    private final Player p1;
+    private final Player p2;
+
+    private final List<ItemStack> p1Items = new ArrayList<>();
+    private final List<ItemStack> p2Items = new ArrayList<>();
+    private static final NamespacedKey FLUX_KEY =
+            new NamespacedKey(Main.getInstance(), "flux_item");
+    private static final NamespacedKey OWNER_KEY =
+            new NamespacedKey(Main.getInstance(), "flux_owner");
+    private static final NamespacedKey VALUE_KEY =
+            new NamespacedKey(Main.getInstance(), "flux_value");
+
+    private TradeState state = TradeState.WAITING;
+    private int p1Flux = 0, p2Flux = 0;
+
+    private final TradeInventory inventory;
+
+
+
+    public TradeSession(Player p1, Player p2) {
+        this.p1 = p1;
+        this.p2 = p2;
+        this.inventory = new TradeInventory(this);
+        p1.openInventory(inventory.getInventoryFor(p1));
+        p2.openInventory(inventory.getInventoryFor(p2));
+    }
+    /**
+     * @return if the player has already accepted
+     */
+    public boolean accept(Player p) {
+        if (p.equals(p1)) {
+            if(state == TradeState.ACCEPTED_P1){
+                return true;
+            }
+            state = state == TradeState.ACCEPTED_P2 ? TradeState.COMPLETED : TradeState.ACCEPTED_P1;
+            updateInventories();
+        } else if(p.equals(p2)){
+            if(state == TradeState.ACCEPTED_P2){
+                return true;
+            }
+            state = state == TradeState.ACCEPTED_P1 ? TradeState.COMPLETED : TradeState.ACCEPTED_P2;
+            updateInventories();
+        }
+        return false;
+    }
+
+    public void undoAccept(Player p){
+        if (p.equals(p1)) {
+            state = state == TradeState.ACCEPTED_P1 ? TradeState.WAITING : TradeState.ACCEPTED_P1;
+            updateInventories();
+        } else if(p.equals(p2)){
+            state = state == TradeState.ACCEPTED_P2 ? TradeState.WAITING : TradeState.ACCEPTED_P2;
+            updateInventories();
+        }
+    }
+
+    private void updateInventories() {
+        inventory.updateInventory(state);
+    }
+
+    public boolean addItem(Player p, ItemStack item) {
+        if(item == null) return false;
+        if (p.equals(p1)) {
+            if(inventory.addItem(p1, item, true)
+                    && inventory.addItem(p2, item, false)){
+                p1Items.add(item);
+                return true;
+            }
+        } else if(p.equals(p2)){
+            if(inventory.addItem(p2, item, true)
+                    && inventory.addItem(p1, item, false)){
+                p2Items.add(item);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean addCosmeticItem(Player p, ItemStack item) {
+        if(item == null) return false;
+        if (p.equals(p1)) {
+            return inventory.addItem(p1, item, true)
+                    && inventory.addItem(p2, item, false);
+        } else if(p.equals(p2)){
+            return inventory.addItem(p2, item, true)
+                    && inventory.addItem(p1, item, false);
+        }
+        return false;
+    }
+
+    public void complete() {
+        sendTradeCompletedMsg();
+        for (ItemStack item : p1Items) {
+            if(item != null){
+                p2.getInventory().addItem(item);
+            }
+        }
+        for (ItemStack item : p2Items) {
+            if(item != null) {
+                p1.getInventory().addItem(item);
+            }
+        }
+        transferFlux();
+        p1Items.clear();
+        p2Items.clear();
+
+    }
+    private void sendReceivingMsg(Audience receiver, Component itemName, int amount){
+        TradeModule.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 )
+                .append(itemName.color(NamedTextColor.GRAY))
+                .append(Component.text((amount> 1 ? " x" + amount : ""), NamedTextColor.GRAY)));
+    }
+
+    private void sendTradeCompletedMsg() {
+        sendTradeCompletedLine(p1, p2);
+        sendTradeCompletedLine(p2, p1);
+
+        sendItemTransfers(p1Items, p1, p2);
+        sendItemTransfers(p2Items, p2, p1);
+
+        sendFluxTransfer(p1Flux, p1, p2);
+        sendFluxTransfer(p2Flux, p2, p1);
+    }
+
+    private void sendTradeCompletedLine(Player receiver, Player other) {
+        TradeModule.sendText(receiver,
+                Component.text("Handel mit ", NamedTextColor.GOLD)
+                        .append(other.displayName().color(NamedTextColor.GRAY))
+                        .append(Component.text(" abgeschlossen!", NamedTextColor.GOLD))
+        );
+    }
+
+    private void sendItemTransfers(List<ItemStack> items, Audience giver, Audience receiver) {
+        for (ItemStack item : items) {
+            if (item == null) continue;
+            sendGivingMsg(giver, item.displayName(), item.getAmount());
+            sendReceivingMsg(receiver, item.displayName(), item.getAmount());
+        }
+    }
+
+    private void sendFluxTransfer(int flux, Audience giver, Audience receiver) {
+        if (flux <= 0) return;
+
+        Component fluxComp = Component.text(flux)
+                .append(Component.text(" Flux"));
+
+        sendGivingMsg(giver, fluxComp, 1);
+        sendReceivingMsg(receiver, fluxComp, 1);
+    }
+
+    private void transferFlux() {
+        if(p2Flux > 0){
+            EconomyModule.getInstance().getEconomyAPI().pay(p2.getUniqueId(), p1.getUniqueId(), p2Flux);
+        }
+        if(p1Flux > 0){
+            EconomyModule.getInstance().getEconomyAPI().pay(p1.getUniqueId(), p2.getUniqueId(), p1Flux);
+        }
+    }
+
+    public void decline() {
+        for (ItemStack item : p1Items) {
+            if(item != null){
+                p1.getInventory().addItem(item);
+            }
+        }
+        for (ItemStack item : p2Items) {
+            if(item != null) {
+                p2.getInventory().addItem(item);
+            }
+        }
+        p1Items.clear();
+        p2Items.clear();
+    }
+
+    public Player getP1() { return p1; }
+    public Player getP2() { return p2; }
+
+    public TradeInventory getTradeInventory() { return inventory; }
+
+    public TradeState getTradeState() {
+        return state;
+    }
+
+    public void removeItem(Player p, ItemStack item, int mySlot) {
+        if(item == null) return;
+        if (p.equals(p1)) {
+            if(inventory.removeItem(p1, item, mySlot)){
+                p1Items.remove(item);
+            }
+        } else if(p.equals(p2)){
+            if(inventory.removeItem(p2, item, mySlot)){
+                p2Items.remove(item);
+            }
+        }
+    }
+
+    public void setTradeState(TradeState state) {
+        this.state = state;
+    }
+
+    public int getMoneyFor(Player p) {
+        if(p1.getUniqueId().equals(p.getUniqueId())){
+            return p1Flux;
+        }
+        if(p2.getUniqueId().equals(p.getUniqueId())){
+          return p2Flux;
+        }
+        return 0;
+    }
+
+    public boolean addFluxValue(Player player, int value) {
+
+        if(p1.getUniqueId().equals(player.getUniqueId())){
+
+            if(addCosmeticItem(p1, getCosmeticFlux(p1, value))){
+                p1Flux += value;
+                refreshAllFlux();
+                return true;
+            }
+
+        }else if(p2.getUniqueId().equals(player.getUniqueId())){
+            if(addCosmeticItem(p2, getCosmeticFlux(p2, value))){
+                p2Flux += value;
+                refreshAllFlux();
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    public ItemStack getCosmeticFlux(Player owner, int value) {
+        ItemStack item = TradeInventory.getFluxItemStack();
+        SkullMeta meta = (SkullMeta) item.getItemMeta();
+
+        meta.displayName(ItalicDeco.remove(Component.text(value + " Flux", NamedTextColor.GOLD)));
+
+        meta.getPersistentDataContainer().set(FLUX_KEY, PersistentDataType.INTEGER, 1);
+        meta.getPersistentDataContainer().set(OWNER_KEY, PersistentDataType.STRING, owner.getUniqueId().toString());
+        meta.getPersistentDataContainer().set(VALUE_KEY, PersistentDataType.INTEGER, value);
+
+        int total = TradeModule.getTradeManager()
+                .getSession(owner)
+                .getMoneyFor(owner);
+
+        meta.lore(List.of(
+                ItalicDeco.remove(Component.text("Lup-sum amount", NamedTextColor.GRAY)),
+                ItalicDeco.remove(Component.text("", NamedTextColor.GRAY)),
+                ItalicDeco.remove(Component.text("Total Flux Offered: ", NamedTextColor.GOLD)),
+                ItalicDeco.remove(Component.text(String.valueOf(total), NamedTextColor.GRAY))
+        ));
+
+        item.setItemMeta(meta);
+        return item;
+    }
+
+    public void refreshFluxItems(Inventory inv) {
+        for (int i = 0; i < inv.getSize(); i++) {
+            ItemStack item = inv.getItem(i);
+            if (item == null) continue;
+
+            ItemMeta meta = item.getItemMeta();
+            if (meta == null) continue;
+
+            PersistentDataContainer pdc = meta.getPersistentDataContainer();
+            if (!pdc.has(FLUX_KEY, PersistentDataType.INTEGER)) continue;
+
+            UUID ownerId = UUID.fromString(pdc.get(OWNER_KEY, PersistentDataType.STRING));
+            int value = pdc.get(VALUE_KEY, PersistentDataType.INTEGER);
+
+            Player owner = ownerId.equals(getP1().getUniqueId())
+                    ? getP1()
+                    : getP2();
+
+            inv.setItem(i, getCosmeticFlux(owner, value));
+        }
+    }
+
+    /**
+     *
+     * @param p The Player who is not the viewer
+     * @return The Player who is the viewer
+     */
+    public Player getViewer(Player p){
+        if(p.equals(p1)) {
+            return p2;
+        }else if(p.equals(p2)) {
+            return p1;
+        }
+        return null;
+    }
+    public static NamespacedKey getFluxKey(){
+        return FLUX_KEY;
+    }
+    public static NamespacedKey getOwnerKey(){
+        return OWNER_KEY;
+    }
+    public static NamespacedKey getValueKey(){
+        return VALUE_KEY;
+    }
+
+    public void removeFlux(Player p, int value) {
+        if (p.equals(p1)) {
+            p1Flux -= value;
+            if (p1Flux < 0) p1Flux = 0;
+        } else if (p.equals(p2)) {
+            p2Flux -= value;
+            if (p2Flux < 0) p2Flux = 0;
+        }
+    }
+    public void refreshAllFlux() {
+        refreshFluxItems(inventory.getInventoryFor(p1));
+        refreshFluxItems(inventory.getInventoryFor(p2));
+    }
+
+    public boolean isP1(Player p) {
+        return p1.equals(p);
+    }
+}

+ 10 - 0
src/main/java/me/lethunderhawk/tradeplugin/trade/TradeState.java

@@ -0,0 +1,10 @@
+package me.lethunderhawk.tradeplugin.trade;
+
+public enum TradeState {
+    WAITING,
+    ACCEPTED_P1,
+    ACCEPTED_P2,
+    COMPLETED,
+    CANCELLED;
+
+}

+ 0 - 0
src/main/resources/clans.yml


+ 0 - 0
src/main/resources/config.yml


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

@@ -0,0 +1,33 @@
+name: BazaarFlux
+version: '${project.version}'
+main: me.lethunderhawk.main.Main
+api-version: 1.21.10
+prefix: BazaarFlux
+commands:
+  eco:
+    description: Economy administration command
+    usage: /eco <set|add|remove|get> <player> <amount>
+    permission: currency.eco
+  trade:
+    description: Send a trade request
+  tradeaccept:
+    description: Accept a trade request
+  clan:
+    description: Clan management command
+    usage: /clan <join|rule>
+  customItem:
+    description: Custom Item management command
+    usage: /customItem
+    permission: customItem.commands
+
+permissions:
+  customItem.commands:
+    description: Allows use of custom Items commands
+    default: op
+  currency.eco:
+    description: Allows economy administration
+    default: op
+  trade.trade:
+    description: Allows initiating a trade
+  trade.acceptTrade:
+    description: Allows accepting a trade

+ 8 - 0
src/main/resources/scoreboards/default.yml

@@ -0,0 +1,8 @@
+title: "&aServer: Dein-Server"
+lines:
+  - ""
+  - "&7Clan: %clan_name%"
+  - "&7Balance: &6%flux_money%"
+  - ""
+  - "&7⌂ %flux_region%"
+  - "&7%flux_formattedTime%"