feat(guild): implement complete domain model, spatio-temporal pricing, repository, and managers with full javadoc
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user