From efd57a3a1ee198fd7aafa4d2a165f981a3ba599b Mon Sep 17 00:00:00 2001 From: GeminiAntigravityCLI Date: Thu, 20 Aug 2026 17:16:05 +0200 Subject: [PATCH] feat(guild): implement complete domain model, spatio-temporal pricing, repository, and managers with full javadoc --- GEMINI.md | 92 +++ src/main/java/fr/luc/gamingcore/Main.java | 123 +++- .../fr/luc/gamingcore/command/AppCommand.java | 262 -------- .../command/BukkitCommandWrapper.java | 37 -- .../gamingcore/command/CommandBuilder.java | 107 ---- .../gamingcore/command/CommandContext.java | 109 ---- .../gamingcore/command/CommandHandler.java | 6 - .../gamingcore/command/CommandManager.java | 101 --- .../argument/ArgumentParseException.java | 8 - .../command/argument/ArgumentParser.java | 30 - .../command/argument/CommandArgument.java | 72 --- .../argument/parsers/DoubleArgument.java | 52 -- .../argument/parsers/IntegerArgument.java | 52 -- .../argument/parsers/PlayerArgument.java | 41 -- .../argument/parsers/StringArgument.java | 66 -- .../command/cooldown/CooldownManager.java | 62 -- .../gamingcore/database/DatabaseManager.java | 150 +++++ .../gamingcore/database/GuildRepository.java | 334 ++++++++++ .../economy/ChunkPriceCalculator.java | 180 ++++++ .../gamingcore/economy/EconomyManager.java | 119 ++++ .../listener/TerritoryProtectionListener.java | 235 +++++++ .../luc/gamingcore/manager/GuildManager.java | 606 ++++++++++++++++++ .../luc/gamingcore/manager/GuildResult.java | 53 ++ .../luc/gamingcore/manager/InviteManager.java | 112 ++++ .../java/fr/luc/gamingcore/model/Guild.java | 526 +++++++++++++++ .../fr/luc/gamingcore/model/GuildClaim.java | 145 +++++ .../fr/luc/gamingcore/model/GuildInvite.java | 96 +++ .../fr/luc/gamingcore/model/GuildMember.java | 132 ++++ .../luc/gamingcore/model/GuildPermission.java | 118 ++++ .../fr/luc/gamingcore/model/GuildRank.java | 175 +++++ .../economy/ChunkPriceCalculatorTest.java | 76 +++ .../luc/gamingcore/model/GuildModelTest.java | 92 +++ 32 files changed, 3348 insertions(+), 1021 deletions(-) create mode 100644 GEMINI.md delete mode 100644 src/main/java/fr/luc/gamingcore/command/AppCommand.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/BukkitCommandWrapper.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/CommandBuilder.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/CommandContext.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/CommandHandler.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/CommandManager.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/argument/ArgumentParseException.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/argument/ArgumentParser.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/argument/CommandArgument.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/argument/parsers/DoubleArgument.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/argument/parsers/IntegerArgument.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/argument/parsers/PlayerArgument.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/argument/parsers/StringArgument.java delete mode 100644 src/main/java/fr/luc/gamingcore/command/cooldown/CooldownManager.java create mode 100644 src/main/java/fr/luc/gamingcore/database/DatabaseManager.java create mode 100644 src/main/java/fr/luc/gamingcore/database/GuildRepository.java create mode 100644 src/main/java/fr/luc/gamingcore/economy/ChunkPriceCalculator.java create mode 100644 src/main/java/fr/luc/gamingcore/economy/EconomyManager.java create mode 100644 src/main/java/fr/luc/gamingcore/listener/TerritoryProtectionListener.java create mode 100644 src/main/java/fr/luc/gamingcore/manager/GuildManager.java create mode 100644 src/main/java/fr/luc/gamingcore/manager/GuildResult.java create mode 100644 src/main/java/fr/luc/gamingcore/manager/InviteManager.java create mode 100644 src/main/java/fr/luc/gamingcore/model/Guild.java create mode 100644 src/main/java/fr/luc/gamingcore/model/GuildClaim.java create mode 100644 src/main/java/fr/luc/gamingcore/model/GuildInvite.java create mode 100644 src/main/java/fr/luc/gamingcore/model/GuildMember.java create mode 100644 src/main/java/fr/luc/gamingcore/model/GuildPermission.java create mode 100644 src/main/java/fr/luc/gamingcore/model/GuildRank.java create mode 100644 src/test/java/fr/luc/gamingcore/economy/ChunkPriceCalculatorTest.java create mode 100644 src/test/java/fr/luc/gamingcore/model/GuildModelTest.java diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..47b39dc --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,92 @@ +# 📜 Instructions & Consignes du Projet - GamingCore (SystĂšme de Guilde) + +Ce document consigne les directives d'architecture, rĂšgles de dĂ©veloppement et spĂ©cifications fonctionnelles du projet **GamingCore**. + +--- + +## 🎯 1. Vision du Projet +DĂ©veloppement d'un systĂšme de **Guilde complet, modulaire et hautement configurable** pour Minecraft (Paper 1.20.1+ / Java 21 LTS - target Java 17). + +--- + +## 📐 2. RĂšgles & Standards de DĂ©veloppement + +### đŸ”č Conception du Code +* **MĂ©thodes & Logique MĂ©tier d'Abord** : Tout le systĂšme doit reposer sur des mĂ©thodes et services mĂ©tier dĂ©couplĂ©s (Managers, Repositories, Calculators), indĂ©pendants du systĂšme de commande ou d'UI. +* **Dynamisme & ModularitĂ©** : Le code doit ĂȘtre dĂ©coupĂ© en parties logiques claires et Ă©volutives. +* **Documentation Obligatoire** : **Chaque mĂ©thode** doit comporter une Javadoc / un commentaire dĂ©taillĂ© explicitant : + * Son rĂŽle et son comportement. + * Ses paramĂštres (`@param`). + * Sa valeur de retour (`@return`). + * Les Ă©ventuelles exceptions levĂ©es (`@throws`). + +### đŸ”č DĂ©pendances & IntĂ©grations +* **Paper API 1.20.4** : API Bukkit/Paper cross-version. +* **Vault API 1.7.1** : Gestion de l'Ă©conomie (dĂ©pĂŽt, retrait, vĂ©rification de solde). +* **WorldGuard 7.0.9 & WorldEdit 7.3.0** : IntĂ©gration et compatibilitĂ© rĂ©gionale. +* **Kyori Adventure & MiniMessage** : Couleurs HEX, gradients et composants de texte modernes. +* **XSeries 13.0.0** : Abstraction des matĂ©riaux, sons et particules. + +--- + +## 🏰 3. SpĂ©cifications MĂ©tier du SystĂšme de Guilde + +### 1. Structure d'une Guilde +* CrĂ©ation soumise Ă  un coĂ»t Ă©conomique (Vault) et au claim obligatoire du chunk initial. +* DonnĂ©es : `UUID`, `nom`, `tag`, `description`, `solde bancaire`, `point de ralliement (home)`, `membres`, `rangs`, `claims`. + +### 2. Rangs & Permissions Dynamiques par Guilde +* Chaque guilde peut crĂ©er, renommer, hiĂ©rarchiser (prioritĂ©) et supprimer ses propres rangs. +* Rangs initiaux par dĂ©faut Ă  la crĂ©ation : + * **👑 Chef (Leader)** : PrioritĂ© maximale (100), toutes les permissions, non supprimable. + * **⚔ Officier (Officer)** : PrioritĂ© intermĂ©diaire (50), gestion des membres et claims. + * **🛡 Membre (Member)** : PrioritĂ© de base (1), rang par dĂ©faut des recrues. +* Permissions granulaires configurables par rang : + * `CLAIM`, `UNCLAIM`, `SET_HOME`, `HOME`, `INVITE`, `KICK`, `PROMOTE`, `DEMOTE`, `MANAGE_RANKS`, `BANK_DEPOSIT`, `BANK_WITHDRAW`, `BUILD`, `BREAK`, `INTERACT`, `CONTAINER`. + +### 3. Moteur Économique & Calcul Spatio-Temporel des Chunks +Le prix d'acquisition d'un nouveau chunk suit la formule spatio-temporelle avec surchauffe glissante sur 24h : + +$$\text{PrixFinal}(N, R) = \left[ P_{\text{base}} \times (1 + \text{Taux})^{(N - 1)^\alpha} + (N - 1) \times \text{Palier} \right] \times \left[ 1 + \left( k_{\text{base}} + k_{\text{taille}} \times \left(\frac{N}{10}\right) \right) \times R^{\delta} \right]$$ + +* **$N$** : Nombre total de chunks possĂ©dĂ©s. +* **$R$** : Nombre de chunks achetĂ©s dans les derniĂšres 24h (dĂ©croissance automatique). +* **Effet d'Ă©chelle** : La surchauffe temporelle est plus sĂ©vĂšre pour une grande guilde que pour une petite. + +### 4. Banque de Guilde +* Solde partagĂ© avec dĂ©pĂŽts libres et retraits sĂ©curisĂ©s par permissions de rang. + +### 5. Invitations +* Invitations temporaires expirables (ex: 60s) avec acceptation/refus. + +### 6. Protection Territoriale & Listeners +* Protection des blocs, conteneurs, portes et explosions sur les chunks de guilde en fonction des permissions de rang du joueur. + +--- + +## đŸ—‚ïž 4. Organisation des Packages + +``` +fr.luc.gamingcore/ + ├── Main.java (Point d'entrĂ©e du plugin) + ├── model/ (EntitĂ©s et Enums du domaine) + │ ├── Guild.java + │ ├── GuildMember.java + │ ├── GuildRank.java + │ ├── GuildPermission.java + │ ├── GuildClaim.java + │ └── GuildInvite.java + ├── economy/ (Gestion Ă©conomique & Formule mathĂ©matique) + │ ├── EconomyManager.java + │ └── ChunkPriceCalculator.java + ├── database/ (Persistance SQLite/MySQL) + │ ├── DatabaseManager.java + │ └── GuildRepository.java + ├── manager/ (Logique mĂ©tier et services) + │ ├── GuildManager.java + │ └── InviteManager.java + ├── listener/ (Interception des Ă©vĂ©nements et protection) + │ └── TerritoryProtectionListener.java + └── util/ (Utilitaires de texte et formatage) + └── TextUtil.java +``` diff --git a/src/main/java/fr/luc/gamingcore/Main.java b/src/main/java/fr/luc/gamingcore/Main.java index e5606f4..7d01c54 100644 --- a/src/main/java/fr/luc/gamingcore/Main.java +++ b/src/main/java/fr/luc/gamingcore/Main.java @@ -2,66 +2,105 @@ package fr.luc.gamingcore; import com.sk89q.worldguard.WorldGuard; import com.sk89q.worldguard.protection.regions.RegionContainer; -import fr.luc.gamingcore.command.CommandManager; +import fr.luc.gamingcore.database.DatabaseManager; +import fr.luc.gamingcore.database.GuildRepository; +import fr.luc.gamingcore.economy.EconomyManager; +import fr.luc.gamingcore.listener.TerritoryProtectionListener; +import fr.luc.gamingcore.manager.GuildManager; import fr.luc.gamingcore.util.TextUtil; import net.milkbowl.vault.economy.Economy; +import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.java.JavaPlugin; +/** + * Classe principale et point d'entrĂ©e du plugin GamingCore (SystĂšme de Guilde). + */ public final class Main extends JavaPlugin { private static Main instance; private Economy economy; private RegionContainer regionContainer; - private CommandManager commandManager; + private DatabaseManager databaseManager; + private GuildRepository guildRepository; + private EconomyManager economyManager; + private GuildManager guildManager; + + /** + * MĂ©thode appelĂ©e lors de l'activation du plugin par le serveur Minecraft. + */ @Override public void onEnable() { instance = this; saveDefaultConfig(); - // 1. Initialiser le prĂ©fixe de messages MiniMessage + // 1. Initialiser le prĂ©fixe de texte MiniMessage TextUtil.setPrefix(getConfig().getString("prefix", "[Guilde] » ")); - // 2. Connexion Ă  WorldGuard + // 2. Initialiser les hooks externes setupWorldGuard(); - - // 3. Connexion Ă  Vault (Économie) setupEconomy(); - // 4. Initialisation du CommandManager dynamique - this.commandManager = new CommandManager(this); + // 3. Initialiser la couche de donnĂ©es SQLite + this.databaseManager = new DatabaseManager(this); + this.databaseManager.initialize(); + this.guildRepository = new GuildRepository(this, databaseManager); - getLogger().info("GamingCore (SystĂšme de Guilde) a ete active avec succes !"); + // 4. Initialiser la couche Ă©conomique + this.economyManager = new EconomyManager(this); + + // 5. Initialiser le service mĂ©tier GuildManager et charger les donnĂ©es + this.guildManager = new GuildManager(this, guildRepository, economyManager); + this.guildManager.loadData(); + + // 6. Enregistrer les Ă©couteurs d'Ă©vĂ©nements + PluginManager pm = getServer().getPluginManager(); + pm.registerEvents(new TerritoryProtectionListener(guildManager), this); + + getLogger().info("GamingCore (SystĂšme de Guilde - CƓur MĂ©tier) a ete active avec succes !"); } + /** + * MĂ©thode appelĂ©e lors de la dĂ©sactivation du plugin. + */ @Override public void onDisable() { - if (commandManager != null) { - commandManager.shutdown(); + if (guildManager != null) { + guildManager.shutdown(); } + if (databaseManager != null) { + databaseManager.close(); + } + getLogger().info("GamingCore a ete desactive !"); } + /** + * Initialise la connexion vers l'API WorldGuard 7 si disponible. + */ private void setupWorldGuard() { try { this.regionContainer = WorldGuard.getInstance().getPlatform().getRegionContainer(); getLogger().info("Connexion avec WorldGuard 7 etablie avec succes !"); } catch (Throwable t) { - getLogger().warning("Impossible d'initialiser WorldGuard : " + t.getMessage()); + getLogger().warning("WorldGuard non disponible ou erreur d'initialisation : " + t.getMessage()); } } + /** + * Enregistre le fournisseur d'Ă©conomie Vault. + */ private void setupEconomy() { if (getServer().getPluginManager().getPlugin("Vault") == null) { - getLogger().warning("Vault n'a pas ete detecte ! Le support economique sera desactive."); + getLogger().warning("Vault n'a pas ete detecte ! Les fonctionnalites payantes seront desactivees."); return; } RegisteredServiceProvider rsp = getServer().getServicesManager().getRegistration(Economy.class); if (rsp == null) { - getLogger().warning("Aucun provider d'economie (ex: Essentials, TheNewEconomy) trouve via Vault !"); + getLogger().warning("Aucun provider d'economie trouve via Vault (ex: EssentialsX, TheNewEconomy) !"); return; } @@ -69,23 +108,75 @@ public final class Main extends JavaPlugin { getLogger().info("Connexion avec l'economie Vault (" + economy.getName() + ") etablie avec succes !"); } + /** + * Obtient l'instance Singleton de la classe principale. + * + * @return L'instance unique de Main. + */ public static Main getInstance() { return instance; } + /** + * Obtient le fournisseur d'Ă©conomie Vault. + * + * @return L'instance Economy. + */ public Economy getEconomy() { return economy; } + /** + * Indique si un service d'Ă©conomie est actif. + * + * @return true si economy != null. + */ public boolean hasEconomy() { return economy != null; } + /** + * Obtient le conteneur de rĂ©gions WorldGuard. + * + * @return L'instance RegionContainer. + */ public RegionContainer getRegionContainer() { return regionContainer; } - public CommandManager getCommandManager() { - return commandManager; + /** + * Obtient le gestionnaire de base de donnĂ©es. + * + * @return L'instance DatabaseManager. + */ + public DatabaseManager getDatabaseManager() { + return databaseManager; + } + + /** + * Obtient le rĂ©fĂ©rentiel de persistance des guildes. + * + * @return L'instance GuildRepository. + */ + public GuildRepository getGuildRepository() { + return guildRepository; + } + + /** + * Obtient le gestionnaire d'Ă©conomie interne. + * + * @return L'instance EconomyManager. + */ + public EconomyManager getEconomyManager() { + return economyManager; + } + + /** + * Obtient le service mĂ©tier central de gestion des guildes. + * + * @return L'instance GuildManager. + */ + public GuildManager getGuildManager() { + return guildManager; } } diff --git a/src/main/java/fr/luc/gamingcore/command/AppCommand.java b/src/main/java/fr/luc/gamingcore/command/AppCommand.java deleted file mode 100644 index e3abc3a..0000000 --- a/src/main/java/fr/luc/gamingcore/command/AppCommand.java +++ /dev/null @@ -1,262 +0,0 @@ -package fr.luc.gamingcore.command; - -import fr.luc.gamingcore.command.argument.ArgumentParseException; -import fr.luc.gamingcore.command.argument.CommandArgument; -import fr.luc.gamingcore.command.cooldown.CooldownManager; -import fr.luc.gamingcore.util.TextUtil; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import java.util.*; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -public class AppCommand { - - private static final Logger LOGGER = Logger.getLogger("GamingCore"); - - private final String name; - private final List aliases; - private final String description; - private final String permission; - private final String permissionMessage; - private final boolean playerOnly; - private final long cooldownMillis; - private final String cooldownBypassPermission; - private final List> arguments; - private final Map subCommands; - private final CommandHandler handler; - - public AppCommand(String name, List aliases, String description, String permission, - String permissionMessage, boolean playerOnly, long cooldownMillis, - String cooldownBypassPermission, List> arguments, - Map subCommands, CommandHandler handler) { - this.name = name.toLowerCase(); - this.aliases = aliases != null ? aliases : Collections.emptyList(); - this.description = description != null ? description : ""; - this.permission = permission; - this.permissionMessage = permissionMessage != null ? permissionMessage : "Tu n'as pas la permission d'exĂ©cuter cette commande."; - this.playerOnly = playerOnly; - this.cooldownMillis = cooldownMillis; - this.cooldownBypassPermission = cooldownBypassPermission != null ? cooldownBypassPermission : "gamingcore.cooldown.bypass"; - this.arguments = arguments != null ? arguments : Collections.emptyList(); - this.subCommands = subCommands != null ? subCommands : Collections.emptyMap(); - this.handler = handler; - } - - public String getName() { - return name; - } - - public List getAliases() { - return aliases; - } - - public String getDescription() { - return description; - } - - public String getPermission() { - return permission; - } - - public boolean isPlayerOnly() { - return playerOnly; - } - - public long getCooldownMillis() { - return cooldownMillis; - } - - public List> getArguments() { - return arguments; - } - - public Map getSubCommands() { - return subCommands; - } - - public AppCommand getSubCommand(String name) { - if (name == null) return null; - String lower = name.toLowerCase(); - AppCommand direct = subCommands.get(lower); - if (direct != null) return direct; - for (AppCommand sub : subCommands.values()) { - if (sub.getAliases().stream().anyMatch(a -> a.equalsIgnoreCase(lower))) { - return sub; - } - } - return null; - } - - public void execute(CommandSender sender, String label, String[] args, CooldownManager cooldownManager, String currentPath) { - String fullPath = currentPath.isEmpty() ? name : currentPath + " " + name; - - // 1. Permission check - if (permission != null && !permission.isEmpty() && !sender.hasPermission(permission)) { - TextUtil.sendMessage(sender, permissionMessage); - return; - } - - // 2. Player-only check - if (playerOnly && !(sender instanceof Player)) { - sender.sendMessage("Cette commande doit ĂȘtre exĂ©cutĂ©e par un joueur."); - return; - } - - // 3. Sub-command check - if (args.length > 0) { - AppCommand sub = getSubCommand(args[0]); - if (sub != null) { - String[] nextArgs = Arrays.copyOfRange(args, 1, args.length); - sub.execute(sender, args[0], nextArgs, cooldownManager, fullPath); - return; - } - } - - // 4. If no sub-command matched but handler is null, show help - if (handler == null) { - sendHelp(sender, fullPath); - return; - } - - Player player = sender instanceof Player ? (Player) sender : null; - - // 5. Cooldown check - if (player != null && cooldownMillis > 0 && !player.hasPermission(cooldownBypassPermission)) { - String cooldownKey = fullPath; - if (cooldownManager.isOnCooldown(player, cooldownKey)) { - double remaining = cooldownManager.getRemainingSeconds(player, cooldownKey); - TextUtil.sendMessage(player, "⏳ Veuillez patienter " + remaining + "s avant de rĂ©utiliser cette commande."); - return; - } - } - - // 6. Argument parsing - Map parsedArgs = new HashMap<>(); - CommandContext preliminaryContext = new CommandContext(sender, label, args, parsedArgs, fullPath); - - int argIndex = 0; - for (int i = 0; i < arguments.size(); i++) { - CommandArgument argument = arguments.get(i); - - if (argIndex >= args.length) { - if (argument.isRequired()) { - TextUtil.sendMessage(sender, "Syntaxe incorrecte : /" + fullPath + " " + getSyntax() + ""); - return; - } else { - parsedArgs.put(argument.getName().toLowerCase(), argument.getDefaultValue()); - continue; - } - } - - String inputString; - if (argument.isGreedy()) { - // Consume all remaining arguments - inputString = String.join(" ", Arrays.copyOfRange(args, argIndex, args.length)); - argIndex = args.length; - } else { - inputString = args[argIndex]; - argIndex++; - } - - try { - Object parsed = argument.getParser().parse(preliminaryContext, inputString); - parsedArgs.put(argument.getName().toLowerCase(), parsed); - } catch (ArgumentParseException e) { - TextUtil.sendMessage(sender, "" + e.getMessage() + ""); - return; - } - } - - // 7. Execution - CommandContext finalContext = new CommandContext(sender, label, args, parsedArgs, fullPath); - try { - handler.execute(finalContext); - - // Apply cooldown on success - if (player != null && cooldownMillis > 0 && !player.hasPermission(cooldownBypassPermission)) { - cooldownManager.applyCooldown(player, fullPath, cooldownMillis); - } - } catch (Exception e) { - TextUtil.sendMessage(sender, "Une erreur est survenue lors de l'exĂ©cution de la commande."); - LOGGER.log(Level.SEVERE, "Erreur lors de l'exĂ©cution de la commande " + fullPath, e); - } - } - - public List tabComplete(CommandSender sender, String label, String[] args, String currentPath) { - String fullPath = currentPath.isEmpty() ? name : currentPath + " " + name; - - if (permission != null && !permission.isEmpty() && !sender.hasPermission(permission)) { - return Collections.emptyList(); - } - - if (args.length == 0) { - return Collections.emptyList(); - } - - if (args.length == 1) { - String prefix = args[0].toLowerCase(); - List suggestions = new ArrayList<>(); - - // Suggest subcommands - for (AppCommand sub : subCommands.values()) { - if (sub.getPermission() == null || sub.getPermission().isEmpty() || sender.hasPermission(sub.getPermission())) { - if (sub.getName().toLowerCase().startsWith(prefix)) { - suggestions.add(sub.getName()); - } - } - } - - // Also check first argument completions if applicable - if (!arguments.isEmpty()) { - CommandContext ctx = new CommandContext(sender, label, args, Collections.emptyMap(), fullPath); - suggestions.addAll(arguments.get(0).getParser().complete(ctx, args[0])); - } - - return suggestions; - } - - // Check if first arg matches a subcommand - AppCommand sub = getSubCommand(args[0]); - if (sub != null) { - String[] nextArgs = Arrays.copyOfRange(args, 1, args.length); - return sub.tabComplete(sender, args[0], nextArgs, fullPath); - } - - // Argument completions - int targetArgIndex = args.length - 1; - if (targetArgIndex < arguments.size()) { - CommandArgument argument = arguments.get(targetArgIndex); - CommandContext ctx = new CommandContext(sender, label, args, Collections.emptyMap(), fullPath); - return argument.getParser().complete(ctx, args[targetArgIndex]); - } - - return Collections.emptyList(); - } - - public String getSyntax() { - if (arguments.isEmpty()) return ""; - return arguments.stream() - .map(CommandArgument::getFormattedSyntax) - .collect(Collectors.joining(" ")); - } - - public void sendHelp(CommandSender sender, String fullPath) { - TextUtil.sendRawMessage(sender, "═══════════ [ " + fullPath.toUpperCase() + " ] ═══════════"); - if (!description.isEmpty()) { - TextUtil.sendRawMessage(sender, " " + description + ""); - } - if (!subCommands.isEmpty()) { - for (AppCommand sub : subCommands.values()) { - if (sub.getPermission() == null || sub.getPermission().isEmpty() || sender.hasPermission(sub.getPermission())) { - String subSyntax = sub.getSyntax(); - String syntaxPart = subSyntax.isEmpty() ? "" : " " + subSyntax; - TextUtil.sendRawMessage(sender, " /" + fullPath + " " + sub.getName() + syntaxPart + " - " + sub.getDescription() + ""); - } - } - } - TextUtil.sendRawMessage(sender, "════════════════════════════════════════"); - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/BukkitCommandWrapper.java b/src/main/java/fr/luc/gamingcore/command/BukkitCommandWrapper.java deleted file mode 100644 index ab957f8..0000000 --- a/src/main/java/fr/luc/gamingcore/command/BukkitCommandWrapper.java +++ /dev/null @@ -1,37 +0,0 @@ -package fr.luc.gamingcore.command; - -import fr.luc.gamingcore.command.cooldown.CooldownManager; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; - -import java.util.List; - -public class BukkitCommandWrapper extends Command { - - private final AppCommand appCommand; - private final CooldownManager cooldownManager; - - public BukkitCommandWrapper(AppCommand appCommand, CooldownManager cooldownManager) { - super(appCommand.getName(), appCommand.getDescription(), "/" + appCommand.getName(), appCommand.getAliases()); - this.appCommand = appCommand; - this.cooldownManager = cooldownManager; - if (appCommand.getPermission() != null && !appCommand.getPermission().isEmpty()) { - setPermission(appCommand.getPermission()); - } - } - - @Override - public boolean execute(CommandSender sender, String commandLabel, String[] args) { - appCommand.execute(sender, commandLabel, args, cooldownManager, ""); - return true; - } - - @Override - public List tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException { - return appCommand.tabComplete(sender, alias, args, ""); - } - - public AppCommand getAppCommand() { - return appCommand; - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/CommandBuilder.java b/src/main/java/fr/luc/gamingcore/command/CommandBuilder.java deleted file mode 100644 index 4c835f7..0000000 --- a/src/main/java/fr/luc/gamingcore/command/CommandBuilder.java +++ /dev/null @@ -1,107 +0,0 @@ -package fr.luc.gamingcore.command; - -import fr.luc.gamingcore.command.argument.CommandArgument; - -import java.util.*; -import java.util.concurrent.TimeUnit; - -public class CommandBuilder { - - private final String name; - private final List aliases = new ArrayList<>(); - private String description = ""; - private String permission = null; - private String permissionMessage = null; - private boolean playerOnly = false; - private long cooldownMillis = 0L; - private String cooldownBypassPermission = "gamingcore.cooldown.bypass"; - private final List> arguments = new ArrayList<>(); - private final Map subCommands = new LinkedHashMap<>(); - private CommandHandler handler = null; - - private CommandBuilder(String name) { - this.name = Objects.requireNonNull(name, "Command name cannot be null").trim(); - } - - public static CommandBuilder create(String name) { - return new CommandBuilder(name); - } - - public CommandBuilder aliases(String... aliases) { - if (aliases != null) { - this.aliases.addAll(Arrays.asList(aliases)); - } - return this; - } - - public CommandBuilder description(String description) { - this.description = description != null ? description : ""; - return this; - } - - public CommandBuilder permission(String permission) { - this.permission = permission; - return this; - } - - public CommandBuilder permissionMessage(String message) { - this.permissionMessage = message; - return this; - } - - public CommandBuilder playerOnly() { - this.playerOnly = true; - return this; - } - - public CommandBuilder playerOnly(boolean playerOnly) { - this.playerOnly = playerOnly; - return this; - } - - public CommandBuilder cooldown(long amount, TimeUnit unit) { - this.cooldownMillis = unit.toMillis(amount); - return this; - } - - public CommandBuilder cooldownSeconds(int seconds) { - return cooldown(seconds, TimeUnit.SECONDS); - } - - public CommandBuilder cooldownBypass(String permission) { - this.cooldownBypassPermission = permission; - return this; - } - - public CommandBuilder argument(CommandArgument argument) { - if (argument != null) { - this.arguments.add(argument); - } - return this; - } - - public CommandBuilder subCommand(AppCommand subCommand) { - if (subCommand != null) { - this.subCommands.put(subCommand.getName().toLowerCase(), subCommand); - } - return this; - } - - public CommandBuilder subCommand(CommandBuilder subBuilder) { - if (subBuilder != null) { - AppCommand built = subBuilder.build(); - this.subCommands.put(built.getName().toLowerCase(), built); - } - return this; - } - - public CommandBuilder executes(CommandHandler handler) { - this.handler = handler; - return this; - } - - public AppCommand build() { - return new AppCommand(name, aliases, description, permission, permissionMessage, - playerOnly, cooldownMillis, cooldownBypassPermission, arguments, subCommands, handler); - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/CommandContext.java b/src/main/java/fr/luc/gamingcore/command/CommandContext.java deleted file mode 100644 index 083df8d..0000000 --- a/src/main/java/fr/luc/gamingcore/command/CommandContext.java +++ /dev/null @@ -1,109 +0,0 @@ -package fr.luc.gamingcore.command; - -import fr.luc.gamingcore.util.TextUtil; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import java.util.Collections; -import java.util.Map; - -public class CommandContext { - - private final CommandSender sender; - private final String label; - private final String[] rawArgs; - private final Map parsedArgs; - private final String fullCommandPath; - - public CommandContext(CommandSender sender, String label, String[] rawArgs, Map parsedArgs, String fullCommandPath) { - this.sender = sender; - this.label = label; - this.rawArgs = rawArgs != null ? rawArgs : new String[0]; - this.parsedArgs = parsedArgs != null ? parsedArgs : Collections.emptyMap(); - this.fullCommandPath = fullCommandPath; - } - - public CommandSender getSender() { - return sender; - } - - public boolean isPlayer() { - return sender instanceof Player; - } - - public boolean isConsole() { - return !isPlayer(); - } - - public Player getPlayer() { - return isPlayer() ? (Player) sender : null; - } - - public String getLabel() { - return label; - } - - public String[] getRawArgs() { - return rawArgs; - } - - public String getFullCommandPath() { - return fullCommandPath; - } - - public boolean has(String argName) { - return parsedArgs.containsKey(argName.toLowerCase()); - } - - @SuppressWarnings("unchecked") - public T get(String argName, Class clazz) { - Object value = parsedArgs.get(argName.toLowerCase()); - if (value == null) { - return null; - } - if (clazz.isInstance(value)) { - return (T) value; - } - throw new ClassCastException("L'argument '" + argName + "' est de type " + value.getClass().getSimpleName() + ", attendu: " + clazz.getSimpleName()); - } - - public Object get(String argName) { - return parsedArgs.get(argName.toLowerCase()); - } - - public String getString(String argName) { - return get(argName, String.class); - } - - public int getInt(String argName) { - Integer val = get(argName, Integer.class); - return val != null ? val : 0; - } - - public double getDouble(String argName) { - Double val = get(argName, Double.class); - return val != null ? val : 0.0; - } - - public Player getTargetPlayer(String argName) { - return get(argName, Player.class); - } - - public void reply(String message) { - TextUtil.sendMessage(sender, message); - } - - public void sendRaw(String message) { - TextUtil.sendRawMessage(sender, message); - } - - public void error(String errorMessage) { - TextUtil.sendMessage(sender, "" + errorMessage + ""); - } - - public void sendActionBar(String message) { - if (isPlayer()) { - TextUtil.sendActionBar(getPlayer(), message); - } - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/CommandHandler.java b/src/main/java/fr/luc/gamingcore/command/CommandHandler.java deleted file mode 100644 index 2e5cbc2..0000000 --- a/src/main/java/fr/luc/gamingcore/command/CommandHandler.java +++ /dev/null @@ -1,6 +0,0 @@ -package fr.luc.gamingcore.command; - -@FunctionalInterface -public interface CommandHandler { - void execute(CommandContext context) throws Exception; -} diff --git a/src/main/java/fr/luc/gamingcore/command/CommandManager.java b/src/main/java/fr/luc/gamingcore/command/CommandManager.java deleted file mode 100644 index 44e5a7d..0000000 --- a/src/main/java/fr/luc/gamingcore/command/CommandManager.java +++ /dev/null @@ -1,101 +0,0 @@ -package fr.luc.gamingcore.command; - -import fr.luc.gamingcore.Main; -import fr.luc.gamingcore.command.cooldown.CooldownManager; -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandMap; -import org.bukkit.command.SimpleCommandMap; -import org.bukkit.plugin.SimplePluginManager; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; - -public class CommandManager { - - private final Main plugin; - private final CooldownManager cooldownManager; - private final Map registeredCommands = new ConcurrentHashMap<>(); - private CommandMap commandMap; - - public CommandManager(Main plugin) { - this.plugin = plugin; - this.cooldownManager = new CooldownManager(); - this.commandMap = resolveCommandMap(); - } - - private CommandMap resolveCommandMap() { - // Try direct Bukkit.getCommandMap() (Paper API) - try { - Method method = Bukkit.getServer().getClass().getMethod("getCommandMap"); - return (CommandMap) method.invoke(Bukkit.getServer()); - } catch (Exception ignored) {} - - // Fallback: Reflection on SimplePluginManager - try { - Field field = SimplePluginManager.class.getDeclaredField("commandMap"); - field.setAccessible(true); - return (CommandMap) field.get(Bukkit.getPluginManager()); - } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Impossible de recuperer le CommandMap de Bukkit !", e); - return null; - } - } - - public void register(AppCommand appCommand) { - if (commandMap == null) { - commandMap = resolveCommandMap(); - if (commandMap == null) { - plugin.getLogger().severe("Impossible d'enregistrer la commande " + appCommand.getName() + " : CommandMap introuvable !"); - return; - } - } - - BukkitCommandWrapper wrapper = new BukkitCommandWrapper(appCommand, cooldownManager); - commandMap.register(plugin.getName().toLowerCase(), wrapper); - registeredCommands.put(appCommand.getName().toLowerCase(), wrapper); - - plugin.getLogger().info("Commande dynamique enregistree : /" + appCommand.getName() + " (alias: " + String.join(", ", appCommand.getAliases()) + ")"); - } - - public void register(CommandBuilder builder) { - if (builder != null) { - register(builder.build()); - } - } - - public void unregister(String commandName) { - if (commandName == null) return; - BukkitCommandWrapper wrapper = registeredCommands.remove(commandName.toLowerCase()); - if (wrapper != null && commandMap instanceof SimpleCommandMap) { - try { - Field knownCommandsField = SimpleCommandMap.class.getDeclaredField("knownCommands"); - knownCommandsField.setAccessible(true); - @SuppressWarnings("unchecked") - Map knownCommands = (Map) knownCommandsField.get(commandMap); - knownCommands.remove(wrapper.getName().toLowerCase()); - for (String alias : wrapper.getAliases()) { - knownCommands.remove(alias.toLowerCase()); - } - wrapper.unregister(commandMap); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Erreur lors du desenregistrement de la commande " + commandName, e); - } - } - } - - public void shutdown() { - for (String name : registeredCommands.keySet()) { - unregister(name); - } - registeredCommands.clear(); - cooldownManager.cleanExpired(); - } - - public CooldownManager getCooldownManager() { - return cooldownManager; - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/argument/ArgumentParseException.java b/src/main/java/fr/luc/gamingcore/command/argument/ArgumentParseException.java deleted file mode 100644 index 1dd7b51..0000000 --- a/src/main/java/fr/luc/gamingcore/command/argument/ArgumentParseException.java +++ /dev/null @@ -1,8 +0,0 @@ -package fr.luc.gamingcore.command.argument; - -public class ArgumentParseException extends Exception { - - public ArgumentParseException(String message) { - super(message); - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/argument/ArgumentParser.java b/src/main/java/fr/luc/gamingcore/command/argument/ArgumentParser.java deleted file mode 100644 index 28a591c..0000000 --- a/src/main/java/fr/luc/gamingcore/command/argument/ArgumentParser.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.luc.gamingcore.command.argument; - -import fr.luc.gamingcore.command.CommandContext; - -import java.util.List; - -@FunctionalInterface -public interface ArgumentParser { - - /** - * Parses the string input into the target typed object. - * - * @param ctx The current command execution context - * @param input The raw argument string entered by the user - * @return The parsed object - * @throws ArgumentParseException If the input is invalid - */ - T parse(CommandContext ctx, String input) throws ArgumentParseException; - - /** - * Provides dynamic tab completions for this argument. - * - * @param ctx The current command context - * @param input The current prefix typed by the user - * @return A list of suggestions - */ - default List complete(CommandContext ctx, String input) { - return List.of(); - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/argument/CommandArgument.java b/src/main/java/fr/luc/gamingcore/command/argument/CommandArgument.java deleted file mode 100644 index 6513730..0000000 --- a/src/main/java/fr/luc/gamingcore/command/argument/CommandArgument.java +++ /dev/null @@ -1,72 +0,0 @@ -package fr.luc.gamingcore.command.argument; - -public class CommandArgument { - - private final String name; - private final String description; - private final ArgumentParser parser; - private final boolean required; - private final T defaultValue; - private final boolean greedy; - - public CommandArgument(String name, String description, ArgumentParser parser, boolean required, T defaultValue, boolean greedy) { - this.name = name; - this.description = description; - this.parser = parser; - this.required = required; - this.defaultValue = defaultValue; - this.greedy = greedy; - } - - public static CommandArgument required(String name, ArgumentParser parser) { - return new CommandArgument<>(name, "", parser, true, null, false); - } - - public static CommandArgument required(String name, String description, ArgumentParser parser) { - return new CommandArgument<>(name, description, parser, true, null, false); - } - - public static CommandArgument optional(String name, ArgumentParser parser) { - return new CommandArgument<>(name, "", parser, false, null, false); - } - - public static CommandArgument optional(String name, ArgumentParser parser, T defaultValue) { - return new CommandArgument<>(name, "", parser, false, defaultValue, false); - } - - public static CommandArgument optional(String name, String description, ArgumentParser parser, T defaultValue) { - return new CommandArgument<>(name, description, parser, false, defaultValue, false); - } - - public static CommandArgument greedy(String name, String description, ArgumentParser parser, boolean required, T defaultValue) { - return new CommandArgument<>(name, description, parser, required, defaultValue, true); - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public ArgumentParser getParser() { - return parser; - } - - public boolean isRequired() { - return required; - } - - public T getDefaultValue() { - return defaultValue; - } - - public boolean isGreedy() { - return greedy; - } - - public String getFormattedSyntax() { - return required ? "<" + name + ">" : "[" + name + "]"; - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/argument/parsers/DoubleArgument.java b/src/main/java/fr/luc/gamingcore/command/argument/parsers/DoubleArgument.java deleted file mode 100644 index 22b2678..0000000 --- a/src/main/java/fr/luc/gamingcore/command/argument/parsers/DoubleArgument.java +++ /dev/null @@ -1,52 +0,0 @@ -package fr.luc.gamingcore.command.argument.parsers; - -import fr.luc.gamingcore.command.CommandContext; -import fr.luc.gamingcore.command.argument.ArgumentParser; -import fr.luc.gamingcore.command.argument.ArgumentParseException; - -import java.util.List; - -public class DoubleArgument implements ArgumentParser { - - private final double min; - private final double max; - - public DoubleArgument() { - this(Double.MIN_VALUE, Double.MAX_VALUE); - } - - public DoubleArgument(double min, double max) { - this.min = min; - this.max = max; - } - - public static DoubleArgument decimal() { - return new DoubleArgument(); - } - - public static DoubleArgument min(double min) { - return new DoubleArgument(min, Double.MAX_VALUE); - } - - public static DoubleArgument range(double min, double max) { - return new DoubleArgument(min, max); - } - - @Override - public Double parse(CommandContext ctx, String input) throws ArgumentParseException { - try { - double value = Double.parseDouble(input.replace(',', '.')); - if (value < min || value > max) { - throw new ArgumentParseException("La valeur doit ĂȘtre comprise entre " + min + " et " + max + "."); - } - return value; - } catch (NumberFormatException e) { - throw new ArgumentParseException("'" + input + "' n'est pas un nombre dĂ©cimal valide."); - } - } - - @Override - public List complete(CommandContext ctx, String input) { - return List.of(); - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/argument/parsers/IntegerArgument.java b/src/main/java/fr/luc/gamingcore/command/argument/parsers/IntegerArgument.java deleted file mode 100644 index a0f87e6..0000000 --- a/src/main/java/fr/luc/gamingcore/command/argument/parsers/IntegerArgument.java +++ /dev/null @@ -1,52 +0,0 @@ -package fr.luc.gamingcore.command.argument.parsers; - -import fr.luc.gamingcore.command.CommandContext; -import fr.luc.gamingcore.command.argument.ArgumentParser; -import fr.luc.gamingcore.command.argument.ArgumentParseException; - -import java.util.List; - -public class IntegerArgument implements ArgumentParser { - - private final int min; - private final int max; - - public IntegerArgument() { - this(Integer.MIN_VALUE, Integer.MAX_VALUE); - } - - public IntegerArgument(int min, int max) { - this.min = min; - this.max = max; - } - - public static IntegerArgument integer() { - return new IntegerArgument(); - } - - public static IntegerArgument min(int min) { - return new IntegerArgument(min, Integer.MAX_VALUE); - } - - public static IntegerArgument range(int min, int max) { - return new IntegerArgument(min, max); - } - - @Override - public Integer parse(CommandContext ctx, String input) throws ArgumentParseException { - try { - int value = Integer.parseInt(input); - if (value < min || value > max) { - throw new ArgumentParseException("Le nombre doit ĂȘtre compris entre " + min + " et " + max + "."); - } - return value; - } catch (NumberFormatException e) { - throw new ArgumentParseException("'" + input + "' n'est pas un nombre entier valide."); - } - } - - @Override - public List complete(CommandContext ctx, String input) { - return List.of(); - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/argument/parsers/PlayerArgument.java b/src/main/java/fr/luc/gamingcore/command/argument/parsers/PlayerArgument.java deleted file mode 100644 index 16a011e..0000000 --- a/src/main/java/fr/luc/gamingcore/command/argument/parsers/PlayerArgument.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.luc.gamingcore.command.argument.parsers; - -import fr.luc.gamingcore.command.CommandContext; -import fr.luc.gamingcore.command.argument.ArgumentParser; -import fr.luc.gamingcore.command.argument.ArgumentParseException; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.List; -import java.util.stream.Collectors; - -public class PlayerArgument implements ArgumentParser { - - public static PlayerArgument player() { - return new PlayerArgument(); - } - - @Override - public Player parse(CommandContext ctx, String input) throws ArgumentParseException { - if (input == null || input.isEmpty()) { - throw new ArgumentParseException("Veuillez spĂ©cifier un pseudo de joueur."); - } - Player target = Bukkit.getPlayerExact(input); - if (target == null) { - target = Bukkit.getPlayer(input); - } - if (target == null || !target.isOnline()) { - throw new ArgumentParseException("Le joueur '" + input + "' n'est pas connectĂ©."); - } - return target; - } - - @Override - public List complete(CommandContext ctx, String input) { - String lower = input != null ? input.toLowerCase() : ""; - return Bukkit.getOnlinePlayers().stream() - .map(Player::getName) - .filter(name -> name.toLowerCase().startsWith(lower)) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/argument/parsers/StringArgument.java b/src/main/java/fr/luc/gamingcore/command/argument/parsers/StringArgument.java deleted file mode 100644 index 047635b..0000000 --- a/src/main/java/fr/luc/gamingcore/command/argument/parsers/StringArgument.java +++ /dev/null @@ -1,66 +0,0 @@ -package fr.luc.gamingcore.command.argument.parsers; - -import fr.luc.gamingcore.command.CommandContext; -import fr.luc.gamingcore.command.argument.ArgumentParser; -import fr.luc.gamingcore.command.argument.ArgumentParseException; - -import java.util.Arrays; -import java.util.List; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -public class StringArgument implements ArgumentParser { - - private final List suggestions; - private final Supplier> dynamicSuggestions; - - public StringArgument() { - this.suggestions = null; - this.dynamicSuggestions = null; - } - - public StringArgument(String... suggestions) { - this.suggestions = Arrays.asList(suggestions); - this.dynamicSuggestions = null; - } - - public StringArgument(Supplier> dynamicSuggestions) { - this.suggestions = null; - this.dynamicSuggestions = dynamicSuggestions; - } - - public static StringArgument word() { - return new StringArgument(); - } - - public static StringArgument choices(String... choices) { - return new StringArgument(choices); - } - - public static StringArgument dynamic(Supplier> supplier) { - return new StringArgument(supplier); - } - - @Override - public String parse(CommandContext ctx, String input) throws ArgumentParseException { - if (input == null || input.trim().isEmpty()) { - throw new ArgumentParseException("Le texte ne peut pas ĂȘtre vide."); - } - return input; - } - - @Override - public List complete(CommandContext ctx, String input) { - List list = suggestions; - if (dynamicSuggestions != null) { - list = dynamicSuggestions.get(); - } - if (list == null || list.isEmpty()) { - return List.of(); - } - String lower = input != null ? input.toLowerCase() : ""; - return list.stream() - .filter(s -> s.toLowerCase().startsWith(lower)) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/fr/luc/gamingcore/command/cooldown/CooldownManager.java b/src/main/java/fr/luc/gamingcore/command/cooldown/CooldownManager.java deleted file mode 100644 index 0649686..0000000 --- a/src/main/java/fr/luc/gamingcore/command/cooldown/CooldownManager.java +++ /dev/null @@ -1,62 +0,0 @@ -package fr.luc.gamingcore.command.cooldown; - -import org.bukkit.entity.Player; - -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -public class CooldownManager { - - private final Map> cooldowns = new ConcurrentHashMap<>(); - - public boolean isOnCooldown(Player player, String commandKey) { - if (player == null || commandKey == null) return false; - long remaining = getRemainingMillis(player, commandKey); - return remaining > 0; - } - - public long getRemainingMillis(Player player, String commandKey) { - if (player == null || commandKey == null) return 0L; - Map userCooldowns = cooldowns.get(player.getUniqueId()); - if (userCooldowns == null) return 0L; - - Long expireAt = userCooldowns.get(commandKey.toLowerCase()); - if (expireAt == null) return 0L; - - long diff = expireAt - System.currentTimeMillis(); - if (diff <= 0) { - userCooldowns.remove(commandKey.toLowerCase()); - return 0L; - } - return diff; - } - - public double getRemainingSeconds(Player player, String commandKey) { - long millis = getRemainingMillis(player, commandKey); - if (millis <= 0) return 0.0; - return Math.round((millis / 1000.0) * 10.0) / 10.0; - } - - public void applyCooldown(Player player, String commandKey, long durationMillis) { - if (player == null || commandKey == null || durationMillis <= 0) return; - cooldowns.computeIfAbsent(player.getUniqueId(), k -> new ConcurrentHashMap<>()) - .put(commandKey.toLowerCase(), System.currentTimeMillis() + durationMillis); - } - - public void clearCooldown(Player player, String commandKey) { - if (player == null || commandKey == null) return; - Map userCooldowns = cooldowns.get(player.getUniqueId()); - if (userCooldowns != null) { - userCooldowns.remove(commandKey.toLowerCase()); - } - } - - public void cleanExpired() { - long now = System.currentTimeMillis(); - cooldowns.forEach((uuid, map) -> { - map.entrySet().removeIf(entry -> entry.getValue() <= now); - }); - cooldowns.entrySet().removeIf(entry -> entry.getValue().isEmpty()); - } -} diff --git a/src/main/java/fr/luc/gamingcore/database/DatabaseManager.java b/src/main/java/fr/luc/gamingcore/database/DatabaseManager.java new file mode 100644 index 0000000..e4f97ba --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/database/DatabaseManager.java @@ -0,0 +1,150 @@ +package fr.luc.gamingcore.database; + +import fr.luc.gamingcore.Main; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Level; + +/** + * Gestionnaire de la connexion et de l'initialisation du schĂ©ma de base de donnĂ©es SQLite. + */ +public class DatabaseManager { + + private final Main plugin; + private final File dbFile; + private Connection connection; + + /** + * Constructeur du DatabaseManager. + * + * @param plugin Instance principale du plugin. + */ + public DatabaseManager(Main plugin) { + this.plugin = plugin; + this.dbFile = new File(plugin.getDataFolder(), "guilds.db"); + } + + /** + * Initialise le dossier de donnĂ©es, ouvre la connexion SQLite et crĂ©e les tables si nĂ©cessaires. + */ + public synchronized void initialize() { + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + try { + connect(); + createTables(); + plugin.getLogger().info("Base de donnees SQLite des guildes initialisee avec succes !"); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur fatale lors de l'initialisation de la base SQLite des guildes !", e); + } + } + + /** + * Établit la connexion JDBC vers le fichier SQLite local. + * + * @throws SQLException Si la connexion Ă©choue. + */ + private synchronized void connect() throws SQLException { + if (connection != null && !connection.isClosed()) { + return; + } + + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException ignored) {} + + String url = "jdbc:sqlite:" + dbFile.getAbsolutePath(); + this.connection = DriverManager.getConnection(url); + } + + /** + * Obtient la connexion active vers la base de donnĂ©es. + * + * @return L'instance Connection active. + * @throws SQLException Si la rĂ©ouverture de connexion Ă©choue. + */ + public synchronized Connection getConnection() throws SQLException { + if (connection == null || connection.isClosed()) { + connect(); + } + return connection; + } + + /** + * CrĂ©e toutes les tables requises pour le systĂšme de guilde. + * + * @throws SQLException En cas d'erreur d'exĂ©cution DDL. + */ + private void createTables() throws SQLException { + try (Statement stmt = getConnection().createStatement()) { + // Table des guildes + stmt.executeUpdate("CREATE TABLE IF NOT EXISTS guilds (" + + "id VARCHAR(36) PRIMARY KEY, " + + "name VARCHAR(16) NOT NULL UNIQUE, " + + "tag VARCHAR(6), " + + "description VARCHAR(128), " + + "balance DOUBLE NOT NULL DEFAULT 0.0, " + + "home_world VARCHAR(64), " + + "home_x DOUBLE NOT NULL DEFAULT 0.0, " + + "home_y DOUBLE NOT NULL DEFAULT 0.0, " + + "home_z DOUBLE NOT NULL DEFAULT 0.0, " + + "home_yaw FLOAT NOT NULL DEFAULT 0.0, " + + "home_pitch FLOAT NOT NULL DEFAULT 0.0, " + + "created_at BIGINT NOT NULL" + + ");"); + + // Table des rangs de guilde + stmt.executeUpdate("CREATE TABLE IF NOT EXISTS guild_ranks (" + + "id VARCHAR(64) NOT NULL, " + + "guild_id VARCHAR(36) NOT NULL, " + + "name VARCHAR(32) NOT NULL, " + + "priority INTEGER NOT NULL DEFAULT 1, " + + "is_default INTEGER NOT NULL DEFAULT 0, " + + "permissions TEXT, " + + "PRIMARY KEY (id, guild_id), " + + "FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE" + + ");"); + + // Table des membres de guilde + stmt.executeUpdate("CREATE TABLE IF NOT EXISTS guild_members (" + + "player_uuid VARCHAR(36) PRIMARY KEY, " + + "guild_id VARCHAR(36) NOT NULL, " + + "rank_id VARCHAR(64) NOT NULL, " + + "joined_at BIGINT NOT NULL, " + + "last_online BIGINT NOT NULL, " + + "contributed_money DOUBLE NOT NULL DEFAULT 0.0, " + + "FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE" + + ");"); + + // Table des claims de territoire + stmt.executeUpdate("CREATE TABLE IF NOT EXISTS guild_claims (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guild_id VARCHAR(36) NOT NULL, " + + "world VARCHAR(64) NOT NULL, " + + "chunk_x INTEGER NOT NULL, " + + "chunk_z INTEGER NOT NULL, " + + "claimed_at BIGINT NOT NULL, " + + "FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE" + + ");"); + } + } + + /** + * Ferme proprement la connexion Ă  la base de donnĂ©es lors de l'arrĂȘt du plugin. + */ + public synchronized void close() { + try { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "Erreur lors de la fermeture de la connexion SQLite", e); + } + } +} diff --git a/src/main/java/fr/luc/gamingcore/database/GuildRepository.java b/src/main/java/fr/luc/gamingcore/database/GuildRepository.java new file mode 100644 index 0000000..8ae0147 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/database/GuildRepository.java @@ -0,0 +1,334 @@ +package fr.luc.gamingcore.database; + +import fr.luc.gamingcore.Main; +import fr.luc.gamingcore.model.*; +import org.bukkit.Bukkit; + +import java.sql.*; +import java.util.*; +import java.util.logging.Level; +import java.util.stream.Collectors; + +/** + * RĂ©fĂ©rentiel (Repository) gĂ©rant la persistance et la synchronisation des donnĂ©es des guildes en base de donnĂ©es. + */ +public class GuildRepository { + + private final Main plugin; + private final DatabaseManager databaseManager; + + /** + * Constructeur du GuildRepository. + * + * @param plugin Instance du plugin principal. + * @param databaseManager Gestionnaire de connexion Ă  la base de donnĂ©es. + */ + public GuildRepository(Main plugin, DatabaseManager databaseManager) { + this.plugin = plugin; + this.databaseManager = databaseManager; + } + + /** + * Charge l'intĂ©gralitĂ© des guildes, de leurs rangs, membres et claims au dĂ©marrage du serveur. + * + * @return Une Map associant l'UUID d'une guilde Ă  son instance Guild chargĂ©e en mĂ©moire. + */ + public Map loadAllGuilds() { + Map guilds = new HashMap<>(); + + try (Connection conn = databaseManager.getConnection()) { + // 1. Charger les guildes + String sqlGuilds = "SELECT * FROM guilds"; + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sqlGuilds)) { + while (rs.next()) { + UUID id = UUID.fromString(rs.getString("id")); + String name = rs.getString("name"); + String tag = rs.getString("tag"); + String description = rs.getString("description"); + double balance = rs.getDouble("balance"); + long createdAt = rs.getLong("created_at"); + + Guild guild = new Guild(id, name, tag, description, balance, createdAt); + + String homeWorld = rs.getString("home_world"); + if (homeWorld != null && !homeWorld.isEmpty()) { + guild.setHomeCoordinates( + homeWorld, + rs.getDouble("home_x"), + rs.getDouble("home_y"), + rs.getDouble("home_z"), + rs.getFloat("home_yaw"), + rs.getFloat("home_pitch") + ); + } + + guilds.put(id, guild); + } + } + + // 2. Charger les rangs + String sqlRanks = "SELECT * FROM guild_ranks"; + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sqlRanks)) { + while (rs.next()) { + String rankId = rs.getString("id"); + UUID guildId = UUID.fromString(rs.getString("guild_id")); + String name = rs.getString("name"); + int priority = rs.getInt("priority"); + boolean isDefault = rs.getInt("is_default") == 1; + String permsRaw = rs.getString("permissions"); + + Set permissions = new HashSet<>(); + if (permsRaw != null && !permsRaw.isEmpty()) { + for (String permName : permsRaw.split(",")) { + try { + permissions.add(GuildPermission.valueOf(permName.trim().toUpperCase())); + } catch (IllegalArgumentException ignored) {} + } + } + + Guild guild = guilds.get(guildId); + if (guild != null) { + GuildRank rank = new GuildRank(rankId, guildId, name, priority, isDefault, permissions); + guild.addRank(rank); + } + } + } + + // 3. Charger les membres + String sqlMembers = "SELECT * FROM guild_members"; + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sqlMembers)) { + while (rs.next()) { + UUID playerUuid = UUID.fromString(rs.getString("player_uuid")); + UUID guildId = UUID.fromString(rs.getString("guild_id")); + String rankId = rs.getString("rank_id"); + long joinedAt = rs.getLong("joined_at"); + long lastOnline = rs.getLong("last_online"); + double contributedMoney = rs.getDouble("contributed_money"); + + Guild guild = guilds.get(guildId); + if (guild != null) { + GuildMember member = new GuildMember(playerUuid, guildId, rankId, joinedAt, lastOnline, contributedMoney); + guild.addMember(member); + } + } + } + + // 4. Charger les claims + String sqlClaims = "SELECT * FROM guild_claims"; + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sqlClaims)) { + while (rs.next()) { + int id = rs.getInt("id"); + UUID guildId = UUID.fromString(rs.getString("guild_id")); + String world = rs.getString("world"); + int chunkX = rs.getInt("chunk_x"); + int chunkZ = rs.getInt("chunk_z"); + long claimedAt = rs.getLong("claimed_at"); + + Guild guild = guilds.get(guildId); + if (guild != null) { + GuildClaim claim = new GuildClaim(id, guildId, world, chunkX, chunkZ, claimedAt); + guild.addClaim(claim); + } + } + } + + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors du chargement des guildes depuis la base SQLite !", e); + } + + return guilds; + } + + /** + * Sauvegarde ou met Ă  jour les informations principales d'une guilde de façon asynchrone. + * + * @param guild La guilde Ă  sauvegarder. + */ + public void saveGuildAsync(Guild guild) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + saveGuild(guild); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la sauvegarde de la guilde " + guild.getName(), e); + } + }); + } + + /** + * Sauvegarde synchrone des donnĂ©es principales d'une guilde. + * + * @param guild La guilde Ă  insĂ©rer ou mettre Ă  jour. + * @throws SQLException En cas d'Ă©chec SQL. + */ + public void saveGuild(Guild guild) throws SQLException { + String sql = "INSERT OR REPLACE INTO guilds (id, name, tag, description, balance, home_world, home_x, home_y, home_z, home_yaw, home_pitch, created_at) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + try (PreparedStatement ps = databaseManager.getConnection().prepareStatement(sql)) { + ps.setString(1, guild.getId().toString()); + ps.setString(2, guild.getName()); + ps.setString(3, guild.getTag()); + ps.setString(4, guild.getDescription()); + ps.setDouble(5, guild.getBalance()); + ps.setString(6, guild.getHomeWorld()); + ps.setDouble(7, guild.getHomeX()); + ps.setDouble(8, guild.getHomeY()); + ps.setDouble(9, guild.getHomeZ()); + ps.setFloat(10, guild.getHomeYaw()); + ps.setFloat(11, guild.getHomePitch()); + ps.setLong(12, guild.getCreatedAt()); + ps.executeUpdate(); + } + } + + /** + * Supprime dĂ©finitivement une guilde et toutes ses donnĂ©es associĂ©es en cascade de maniĂšre asynchrone. + * + * @param guildId Le UUID de la guilde Ă  supprimer. + */ + public void deleteGuildAsync(UUID guildId) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String sql = "DELETE FROM guilds WHERE id = ?"; + try (PreparedStatement ps = databaseManager.getConnection().prepareStatement(sql)) { + ps.setString(1, guildId.toString()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la suppression de la guilde " + guildId, e); + } + }); + } + + /** + * Sauvegarde un rang de guilde de façon asynchrone. + * + * @param rank Le rang Ă  sauvegarder. + */ + public void saveRankAsync(GuildRank rank) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String sql = "INSERT OR REPLACE INTO guild_ranks (id, guild_id, name, priority, is_default, permissions) " + + "VALUES (?, ?, ?, ?, ?, ?)"; + try (PreparedStatement ps = databaseManager.getConnection().prepareStatement(sql)) { + ps.setString(1, rank.getId()); + ps.setString(2, rank.getGuildId().toString()); + ps.setString(3, rank.getName()); + ps.setInt(4, rank.getPriority()); + ps.setInt(5, rank.isDefault() ? 1 : 0); + String permsStr = rank.getPermissions().stream().map(Enum::name).collect(Collectors.joining(",")); + ps.setString(6, permsStr); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la sauvegarde du rang " + rank.getName(), e); + } + }); + } + + /** + * Supprime un rang de guilde de façon asynchrone. + * + * @param rankId L'identifiant du rang. + * @param guildId L'UUID de la guilde. + */ + public void deleteRankAsync(String rankId, UUID guildId) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String sql = "DELETE FROM guild_ranks WHERE id = ? AND guild_id = ?"; + try (PreparedStatement ps = databaseManager.getConnection().prepareStatement(sql)) { + ps.setString(1, rankId); + ps.setString(2, guildId.toString()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la suppression du rang " + rankId, e); + } + }); + } + + /** + * Sauvegarde un membre de guilde de façon asynchrone. + * + * @param member Le membre Ă  sauvegarder. + */ + public void saveMemberAsync(GuildMember member) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String sql = "INSERT OR REPLACE INTO guild_members (player_uuid, guild_id, rank_id, joined_at, last_online, contributed_money) " + + "VALUES (?, ?, ?, ?, ?, ?)"; + try (PreparedStatement ps = databaseManager.getConnection().prepareStatement(sql)) { + ps.setString(1, member.getPlayerUuid().toString()); + ps.setString(2, member.getGuildId().toString()); + ps.setString(3, member.getRankId()); + ps.setLong(4, member.getJoinedAt()); + ps.setLong(5, member.getLastOnline()); + ps.setDouble(6, member.getContributedMoney()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la sauvegarde du membre " + member.getPlayerUuid(), e); + } + }); + } + + /** + * Supprime un membre de guilde de façon asynchrone. + * + * @param playerUuid Le UUID du joueur Ă  supprimer de la table. + */ + public void deleteMemberAsync(UUID playerUuid) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String sql = "DELETE FROM guild_members WHERE player_uuid = ?"; + try (PreparedStatement ps = databaseManager.getConnection().prepareStatement(sql)) { + ps.setString(1, playerUuid.toString()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la suppression du membre " + playerUuid, e); + } + }); + } + + /** + * InsĂšre un nouveau claim de territoire en base de donnĂ©es et met Ă  jour son identifiant gĂ©nĂ©rĂ©. + * + * @param claim Le claim Ă  insĂ©rer. + */ + public void saveClaimAsync(GuildClaim claim) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String sql = "INSERT INTO guild_claims (guild_id, world, chunk_x, chunk_z, claimed_at) VALUES (?, ?, ?, ?, ?)"; + try (PreparedStatement ps = databaseManager.getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, claim.getGuildId().toString()); + ps.setString(2, claim.getWorldName()); + ps.setInt(3, claim.getChunkX()); + ps.setInt(4, claim.getChunkZ()); + ps.setLong(5, claim.getClaimedAt()); + ps.executeUpdate(); + + try (ResultSet rs = ps.getGeneratedKeys()) { + if (rs.next()) { + claim.setId(rs.getInt(1)); + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la sauvegarde du claim " + claim, e); + } + }); + } + + /** + * Supprime un claim de territoire de la base de donnĂ©es selon ses coordonnĂ©es de chunk de façon asynchrone. + * + * @param world Le nom du monde. + * @param chunkX L'abscisse du chunk. + * @param chunkZ L'ordonnĂ©e du chunk. + */ + public void deleteClaimAsync(String world, int chunkX, int chunkZ) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String sql = "DELETE FROM guild_claims WHERE world = ? AND chunk_x = ? AND chunk_z = ?"; + try (PreparedStatement ps = databaseManager.getConnection().prepareStatement(sql)) { + ps.setString(1, world); + ps.setInt(2, chunkX); + ps.setInt(3, chunkZ); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la suppression du claim " + world + ":" + chunkX + "," + chunkZ, e); + } + }); + } +} diff --git a/src/main/java/fr/luc/gamingcore/economy/ChunkPriceCalculator.java b/src/main/java/fr/luc/gamingcore/economy/ChunkPriceCalculator.java new file mode 100644 index 0000000..b5ac81f --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/economy/ChunkPriceCalculator.java @@ -0,0 +1,180 @@ +package fr.luc.gamingcore.economy; + +import fr.luc.gamingcore.model.Guild; + +/** + * Calculateur du coĂ»t des chunks territoriaux utilisant la formule spatio-temporelle avec surchauffe glissante. + */ +public class ChunkPriceCalculator { + + // ParamĂštres spatiaux + private double basePrice = 1000.0; + private double growthRate = 0.08; + private double alpha = 0.95; + private double linearStep = 150.0; + + // ParamĂštres temporels de surchauffe (fenĂȘtre 24h) + private double kBase = 0.05; + private double kSize = 0.03; + private double delta = 1.15; + private long recentWindowMillis = 24L * 60L * 60L * 1000L; // 24 heures + + /** + * Constructeur par dĂ©faut avec les paramĂštres recommandĂ©s. + */ + public ChunkPriceCalculator() {} + + /** + * Constructeur personnalisĂ© permettant de configurer l'ensemble des paramĂštres de la formule. + * + * @param basePrice Prix de base du premier chunk. + * @param growthRate Taux de croissance exponentiel de base (ex: 0.08 pour +8%). + * @param alpha Facteur d'amortissement de l'exposant. + * @param linearStep Ajout linĂ©aire fixe par chunk possĂ©dĂ©. + * @param kBase Surchauffe minimale par claim rĂ©cent (ex: 0.05 pour +5%). + * @param kSize Coefficient d'amplification de la surchauffe par tranche de 10 chunks possĂ©dĂ©s. + * @param delta Exposant d'agressivitĂ© de la surchauffe temporelle. + * @param recentWindowMillis FenĂȘtre de temps glissante en millisecondes (ex: 24h). + */ + public ChunkPriceCalculator(double basePrice, double growthRate, double alpha, double linearStep, + double kBase, double kSize, double delta, long recentWindowMillis) { + this.basePrice = Math.max(1.0, basePrice); + this.growthRate = Math.max(0.0, growthRate); + this.alpha = Math.max(0.1, alpha); + this.linearStep = Math.max(0.0, linearStep); + this.kBase = Math.max(0.0, kBase); + this.kSize = Math.max(0.0, kSize); + this.delta = Math.max(0.5, delta); + this.recentWindowMillis = Math.max(1000L, recentWindowMillis); + } + + /** + * Calcule le prix d'achat du prochain chunk pour une guilde donnĂ©e. + * Prend en compte son nombre total de chunks et ses achats rĂ©cents dans les derniĂšres 24h. + * + * @param guild La guilde souhaitant effectuer le claim. + * @return Le prix exact calculĂ© et arrondi Ă  2 dĂ©cimales. + */ + public double calculateNextChunkPrice(Guild guild) { + if (guild == null) { + return basePrice; + } + + int currentChunks = guild.getClaimCount(); + long cutoff = System.currentTimeMillis() - recentWindowMillis; + int recentClaims = guild.getRecentClaimsCount(cutoff); + + return calculatePrice(currentChunks + 1, recentClaims); + } + + /** + * Calcule le prix d'un chunk selon le numĂ©ro du chunk visĂ© et le nombre de claims rĂ©cents. + * + * @param chunkNumber NumĂ©ro du chunk Ă  acquĂ©rir (N >= 1). + * @param recentClaimsCount Nombre de claims effectuĂ©s dans les derniĂšres 24h (R >= 0). + * @return Le coĂ»t final aprĂšs application de la surchauffe. + */ + public double calculatePrice(int chunkNumber, int recentClaimsCount) { + int n = Math.max(1, chunkNumber); + int r = Math.max(0, recentClaimsCount); + + double structuralPrice = calculateStructuralPrice(n); + double overheatMultiplier = calculateOverheatMultiplier(n, r); + + double rawFinal = structuralPrice * overheatMultiplier; + return Math.round(rawFinal * 100.0) / 100.0; + } + + /** + * Calcule le prix structurel de base liĂ© uniquement Ă  la taille de la guilde : + * P_structurel(N) = P_base * (1 + Taux)^((N-1)^alpha) + (N-1) * Palier + * + * @param chunkNumber NumĂ©ro du chunk (N >= 1). + * @return Le prix structurel hors surchauffe. + */ + public double calculateStructuralPrice(int chunkNumber) { + int n = Math.max(1, chunkNumber); + if (n == 1) { + return basePrice; + } + + double exponent = Math.pow(n - 1, alpha); + double geometricPart = basePrice * Math.pow(1.0 + growthRate, exponent); + double linearPart = (n - 1) * linearStep; + + return geometricPart + linearPart; + } + + /** + * Calcule le multiplicateur de surchauffe temporelle : + * M_surchauffe(N, R) = 1 + (k_base + k_taille * (N / 10)) * R^delta + * + * @param chunkNumber Nombre de chunks de la guilde. + * @param recentClaimsCount Nombre de claims rĂ©cents (derniĂšres 24h). + * @return Le coefficient multiplicateur (>= 1.0). + */ + public double calculateOverheatMultiplier(int chunkNumber, int recentClaimsCount) { + if (recentClaimsCount <= 0) { + return 1.0; + } + + int n = Math.max(1, chunkNumber); + int r = recentClaimsCount; + + double sizeScaledFactor = kBase + (kSize * (n / 10.0)); + double velocityFactor = Math.pow(r, delta); + + return 1.0 + (sizeScaledFactor * velocityFactor); + } + + /** + * Calcule le coĂ»t cumulĂ© total pour acheter une sĂ©rie de plusieurs chunks consĂ©cutifs. + * + * @param guild La guilde concernĂ©e. + * @param chunksToBuy Le nombre de chunks Ă  acheter d'un coup. + * @return La somme totale des prix de chaque chunk avec surchauffe progressive. + */ + public double calculateTotalPriceForMultipleChunks(Guild guild, int chunksToBuy) { + if (guild == null || chunksToBuy <= 0) { + return 0.0; + } + + int currentChunks = guild.getClaimCount(); + long cutoff = System.currentTimeMillis() - recentWindowMillis; + int recentClaims = guild.getRecentClaimsCount(cutoff); + + double total = 0.0; + for (int i = 0; i < chunksToBuy; i++) { + int targetN = currentChunks + 1 + i; + int targetR = recentClaims + i; + total += calculatePrice(targetN, targetR); + } + + return Math.round(total * 100.0) / 100.0; + } + + // Getters et Setters pour la configuration dynamique + public double getBasePrice() { return basePrice; } + public void setBasePrice(double basePrice) { this.basePrice = basePrice; } + + public double getGrowthRate() { return growthRate; } + public void setGrowthRate(double growthRate) { this.growthRate = growthRate; } + + public double getAlpha() { return alpha; } + public void setAlpha(double alpha) { this.alpha = alpha; } + + public double getLinearStep() { return linearStep; } + public void setLinearStep(double linearStep) { this.linearStep = linearStep; } + + public double getKBase() { return kBase; } + public void setKBase(double kBase) { this.kBase = kBase; } + + public double getKSize() { return kSize; } + public void setKSize(double kSize) { this.kSize = kSize; } + + public double getDelta() { return delta; } + public void setDelta(double delta) { this.delta = delta; } + + public long getRecentWindowMillis() { return recentWindowMillis; } + public void setRecentWindowMillis(long recentWindowMillis) { this.recentWindowMillis = recentWindowMillis; } +} diff --git a/src/main/java/fr/luc/gamingcore/economy/EconomyManager.java b/src/main/java/fr/luc/gamingcore/economy/EconomyManager.java new file mode 100644 index 0000000..761ab00 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/economy/EconomyManager.java @@ -0,0 +1,119 @@ +package fr.luc.gamingcore.economy; + +import fr.luc.gamingcore.Main; +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; + +import java.text.NumberFormat; +import java.util.Locale; + +/** + * Gestionnaire d'interaction avec le systĂšme Ă©conomique Vault. + */ +public class EconomyManager { + + private final Main plugin; + private final NumberFormat numberFormat; + + /** + * Constructeur de l'EconomyManager. + * + * @param plugin Instance principale du plugin. + */ + public EconomyManager(Main plugin) { + this.plugin = plugin; + this.numberFormat = NumberFormat.getNumberInstance(Locale.FRANCE); + this.numberFormat.setMaximumFractionDigits(2); + this.numberFormat.setMinimumFractionDigits(0); + } + + /** + * VĂ©rifie si le service Ă©conomique Vault est disponible et connectĂ©. + * + * @return true si Vault est chargĂ© avec un provider Ă©conomique actif. + */ + public boolean isAvailable() { + return plugin.getEconomy() != null; + } + + /** + * Obtient l'instance sous-jacente de l'Ă©conomie Vault. + * + * @return L'Economy de Vault ou null si non disponible. + */ + public Economy getVaultEconomy() { + return plugin.getEconomy(); + } + + /** + * Obtient le solde financier actuel d'un joueur. + * + * @param player Le joueur concernĂ©. + * @return Le solde du joueur (0.0 si l'Ă©conomie est indisponible). + */ + public double getBalance(OfflinePlayer player) { + if (!isAvailable() || player == null) { + return 0.0; + } + return plugin.getEconomy().getBalance(player); + } + + /** + * VĂ©rifie si un joueur possĂšde au moins le montant spĂ©cifiĂ©. + * + * @param player Le joueur Ă  vĂ©rifier. + * @param amount Le montant requis. + * @return true si le solde du joueur est supĂ©rieur ou Ă©gal au montant requis. + */ + public boolean has(OfflinePlayer player, double amount) { + if (!isAvailable() || player == null || amount <= 0) { + return false; + } + return plugin.getEconomy().has(player, amount); + } + + /** + * DĂ©bite un montant du compte d'un joueur. + * + * @param player Le joueur Ă  dĂ©biter. + * @param amount Le montant Ă  retirer. + * @return true si la transaction bancaire a rĂ©ussi, false sinon. + */ + public boolean withdraw(OfflinePlayer player, double amount) { + if (!isAvailable() || player == null || amount <= 0) { + return false; + } + EconomyResponse response = plugin.getEconomy().withdrawPlayer(player, amount); + return response.transactionSuccess(); + } + + /** + * CrĂ©dite un montant sur le compte d'un joueur. + * + * @param player Le joueur Ă  crĂ©diter. + * @param amount Le montant Ă  ajouter. + * @return true si la transaction bancaire a rĂ©ussi, false sinon. + */ + public boolean deposit(OfflinePlayer player, double amount) { + if (!isAvailable() || player == null || amount <= 0) { + return false; + } + EconomyResponse response = plugin.getEconomy().depositPlayer(player, amount); + return response.transactionSuccess(); + } + + /** + * Formate un montant numĂ©rique en chaĂźne lisible avec sĂ©parateurs de milliers et symbole monĂ©taire (ex: "1 250 $"). + * + * @param amount Le montant Ă  formater. + * @return La chaĂźne formatĂ©e. + */ + public String format(double amount) { + if (isAvailable()) { + return plugin.getEconomy().format(amount); + } + return numberFormat.format(amount) + " $"; + } +} diff --git a/src/main/java/fr/luc/gamingcore/listener/TerritoryProtectionListener.java b/src/main/java/fr/luc/gamingcore/listener/TerritoryProtectionListener.java new file mode 100644 index 0000000..6928a97 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/listener/TerritoryProtectionListener.java @@ -0,0 +1,235 @@ +package fr.luc.gamingcore.listener; + +import fr.luc.gamingcore.manager.GuildManager; +import fr.luc.gamingcore.model.Guild; +import fr.luc.gamingcore.model.GuildPermission; +import fr.luc.gamingcore.util.TextUtil; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +import java.util.Objects; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerMoveEvent; + +/** + * Écouteur d'Ă©vĂ©nements garantissant la protection territoriale des chunks de guilde contre le grief et les intrusions. + */ +public class TerritoryProtectionListener implements Listener { + + private final GuildManager guildManager; + + /** + * Constructeur de l'Ă©couteur de protection territoriale. + * + * @param guildManager Le gestionnaire de guildes. + */ + public TerritoryProtectionListener(GuildManager guildManager) { + this.guildManager = guildManager; + } + + /** + * Intercepte la destruction de blocs et vĂ©rifie si le joueur possĂšde la permission de casser dans le claim. + * + * @param event L'Ă©vĂ©nement BlockBreakEvent. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + Player player = event.getPlayer(); + if (player.hasPermission("guild.bypass")) { + return; + } + + Block block = event.getBlock(); + Guild guildAtBlock = guildManager.getGuildAt(block.getWorld().getName(), block.getX() >> 4, block.getZ() >> 4); + if (guildAtBlock == null) { + return; // Territoire libre + } + + Guild playerGuild = guildManager.getGuildByPlayer(player.getUniqueId()); + if (playerGuild == null || !playerGuild.getId().equals(guildAtBlock.getId())) { + event.setCancelled(true); + TextUtil.sendActionBar(player, "⛔ Ce territoire appartient Ă  la guilde " + guildAtBlock.getName() + " !"); + return; + } + + // Le joueur est dans sa propre guilde : vĂ©rifier la permission BREAK + if (!guildAtBlock.hasPermission(player.getUniqueId(), GuildPermission.BREAK)) { + event.setCancelled(true); + TextUtil.sendActionBar(player, "Votre rang de guilde ne vous autorise pas Ă  casser des blocs."); + } + } + + /** + * Intercepte la pose de blocs dans un territoire revendiquĂ©. + * + * @param event L'Ă©vĂ©nement BlockPlaceEvent. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBlockPlace(BlockPlaceEvent event) { + Player player = event.getPlayer(); + if (player.hasPermission("guild.bypass")) { + return; + } + + Block block = event.getBlock(); + Guild guildAtBlock = guildManager.getGuildAt(block.getWorld().getName(), block.getX() >> 4, block.getZ() >> 4); + if (guildAtBlock == null) { + return; + } + + Guild playerGuild = guildManager.getGuildByPlayer(player.getUniqueId()); + if (playerGuild == null || !playerGuild.getId().equals(guildAtBlock.getId())) { + event.setCancelled(true); + TextUtil.sendActionBar(player, "⛔ Ce territoire appartient Ă  la guilde " + guildAtBlock.getName() + " !"); + return; + } + + // VĂ©rifier la permission BUILD + if (!guildAtBlock.hasPermission(player.getUniqueId(), GuildPermission.BUILD)) { + event.setCancelled(true); + TextUtil.sendActionBar(player, "Votre rang de guilde ne vous autorise pas Ă  poser des blocs."); + } + } + + /** + * Intercepte les interactions avec les conteneurs (coffres, fours) et mĂ©canismes (portes, boutons, leviers). + * + * @param event L'Ă©vĂ©nement PlayerInteractEvent. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null) { + return; + } + + Player player = event.getPlayer(); + if (player.hasPermission("guild.bypass")) { + return; + } + + Block block = event.getClickedBlock(); + Guild guildAtBlock = guildManager.getGuildAt(block.getWorld().getName(), block.getX() >> 4, block.getZ() >> 4); + if (guildAtBlock == null) { + return; + } + + Guild playerGuild = guildManager.getGuildByPlayer(player.getUniqueId()); + boolean isSameGuild = playerGuild != null && playerGuild.getId().equals(guildAtBlock.getId()); + + Material type = block.getType(); + boolean isContainer = isContainerMaterial(type); + boolean isMechanism = isMechanismMaterial(type); + + if (!isSameGuild) { + if (isContainer || isMechanism) { + event.setCancelled(true); + TextUtil.sendActionBar(player, "⛔ Ce territoire appartient Ă  la guilde " + guildAtBlock.getName() + " !"); + } + return; + } + + // VĂ©rification des permissions spĂ©cifiques de rang + if (isContainer && !guildAtBlock.hasPermission(player.getUniqueId(), GuildPermission.CONTAINER)) { + event.setCancelled(true); + TextUtil.sendActionBar(player, "Votre rang ne vous autorise pas Ă  ouvrir les conteneurs de guilde."); + } else if (isMechanism && !guildAtBlock.hasPermission(player.getUniqueId(), GuildPermission.INTERACT)) { + event.setCancelled(true); + TextUtil.sendActionBar(player, "Votre rang ne vous autorise pas Ă  interagir avec ces mĂ©canismes."); + } + } + + /** + * ProtĂšge les blocs de guilde contre les explosions de crĂ©atures ou de TNT. + * + * @param event L'Ă©vĂ©nement EntityExplodeEvent. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onEntityExplode(EntityExplodeEvent event) { + event.blockList().removeIf(block -> guildManager.isChunkClaimed( + block.getWorld().getName(), + block.getX() >> 4, + block.getZ() >> 4 + )); + } + + /** + * ProtĂšge les blocs de guilde contre les explosions de blocs (ex: cristaux de l'End, lits dans le Nether). + * + * @param event L'Ă©vĂ©nement BlockExplodeEvent. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBlockExplode(BlockExplodeEvent event) { + event.blockList().removeIf(block -> guildManager.isChunkClaimed( + block.getWorld().getName(), + block.getX() >> 4, + block.getZ() >> 4 + )); + } + + /** + * DĂ©tecte le passage d'une frontiĂšre de chunk et affiche une notification Action Bar lors de l'entrĂ©e/sortie de territoire. + * + * @param event L'Ă©vĂ©nement PlayerMoveEvent. + */ + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerMove(PlayerMoveEvent event) { + Location from = event.getFrom(); + Location to = event.getTo(); + if (to == null) return; + + int fromChunkX = from.getBlockX() >> 4; + int fromChunkZ = from.getBlockZ() >> 4; + int toChunkX = to.getBlockX() >> 4; + int toChunkZ = to.getBlockZ() >> 4; + + if (fromChunkX == toChunkX && fromChunkZ == toChunkZ && from.getWorld().equals(to.getWorld())) { + return; // MĂȘme chunk + } + + String worldName = to.getWorld().getName(); + Guild previousGuild = guildManager.getGuildAt(from.getWorld().getName(), fromChunkX, fromChunkZ); + Guild currentGuild = guildManager.getGuildAt(worldName, toChunkX, toChunkZ); + + if (Objects.equals(previousGuild, currentGuild)) { + return; + } + + Player player = event.getPlayer(); + if (currentGuild != null) { + Guild playerGuild = guildManager.getGuildByPlayer(player.getUniqueId()); + boolean isOwn = playerGuild != null && playerGuild.getId().equals(currentGuild.getId()); + String color = isOwn ? "" : ""; + TextUtil.sendActionBar(player, "EntrĂ©e dans le territoire : " + color + "" + currentGuild.getName() + " (" + currentGuild.getTag() + ")"); + } else if (previousGuild != null) { + TextUtil.sendActionBar(player, "Vous entrez en Zone Libre (Wilderness)."); + } + } + + // ========================================== + // MÉTHODES UTILITAIRES + // ========================================== + + private boolean isContainerMaterial(Material mat) { + String name = mat.name(); + return name.contains("CHEST") || name.contains("BARREL") || name.contains("SHULKER") || + name.contains("FURNACE") || name.contains("HOPPER") || name.contains("DISPENSER") || + name.contains("DROPPER") || name.contains("BREWING_STAND"); + } + + private boolean isMechanismMaterial(Material mat) { + String name = mat.name(); + return name.contains("DOOR") || name.contains("BUTTON") || name.contains("LEVER") || + name.contains("GATE") || name.contains("PRESSURE_PLATE"); + } +} diff --git a/src/main/java/fr/luc/gamingcore/manager/GuildManager.java b/src/main/java/fr/luc/gamingcore/manager/GuildManager.java new file mode 100644 index 0000000..7864710 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/manager/GuildManager.java @@ -0,0 +1,606 @@ +package fr.luc.gamingcore.manager; + +import fr.luc.gamingcore.Main; +import fr.luc.gamingcore.database.GuildRepository; +import fr.luc.gamingcore.economy.ChunkPriceCalculator; +import fr.luc.gamingcore.economy.EconomyManager; +import fr.luc.gamingcore.model.*; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +/** + * Service central orchestrant l'intĂ©gralitĂ© de la logique mĂ©tier du systĂšme de guilde. + */ +public class GuildManager { + + private static final Pattern GUILD_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{3,16}$"); + + private final Main plugin; + private final GuildRepository repository; + private final EconomyManager economyManager; + private final ChunkPriceCalculator priceCalculator; + private final InviteManager inviteManager; + + // Caches d'accĂšs rapide + private final Map guildsById = new ConcurrentHashMap<>(); + private final Map guildsByName = new ConcurrentHashMap<>(); + private final Map playerGuildMap = new ConcurrentHashMap<>(); + private final Map chunkGuildMap = new ConcurrentHashMap<>(); + + /** + * Constructeur du GuildManager. + * + * @param plugin Instance du plugin principal. + * @param repository RĂ©fĂ©rentiel de persistance des donnĂ©es. + * @param economyManager Gestionnaire de l'Ă©conomie Vault. + */ + public GuildManager(Main plugin, GuildRepository repository, EconomyManager economyManager) { + this.plugin = plugin; + this.repository = repository; + this.economyManager = economyManager; + this.priceCalculator = new ChunkPriceCalculator(); + this.inviteManager = new InviteManager(); + } + + /** + * Charge l'ensemble des donnĂ©es de la base en mĂ©moire et reconstruit les index de recherche. + */ + public void loadData() { + guildsById.clear(); + guildsByName.clear(); + playerGuildMap.clear(); + chunkGuildMap.clear(); + + Map loaded = repository.loadAllGuilds(); + for (Guild guild : loaded.values()) { + indexGuild(guild); + } + + plugin.getLogger().info("Charges " + guildsById.size() + " guildes (" + playerGuildMap.size() + " membres, " + chunkGuildMap.size() + " claims)."); + } + + /** + * ArrĂȘte proprement le gestionnaire et libĂšre les ressources. + */ + public void shutdown() { + guildsById.clear(); + guildsByName.clear(); + playerGuildMap.clear(); + chunkGuildMap.clear(); + inviteManager.cleanExpired(); + } + + // ========================================== + // MÉTHODES DE CRÉATION & DISSOLUTION + // ========================================== + + /** + * CrĂ©e une nouvelle guilde pour un joueur avec prĂ©lĂšvement financier et claim obligatoire du premier chunk. + * + * @param creator Le joueur fondateur. + * @param name Nom de la guilde (3 Ă  16 caractĂšres alphanumĂ©riques). + * @param tag Tag court optionnel (ex: "LEG"). + * @param creationCost CoĂ»t financier requis pour la crĂ©ation. + * @return GuildResult indiquant le succĂšs ou la raison de l'Ă©chec. + */ + public GuildResult createGuild(Player creator, String name, String tag, double creationCost) { + if (creator == null) { + return GuildResult.failure("Joueur invalide."); + } + + // 1. VĂ©rifier si le joueur est dĂ©jĂ  dans une guilde + if (isInGuild(creator.getUniqueId())) { + return GuildResult.failure("Vous appartenez dĂ©jĂ  Ă  une guilde."); + } + + // 2. Valider le format du nom de la guilde + if (name == null || !GUILD_NAME_PATTERN.matcher(name).matches()) { + return GuildResult.failure("Le nom de guilde doit comporter entre 3 et 16 caractĂšres alphanumĂ©riques."); + } + + // 3. VĂ©rifier l'unicitĂ© du nom + if (getGuildByName(name) != null) { + return GuildResult.failure("Une guilde portant le nom '" + name + "' existe dĂ©jĂ ."); + } + + // 4. VĂ©rifier si le chunk actuel est disponible + Location loc = creator.getLocation(); + Chunk chunk = loc.getChunk(); + String worldName = loc.getWorld().getName(); + int chunkX = chunk.getX(); + int chunkZ = chunk.getZ(); + + if (isChunkClaimed(worldName, chunkX, chunkZ)) { + return GuildResult.failure("Le chunk oĂč vous vous trouvez est dĂ©jĂ  revendiquĂ© par une autre guilde."); + } + + // 5. VĂ©rifier et prĂ©lever l'argent du joueur + if (creationCost > 0) { + if (!economyManager.has(creator, creationCost)) { + return GuildResult.failure("Vous n'avez pas assez d'argent pour fonder une guilde (Requis: " + economyManager.format(creationCost) + ")."); + } + boolean success = economyManager.withdraw(creator, creationCost); + if (!success) { + return GuildResult.failure("Échec de la transaction financiĂšre lors de la crĂ©ation de la guilde."); + } + } + + // 6. Instancier la nouvelle guilde + UUID guildId = UUID.randomUUID(); + Guild guild = new Guild(guildId, name, tag, "Guilde fondĂ©e par " + creator.getName(), 0.0, System.currentTimeMillis()); + + // 7. Configurer les rangs par dĂ©faut + GuildRank leaderRank = new GuildRank("leader", guildId, "Chef", 100, false, EnumSet.allOf(GuildPermission.class)); + GuildRank officerRank = new GuildRank("officer", guildId, "Officier", 50, false, EnumSet.of( + GuildPermission.CLAIM, GuildPermission.UNCLAIM, GuildPermission.INVITE, GuildPermission.KICK, + GuildPermission.SET_HOME, GuildPermission.HOME, GuildPermission.BANK_DEPOSIT, GuildPermission.BANK_WITHDRAW, + GuildPermission.BUILD, GuildPermission.BREAK, GuildPermission.INTERACT, GuildPermission.CONTAINER + )); + GuildRank memberRank = new GuildRank("member", guildId, "Membre", 1, true, EnumSet.of( + GuildPermission.HOME, GuildPermission.BANK_DEPOSIT, GuildPermission.BUILD, + GuildPermission.BREAK, GuildPermission.INTERACT, GuildPermission.CONTAINER + )); + + guild.addRank(leaderRank); + guild.addRank(officerRank); + guild.addRank(memberRank); + + // 8. Ajouter le fondateur comme chef + GuildMember founder = new GuildMember(creator.getUniqueId(), guildId, leaderRank.getId(), System.currentTimeMillis(), System.currentTimeMillis(), 0.0); + guild.addMember(founder); + + // 9. Claimer le 1er chunk et dĂ©finir le home + GuildClaim initialClaim = new GuildClaim(0, guildId, worldName, chunkX, chunkZ, System.currentTimeMillis()); + guild.addClaim(initialClaim); + guild.setHome(loc); + + // 10. Indexer et persister en BDD + indexGuild(guild); + repository.saveGuildAsync(guild); + repository.saveRankAsync(leaderRank); + repository.saveRankAsync(officerRank); + repository.saveRankAsync(memberRank); + repository.saveMemberAsync(founder); + repository.saveClaimAsync(initialClaim); + + return GuildResult.success("La guilde " + name + " a Ă©tĂ© créée avec succĂšs !"); + } + + /** + * Dissout dĂ©finitivement une guilde existante. Seul le chef de guilde peut initier cette action. + * + * @param actor Le joueur demandant la suppression. + * @param guild La guilde Ă  dissoudre. + * @return GuildResult. + */ + public GuildResult disbandGuild(Player actor, Guild guild) { + if (actor == null || guild == null) { + return GuildResult.failure("ParamĂštres invalides."); + } + + // VĂ©rifier si l'acteur est bien le chef de la guilde + GuildMember actorMember = guild.getMember(actor.getUniqueId()); + if (actorMember == null) { + return GuildResult.failure("Vous ne faites pas partie de cette guilde."); + } + + GuildRank actorRank = guild.getRank(actorMember.getRankId()); + GuildRank leaderRank = guild.getLeaderRank(); + if (actorRank == null || leaderRank == null || !actorRank.equals(leaderRank)) { + return GuildResult.failure("Seul le Chef de la guilde peut dissoudre la guilde."); + } + + // DĂ©sindexer la guilde + unindexGuild(guild); + + // Supprimer de la base de donnĂ©es + repository.deleteGuildAsync(guild.getId()); + + return GuildResult.success("La guilde " + guild.getName() + " a Ă©tĂ© dissoute."); + } + + // ========================================== + // GESTION DES MEMBRES & RANGS + // ========================================== + + /** + * IntĂšgre un joueur dans une guilde en lui attribuant le rang par dĂ©faut. + * + * @param guild La guilde Ă  rejoindre. + * @param player Le nouveau membre. + * @return GuildResult. + */ + public GuildResult addMember(Guild guild, Player player) { + if (guild == null || player == null) { + return GuildResult.failure("ParamĂštres invalides."); + } + + if (isInGuild(player.getUniqueId())) { + return GuildResult.failure("Le joueur appartient dĂ©jĂ  Ă  une guilde."); + } + + GuildRank defaultRank = guild.getDefaultRank(); + if (defaultRank == null) { + return GuildResult.failure("La guilde ne possĂšde aucun rang par dĂ©faut."); + } + + GuildMember member = new GuildMember(player.getUniqueId(), guild.getId(), defaultRank.getId(), System.currentTimeMillis(), System.currentTimeMillis(), 0.0); + guild.addMember(member); + playerGuildMap.put(player.getUniqueId(), guild.getId()); + repository.saveMemberAsync(member); + + return GuildResult.success("Le joueur " + player.getName() + " a rejoint la guilde !"); + } + + /** + * Expulse un membre d'une guilde avec vĂ©rification de la hiĂ©rarchie des rangs. + * + * @param actor Le joueur initiateur de l'expulsion. + * @param guild La guilde concernĂ©e. + * @param targetUuid L'UUID du membre Ă  expulser. + * @return GuildResult. + */ + public GuildResult kickMember(Player actor, Guild guild, UUID targetUuid) { + if (actor == null || guild == null || targetUuid == null) { + return GuildResult.failure("ParamĂštres invalides."); + } + + if (actor.getUniqueId().equals(targetUuid)) { + return GuildResult.failure("Vous ne pouvez pas vous expulser vous-mĂȘme. Utilisez /guild leave."); + } + + GuildRank actorRank = guild.getMemberRank(actor.getUniqueId()); + if (actorRank == null || !actorRank.hasPermission(GuildPermission.KICK)) { + return GuildResult.failure("Vous n'avez pas la permission d'expulser des membres."); + } + + GuildMember targetMember = guild.getMember(targetUuid); + if (targetMember == null) { + return GuildResult.failure("Ce joueur n'est pas membre de la guilde."); + } + + GuildRank targetRank = guild.getRank(targetMember.getRankId()); + if (targetRank != null && targetRank.getPriority() >= actorRank.getPriority()) { + return GuildResult.failure("Vous ne pouvez pas expulser un membre ayant un rang supĂ©rieur ou Ă©gal au vĂŽtre."); + } + + guild.removeMember(targetUuid); + playerGuildMap.remove(targetUuid); + repository.deleteMemberAsync(targetUuid); + + return GuildResult.success("Le membre a Ă©tĂ© expulsĂ© de la guilde."); + } + + /** + * Permet Ă  un membre de quitter volontairement sa guilde. + * + * @param player Le joueur souhaitant partir. + * @param guild La guilde quittĂ©e. + * @return GuildResult. + */ + public GuildResult leaveGuild(Player player, Guild guild) { + if (player == null || guild == null) { + return GuildResult.failure("ParamĂštres invalides."); + } + + GuildMember member = guild.getMember(player.getUniqueId()); + if (member == null) { + return GuildResult.failure("Vous n'ĂȘtes pas membre de cette guilde."); + } + + GuildRank leaderRank = guild.getLeaderRank(); + if (leaderRank != null && member.getRankId().equalsIgnoreCase(leaderRank.getId())) { + return GuildResult.failure("En tant que Chef, vous ne pouvez pas quitter la guilde. TransfĂ©rez le lead ou dissolvez la guilde."); + } + + guild.removeMember(player.getUniqueId()); + playerGuildMap.remove(player.getUniqueId()); + repository.deleteMemberAsync(player.getUniqueId()); + + return GuildResult.success("Vous avez quittĂ© la guilde."); + } + + /** + * Modifie le rang d'un membre avec contrĂŽle de prioritĂ© hiĂ©rarchique. + * + * @param actor Le joueur initiateur. + * @param guild La guilde concernĂ©e. + * @param targetUuid Le membre cible. + * @param newRankId L'identifiant du nouveau rang. + * @return GuildResult. + */ + public GuildResult setMemberRank(Player actor, Guild guild, UUID targetUuid, String newRankId) { + if (actor == null || guild == null || targetUuid == null || newRankId == null) { + return GuildResult.failure("ParamĂštres invalides."); + } + + GuildRank actorRank = guild.getMemberRank(actor.getUniqueId()); + if (actorRank == null) { + return GuildResult.failure("Vous ne faites pas partie de cette guilde."); + } + + GuildMember targetMember = guild.getMember(targetUuid); + if (targetMember == null) { + return GuildResult.failure("Le joueur cible n'est pas dans la guilde."); + } + + GuildRank newRank = guild.getRank(newRankId); + if (newRank == null) { + return GuildResult.failure("Le rang spĂ©cifiĂ© n'existe pas."); + } + + GuildRank targetCurrentRank = guild.getRank(targetMember.getRankId()); + boolean isPromote = targetCurrentRank == null || newRank.getPriority() > targetCurrentRank.getPriority(); + + // VĂ©rification des permissions + if (isPromote && !actorRank.hasPermission(GuildPermission.PROMOTE)) { + return GuildResult.failure("Vous n'avez pas la permission de promouvoir des membres."); + } + if (!isPromote && !actorRank.hasPermission(GuildPermission.DEMOTE)) { + return GuildResult.failure("Vous n'avez pas la permission de rĂ©trograder des membres."); + } + + // ContrĂŽle hiĂ©rarchique : l'acteur ne peut pas affecter un rang supĂ©rieur ou Ă©gal au sien + GuildRank leaderRank = guild.getLeaderRank(); + boolean isLeader = actorRank.equals(leaderRank); + if (!isLeader && newRank.getPriority() >= actorRank.getPriority()) { + return GuildResult.failure("Vous ne pouvez pas attribuer un rang supĂ©rieur ou Ă©gal au vĂŽtre."); + } + + targetMember.setRankId(newRank.getId()); + repository.saveMemberAsync(targetMember); + + return GuildResult.success("Le rang du membre a Ă©tĂ© mis Ă  jour vers " + newRank.getName() + "."); + } + + // ========================================== + // GESTION DES CLAIMS (TERRITOIRES) + // ========================================== + + /** + * Revendique un chunk territorial pour une guilde en dĂ©bitant son coĂ»t depuis la banque de guilde. + * + * @param player Le joueur initiant le claim. + * @param guild La guilde pour laquelle claimer. + * @param world Le monde Minecraft. + * @param chunkX La coordonnĂ©e X du chunk. + * @param chunkZ La coordonnĂ©e Z du chunk. + * @return GuildResult. + */ + public GuildResult claimChunk(Player player, Guild guild, World world, int chunkX, int chunkZ) { + if (player == null || guild == null || world == null) { + return GuildResult.failure("ParamĂštres invalides."); + } + + String worldName = world.getName(); + + // 1. VĂ©rifier la permission + if (!guild.hasPermission(player.getUniqueId(), GuildPermission.CLAIM)) { + return GuildResult.failure("Votre rang ne vous autorise pas Ă  revendiquer de nouveaux territoires."); + } + + // 2. VĂ©rifier si le chunk est dĂ©jĂ  claimĂ© + if (isChunkClaimed(worldName, chunkX, chunkZ)) { + return GuildResult.failure("Ce chunk est dĂ©jĂ  revendiquĂ©."); + } + + // 3. Calculer le prix du chunk avec la formule spatio-temporelle + double price = priceCalculator.calculateNextChunkPrice(guild); + + // 4. VĂ©rifier si la banque de guilde possĂšde les fonds + if (!guild.hasBalance(price)) { + return GuildResult.failure("La banque de guilde n'a pas assez d'argent (Requis: " + economyManager.format(price) + ", Solde: " + economyManager.format(guild.getBalance()) + ")."); + } + + // 5. DĂ©biter la banque et enregistrer le claim + guild.withdraw(price); + GuildClaim claim = new GuildClaim(0, guild.getId(), worldName, chunkX, chunkZ, System.currentTimeMillis()); + guild.addClaim(claim); + chunkGuildMap.put(getChunkKey(worldName, chunkX, chunkZ), guild.getId()); + + // 6. Sauvegarder en BDD + repository.saveGuildAsync(guild); + repository.saveClaimAsync(claim); + + return GuildResult.success("Chunk revendiquĂ© avec succĂšs pour " + economyManager.format(price) + " ! (Total: " + guild.getClaimCount() + " chunks)"); + } + + /** + * Abandonne un chunk possĂ©dĂ© par la guilde. + * + * @param player Le joueur initiateur. + * @param guild La guilde propriĂ©taire. + * @param world Le monde Minecraft. + * @param chunkX La coordonnĂ©e X du chunk. + * @param chunkZ La coordonnĂ©e Z du chunk. + * @return GuildResult. + */ + public GuildResult unclaimChunk(Player player, Guild guild, World world, int chunkX, int chunkZ) { + if (player == null || guild == null || world == null) { + return GuildResult.failure("ParamĂštres invalides."); + } + + String worldName = world.getName(); + + // 1. VĂ©rifier la permission + if (!guild.hasPermission(player.getUniqueId(), GuildPermission.UNCLAIM)) { + return GuildResult.failure("Votre rang ne vous autorise pas Ă  abandonner des territoires."); + } + + // 2. VĂ©rifier si le chunk appartient Ă  la guilde + if (!guild.isClaimed(worldName, chunkX, chunkZ)) { + return GuildResult.failure("Ce chunk n'appartient pas Ă  votre guilde."); + } + + // 3. EmpĂȘcher d'avoir 0 chunk si c'est le dernier + if (guild.getClaimCount() <= 1) { + return GuildResult.failure("Vous ne pouvez pas abandonner le dernier chunk de votre guilde."); + } + + // 4. Supprimer le claim + guild.removeClaim(worldName, chunkX, chunkZ); + chunkGuildMap.remove(getChunkKey(worldName, chunkX, chunkZ)); + repository.deleteClaimAsync(worldName, chunkX, chunkZ); + + return GuildResult.success("Le chunk a Ă©tĂ© libĂ©rĂ©."); + } + + // ========================================== + // GESTION DE LA BANQUE + // ========================================== + + /** + * DĂ©pose de l'argent depuis le compte personnel d'un joueur vers la banque de guilde. + * + * @param player Le joueur dĂ©posant. + * @param guild La guilde bĂ©nĂ©ficiaire. + * @param amount Le montant Ă  dĂ©poser. + * @return GuildResult. + */ + public GuildResult depositMoney(Player player, Guild guild, double amount) { + if (player == null || guild == null || amount <= 0) { + return GuildResult.failure("Montant invalide."); + } + + if (!guild.hasPermission(player.getUniqueId(), GuildPermission.BANK_DEPOSIT)) { + return GuildResult.failure("Votre rang ne vous autorise pas Ă  dĂ©poser dans la banque."); + } + + if (!economyManager.has(player, amount)) { + return GuildResult.failure("Vous n'avez pas assez d'argent sur votre compte personnel."); + } + + boolean withdrawn = economyManager.withdraw(player, amount); + if (!withdrawn) { + return GuildResult.failure("Échec de la transaction bancaire."); + } + + guild.deposit(amount); + GuildMember member = guild.getMember(player.getUniqueId()); + if (member != null) { + member.addContribution(amount); + repository.saveMemberAsync(member); + } + repository.saveGuildAsync(guild); + + return GuildResult.success("Vous avez dĂ©posĂ© " + economyManager.format(amount) + " dans la banque de guilde ! (Nouveau solde: " + economyManager.format(guild.getBalance()) + ")"); + } + + /** + * Retire de l'argent de la banque de guilde vers le compte d'un joueur. + * + * @param player Le joueur bĂ©nĂ©ficiaire. + * @param guild La guilde dĂ©bitĂ©e. + * @param amount Le montant Ă  retirer. + * @return GuildResult. + */ + public GuildResult withdrawMoney(Player player, Guild guild, double amount) { + if (player == null || guild == null || amount <= 0) { + return GuildResult.failure("Montant invalide."); + } + + if (!guild.hasPermission(player.getUniqueId(), GuildPermission.BANK_WITHDRAW)) { + return GuildResult.failure("Votre rang ne vous autorise pas Ă  retirer de l'argent de la banque."); + } + + if (!guild.hasBalance(amount)) { + return GuildResult.failure("Le solde de la banque de guilde est insuffisant (Solde actuel: " + economyManager.format(guild.getBalance()) + ")."); + } + + boolean withdrawnFromGuild = guild.withdraw(amount); + if (!withdrawnFromGuild) { + return GuildResult.failure("Impossible de dĂ©biter la banque de guilde."); + } + + economyManager.deposit(player, amount); + repository.saveGuildAsync(guild); + + return GuildResult.success("Vous avez retirĂ© " + economyManager.format(amount) + " de la banque de guilde."); + } + + // ========================================== + // GETTERS & LOOKUPS RAPIDES + // ========================================== + + public Guild getGuildById(UUID guildId) { + if (guildId == null) return null; + return guildsById.get(guildId); + } + + public Guild getGuildByName(String name) { + if (name == null) return null; + UUID id = guildsByName.get(name.toLowerCase()); + return id != null ? guildsById.get(id) : null; + } + + public Guild getGuildByPlayer(UUID playerUuid) { + if (playerUuid == null) return null; + UUID guildId = playerGuildMap.get(playerUuid); + return guildId != null ? guildsById.get(guildId) : null; + } + + public boolean isInGuild(UUID playerUuid) { + return playerGuildMap.containsKey(playerUuid); + } + + public Guild getGuildAt(String world, int chunkX, int chunkZ) { + UUID guildId = chunkGuildMap.get(getChunkKey(world, chunkX, chunkZ)); + return guildId != null ? guildsById.get(guildId) : null; + } + + public boolean isChunkClaimed(String world, int chunkX, int chunkZ) { + return chunkGuildMap.containsKey(getChunkKey(world, chunkX, chunkZ)); + } + + public Collection getAllGuilds() { + return Collections.unmodifiableCollection(guildsById.values()); + } + + public ChunkPriceCalculator getPriceCalculator() { + return priceCalculator; + } + + public EconomyManager getEconomyManager() { + return economyManager; + } + + public InviteManager getInviteManager() { + return inviteManager; + } + + // ========================================== + // MÉTHODES INTERNES D'INDEXATION + // ========================================== + + private void indexGuild(Guild guild) { + guildsById.put(guild.getId(), guild); + guildsByName.put(guild.getName().toLowerCase(), guild.getId()); + for (GuildMember member : guild.getMembers()) { + playerGuildMap.put(member.getPlayerUuid(), guild.getId()); + } + for (GuildClaim claim : guild.getClaims()) { + chunkGuildMap.put(getChunkKey(claim.getWorldName(), claim.getChunkX(), claim.getChunkZ()), guild.getId()); + } + } + + private void unindexGuild(Guild guild) { + guildsById.remove(guild.getId()); + guildsByName.remove(guild.getName().toLowerCase()); + for (GuildMember member : guild.getMembers()) { + playerGuildMap.remove(member.getPlayerUuid()); + } + for (GuildClaim claim : guild.getClaims()) { + chunkGuildMap.remove(getChunkKey(claim.getWorldName(), claim.getChunkX(), claim.getChunkZ())); + } + } + + private String getChunkKey(String world, int x, int z) { + return (world != null ? world.toLowerCase() : "world") + ":" + x + "," + z; + } +} diff --git a/src/main/java/fr/luc/gamingcore/manager/GuildResult.java b/src/main/java/fr/luc/gamingcore/manager/GuildResult.java new file mode 100644 index 0000000..5dda010 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/manager/GuildResult.java @@ -0,0 +1,53 @@ +package fr.luc.gamingcore.manager; + +/** + * Objet de rĂ©sultat immuable retournĂ© par les mĂ©thodes mĂ©tier du GuildManager. + */ +public final class GuildResult { + + private final boolean success; + private final String message; + + private GuildResult(boolean success, String message) { + this.success = success; + this.message = message != null ? message : ""; + } + + /** + * CrĂ©e un rĂ©sultat de succĂšs avec un message optionnel. + * + * @param message Message d'information ou de confirmation. + * @return GuildResult avec success = true. + */ + public static GuildResult success(String message) { + return new GuildResult(true, message); + } + + /** + * CrĂ©e un rĂ©sultat d'Ă©chec avec un message explicatif. + * + * @param errorMessage Raison de l'Ă©chec de l'action. + * @return GuildResult avec success = false. + */ + public static GuildResult failure(String errorMessage) { + return new GuildResult(false, errorMessage); + } + + /** + * Indique si l'opĂ©ration a rĂ©ussi. + * + * @return true en cas de succĂšs, false en cas d'Ă©chec. + */ + public boolean isSuccess() { + return success; + } + + /** + * Obtient le message descriptif du rĂ©sultat. + * + * @return Le message formattĂ©. + */ + public String getMessage() { + return message; + } +} diff --git a/src/main/java/fr/luc/gamingcore/manager/InviteManager.java b/src/main/java/fr/luc/gamingcore/manager/InviteManager.java new file mode 100644 index 0000000..cb7d705 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/manager/InviteManager.java @@ -0,0 +1,112 @@ +package fr.luc.gamingcore.manager; + +import fr.luc.gamingcore.model.Guild; +import fr.luc.gamingcore.model.GuildInvite; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Gestionnaire des invitations temporaires envoyĂ©es aux joueurs pour intĂ©grer une guilde. + */ +public class InviteManager { + + // Map: Target Player UUID -> Map(Guild UUID -> GuildInvite) + private final Map> invites = new ConcurrentHashMap<>(); + private final long defaultDurationMillis = 60L * 1000L; // 60 secondes + + /** + * Envoie ou renouvelle une invitation pour un joueur vers une guilde. + * + * @param guild La guilde Ă©mettrice. + * @param sender Le joueur qui a initiĂ© l'invitation. + * @param target Le joueur invitĂ©. + * @return L'instance GuildInvite créée. + */ + public GuildInvite sendInvite(Guild guild, Player sender, Player target) { + return sendInvite(guild, sender, target, defaultDurationMillis); + } + + /** + * Envoie une invitation avec une durĂ©e personnalisĂ©e. + * + * @param guild La guilde Ă©mettrice. + * @param sender Le joueur Ă©metteur. + * @param target Le joueur destinataire. + * @param durationMillis DurĂ©e de validitĂ© de l'invitation en millisecondes. + * @return L'instance GuildInvite créée. + */ + public GuildInvite sendInvite(Guild guild, Player sender, Player target, long durationMillis) { + if (guild == null || sender == null || target == null) { + return null; + } + + GuildInvite invite = new GuildInvite(guild.getId(), target.getUniqueId(), sender.getUniqueId(), durationMillis); + invites.computeIfAbsent(target.getUniqueId(), k -> new ConcurrentHashMap<>()) + .put(guild.getId(), invite); + + return invite; + } + + /** + * RĂ©cupĂšre une invitation active et non expirĂ©e pour un joueur et une guilde donnĂ©s. + * + * @param target Le joueur invitĂ©. + * @param guildId L'UUID de la guilde. + * @return L'invitation valide ou null si inexistante ou expirĂ©e. + */ + public GuildInvite getValidInvite(Player target, UUID guildId) { + if (target == null || guildId == null) return null; + Map playerInvites = invites.get(target.getUniqueId()); + if (playerInvites == null) return null; + + GuildInvite invite = playerInvites.get(guildId); + if (invite != null) { + if (invite.isExpired()) { + playerInvites.remove(guildId); + return null; + } + return invite; + } + return null; + } + + /** + * VĂ©rifie si un joueur possĂšde une invitation valide non expirĂ©e pour une guilde. + * + * @param target Le joueur cible. + * @param guildId L'UUID de la guilde. + * @return true si une invitation valide existe, false sinon. + */ + public boolean hasValidInvite(Player target, UUID guildId) { + return getValidInvite(target, guildId) != null; + } + + /** + * Supprime une invitation (suite Ă  une acceptation, un refus ou une annulation). + * + * @param target Le joueur invitĂ©. + * @param guildId L'UUID de la guilde. + */ + public void removeInvite(Player target, UUID guildId) { + if (target == null || guildId == null) return; + Map playerInvites = invites.get(target.getUniqueId()); + if (playerInvites != null) { + playerInvites.remove(guildId); + if (playerInvites.isEmpty()) { + invites.remove(target.getUniqueId()); + } + } + } + + /** + * Nettoie toutes les invitations expirĂ©es du cache mĂ©moire. + */ + public void cleanExpired() { + invites.forEach((uuid, map) -> { + map.values().removeIf(GuildInvite::isExpired); + }); + invites.entrySet().removeIf(entry -> entry.getValue().isEmpty()); + } +} diff --git a/src/main/java/fr/luc/gamingcore/model/Guild.java b/src/main/java/fr/luc/gamingcore/model/Guild.java new file mode 100644 index 0000000..e6268a3 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/model/Guild.java @@ -0,0 +1,526 @@ +package fr.luc.gamingcore.model; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * EntitĂ© principale reprĂ©sentant une guilde complĂšte. + * Regroupe ses membres, ses rangs hiĂ©rarchiques, ses claims territoriaux et sa banque. + */ +public class Guild { + + private final UUID id; + private String name; + private String tag; + private String description; + private double balance; + + // Home de guilde + private String homeWorld; + private double homeX; + private double homeY; + private double homeZ; + private float homeYaw; + private float homePitch; + + private final long createdAt; + + // Collections + private final Map members = new ConcurrentHashMap<>(); + private final Map ranks = new ConcurrentHashMap<>(); + private final List claims = new CopyOnWriteArrayList<>(); + + /** + * Constructeur complet d'une guilde. + * + * @param id Identifiant unique UUID de la guilde. + * @param name Nom de la guilde (ex: "Imperium"). + * @param tag Tag/Sigle court de la guilde (ex: "IMP"). + * @param description Description textuelle de la guilde. + * @param balance Solde bancaire initial de la guilde. + * @param createdAt Date et heure de crĂ©ation en millisecondes. + */ + public Guild(UUID id, String name, String tag, String description, double balance, long createdAt) { + this.id = Objects.requireNonNull(id, "L'UUID de la guilde ne peut pas ĂȘtre nul."); + this.name = Objects.requireNonNull(name, "Le nom de la guilde ne peut pas ĂȘtre nul."); + this.tag = tag != null ? tag : ""; + this.description = description != null ? description : "Une nouvelle guilde prometteuse."; + this.balance = Math.max(0.0, balance); + this.createdAt = createdAt > 0 ? createdAt : System.currentTimeMillis(); + } + + /** + * Obtient l'identifiant unique UUID de la guilde. + * + * @return L'UUID de la guilde. + */ + public UUID getId() { + return id; + } + + /** + * Obtient le nom unique de la guilde. + * + * @return Le nom de la guilde. + */ + public String getName() { + return name; + } + + /** + * Modifie le nom de la guilde. + * + * @param name Le nouveau nom. + */ + public void setName(String name) { + this.name = Objects.requireNonNull(name, "Le nom ne peut pas ĂȘtre nul."); + } + + /** + * Obtient le tag / prĂ©fixe court de la guilde. + * + * @return Le tag (ex: "LEG"). + */ + public String getTag() { + return tag; + } + + /** + * Modifie le tag court de la guilde. + * + * @param tag Le nouveau tag. + */ + public void setTag(String tag) { + this.tag = tag != null ? tag : ""; + } + + /** + * Obtient la description textuelle de la guilde. + * + * @return La description. + */ + public String getDescription() { + return description; + } + + /** + * Modifie la description de la guilde. + * + * @param description La nouvelle description. + */ + public void setDescription(String description) { + this.description = description != null ? description : ""; + } + + /** + * Obtient le solde actuel de la banque de guilde. + * + * @return Le montant disponible. + */ + public double getBalance() { + return balance; + } + + /** + * DĂ©finit directement le solde bancaire de la guilde. + * + * @param balance Le nouveau montant. + */ + public void setBalance(double balance) { + this.balance = Math.max(0.0, balance); + } + + /** + * DĂ©pose une somme d'argent dans la banque de guilde. + * + * @param amount Montant positif Ă  ajouter. + */ + public void deposit(double amount) { + if (amount > 0) { + this.balance += amount; + } + } + + /** + * Tente de retirer une somme d'argent depuis la banque de guilde. + * + * @param amount Montant Ă  retirer. + * @return true si le retrait a rĂ©ussi, false si le solde Ă©tait insuffisant. + */ + public boolean withdraw(double amount) { + if (amount > 0 && this.balance >= amount) { + this.balance -= amount; + return true; + } + return false; + } + + /** + * VĂ©rifie si la banque de guilde possĂšde au moins un montant spĂ©cifiĂ©. + * + * @param amount Le montant requis. + * @return true si balance >= amount. + */ + public boolean hasBalance(double amount) { + return this.balance >= amount; + } + + /** + * Obtient la date et heure de crĂ©ation de la guilde en millisecondes. + * + * @return Le timestamp de crĂ©ation. + */ + public long getCreatedAt() { + return createdAt; + } + + // ========================================== + // GESTION DU HOME DE GUILDE + // ========================================== + + /** + * DĂ©finit les coordonnĂ©es du point de ralliement (home) de la guilde. + * + * @param loc L'emplacement Bukkit du home. + */ + public void setHome(Location loc) { + if (loc != null && loc.getWorld() != null) { + this.homeWorld = loc.getWorld().getName(); + this.homeX = loc.getX(); + this.homeY = loc.getY(); + this.homeZ = loc.getZ(); + this.homeYaw = loc.getYaw(); + this.homePitch = loc.getPitch(); + } else { + this.homeWorld = null; + } + } + + /** + * RĂ©cupĂšre l'emplacement Bukkit du home de guilde si configurĂ© et si le monde existe. + * + * @return La Location du home ou null si aucun home n'est configurĂ©. + */ + public Location getHomeLocation() { + if (homeWorld == null) return null; + World world = Bukkit.getWorld(homeWorld); + if (world == null) return null; + return new Location(world, homeX, homeY, homeZ, homeYaw, homePitch); + } + + public String getHomeWorld() { + return homeWorld; + } + + public double getHomeX() { + return homeX; + } + + public double getHomeY() { + return homeY; + } + + public double getHomeZ() { + return homeZ; + } + + public float getHomeYaw() { + return homeYaw; + } + + public float getHomePitch() { + return homePitch; + } + + public void setHomeCoordinates(String world, double x, double y, double z, float yaw, float pitch) { + this.homeWorld = world; + this.homeX = x; + this.homeY = y; + this.homeZ = z; + this.homeYaw = yaw; + this.homePitch = pitch; + } + + // ========================================== + // GESTION DES MEMBRES + // ========================================== + + /** + * Obtient la liste de tous les membres de la guilde. + * + * @return Une collection non modifiable des membres. + */ + public Collection getMembers() { + return Collections.unmodifiableCollection(members.values()); + } + + /** + * Obtient le nombre total de membres dans la guilde. + * + * @return Le nombre de membres. + */ + public int getMemberCount() { + return members.size(); + } + + /** + * RĂ©cupĂšre un membre spĂ©cifique par son UUID. + * + * @param playerUuid Le UUID du joueur. + * @return L'instance GuildMember ou null si le joueur n'est pas membre. + */ + public GuildMember getMember(UUID playerUuid) { + if (playerUuid == null) return null; + return members.get(playerUuid); + } + + /** + * VĂ©rifie si un joueur est membre de cette guilde. + * + * @param playerUuid Le UUID du joueur. + * @return true si le joueur est membre, false sinon. + */ + public boolean isMember(UUID playerUuid) { + if (playerUuid == null) return false; + return members.containsKey(playerUuid); + } + + /** + * Ajoute un nouveau membre dans la guilde. + * + * @param member Le membre Ă  ajouter. + */ + public void addMember(GuildMember member) { + if (member != null) { + members.put(member.getPlayerUuid(), member); + } + } + + /** + * Retire un membre de la guilde. + * + * @param playerUuid Le UUID du joueur Ă  retirer. + * @return Le membre retirĂ© ou null s'il n'Ă©tait pas prĂ©sent. + */ + public GuildMember removeMember(UUID playerUuid) { + if (playerUuid == null) return null; + return members.remove(playerUuid); + } + + // ========================================== + // GESTION DES RANGS + // ========================================== + + /** + * Obtient tous les rangs configurĂ©s pour cette guilde. + * + * @return Collection non modifiable des rangs. + */ + public Collection getRanks() { + return Collections.unmodifiableCollection(ranks.values()); + } + + /** + * RĂ©cupĂšre un rang par son identifiant unique. + * + * @param rankId L'identifiant du rang. + * @return L'instance GuildRank ou null. + */ + public GuildRank getRank(String rankId) { + if (rankId == null) return null; + return ranks.get(rankId.toLowerCase()); + } + + /** + * Ajoute ou met Ă  jour un rang dans la guilde. + * + * @param rank Le rang Ă  enregistrer. + */ + public void addRank(GuildRank rank) { + if (rank != null) { + ranks.put(rank.getId().toLowerCase(), rank); + } + } + + /** + * Supprime un rang de la guilde. + * + * @param rankId L'ID du rang Ă  supprimer. + * @return Le rang supprimĂ© ou null. + */ + public GuildRank removeRank(String rankId) { + if (rankId == null) return null; + return ranks.remove(rankId.toLowerCase()); + } + + /** + * Obtient le rang par dĂ©faut attribuĂ© aux nouvelles recrues. + * + * @return Le GuildRank par dĂ©faut ou le rang avec la plus faible prioritĂ©. + */ + public GuildRank getDefaultRank() { + for (GuildRank rank : ranks.values()) { + if (rank.isDefault()) { + return rank; + } + } + // Fallback: rang avec la plus faible prioritĂ© + return ranks.values().stream() + .min(Comparator.comparingInt(GuildRank::getPriority)) + .orElse(null); + } + + /** + * Obtient le rang de Chef (la prioritĂ© la plus Ă©levĂ©e). + * + * @return Le GuildRank avec la prioritĂ© maximale. + */ + public GuildRank getLeaderRank() { + return ranks.values().stream() + .max(Comparator.comparingInt(GuildRank::getPriority)) + .orElse(null); + } + + /** + * RĂ©cupĂšre le membre qui occupe le rang de Chef de la guilde. + * + * @return Le GuildMember chef ou null. + */ + public GuildMember getLeaderMember() { + GuildRank leaderRank = getLeaderRank(); + if (leaderRank == null) return null; + for (GuildMember member : members.values()) { + if (member.getRankId().equalsIgnoreCase(leaderRank.getId())) { + return member; + } + } + return null; + } + + /** + * RĂ©cupĂšre le rang actuel d'un membre de la guilde. + * + * @param playerUuid Le UUID du joueur. + * @return Son GuildRank ou null s'il n'est pas membre ou n'a pas de rang valide. + */ + public GuildRank getMemberRank(UUID playerUuid) { + GuildMember member = getMember(playerUuid); + if (member == null) return null; + return getRank(member.getRankId()); + } + + /** + * VĂ©rifie si un membre de la guilde possĂšde une permission donnĂ©e. + * + * @param playerUuid Le UUID du joueur. + * @param permission La permission recherchĂ©e. + * @return true si le joueur est membre et que son rang possĂšde la permission, false sinon. + */ + public boolean hasPermission(UUID playerUuid, GuildPermission permission) { + GuildRank rank = getMemberRank(playerUuid); + if (rank == null) return false; + return rank.hasPermission(permission); + } + + // ========================================== + // GESTION DES CLAIMS (TERRITOIRES) + // ========================================== + + /** + * Obtient la liste de tous les territoires (chunks) possĂ©dĂ©s par la guilde. + * + * @return Une liste non modifiable des claims. + */ + public List getClaims() { + return Collections.unmodifiableList(claims); + } + + /** + * Obtient le nombre total de chunks revendiquĂ©s par la guilde. + * + * @return Le nombre de claims. + */ + public int getClaimCount() { + return claims.size(); + } + + /** + * Ajoute un nouveau chunk revendiquĂ© Ă  la guilde. + * + * @param claim Le claim Ă  ajouter. + */ + public void addClaim(GuildClaim claim) { + if (claim != null && !claims.contains(claim)) { + claims.add(claim); + } + } + + /** + * Retire un claim de la guilde selon ses coordonnĂ©es. + * + * @param world Nom du monde. + * @param chunkX CoordonnĂ©e X du chunk. + * @param chunkZ CoordonnĂ©e Z du chunk. + * @return Le GuildClaim supprimĂ© ou null si non trouvĂ©. + */ + public GuildClaim removeClaim(String world, int chunkX, int chunkZ) { + for (GuildClaim claim : claims) { + if (claim.matches(world, chunkX, chunkZ)) { + claims.remove(claim); + return claim; + } + } + return null; + } + + /** + * VĂ©rifie si un chunk spĂ©cifique appartient Ă  cette guilde. + * + * @param world Nom du monde. + * @param chunkX CoordonnĂ©e X du chunk. + * @param chunkZ CoordonnĂ©e Z du chunk. + * @return true si la guilde possĂšde ce chunk, false sinon. + */ + public boolean isClaimed(String world, int chunkX, int chunkZ) { + for (GuildClaim claim : claims) { + if (claim.matches(world, chunkX, chunkZ)) { + return true; + } + } + return false; + } + + /** + * Calcule le nombre de chunks achetĂ©s par la guilde durant une pĂ©riode rĂ©cente (ex: les derniĂšres 24h). + * + * @param sinceMillis Le timestamp Ă  partir duquel compter les claims (ex: now - 24h). + * @return Le nombre de claims rĂ©cents. + */ + public int getRecentClaimsCount(long sinceMillis) { + int count = 0; + for (GuildClaim claim : claims) { + if (claim.getClaimedAt() >= sinceMillis) { + count++; + } + } + return count; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Guild guild = (Guild) o; + return Objects.equals(id, guild.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/fr/luc/gamingcore/model/GuildClaim.java b/src/main/java/fr/luc/gamingcore/model/GuildClaim.java new file mode 100644 index 0000000..9eba327 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/model/GuildClaim.java @@ -0,0 +1,145 @@ +package fr.luc.gamingcore.model; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.World; + +import java.util.Objects; +import java.util.UUID; + +/** + * ReprĂ©sente un territoire (chunk de 16x16 blocs) revendiquĂ© par une guilde. + */ +public class GuildClaim { + + private int id; + private final UUID guildId; + private final String worldName; + private final int chunkX; + private final int chunkZ; + private final long claimedAt; + + /** + * Constructeur d'un claim de territoire de guilde. + * + * @param id Identifiant unique du claim en base de donnĂ©es (0 si nouveau). + * @param guildId Identifiant UUID de la guilde propriĂ©taire. + * @param worldName Nom du monde Minecraft. + * @param chunkX CoordonnĂ©e X du chunk. + * @param chunkZ CoordonnĂ©e Z du chunk. + * @param claimedAt Horodatage en millisecondes du moment de l'achat du claim. + */ + public GuildClaim(int id, UUID guildId, String worldName, int chunkX, int chunkZ, long claimedAt) { + this.id = id; + this.guildId = Objects.requireNonNull(guildId, "Le UUID de la guilde ne peut pas ĂȘtre nul."); + this.worldName = Objects.requireNonNull(worldName, "Le nom du monde ne peut pas ĂȘtre nul."); + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.claimedAt = claimedAt > 0 ? claimedAt : System.currentTimeMillis(); + } + + /** + * Obtient l'identifiant de base de donnĂ©es du claim. + * + * @return L'ID numĂ©rique. + */ + public int getId() { + return id; + } + + /** + * Modifie l'identifiant gĂ©nĂ©rĂ© aprĂšs insertion en base de donnĂ©es. + * + * @param id Le nouvel ID. + */ + public void setId(int id) { + this.id = id; + } + + /** + * Obtient le UUID de la guilde propriĂ©taire de ce territoire. + * + * @return L'UUID de la guilde. + */ + public UUID getGuildId() { + return guildId; + } + + /** + * Obtient le nom du monde oĂč se situe ce chunk. + * + * @return Le nom du monde Minecraft. + */ + public String getWorldName() { + return worldName; + } + + /** + * Obtient la coordonnĂ©e X du chunk. + * + * @return L'abscisse de chunk. + */ + public int getChunkX() { + return chunkX; + } + + /** + * Obtient la coordonnĂ©e Z du chunk. + * + * @return L'ordonnĂ©e de chunk. + */ + public int getChunkZ() { + return chunkZ; + } + + /** + * Obtient la date et heure de revendication du chunk en millisecondes. + * UtilisĂ© pour calculer la surchauffe temporelle glissante sur 24h. + * + * @return Timestamp d'achat. + */ + public long getClaimedAt() { + return claimedAt; + } + + /** + * VĂ©rifie si ce claim correspond aux coordonnĂ©es et monde spĂ©cifiĂ©s. + * + * @param world Nom du monde. + * @param chunkX CoordonnĂ©e X du chunk. + * @param chunkZ CoordonnĂ©e Z du chunk. + * @return true si les coordonnĂ©es correspondent exactement, false sinon. + */ + public boolean matches(String world, int chunkX, int chunkZ) { + return this.worldName.equalsIgnoreCase(world) && this.chunkX == chunkX && this.chunkZ == chunkZ; + } + + /** + * RĂ©cupĂšre l'instance Bukkit Chunk correspondante si le monde est chargĂ©. + * + * @return Le Chunk Bukkit ou null si le monde n'est pas chargĂ©. + */ + public Chunk toBukkitChunk() { + World world = Bukkit.getWorld(worldName); + if (world == null) return null; + return world.getChunkAt(chunkX, chunkZ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GuildClaim that = (GuildClaim) o; + return chunkX == that.chunkX && chunkZ == that.chunkZ && Objects.equals(worldName, that.worldName); + } + + @Override + public int hashCode() { + return Objects.hash(worldName, chunkX, chunkZ); + } + + @Override + public String toString() { + return worldName + ":" + chunkX + "," + chunkZ; + } +} diff --git a/src/main/java/fr/luc/gamingcore/model/GuildInvite.java b/src/main/java/fr/luc/gamingcore/model/GuildInvite.java new file mode 100644 index 0000000..6ae4ba5 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/model/GuildInvite.java @@ -0,0 +1,96 @@ +package fr.luc.gamingcore.model; + +import java.util.Objects; +import java.util.UUID; + +/** + * ReprĂ©sente une invitation temporaire envoyĂ©e Ă  un joueur pour rejoindre une guilde. + */ +public class GuildInvite { + + private final UUID guildId; + private final UUID targetPlayerUuid; + private final UUID senderUuid; + private final long createdAt; + private final long expireAt; + + /** + * Constructeur d'une invitation de guilde. + * + * @param guildId UUID de la guilde concernĂ©e. + * @param targetPlayerUuid UUID du joueur invitĂ©. + * @param senderUuid UUID du joueur ayant envoyĂ© l'invitation. + * @param durationMillis DurĂ©e de validitĂ© de l'invitation en millisecondes. + */ + public GuildInvite(UUID guildId, UUID targetPlayerUuid, UUID senderUuid, long durationMillis) { + this.guildId = Objects.requireNonNull(guildId, "Le UUID de la guilde ne peut pas ĂȘtre nul."); + this.targetPlayerUuid = Objects.requireNonNull(targetPlayerUuid, "Le UUID du joueur cible ne peut pas ĂȘtre nul."); + this.senderUuid = Objects.requireNonNull(senderUuid, "Le UUID de l'expĂ©diteur ne peut pas ĂȘtre nul."); + this.createdAt = System.currentTimeMillis(); + this.expireAt = this.createdAt + Math.max(1000L, durationMillis); + } + + /** + * Obtient le UUID de la guilde qui a Ă©mis l'invitation. + * + * @return L'UUID de la guilde. + */ + public UUID getGuildId() { + return guildId; + } + + /** + * Obtient le UUID du joueur recevant l'invitation. + * + * @return L'UUID du destinataire. + */ + public UUID getTargetPlayerUuid() { + return targetPlayerUuid; + } + + /** + * Obtient le UUID du joueur qui a initiĂ© l'invitation. + * + * @return L'UUID de l'Ă©metteur. + */ + public UUID getSenderUuid() { + return senderUuid; + } + + /** + * Obtient la date et heure d'Ă©mission de l'invitation en millisecondes. + * + * @return Timestamp de crĂ©ation. + */ + public long getCreatedAt() { + return createdAt; + } + + /** + * Obtient la date et heure d'expiration de l'invitation en millisecondes. + * + * @return Timestamp d'expiration. + */ + public long getExpireAt() { + return expireAt; + } + + /** + * VĂ©rifie si l'invitation a dĂ©passĂ© son dĂ©lai de validitĂ©. + * + * @return true si l'invitation est expirĂ©e, false si elle est encore valide. + */ + public boolean isExpired() { + return System.currentTimeMillis() > expireAt; + } + + /** + * Obtient le nombre de secondes restantes avant expiration. + * + * @return Les secondes restantes (0 si expirĂ©e). + */ + public long getRemainingSeconds() { + long remaining = (expireAt - System.currentTimeMillis()) / 1000L; + return Math.max(0L, remaining); + } +} diff --git a/src/main/java/fr/luc/gamingcore/model/GuildMember.java b/src/main/java/fr/luc/gamingcore/model/GuildMember.java new file mode 100644 index 0000000..9906e77 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/model/GuildMember.java @@ -0,0 +1,132 @@ +package fr.luc.gamingcore.model; + +import java.util.Objects; +import java.util.UUID; + +/** + * ReprĂ©sente un joueur membre d'une guilde. + */ +public class GuildMember { + + private final UUID playerUuid; + private final UUID guildId; + private String rankId; + private final long joinedAt; + private long lastOnline; + private double contributedMoney; + + /** + * Constructeur d'un membre de guilde. + * + * @param playerUuid Identifiant UUID du joueur. + * @param guildId Identifiant UUID de la guilde. + * @param rankId Identifiant du rang occupĂ© par le membre. + * @param joinedAt Horodatage en millisecondes de l'adhĂ©sion Ă  la guilde. + * @param lastOnline Horodatage en millisecondes de la derniĂšre connexion. + * @param contributedMoney Montant total d'argent dĂ©posĂ© par ce joueur dans la banque de guilde. + */ + public GuildMember(UUID playerUuid, UUID guildId, String rankId, long joinedAt, long lastOnline, double contributedMoney) { + this.playerUuid = Objects.requireNonNull(playerUuid, "Le UUID du joueur ne peut pas ĂȘtre nul."); + this.guildId = Objects.requireNonNull(guildId, "Le UUID de la guilde ne peut pas ĂȘtre nul."); + this.rankId = Objects.requireNonNull(rankId, "Le rangId ne peut pas ĂȘtre nul."); + this.joinedAt = joinedAt > 0 ? joinedAt : System.currentTimeMillis(); + this.lastOnline = lastOnline > 0 ? lastOnline : System.currentTimeMillis(); + this.contributedMoney = Math.max(0.0, contributedMoney); + } + + /** + * Obtient le UUID du joueur. + * + * @return L'UUID unique du joueur. + */ + public UUID getPlayerUuid() { + return playerUuid; + } + + /** + * Obtient le UUID de la guilde Ă  laquelle le membre appartient. + * + * @return L'UUID de la guilde. + */ + public UUID getGuildId() { + return guildId; + } + + /** + * Obtient l'identifiant du rang du membre. + * + * @return L'ID du rang. + */ + public String getRankId() { + return rankId; + } + + /** + * Modifie le rang du membre au sein de sa guilde. + * + * @param rankId Le nouvel identifiant de rang. + */ + public void setRankId(String rankId) { + this.rankId = Objects.requireNonNull(rankId, "Le rangId ne peut pas ĂȘtre nul."); + } + + /** + * Obtient la date et l'heure d'adhĂ©sion du membre en millisecondes. + * + * @return Timestamp d'arrivĂ©e dans la guilde. + */ + public long getJoinedAt() { + return joinedAt; + } + + /** + * Obtient la date et l'heure de derniĂšre connexion connue en millisecondes. + * + * @return Timestamp de derniĂšre prĂ©sence. + */ + public long getLastOnline() { + return lastOnline; + } + + /** + * Met Ă  jour l'horodatage de derniĂšre connexion du membre. + * + * @param lastOnline Nouveau timestamp. + */ + public void setLastOnline(long lastOnline) { + this.lastOnline = lastOnline; + } + + /** + * Obtient la somme totale dĂ©posĂ©e par le membre dans la banque de guilde depuis son arrivĂ©e. + * + * @return Montant d'argent total contribuĂ©. + */ + public double getContributedMoney() { + return contributedMoney; + } + + /** + * Ajoute une somme au montant total des contributions financiĂšres du membre. + * + * @param amount Montant positif Ă  ajouter. + */ + public void addContribution(double amount) { + if (amount > 0) { + this.contributedMoney += amount; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GuildMember that = (GuildMember) o; + return Objects.equals(playerUuid, that.playerUuid); + } + + @Override + public int hashCode() { + return Objects.hash(playerUuid); + } +} diff --git a/src/main/java/fr/luc/gamingcore/model/GuildPermission.java b/src/main/java/fr/luc/gamingcore/model/GuildPermission.java new file mode 100644 index 0000000..c94e1b0 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/model/GuildPermission.java @@ -0,0 +1,118 @@ +package fr.luc.gamingcore.model; + +/** + * ÉnumĂ©ration des permissions granulaires attribuables aux rangs de guilde. + */ +public enum GuildPermission { + + // --- Gestion Territoriale --- + /** + * Permet de revendiquer (claim) un nouveau chunk pour la guilde. + */ + CLAIM("Revendiquer un chunk", "Territoire"), + + /** + * Permet d'abandonner (unclaim) un chunk possĂ©dĂ© par la guilde. + */ + UNCLAIM("LibĂ©rer un chunk", "Territoire"), + + /** + * Permet de dĂ©finir le point de ralliement (home) de la guilde. + */ + SET_HOME("DĂ©finir le home de guilde", "Territoire"), + + /** + * Permet de se tĂ©lĂ©porter au point de ralliement de la guilde. + */ + HOME("Se tĂ©lĂ©porter au home de guilde", "Territoire"), + + // --- Gestion des Membres --- + /** + * Permet d'inviter de nouveaux joueurs Ă  rejoindre la guilde. + */ + INVITE("Inviter des joueurs", "Membres"), + + /** + * Permet d'expulser un membre ayant un rang strictement infĂ©rieur. + */ + KICK("Expulser des membres", "Membres"), + + /** + * Permet de promouvoir un membre Ă  un rang supĂ©rieur (infĂ©rieur au sien). + */ + PROMOTE("Promouvoir des membres", "Membres"), + + /** + * Permet de rĂ©trograder le rang d'un membre infĂ©rieur. + */ + DEMOTE("RĂ©trograder des membres", "Membres"), + + // --- Administration Interne --- + /** + * Permet de crĂ©er, modifier, renommer et configurer les permissions des rangs de la guilde. + */ + MANAGE_RANKS("GĂ©rer les rangs et permissions", "Administration"), + + /** + * Permet de modifier les informations de la guilde (description, tag). + */ + MANAGE_GUILD("Modifier les infos de guilde", "Administration"), + + // --- Gestion Économique --- + /** + * Permet de dĂ©poser de l'argent dans la banque de guilde. + */ + BANK_DEPOSIT("DĂ©poser dans la banque", "Économie"), + + /** + * Permet de retirer de l'argent depuis la banque de guilde. + */ + BANK_WITHDRAW("Retirer de la banque", "Économie"), + + // --- Interactions dans les Claims --- + /** + * Permet de poser des blocs dans les territoires de la guilde. + */ + BUILD("Poser des blocs", "Interactions"), + + /** + * Permet de casser des blocs dans les territoires de la guilde. + */ + BREAK("Casser des blocs", "Interactions"), + + /** + * Permet d'interagir avec les portes, boutons, leviers et trappes. + */ + INTERACT("Utiliser portes / boutons / leviers", "Interactions"), + + /** + * Permet d'ouvrir les conteneurs (coffres, fours, shulkers, barils). + */ + CONTAINER("Ouvrir les coffres et conteneurs", "Interactions"); + + private final String description; + private final String category; + + GuildPermission(String description, String category) { + this.description = description; + this.category = category; + } + + /** + * Obtient la description textuelle lisible de la permission. + * + * @return La description en français de la permission. + */ + public String getDescription() { + return description; + } + + /** + * Obtient la catĂ©gorie thĂ©matique de la permission. + * + * @return Le nom de la catĂ©gorie (ex: Territoire, Membres, Économie). + */ + public String getCategory() { + return category; + } +} diff --git a/src/main/java/fr/luc/gamingcore/model/GuildRank.java b/src/main/java/fr/luc/gamingcore/model/GuildRank.java new file mode 100644 index 0000000..767c886 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/model/GuildRank.java @@ -0,0 +1,175 @@ +package fr.luc.gamingcore.model; + +import java.util.*; + +/** + * ReprĂ©sente un rang personnalisable au sein d'une guilde. + * Chaque guilde possĂšde ses propres rangs avec une prioritĂ© hiĂ©rarchique et un ensemble de permissions. + */ +public class GuildRank { + + private final String id; + private final UUID guildId; + private String name; + private int priority; + private boolean isDefault; + private final Set permissions; + + /** + * Constructeur d'un rang de guilde. + * + * @param id Identifiant unique du rang (UUID ou clĂ© unique). + * @param guildId Identifiant de la guilde propriĂ©taire du rang. + * @param name Nom d'affichage du rang (ex: Chef, Officier, Soldat). + * @param priority Niveau de prioritĂ© hiĂ©rarchique (ex: 100 pour le chef, 1 pour recrue). + * @param isDefault Indique si ce rang est attribuĂ© par dĂ©faut aux nouveaux membres. + * @param permissions Ensemble initial des permissions accordĂ©es Ă  ce rang. + */ + public GuildRank(String id, UUID guildId, String name, int priority, boolean isDefault, Set permissions) { + this.id = Objects.requireNonNull(id, "L'identifiant du rang ne peut pas ĂȘtre nul."); + this.guildId = Objects.requireNonNull(guildId, "L'identifiant de la guilde ne peut pas ĂȘtre nul."); + this.name = Objects.requireNonNull(name, "Le nom du rang ne peut pas ĂȘtre nul."); + this.priority = priority; + this.isDefault = isDefault; + this.permissions = permissions != null ? new HashSet<>(permissions) : new HashSet<>(); + } + + /** + * Obtient l'identifiant unique du rang. + * + * @return L'identifiant string du rang. + */ + public String getId() { + return id; + } + + /** + * Obtient l'identifiant UUID de la guilde Ă  laquelle appartient ce rang. + * + * @return L'UUID de la guilde parente. + */ + public UUID getGuildId() { + return guildId; + } + + /** + * Obtient le nom d'affichage actuel du rang. + * + * @return Le nom du rang. + */ + public String getName() { + return name; + } + + /** + * DĂ©finit un nouveau nom d'affichage pour ce rang. + * + * @param name Le nouveau nom du rang. + */ + public void setName(String name) { + this.name = Objects.requireNonNull(name, "Le nom du rang ne peut pas ĂȘtre nul."); + } + + /** + * Obtient le niveau de prioritĂ© hiĂ©rarchique du rang (plus le chiffre est Ă©levĂ©, plus le pouvoir est grand). + * + * @return L'entier de prioritĂ©. + */ + public int getPriority() { + return priority; + } + + /** + * Modifie le niveau de prioritĂ© hiĂ©rarchique du rang. + * + * @param priority La nouvelle valeur de prioritĂ©. + */ + public void setPriority(int priority) { + this.priority = priority; + } + + /** + * Indique si ce rang est le rang attribuĂ© par dĂ©faut lors de l'intĂ©gration d'un nouveau membre. + * + * @return true si c'est le rang par dĂ©faut, sinon false. + */ + public boolean isDefault() { + return isDefault; + } + + /** + * DĂ©finit si ce rang doit ĂȘtre le rang par dĂ©faut pour les nouvelles recrues. + * + * @param isDefault Valeur boolĂ©enne. + */ + public void setDefault(boolean isDefault) { + this.isDefault = isDefault; + } + + /** + * Obtient une vue non modifiable de l'ensemble des permissions accordĂ©es Ă  ce rang. + * + * @return L'ensemble des permissions du rang. + */ + public Set getPermissions() { + return Collections.unmodifiableSet(permissions); + } + + /** + * VĂ©rifie si ce rang possĂšde une permission spĂ©cifique. + * + * @param permission La permission Ă  vĂ©rifier. + * @return true si le rang possĂšde la permission, false sinon. + */ + public boolean hasPermission(GuildPermission permission) { + if (permission == null) return false; + return permissions.contains(permission); + } + + /** + * Ajoute une permission Ă  ce rang. + * + * @param permission La permission Ă  accorder. + * @return true si la permission a Ă©tĂ© ajoutĂ©e, false si elle Ă©tait dĂ©jĂ  prĂ©sente. + */ + public boolean addPermission(GuildPermission permission) { + if (permission == null) return false; + return permissions.add(permission); + } + + /** + * Retire une permission de ce rang. + * + * @param permission La permission Ă  rĂ©voquer. + * @return true si la permission a Ă©tĂ© retirĂ©e, false si elle n'Ă©tait pas prĂ©sente. + */ + public boolean removePermission(GuildPermission permission) { + if (permission == null) return false; + return permissions.remove(permission); + } + + /** + * DĂ©finit l'ensemble complet des permissions pour ce rang. + * + * @param newPermissions La nouvelle collection de permissions. + */ + public void setPermissions(Collection newPermissions) { + this.permissions.clear(); + if (newPermissions != null) { + this.permissions.addAll(newPermissions); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GuildRank guildRank = (GuildRank) o; + return Objects.equals(id, guildRank.id) && Objects.equals(guildId, guildRank.guildId); + } + + @Override + public int hashCode() { + return Objects.hash(id, guildId); + } +} diff --git a/src/test/java/fr/luc/gamingcore/economy/ChunkPriceCalculatorTest.java b/src/test/java/fr/luc/gamingcore/economy/ChunkPriceCalculatorTest.java new file mode 100644 index 0000000..49655a6 --- /dev/null +++ b/src/test/java/fr/luc/gamingcore/economy/ChunkPriceCalculatorTest.java @@ -0,0 +1,76 @@ +package fr.luc.gamingcore.economy; + +import fr.luc.gamingcore.model.Guild; +import fr.luc.gamingcore.model.GuildClaim; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class ChunkPriceCalculatorTest { + + private ChunkPriceCalculator calculator; + + @BeforeEach + public void setUp() { + this.calculator = new ChunkPriceCalculator(); + } + + @Test + public void testFirstChunkPriceIsBasePrice() { + double firstChunk = calculator.calculatePrice(1, 0); + assertEquals(1000.0, firstChunk, 0.01); + } + + @Test + public void testStructuralPriceIncreasesWithChunkCount() { + double chunk1 = calculator.calculatePrice(1, 0); + double chunk2 = calculator.calculatePrice(2, 0); + double chunk5 = calculator.calculatePrice(5, 0); + double chunk10 = calculator.calculatePrice(10, 0); + + assertTrue(chunk2 > chunk1); + assertTrue(chunk5 > chunk2); + assertTrue(chunk10 > chunk5); + } + + @Test + public void testOverheatMultiplierAppliesWithRecentClaims() { + // Chunk 6 with 0 claims in last 24h + double normalPrice = calculator.calculatePrice(6, 0); + + // Chunk 6 with 3 claims in last 24h + double overheatedPrice = calculator.calculatePrice(6, 3); + + assertTrue(overheatedPrice > normalPrice); + } + + @Test + public void testLargeGuildOverheatIsMoreSevereThanSmallGuild() { + // Small guild (5 chunks): buying with 4 recent claims + double smallMultiplier = calculator.calculateOverheatMultiplier(5, 4); + + // Large guild (50 chunks): buying with 4 recent claims + double largeMultiplier = calculator.calculateOverheatMultiplier(50, 4); + + assertTrue(largeMultiplier > smallMultiplier, "Large guild should have a significantly higher overheat penalty"); + } + + @Test + public void testGuildIntegrationWithCalculator() { + Guild guild = new Guild(UUID.randomUUID(), "TestGuild", "TST", "Description", 5000.0, System.currentTimeMillis()); + + // 1st claim (already has 0 chunks) + double nextPrice1 = calculator.calculateNextChunkPrice(guild); + assertEquals(1000.0, nextPrice1, 0.01); + + // Add 1 claim created right now + guild.addClaim(new GuildClaim(1, guild.getId(), "world", 10, 20, System.currentTimeMillis())); + + // Next chunk price (Chunk 2 with R = 1) + double nextPrice2 = calculator.calculateNextChunkPrice(guild); + assertTrue(nextPrice2 > 1000.0); + } +} diff --git a/src/test/java/fr/luc/gamingcore/model/GuildModelTest.java b/src/test/java/fr/luc/gamingcore/model/GuildModelTest.java new file mode 100644 index 0000000..01956c6 --- /dev/null +++ b/src/test/java/fr/luc/gamingcore/model/GuildModelTest.java @@ -0,0 +1,92 @@ +package fr.luc.gamingcore.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.EnumSet; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class GuildModelTest { + + private Guild guild; + private UUID leaderUuid; + private UUID memberUuid; + private GuildRank leaderRank; + private GuildRank memberRank; + + @BeforeEach + public void setUp() { + UUID guildId = UUID.randomUUID(); + leaderUuid = UUID.randomUUID(); + memberUuid = UUID.randomUUID(); + + guild = new Guild(guildId, "Valhalla", "VAL", "Guilde nordique", 500.0, System.currentTimeMillis()); + + leaderRank = new GuildRank("leader", guildId, "Chef", 100, false, EnumSet.allOf(GuildPermission.class)); + memberRank = new GuildRank("member", guildId, "Membre", 1, true, EnumSet.of(GuildPermission.HOME, GuildPermission.BANK_DEPOSIT)); + + guild.addRank(leaderRank); + guild.addRank(memberRank); + + guild.addMember(new GuildMember(leaderUuid, guildId, "leader", System.currentTimeMillis(), System.currentTimeMillis(), 0.0)); + guild.addMember(new GuildMember(memberUuid, guildId, "member", System.currentTimeMillis(), System.currentTimeMillis(), 0.0)); + } + + @Test + public void testGuildMembershipAndRanks() { + assertEquals(2, guild.getMemberCount()); + assertTrue(guild.isMember(leaderUuid)); + assertTrue(guild.isMember(memberUuid)); + + assertEquals(leaderRank, guild.getMemberRank(leaderUuid)); + assertEquals(memberRank, guild.getMemberRank(memberUuid)); + assertEquals(memberRank, guild.getDefaultRank()); + assertEquals(leaderRank, guild.getLeaderRank()); + } + + @Test + public void testPermissions() { + // Leader has all permissions + assertTrue(guild.hasPermission(leaderUuid, GuildPermission.CLAIM)); + assertTrue(guild.hasPermission(leaderUuid, GuildPermission.KICK)); + assertTrue(guild.hasPermission(leaderUuid, GuildPermission.BANK_WITHDRAW)); + + // Member only has HOME and BANK_DEPOSIT + assertTrue(guild.hasPermission(memberUuid, GuildPermission.HOME)); + assertTrue(guild.hasPermission(memberUuid, GuildPermission.BANK_DEPOSIT)); + assertFalse(guild.hasPermission(memberUuid, GuildPermission.CLAIM)); + assertFalse(guild.hasPermission(memberUuid, GuildPermission.BANK_WITHDRAW)); + } + + @Test + public void testBankOperations() { + assertEquals(500.0, guild.getBalance()); + + guild.deposit(250.0); + assertEquals(750.0, guild.getBalance()); + + boolean withdrawSuccess = guild.withdraw(300.0); + assertTrue(withdrawSuccess); + assertEquals(450.0, guild.getBalance()); + + boolean withdrawTooMuch = guild.withdraw(1000.0); + assertFalse(withdrawTooMuch); + assertEquals(450.0, guild.getBalance()); + } + + @Test + public void testClaimManagement() { + GuildClaim claim1 = new GuildClaim(1, guild.getId(), "world", 5, 10, System.currentTimeMillis()); + guild.addClaim(claim1); + + assertEquals(1, guild.getClaimCount()); + assertTrue(guild.isClaimed("world", 5, 10)); + assertFalse(guild.isClaimed("world", 5, 11)); + + guild.removeClaim("world", 5, 10); + assertEquals(0, guild.getClaimCount()); + assertFalse(guild.isClaimed("world", 5, 10)); + } +}