feat(guild): implement complete domain model, spatio-temporal pricing, repository, and managers with full javadoc

This commit is contained in:
2026-08-20 17:16:05 +02:00
parent 0ba297a648
commit efd57a3a1e
32 changed files with 3348 additions and 1021 deletions
+92
View File
@@ -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
```
+107 -16
View File
@@ -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", "<gradient:#00C9FF:#92FE9D>[Guilde]</gradient> <dark_gray>»</dark_gray> <gray>"));
// 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<Economy> 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;
}
}
@@ -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<String> 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<CommandArgument<?>> arguments;
private final Map<String, AppCommand> subCommands;
private final CommandHandler handler;
public AppCommand(String name, List<String> aliases, String description, String permission,
String permissionMessage, boolean playerOnly, long cooldownMillis,
String cooldownBypassPermission, List<CommandArgument<?>> arguments,
Map<String, AppCommand> 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 : "<red>Tu n'as pas la permission d'exécuter cette commande.</red>";
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<String> 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<CommandArgument<?>> getArguments() {
return arguments;
}
public Map<String, AppCommand> 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, "<red>⏳ Veuillez patienter <yellow>" + remaining + "s</yellow> avant de réutiliser cette commande.</red>");
return;
}
}
// 6. Argument parsing
Map<String, Object> 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, "<red>Syntaxe incorrecte : <yellow>/" + fullPath + " " + getSyntax() + "</yellow></red>");
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, "<red>" + e.getMessage() + "</red>");
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, "<red>Une erreur est survenue lors de l'exécution de la commande.</red>");
LOGGER.log(Level.SEVERE, "Erreur lors de l'exécution de la commande " + fullPath, e);
}
}
public List<String> 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<String> 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, "<gradient:#00C9FF:#92FE9D>═══════════ [ " + fullPath.toUpperCase() + " ] ═══════════</gradient>");
if (!description.isEmpty()) {
TextUtil.sendRawMessage(sender, " <gray>" + description + "</gray>");
}
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, " <yellow>/" + fullPath + " " + sub.getName() + syntaxPart + "</yellow> <dark_gray>-</dark_gray> <gray>" + sub.getDescription() + "</gray>");
}
}
}
TextUtil.sendRawMessage(sender, "<gradient:#00C9FF:#92FE9D>════════════════════════════════════════</gradient>");
}
}
@@ -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<String> tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException {
return appCommand.tabComplete(sender, alias, args, "");
}
public AppCommand getAppCommand() {
return appCommand;
}
}
@@ -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<String> 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<CommandArgument<?>> arguments = new ArrayList<>();
private final Map<String, AppCommand> 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);
}
}
@@ -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<String, Object> parsedArgs;
private final String fullCommandPath;
public CommandContext(CommandSender sender, String label, String[] rawArgs, Map<String, Object> 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> T get(String argName, Class<T> 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, "<red>" + errorMessage + "</red>");
}
public void sendActionBar(String message) {
if (isPlayer()) {
TextUtil.sendActionBar(getPlayer(), message);
}
}
}
@@ -1,6 +0,0 @@
package fr.luc.gamingcore.command;
@FunctionalInterface
public interface CommandHandler {
void execute(CommandContext context) throws Exception;
}
@@ -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<String, BukkitCommandWrapper> 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<String, Command> knownCommands = (Map<String, Command>) 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;
}
}
@@ -1,8 +0,0 @@
package fr.luc.gamingcore.command.argument;
public class ArgumentParseException extends Exception {
public ArgumentParseException(String message) {
super(message);
}
}
@@ -1,30 +0,0 @@
package fr.luc.gamingcore.command.argument;
import fr.luc.gamingcore.command.CommandContext;
import java.util.List;
@FunctionalInterface
public interface ArgumentParser<T> {
/**
* 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<String> complete(CommandContext ctx, String input) {
return List.of();
}
}
@@ -1,72 +0,0 @@
package fr.luc.gamingcore.command.argument;
public class CommandArgument<T> {
private final String name;
private final String description;
private final ArgumentParser<T> parser;
private final boolean required;
private final T defaultValue;
private final boolean greedy;
public CommandArgument(String name, String description, ArgumentParser<T> 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 <T> CommandArgument<T> required(String name, ArgumentParser<T> parser) {
return new CommandArgument<>(name, "", parser, true, null, false);
}
public static <T> CommandArgument<T> required(String name, String description, ArgumentParser<T> parser) {
return new CommandArgument<>(name, description, parser, true, null, false);
}
public static <T> CommandArgument<T> optional(String name, ArgumentParser<T> parser) {
return new CommandArgument<>(name, "", parser, false, null, false);
}
public static <T> CommandArgument<T> optional(String name, ArgumentParser<T> parser, T defaultValue) {
return new CommandArgument<>(name, "", parser, false, defaultValue, false);
}
public static <T> CommandArgument<T> optional(String name, String description, ArgumentParser<T> parser, T defaultValue) {
return new CommandArgument<>(name, description, parser, false, defaultValue, false);
}
public static <T> CommandArgument<T> greedy(String name, String description, ArgumentParser<T> 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<T> 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 + "]";
}
}
@@ -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<Double> {
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<String> complete(CommandContext ctx, String input) {
return List.of();
}
}
@@ -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<Integer> {
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<String> complete(CommandContext ctx, String input) {
return List.of();
}
}
@@ -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<Player> {
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 '<red>" + input + "</red>' n'est pas connecté.");
}
return target;
}
@Override
public List<String> 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());
}
}
@@ -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<String> {
private final List<String> suggestions;
private final Supplier<List<String>> dynamicSuggestions;
public StringArgument() {
this.suggestions = null;
this.dynamicSuggestions = null;
}
public StringArgument(String... suggestions) {
this.suggestions = Arrays.asList(suggestions);
this.dynamicSuggestions = null;
}
public StringArgument(Supplier<List<String>> 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<List<String>> 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<String> complete(CommandContext ctx, String input) {
List<String> 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());
}
}
@@ -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<UUID, Map<String, Long>> 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<String, Long> 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<String, Long> 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());
}
}
@@ -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);
}
}
}
@@ -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<UUID, Guild> loadAllGuilds() {
Map<UUID, Guild> 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<GuildPermission> 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);
}
});
}
}
@@ -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; }
}
@@ -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) + " $";
}
}
@@ -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, "<red>⛔ Ce territoire appartient à la guilde <yellow>" + guildAtBlock.getName() + "</yellow> !</red>");
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, "<red>Votre rang de guilde ne vous autorise pas à casser des blocs.</red>");
}
}
/**
* 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, "<red>⛔ Ce territoire appartient à la guilde <yellow>" + guildAtBlock.getName() + "</yellow> !</red>");
return;
}
// Vérifier la permission BUILD
if (!guildAtBlock.hasPermission(player.getUniqueId(), GuildPermission.BUILD)) {
event.setCancelled(true);
TextUtil.sendActionBar(player, "<red>Votre rang de guilde ne vous autorise pas à poser des blocs.</red>");
}
}
/**
* 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, "<red>⛔ Ce territoire appartient à la guilde <yellow>" + guildAtBlock.getName() + "</yellow> !</red>");
}
return;
}
// Vérification des permissions spécifiques de rang
if (isContainer && !guildAtBlock.hasPermission(player.getUniqueId(), GuildPermission.CONTAINER)) {
event.setCancelled(true);
TextUtil.sendActionBar(player, "<red>Votre rang ne vous autorise pas à ouvrir les conteneurs de guilde.</red>");
} else if (isMechanism && !guildAtBlock.hasPermission(player.getUniqueId(), GuildPermission.INTERACT)) {
event.setCancelled(true);
TextUtil.sendActionBar(player, "<red>Votre rang ne vous autorise pas à interagir avec ces mécanismes.</red>");
}
}
/**
* 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 ? "<green>" : "<yellow>";
TextUtil.sendActionBar(player, "<gray>Entrée dans le territoire : </gray>" + color + "<bold>" + currentGuild.getName() + "</bold></color> <dark_gray>(" + currentGuild.getTag() + ")</dark_gray>");
} else if (previousGuild != null) {
TextUtil.sendActionBar(player, "<gray>Vous entrez en <green>Zone Libre</green> (Wilderness).</gray>");
}
}
// ==========================================
// 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");
}
}
@@ -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<UUID, Guild> guildsById = new ConcurrentHashMap<>();
private final Map<String, UUID> guildsByName = new ConcurrentHashMap<>();
private final Map<UUID, UUID> playerGuildMap = new ConcurrentHashMap<>();
private final Map<String, UUID> 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<UUID, Guild> 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 <white><bold>" + name + "</bold></white> 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 <white>" + guild.getName() + "</white> 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 <white>" + player.getName() + "</white> 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 <white>" + newRank.getName() + "</white>.");
}
// ==========================================
// 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 <yellow>" + economyManager.format(price) + "</yellow> ! (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é <green>" + economyManager.format(amount) + "</green> 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é <green>" + economyManager.format(amount) + "</green> 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<Guild> 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;
}
}
@@ -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;
}
}
@@ -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<UUID, Map<UUID, GuildInvite>> 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<UUID, GuildInvite> 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<UUID, GuildInvite> 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());
}
}
@@ -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<UUID, GuildMember> members = new ConcurrentHashMap<>();
private final Map<String, GuildRank> ranks = new ConcurrentHashMap<>();
private final List<GuildClaim> 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<GuildMember> 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<GuildRank> 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<GuildClaim> 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);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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<GuildPermission> 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<GuildPermission> 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<GuildPermission> 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<GuildPermission> 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);
}
}
@@ -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);
}
}
@@ -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));
}
}