diff --git a/src/main/java/fr/luc/gamingcore/Main.java b/src/main/java/fr/luc/gamingcore/Main.java index b88a6f2..b028e69 100644 --- a/src/main/java/fr/luc/gamingcore/Main.java +++ b/src/main/java/fr/luc/gamingcore/Main.java @@ -74,6 +74,7 @@ public final class Main extends JavaPlugin { // 8. Enregistrer les écouteurs d'événements PluginManager pm = getServer().getPluginManager(); pm.registerEvents(new TerritoryProtectionListener(this), this); + pm.registerEvents(new fr.luc.gamingcore.listener.GuildChatListener(this), this); getLogger().info("GamingCore (Système de Guilde - Cœur Métier & Commandes) a ete active avec succes !"); } diff --git a/src/main/java/fr/luc/gamingcore/command/GuildCommands.java b/src/main/java/fr/luc/gamingcore/command/GuildCommands.java index 9b21910..bbc7dc0 100644 --- a/src/main/java/fr/luc/gamingcore/command/GuildCommands.java +++ b/src/main/java/fr/luc/gamingcore/command/GuildCommands.java @@ -28,6 +28,7 @@ public class GuildCommands { private static final Pattern GUILD_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{3,16}$"); private static final int HELP_PAGE_SIZE = 8; private static final int GUILD_LIST_PAGE_SIZE = 6; + private static final int LOGS_PAGE_SIZE = 6; private final Main plugin; private final GuildManager guildManager; @@ -75,6 +76,107 @@ public class GuildCommands { }) ) + // ------------------------------------------------------------- + // Sous-commande : /guild chat + // ------------------------------------------------------------- + .subcommand(BetterMcCommands.subBuilder("chat") + .description("Envoyer un message dans le chat privé de votre guilde (ou préfixez par #)") + .aliases("c", "msg") + .playerOnly() + .argument(Arguments.string("message").greedy().description("Message à envoyer")) + .validate(context -> { + if (!guildManager.isInGuild(context.getPlayer().getUniqueId())) { + return ValidationResult.invalid(msg.get("guild.not_in_guild")); + } + return ValidationResult.valid(); + }) + .executes(context -> { + Player player = context.getPlayer(); + Guild guild = guildManager.getGuildByPlayer(player.getUniqueId()); + String chatMsg = context.getString("message"); + + GuildMember member = guild.getMember(player.getUniqueId()); + GuildRank rank = member != null ? guild.getRank(member.getRankId()) : null; + String rankName = rank != null ? rank.getName() : "Membre"; + + String formatted = msg.get("guild.chat.format", + "{guild}", guild.getFormattedName(), + "{tag}", guild.getFormattedTag(), + "{color}", guild.getColor(), + "{rank}", rankName, + "{player}", player.getName(), + "{message}", chatMsg + ); + + for (GuildMember m : guild.getMembers()) { + if (m.getOfflinePlayer() != null && m.getOfflinePlayer().isOnline()) { + Player onlinePlayer = m.getOfflinePlayer().getPlayer(); + if (onlinePlayer != null) { + TextUtil.sendMessage(onlinePlayer, formatted); + } + } + } + }) + ) + + // ------------------------------------------------------------- + // Sous-commande : /guild history [page] + // ------------------------------------------------------------- + .subcommand(BetterMcCommands.subBuilder("history") + .description("Consulter l'historique des actions et transactions de votre guilde") + .aliases("logs", "journal", "historique") + .playerOnly() + .argument(Arguments.integer("page", 1, 100).optional().description("Numéro de page")) + .validate(context -> { + if (!guildManager.isInGuild(context.getPlayer().getUniqueId())) { + return ValidationResult.invalid(msg.get("guild.not_in_guild")); + } + return ValidationResult.valid(); + }) + .executes(context -> { + Player player = context.getPlayer(); + Guild guild = guildManager.getGuildByPlayer(player.getUniqueId()); + int page = context.getOptional("page", Integer.class).orElse(1); + + plugin.getGuildRepository().getLogsAsync(guild.getId(), 100, logs -> { + if (logs.isEmpty()) { + context.reply(msg.get("guild.history.empty")); + return; + } + + int totalLogs = logs.size(); + int maxPage = (int) Math.ceil((double) totalLogs / LOGS_PAGE_SIZE); + int finalPage = Math.min(Math.max(1, page), maxPage); + + context.reply(msg.get("guild.history.header", + "{page}", String.valueOf(finalPage), + "{max_page}", String.valueOf(maxPage) + )); + + int startIndex = (finalPage - 1) * LOGS_PAGE_SIZE; + int endIndex = Math.min(startIndex + LOGS_PAGE_SIZE, totalLogs); + + for (int i = startIndex; i < endIndex; i++) { + GuildLog log = logs.get(i); + context.reply(msg.get("guild.history.item", + "{date}", log.getFormattedDate(), + "{actor}", log.getActorName(), + "{details}", log.getDetails() + )); + } + + int prevPage = Math.max(1, finalPage - 1); + int nextPage = Math.min(maxPage, finalPage + 1); + + String pagination = msg.get("guild.history.pagination", + "{prev_page}", String.valueOf(prevPage), + "{next_page}", String.valueOf(nextPage) + ); + context.reply(pagination); + }); + }) + ) + // ------------------------------------------------------------- // Sous-commande : /guild list [page] // ------------------------------------------------------------- @@ -91,7 +193,6 @@ public class GuildCommands { return; } - // Trier par nombre de membres décroissant allGuilds.sort((g1, g2) -> Integer.compare(g2.getMemberCount(), g1.getMemberCount())); int totalGuilds = allGuilds.size(); @@ -323,7 +424,10 @@ public class GuildCommands { inviteManager.removeInvite(player, targetGuild.getId()); GuildResult result = guildManager.addMember(targetGuild, player); if (result.isSuccess()) { - context.reply(msg.get("guild.invite.accepted", "{player}", player.getName())); + context.reply(msg.get("guild.invite.accepted_self", + "{guild}", targetGuild.getFormattedName(), + "{tag}", targetGuild.getFormattedTag() + )); } else { context.replyError(result.getMessage()); } @@ -357,7 +461,6 @@ public class GuildCommands { inviteManager.removeInvite(player, targetGuild.getId()); context.reply(msg.get("guild.invite.declined_target", "{guild}", targetGuild.getFormattedName())); - // Notifier l'émetteur si en ligne GuildMember leader = targetGuild.getLeaderMember(); if (leader != null && leader.getOfflinePlayer() != null && leader.getOfflinePlayer().isOnline()) { Player leaderPlayer = leader.getOfflinePlayer().getPlayer(); @@ -500,7 +603,7 @@ public class GuildCommands { // Sous-commande : /guild unclaim // ------------------------------------------------------------- .subcommand(BetterMcCommands.subBuilder("unclaim") - .description("Libérer le chunk actuel") + .description("Libérer le chunk actuel possédé par la guilde") .playerOnly() .validate(context -> { if (!guildManager.isInGuild(context.getPlayer().getUniqueId())) { @@ -519,6 +622,7 @@ public class GuildCommands { } return ValidationResult.valid(); }) + .requireConfirmation(Duration.ofSeconds(15)) .executes(context -> { Player player = context.getPlayer(); Guild guild = guildManager.getGuildByPlayer(player.getUniqueId()); @@ -831,6 +935,130 @@ public class GuildCommands { }) ) + // ------------------------------------------------------------- + // Sous-commande : /guild ranks (ou /guild rank) + // ------------------------------------------------------------- + .subcommand(BetterMcCommands.subBuilder("ranks") + .description("Afficher les rangs et permissions de votre guilde") + .aliases("rank", "rangs", "rang") + .playerOnly() + .validate(context -> { + if (!guildManager.isInGuild(context.getPlayer().getUniqueId())) { + return ValidationResult.invalid(msg.get("guild.not_in_guild")); + } + return ValidationResult.valid(); + }) + .executes(context -> { + Player player = context.getPlayer(); + Guild guild = guildManager.getGuildByPlayer(player.getUniqueId()); + + context.reply(msg.get("guild.ranks.header")); + for (GuildRank r : guild.getRanks()) { + String perms = r.getPermissions().stream().map(Enum::name).collect(Collectors.joining(", ")); + if (perms.isEmpty()) perms = "Aucune"; + context.reply(msg.get("guild.ranks.item", + "{name}", r.getName(), + "{id}", r.getId(), + "{priority}", String.valueOf(r.getPriority()), + "{permissions}", perms + )); + } + }) + // /guild rank create + .subcommand(BetterMcCommands.subBuilder("create") + .description("Créer un nouveau rang de guilde") + .playerOnly() + .argument(Arguments.string("id").description("ID unique du rang")) + .argument(Arguments.string("nom").description("Nom affiché")) + .argument(Arguments.integer("priorite", 1, 99).description("Priorité (1-99)")) + .executes(context -> { + Player player = context.getPlayer(); + Guild guild = guildManager.getGuildByPlayer(player.getUniqueId()); + String id = context.getString("id"); + String name = context.getString("nom"); + int priority = context.getInt("priorite"); + + GuildResult res = guildManager.createRank(player, guild, id, name, priority); + if (res.isSuccess()) { + context.replySuccess(res.getMessage()); + } else { + context.replyError(res.getMessage()); + } + }) + ) + // /guild rank delete + .subcommand(BetterMcCommands.subBuilder("delete") + .description("Supprimer un rang de guilde") + .aliases("supprimer", "remove") + .playerOnly() + .argument(Arguments.string("id").description("ID du rang")) + .executes(context -> { + Player player = context.getPlayer(); + Guild guild = guildManager.getGuildByPlayer(player.getUniqueId()); + String id = context.getString("id"); + + GuildResult res = guildManager.deleteRank(player, guild, id); + if (res.isSuccess()) { + context.replySuccess(res.getMessage()); + } else { + context.replyError(res.getMessage()); + } + }) + ) + // /guild rank setperm + .subcommand(BetterMcCommands.subBuilder("setperm") + .description("Modifier une permission pour un rang") + .playerOnly() + .argument(Arguments.string("id").description("ID du rang")) + .argument(Arguments.enumOf("permission", GuildPermission.class).description("Permission")) + .argument(Arguments.bool("valeur").description("true ou false")) + .executes(context -> { + Player player = context.getPlayer(); + Guild guild = guildManager.getGuildByPlayer(player.getUniqueId()); + String id = context.getString("id"); + GuildPermission perm = context.get("permission", GuildPermission.class); + boolean state = context.getBoolean("valeur"); + + GuildResult res = guildManager.setRankPermission(player, guild, id, perm, state); + if (res.isSuccess()) { + context.replySuccess(res.getMessage()); + } else { + context.replyError(res.getMessage()); + } + }) + ) + ) + + // ------------------------------------------------------------- + // Sous-commande : /guild setrank + // ------------------------------------------------------------- + .subcommand(BetterMcCommands.subBuilder("setrank") + .description("Modifier le rang d'un membre de la guilde") + .aliases("promote", "demote") + .playerOnly() + .argument(Arguments.player("joueur").description("Membre cible")) + .argument(Arguments.string("rang").description("ID du rang cible")) + .validate(context -> { + if (!guildManager.isInGuild(context.getPlayer().getUniqueId())) { + return ValidationResult.invalid(msg.get("guild.not_in_guild")); + } + return ValidationResult.valid(); + }) + .executes(context -> { + Player player = context.getPlayer(); + Guild guild = guildManager.getGuildByPlayer(player.getUniqueId()); + Player target = context.getTargetPlayer("joueur"); + String rankId = context.getString("rang"); + + GuildResult res = guildManager.setMemberRank(player, guild, target.getUniqueId(), rankId); + if (res.isSuccess()) { + context.replySuccess(res.getMessage()); + } else { + context.replyError(res.getMessage()); + } + }) + ) + // ------------------------------------------------------------- // Sous-commande : /guild reload // ------------------------------------------------------------- @@ -853,6 +1081,10 @@ public class GuildCommands { List entries = new ArrayList<>(); entries.add(new HelpEntry("/guild create [tag]", "Fonder une nouvelle guilde et revendiquer le 1er chunk", "/guild create ")); entries.add(new HelpEntry("/guild list [page]", "Afficher la liste des guildes du serveur", "/guild list ")); + entries.add(new HelpEntry("/guild chat ", "Envoyer un message dans le canal privé de guilde (ou #message)", "/guild chat ")); + entries.add(new HelpEntry("/guild history [page]", "Consulter le journal des actions et transactions de la guilde", "/guild history ")); + entries.add(new HelpEntry("/guild ranks", "Afficher et gérer les rangs et permissions de la guilde", "/guild ranks")); + entries.add(new HelpEntry("/guild setrank ", "Modifier le rang d'un membre de la guilde", "/guild setrank ")); entries.add(new HelpEntry("/guild info [guilde]", "Afficher les statistiques détaillées d'une guilde", "/guild info ")); entries.add(new HelpEntry("/guild members [guilde]", "Afficher la liste des membres classés par rang", "/guild members ")); entries.add(new HelpEntry("/guild color <#hex>", "Modifier la couleur hexadécimale de la guilde", "/guild color ")); diff --git a/src/main/java/fr/luc/gamingcore/database/DatabaseManager.java b/src/main/java/fr/luc/gamingcore/database/DatabaseManager.java index be42be2..81c4db9 100644 --- a/src/main/java/fr/luc/gamingcore/database/DatabaseManager.java +++ b/src/main/java/fr/luc/gamingcore/database/DatabaseManager.java @@ -140,6 +140,18 @@ public class DatabaseManager { "claimed_at BIGINT NOT NULL, " + "FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE" + ");"); + + // Table de l'historique d'actions (logs) + stmt.executeUpdate("CREATE TABLE IF NOT EXISTS guild_logs (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guild_id VARCHAR(36) NOT NULL, " + + "actor_uuid VARCHAR(36), " + + "actor_name VARCHAR(32) NOT NULL, " + + "action_type VARCHAR(32) NOT NULL, " + + "details TEXT NOT NULL, " + + "timestamp BIGINT NOT NULL, " + + "FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE" + + ");"); } } diff --git a/src/main/java/fr/luc/gamingcore/database/GuildRepository.java b/src/main/java/fr/luc/gamingcore/database/GuildRepository.java index f628408..f188554 100644 --- a/src/main/java/fr/luc/gamingcore/database/GuildRepository.java +++ b/src/main/java/fr/luc/gamingcore/database/GuildRepository.java @@ -337,4 +337,60 @@ public class GuildRepository { } }); } + + /** + * Sauvegarde une entrée d'historique de guilde de façon asynchrone. + * + * @param log L'entrée de log à sauvegarder. + */ + public void saveLogAsync(fr.luc.gamingcore.model.GuildLog log) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String sql = "INSERT INTO guild_logs (guild_id, actor_uuid, actor_name, action_type, details, timestamp) VALUES (?, ?, ?, ?, ?, ?)"; + try (PreparedStatement ps = databaseManager.getConnection().prepareStatement(sql)) { + ps.setString(1, log.getGuildId().toString()); + ps.setString(2, log.getActorUuid() != null ? log.getActorUuid().toString() : null); + ps.setString(3, log.getActorName()); + ps.setString(4, log.getActionType()); + ps.setString(5, log.getDetails()); + ps.setLong(6, log.getTimestamp()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la sauvegarde du log de guilde", e); + } + }); + } + + /** + * Récupère de façon asynchrone les dernières entrées de log d'une guilde. + * + * @param guildId L'UUID de la guilde. + * @param limit Nombre maximum d'entrées à récupérer. + * @param callback Callback recevant la liste des logs ordonnée du plus récent au plus ancien. + */ + public void getLogsAsync(UUID guildId, int limit, java.util.function.Consumer> callback) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + java.util.List list = new java.util.ArrayList<>(); + String sql = "SELECT id, guild_id, actor_uuid, actor_name, action_type, details, timestamp FROM guild_logs WHERE guild_id = ? ORDER BY timestamp DESC LIMIT ?"; + try (PreparedStatement ps = databaseManager.getConnection().prepareStatement(sql)) { + ps.setString(1, guildId.toString()); + ps.setInt(2, limit); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + long id = rs.getLong("id"); + String actorUuidStr = rs.getString("actor_uuid"); + UUID actorUuid = actorUuidStr != null ? UUID.fromString(actorUuidStr) : null; + String actorName = rs.getString("actor_name"); + String actionType = rs.getString("action_type"); + String details = rs.getString("details"); + long timestamp = rs.getLong("timestamp"); + + list.add(new fr.luc.gamingcore.model.GuildLog(id, guildId, actorUuid, actorName, actionType, details, timestamp)); + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors du chargement des logs de la guilde " + guildId, e); + } + Bukkit.getScheduler().runTask(plugin, () -> callback.accept(list)); + }); + } } diff --git a/src/main/java/fr/luc/gamingcore/listener/GuildChatListener.java b/src/main/java/fr/luc/gamingcore/listener/GuildChatListener.java new file mode 100644 index 0000000..da77602 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/listener/GuildChatListener.java @@ -0,0 +1,97 @@ +package fr.luc.gamingcore.listener; + +import fr.luc.gamingcore.Main; +import fr.luc.gamingcore.manager.GuildManager; +import fr.luc.gamingcore.model.Guild; +import fr.luc.gamingcore.model.GuildMember; +import fr.luc.gamingcore.model.GuildRank; +import fr.luc.gamingcore.util.MessageManager; +import fr.luc.gamingcore.util.TextUtil; +import io.papermc.paper.event.player.AsyncChatEvent; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; + +/** + * Écouteur gérant le chat privé de guilde. + * Permet aux joueurs d'écrire dans le chat de guilde en préfixant leur message par '#' (ex: # Salut la guilde !). + */ +public class GuildChatListener implements Listener { + + private final Main plugin; + private final GuildManager guildManager; + private final MessageManager messageManager; + + /** + * Constructeur de l'écouteur de chat de guilde. + * + * @param plugin Instance principale du plugin. + */ + public GuildChatListener(Main plugin) { + this.plugin = plugin; + this.guildManager = plugin.getGuildManager(); + this.messageManager = plugin.getMessageManager(); + } + + /** + * Intercepte les messages de chat commençant par '#' pour les rediriger exclusivement vers la guilde du joueur. + * + * @param event L'événement AsyncChatEvent de Paper. + */ + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + public void onAsyncChat(AsyncChatEvent event) { + String rawText = PlainTextComponentSerializer.plainText().serialize(event.message()).trim(); + if (!rawText.startsWith("#")) { + return; + } + + event.setCancelled(true); + Player sender = event.getPlayer(); + + Guild guild = guildManager.getGuildByPlayer(sender.getUniqueId()); + if (guild == null) { + messageManager.send(sender, "guild.not_in_guild"); + return; + } + + String guildMsg = rawText.substring(1).trim(); + if (guildMsg.isEmpty()) { + return; + } + + sendGuildChatMessage(guild, sender, guildMsg); + } + + /** + * Diffuse un message formaté dans le canal privé de la guilde à tous les membres connectés. + * + * @param guild La guilde émettrice. + * @param sender Le joueur ayant envoyé le message. + * @param message Le texte du message. + */ + public void sendGuildChatMessage(Guild guild, Player sender, String message) { + GuildMember member = guild.getMember(sender.getUniqueId()); + GuildRank rank = member != null ? guild.getRank(member.getRankId()) : null; + String rankName = rank != null ? rank.getName() : "Membre"; + + String formatted = messageManager.get("guild.chat.format", + "{guild}", guild.getFormattedName(), + "{tag}", guild.getFormattedTag(), + "{color}", guild.getColor(), + "{rank}", rankName, + "{player}", sender.getName(), + "{message}", message + ); + + for (GuildMember m : guild.getMembers()) { + if (m.getOfflinePlayer() != null && m.getOfflinePlayer().isOnline()) { + Player onlinePlayer = m.getOfflinePlayer().getPlayer(); + if (onlinePlayer != null) { + TextUtil.sendMessage(onlinePlayer, formatted); + } + } + } + } +} diff --git a/src/main/java/fr/luc/gamingcore/listener/TerritoryProtectionListener.java b/src/main/java/fr/luc/gamingcore/listener/TerritoryProtectionListener.java index 794ab56..38ec5bc 100644 --- a/src/main/java/fr/luc/gamingcore/listener/TerritoryProtectionListener.java +++ b/src/main/java/fr/luc/gamingcore/listener/TerritoryProtectionListener.java @@ -28,7 +28,7 @@ import java.util.Objects; /** * Écouteur d'événements garantissant la protection territoriale complète des chunks de guilde : - * anti-grief, anti-pvp, anti-pistons transfrontaliers, anti-fluides et protection des entités/conteneurs. + * anti-grief, anti-pvp, anti-pistons transfrontaliers, anti-fluides, anti-feu et protection des entités/conteneurs. */ public class TerritoryProtectionListener implements Listener { @@ -257,6 +257,7 @@ public class TerritoryProtectionListener implements Listener { /** * Empêche les pistons de pousser des blocs à travers les frontières d'un claim étranger. + * Les pistons fonctionnant au sein d'une même guilde ou en territoire libre sont totalement autorisés. * * @param event L'événement BlockPistonExtendEvent. */ @@ -266,15 +267,39 @@ public class TerritoryProtectionListener implements Listener { String worldName = piston.getWorld().getName(); Guild guildPiston = guildManager.getGuildAt(worldName, piston.getX() >> 4, piston.getZ() >> 4); + if (event.getBlocks().isEmpty()) { + // Piston qui pousse de l'air + Block targetAir = piston.getRelative(event.getDirection()); + Guild guildTarget = guildManager.getGuildAt(worldName, targetAir.getX() >> 4, targetAir.getZ() >> 4); + if (guildPiston == null && guildTarget != null) { + event.setCancelled(true); + } else if (guildPiston != null && guildTarget != null && !guildPiston.getId().equals(guildTarget.getId())) { + event.setCancelled(true); + } + return; + } + for (Block block : event.getBlocks()) { Block targetBlock = block.getRelative(event.getDirection()); Guild guildBlock = guildManager.getGuildAt(worldName, block.getX() >> 4, block.getZ() >> 4); Guild guildTarget = guildManager.getGuildAt(worldName, targetBlock.getX() >> 4, targetBlock.getZ() >> 4); - // Si le piston ou les blocs déplacés traversent une frontière de guilde différente - if (!Objects.equals(guildPiston, guildBlock) || !Objects.equals(guildPiston, guildTarget)) { - event.setCancelled(true); - return; + if (guildPiston != null) { + // Piston appartenant à une guilde + if (guildBlock != null && !guildBlock.getId().equals(guildPiston.getId())) { + event.setCancelled(true); + return; + } + if (guildTarget != null && !guildTarget.getId().equals(guildPiston.getId())) { + event.setCancelled(true); + return; + } + } else { + // Piston en zone libre (wilderness) : ne doit toucher à aucun territoire revendiqué + if (guildBlock != null || guildTarget != null) { + event.setCancelled(true); + return; + } } } } @@ -292,9 +317,16 @@ public class TerritoryProtectionListener implements Listener { for (Block block : event.getBlocks()) { Guild guildBlock = guildManager.getGuildAt(worldName, block.getX() >> 4, block.getZ() >> 4); - if (!Objects.equals(guildPiston, guildBlock)) { - event.setCancelled(true); - return; + if (guildPiston != null) { + if (guildBlock != null && !guildBlock.getId().equals(guildPiston.getId())) { + event.setCancelled(true); + return; + } + } else { + if (guildBlock != null) { + event.setCancelled(true); + return; + } } } } @@ -322,7 +354,58 @@ public class TerritoryProtectionListener implements Listener { Guild guildFrom = guildManager.getGuildAt(worldName, fromChunkX, fromChunkZ); Guild guildTo = guildManager.getGuildAt(worldName, toChunkX, toChunkZ); - if (guildTo != null && !Objects.equals(guildFrom, guildTo)) { + if (guildTo != null) { + if (guildFrom == null || !guildFrom.getId().equals(guildTo.getId())) { + event.setCancelled(true); + } + } + } + + /** + * Empêche la destruction par combustion des blocs sur les territoires revendiqués. + * + * @param event L'événement BlockBurnEvent. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBlockBurn(BlockBurnEvent event) { + Block block = event.getBlock(); + if (guildManager.isChunkClaimed(block.getWorld().getName(), block.getX() >> 4, block.getZ() >> 4)) { + event.setCancelled(true); + } + } + + /** + * Empêche la propagation du feu sur les territoires revendiqués. + * + * @param event L'événement BlockSpreadEvent. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBlockSpread(BlockSpreadEvent event) { + Material source = event.getSource().getType(); + Material newState = event.getNewState().getType(); + + if (source == Material.FIRE || source == Material.SOUL_FIRE || newState == Material.FIRE || newState == Material.SOUL_FIRE) { + Block block = event.getBlock(); + if (guildManager.isChunkClaimed(block.getWorld().getName(), block.getX() >> 4, block.getZ() >> 4)) { + event.setCancelled(true); + } + } + } + + /** + * Empêche l'allumage non autorisé par lave ou propagation de feu sur les claims. + * + * @param event L'événement BlockIgniteEvent. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBlockIgnite(BlockIgniteEvent event) { + Block block = event.getBlock(); + if (!guildManager.isChunkClaimed(block.getWorld().getName(), block.getX() >> 4, block.getZ() >> 4)) { + return; + } + + BlockIgniteEvent.IgniteCause cause = event.getCause(); + if (cause == BlockIgniteEvent.IgniteCause.SPREAD || cause == BlockIgniteEvent.IgniteCause.LAVA || cause == BlockIgniteEvent.IgniteCause.FIREBALL) { event.setCancelled(true); } } diff --git a/src/main/java/fr/luc/gamingcore/manager/GuildManager.java b/src/main/java/fr/luc/gamingcore/manager/GuildManager.java index edebe63..c42323c 100644 --- a/src/main/java/fr/luc/gamingcore/manager/GuildManager.java +++ b/src/main/java/fr/luc/gamingcore/manager/GuildManager.java @@ -5,6 +5,9 @@ import fr.luc.gamingcore.database.GuildRepository; import fr.luc.gamingcore.economy.ChunkPriceCalculator; import fr.luc.gamingcore.economy.EconomyManager; import fr.luc.gamingcore.model.*; +import fr.luc.gamingcore.util.MessageManager; +import fr.luc.gamingcore.util.TextUtil; +import fr.luc.gamingcore.worldguard.WorldGuardManager; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.World; @@ -12,58 +15,48 @@ import org.bukkit.entity.Player; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; +import java.util.stream.Collectors; /** - * Service central orchestrant l'intégralité de la logique métier du système de guilde. + * Service métier central et autonome pour la gestion des guildes, de leurs territoires, + * de leurs finances, de leurs rangs et de leur cycle de vie complet. */ 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 fr.luc.gamingcore.worldguard.WorldGuardManager worldGuardManager; + private final WorldGuardManager worldGuardManager; private final ChunkPriceCalculator priceCalculator; private final InviteManager inviteManager; + private final MessageManager messageManager; - // Caches d'accès rapide + // Indexations en mémoire pour des lookups O(1) private final Map guildsById = new ConcurrentHashMap<>(); private final Map guildsByName = new ConcurrentHashMap<>(); private final Map playerGuildMap = new ConcurrentHashMap<>(); private final Map chunkGuildMap = new ConcurrentHashMap<>(); /** - * Constructeur complet du GuildManager avec WorldGuardManager. + * Constructeur complet du gestionnaire de guilde avec WorldGuard. * - * @param plugin Instance du plugin principal. - * @param repository Référentiel de persistance des données. - * @param economyManager Gestionnaire de l'économie Vault. - * @param worldGuardManager Gestionnaire des régions WorldGuard. + * @param plugin Instance principale du plugin. + * @param repository Couche d'accès aux données. + * @param economyManager Gestionnaire d'économie Vault. + * @param worldGuardManager Gestionnaire WorldGuard. */ - public GuildManager(Main plugin, GuildRepository repository, EconomyManager economyManager, fr.luc.gamingcore.worldguard.WorldGuardManager worldGuardManager) { + public GuildManager(Main plugin, GuildRepository repository, EconomyManager economyManager, WorldGuardManager worldGuardManager) { this.plugin = plugin; this.repository = repository; this.economyManager = economyManager; this.worldGuardManager = worldGuardManager; this.priceCalculator = new ChunkPriceCalculator(); this.inviteManager = new InviteManager(); + this.messageManager = plugin.getMessageManager(); } /** - * Constructeur de compatibilité 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, repository, economyManager, null); - } - - /** - * Charge l'ensemble des données de la base en mémoire et reconstruit les index de recherche et régions WorldGuard. + * Charge toutes les données depuis la base de données SQLite en mémoire. */ public void loadData() { guildsById.clear(); @@ -71,157 +64,169 @@ public class GuildManager { playerGuildMap.clear(); chunkGuildMap.clear(); - Map loaded = repository.loadAllGuilds(); - for (Guild guild : loaded.values()) { + Collection loadedGuilds = repository.loadAllGuilds().values(); + int totalMembers = 0; + int totalClaims = 0; + + for (Guild guild : loadedGuilds) { indexGuild(guild); + totalMembers += guild.getMemberCount(); + totalClaims += guild.getClaimCount(); } - if (worldGuardManager != null) { - worldGuardManager.syncAllGuilds(guildsById.values()); - } + plugin.getLogger().info("Charges " + loadedGuilds.size() + " guildes (" + totalMembers + " membres, " + totalClaims + " claims)."); - plugin.getLogger().info("Charges " + guildsById.size() + " guildes (" + playerGuildMap.size() + " membres, " + chunkGuildMap.size() + " claims)."); + // Synchronisation WorldGuard globale + if (worldGuardManager != null && worldGuardManager.isEnabled()) { + worldGuardManager.syncAllGuilds(new ArrayList<>(loadedGuilds)); + } } /** - * Arrête proprement le gestionnaire et libère les ressources. + * Arrêt propre du service métier. */ public void shutdown() { guildsById.clear(); guildsByName.clear(); playerGuildMap.clear(); chunkGuildMap.clear(); - inviteManager.cleanExpired(); } // ========================================== - // MÉTHODES DE CRÉATION & DISSOLUTION + // NOTIFICATIONS & LOGS DE GUILDE // ========================================== /** - * Crée une nouvelle guilde pour un joueur avec prélèvement financier et claim obligatoire du premier chunk. + * Diffuse un message à tous les membres connectés d'une guilde. * - * @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. + * @param guild La guilde cible. + * @param messageKey La clé du message dans messages.yml. + * @param placeholders Paires clé/valeur pour le remplacement. */ - public GuildResult createGuild(Player creator, String name, String tag, double creationCost) { - return createGuild(creator, name, tag, "#00C9FF", creationCost); + public void broadcastToGuild(Guild guild, String messageKey, String... placeholders) { + if (guild == null) return; + String formatted = messageManager.get(messageKey, placeholders); + for (GuildMember member : guild.getMembers()) { + if (member.getOfflinePlayer() != null && member.getOfflinePlayer().isOnline()) { + Player onlinePlayer = member.getOfflinePlayer().getPlayer(); + if (onlinePlayer != null) { + TextUtil.sendMessage(onlinePlayer, formatted); + } + } + } } /** - * Crée une nouvelle guilde pour un joueur avec prélèvement financier, couleur personnalisée et claim initial. + * Enregistre un événement dans l'historique de la guilde. * - * @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 color Couleur hexadécimale (ex: "#00C9FF"). - * @param creationCost Coût financier requis pour la création. - * @return GuildResult indiquant le succès ou la raison de l'échec. + * @param guild La guilde concernée. + * @param actor Le joueur initiateur (peut être null). + * @param actionType Le type d'action. + * @param details Description textuelle. */ - public GuildResult createGuild(Player creator, String name, String tag, String color, double creationCost) { - if (creator == null) { - return GuildResult.failure("Joueur invalide."); + public void logAction(Guild guild, Player actor, String actionType, String details) { + if (guild == null) return; + UUID actorUuid = actor != null ? actor.getUniqueId() : null; + String actorName = actor != null ? actor.getName() : "Système"; + GuildLog log = new GuildLog(0, guild.getId(), actorUuid, actorName, actionType, details, System.currentTimeMillis()); + repository.saveLogAsync(log); + } + + // ========================================== + // CYCLE DE VIE D'UNE GUILDE + // ========================================== + + /** + * Crée une nouvelle guilde. + */ + public GuildResult createGuild(Player founder, String name, String tag, double creationCost) { + if (founder == null || name == null || tag == null) { + return GuildResult.failure("Paramètres invalides."); } - // 1. Vérifier si le joueur est déjà dans une guilde - if (isInGuild(creator.getUniqueId())) { + UUID founderUuid = founder.getUniqueId(); + + if (isInGuild(founderUuid)) { 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à."); + return GuildResult.failure("Une guilde portant ce nom existe déjà."); } - // 4. Vérifier si le chunk actuel est disponible - Location loc = creator.getLocation(); + Location loc = founder.getLocation(); Chunk chunk = loc.getChunk(); - String worldName = loc.getWorld().getName(); int chunkX = chunk.getX(); int chunkZ = chunk.getZ(); + String worldName = loc.getWorld().getName(); 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) + ")."); + if (!economyManager.has(founder, creationCost)) { + return GuildResult.failure("Vous n'avez pas les fonds nécessaires (" + economyManager.format(creationCost) + ") pour créer une guilde."); } - 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."); + if (!economyManager.withdraw(founder, creationCost)) { + return GuildResult.failure("Échec de la transaction économique."); } } - // 6. Instancier la nouvelle guilde UUID guildId = UUID.randomUUID(); - Guild guild = new Guild(guildId, name, tag, color, "Guilde fondée par " + creator.getName(), 0.0, System.currentTimeMillis()); + long now = System.currentTimeMillis(); + Guild guild = new Guild(guildId, name, tag.toUpperCase(), "Guilde fondée par " + founder.getName(), 0.0, now); + guild.setHome(loc); - // 7. Configurer les rangs par défaut + // Rangs initiaux 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.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 + 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); + GuildMember founderMember = new GuildMember(founderUuid, guildId, "leader", now, now, 0.0); + guild.addMember(founderMember); - // 9. Claimer le 1er chunk et définir le home - GuildClaim initialClaim = new GuildClaim(0, guildId, worldName, chunkX, chunkZ, System.currentTimeMillis()); + GuildClaim initialClaim = new GuildClaim(0, guildId, worldName, chunkX, chunkZ, now); 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.saveMemberAsync(founderMember); repository.saveClaimAsync(initialClaim); - // 11. Créer la région WorldGuard if (worldGuardManager != null) { worldGuardManager.createOrUpdateRegion(guild, loc.getWorld(), chunkX, chunkZ); } - return GuildResult.success("La guilde " + name + " a été créée avec succès !"); + logAction(guild, founder, "CREATION", "Création de la guilde avec le chunk initial (" + chunkX + ", " + chunkZ + ")"); + + return GuildResult.success("La guilde " + guild.getFormattedName() + " 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. + * Dissout définitivement une guilde existante. */ 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."); @@ -233,15 +238,13 @@ public class GuildManager { return GuildResult.failure("Seul le Chef de la guilde peut dissoudre la guilde."); } - // Supprimer toutes les régions WorldGuard associées + logAction(guild, actor, "DISBAND", "Dissolution de la guilde par " + actor.getName()); + if (worldGuardManager != null) { worldGuardManager.removeGuildRegions(guild); } - // Désindexer la guilde unindexGuild(guild); - - // Supprimer de la base de données repository.deleteGuildAsync(guild.getId()); return GuildResult.success("La guilde " + guild.getName() + " a été dissoute."); @@ -252,11 +255,7 @@ public class GuildManager { // ========================================== /** - * 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. + * Intègre un joueur dans une guilde. */ public GuildResult addMember(Guild guild, Player player) { if (guild == null || player == null) { @@ -277,21 +276,18 @@ public class GuildManager { playerGuildMap.put(player.getUniqueId(), guild.getId()); repository.saveMemberAsync(member); - // Synchroniser WorldGuard if (worldGuardManager != null) { worldGuardManager.syncGuildMembers(guild); } + logAction(guild, player, "JOIN", player.getName() + " a rejoint la guilde (" + defaultRank.getName() + ")"); + broadcastToGuild(guild, "guild.broadcast.member_joined", "{player}", player.getName()); + return GuildResult.success("Le joueur " + player.getName() + " a rejoint la guilde !"); } /** - * Expulse un membre d'une guilde avec vérification de la hiérarchie des rangs. - * - * @param actor Le joueur initiateur de l'expulsion. - * @param guild La guilde concernée. - * @param targetUuid L'UUID du membre à expulser. - * @return GuildResult. + * Expulse un membre d'une guilde. */ public GuildResult kickMember(Player actor, Guild guild, UUID targetUuid) { if (actor == null || guild == null || targetUuid == null) { @@ -317,24 +313,26 @@ public class GuildManager { return GuildResult.failure("Vous ne pouvez pas expulser un membre ayant un rang supérieur ou égal au vôtre."); } + String targetName = targetMember.getOfflinePlayer() != null && targetMember.getOfflinePlayer().getName() != null + ? targetMember.getOfflinePlayer().getName() + : targetUuid.toString().substring(0, 8); + guild.removeMember(targetUuid); playerGuildMap.remove(targetUuid); repository.deleteMemberAsync(targetUuid); - // Synchroniser WorldGuard if (worldGuardManager != null) { worldGuardManager.syncGuildMembers(guild); } + logAction(guild, actor, "KICK", targetName + " a été expulsé par " + actor.getName()); + broadcastToGuild(guild, "guild.broadcast.member_kicked", "{player}", targetName, "{actor}", actor.getName()); + 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) { @@ -355,22 +353,18 @@ public class GuildManager { playerGuildMap.remove(player.getUniqueId()); repository.deleteMemberAsync(player.getUniqueId()); - // Synchroniser WorldGuard if (worldGuardManager != null) { worldGuardManager.syncGuildMembers(guild); } + logAction(guild, player, "LEAVE", player.getName() + " a quitté la guilde."); + broadcastToGuild(guild, "guild.broadcast.member_left", "{player}", player.getName()); + 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. + * Modifie le rang d'un membre. */ public GuildResult setMemberRank(Player actor, Guild guild, UUID targetUuid, String newRankId) { if (actor == null || guild == null || targetUuid == null || newRankId == null) { @@ -395,7 +389,6 @@ public class GuildManager { 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."); } @@ -403,7 +396,6 @@ public class GuildManager { 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()) { @@ -413,26 +405,141 @@ public class GuildManager { targetMember.setRankId(newRank.getId()); repository.saveMemberAsync(targetMember); - // Synchroniser WorldGuard (au cas où le chef a changé) if (worldGuardManager != null) { worldGuardManager.syncGuildMembers(guild); } + String targetName = targetMember.getOfflinePlayer() != null && targetMember.getOfflinePlayer().getName() != null + ? targetMember.getOfflinePlayer().getName() + : targetUuid.toString().substring(0, 8); + + logAction(guild, actor, "RANK_CHANGE", targetName + " assigné au rang " + newRank.getName()); + broadcastToGuild(guild, "guild.broadcast.rank_changed", "{player}", targetName, "{rank}", newRank.getName(), "{actor}", actor.getName()); + return GuildResult.success("Le rang du membre a été mis à jour vers " + newRank.getName() + "."); } + /** + * Crée un nouveau rang personnalisé pour la guilde. + */ + public GuildResult createRank(Player actor, Guild guild, String rankId, String name, int priority) { + if (actor == null || guild == null || rankId == null || name == null) { + return GuildResult.failure("Paramètres invalides."); + } + + GuildRank actorRank = guild.getMemberRank(actor.getUniqueId()); + if (actorRank == null || !actorRank.hasPermission(GuildPermission.MANAGE_RANKS)) { + return GuildResult.failure("Votre rang ne vous autorise pas à gérer les rangs."); + } + + GuildRank leaderRank = guild.getLeaderRank(); + boolean isLeader = actorRank.equals(leaderRank); + if (!isLeader && priority >= actorRank.getPriority()) { + return GuildResult.failure("Vous ne pouvez pas créer un rang ayant une priorité supérieure ou égale à la vôtre."); + } + + String cleanId = rankId.toLowerCase(); + if (guild.getRank(cleanId) != null) { + return GuildResult.failure("Un rang avec cet identifiant existe déjà."); + } + + GuildRank newRank = new GuildRank(cleanId, guild.getId(), name, priority, false, EnumSet.of(GuildPermission.HOME, GuildPermission.BANK_DEPOSIT, GuildPermission.BUILD, GuildPermission.BREAK, GuildPermission.INTERACT, GuildPermission.CONTAINER)); + guild.addRank(newRank); + repository.saveRankAsync(newRank); + + logAction(guild, actor, "RANK_CREATE", "Création du rang " + name + " (Priorité: " + priority + ")"); + + return GuildResult.success("Le rang " + name + " (ID: " + cleanId + ", Priorité: " + priority + ") a été créé avec succès !"); + } + + /** + * Supprime un rang existant de la guilde. + */ + public GuildResult deleteRank(Player actor, Guild guild, String rankId) { + if (actor == null || guild == null || rankId == null) { + return GuildResult.failure("Paramètres invalides."); + } + + GuildRank actorRank = guild.getMemberRank(actor.getUniqueId()); + if (actorRank == null || !actorRank.hasPermission(GuildPermission.MANAGE_RANKS)) { + return GuildResult.failure("Votre rang ne vous autorise pas à gérer les rangs."); + } + + GuildRank rank = guild.getRank(rankId.toLowerCase()); + if (rank == null) { + return GuildResult.failure("Le rang spécifié n'existe pas."); + } + + GuildRank leaderRank = guild.getLeaderRank(); + GuildRank defaultRank = guild.getDefaultRank(); + + if (rank.equals(leaderRank)) { + return GuildResult.failure("Vous ne pouvez pas supprimer le rang de Chef."); + } + if (rank.equals(defaultRank)) { + return GuildResult.failure("Vous ne pouvez pas supprimer le rang par défaut."); + } + + GuildRank fallbackRank = defaultRank != null ? defaultRank : guild.getRanks().stream().filter(r -> !r.equals(rank)).findFirst().orElse(null); + if (fallbackRank == null) { + return GuildResult.failure("Impossible de supprimer ce rang sans rang de repli."); + } + + // Migrer les membres occupant ce rang + for (GuildMember m : guild.getMembers()) { + if (m.getRankId().equalsIgnoreCase(rank.getId())) { + m.setRankId(fallbackRank.getId()); + repository.saveMemberAsync(m); + } + } + + guild.removeRank(rank.getId()); + repository.deleteRankAsync(rank.getId(), guild.getId()); + + logAction(guild, actor, "RANK_DELETE", "Suppression du rang " + rank.getName()); + + return GuildResult.success("Le rang " + rank.getName() + " a été supprimé."); + } + + /** + * Active ou désactive une permission spécifique pour un rang. + */ + public GuildResult setRankPermission(Player actor, Guild guild, String rankId, GuildPermission permission, boolean state) { + if (actor == null || guild == null || rankId == null || permission == null) { + return GuildResult.failure("Paramètres invalides."); + } + + GuildRank actorRank = guild.getMemberRank(actor.getUniqueId()); + if (actorRank == null || !actorRank.hasPermission(GuildPermission.MANAGE_RANKS)) { + return GuildResult.failure("Votre rang ne vous autorise pas à gérer les rangs."); + } + + GuildRank rank = guild.getRank(rankId.toLowerCase()); + if (rank == null) { + return GuildResult.failure("Le rang spécifié n'existe pas."); + } + + GuildRank leaderRank = guild.getLeaderRank(); + if (rank.equals(leaderRank)) { + return GuildResult.failure("Vous ne pouvez pas modifier les permissions du Chef."); + } + + if (state) { + rank.addPermission(permission); + } else { + rank.removePermission(permission); + } + + repository.saveRankAsync(rank); + logAction(guild, actor, "RANK_PERM", "Permission " + permission.name() + " -> " + state + " pour " + rank.getName()); + + return GuildResult.success("Permission " + permission.name() + " mise à " + state + " pour le rang " + rank.getName() + "."); + } + // ========================================== // PERSONNALISATION (COULEUR & TAG) // ========================================== - /** - * Modifie la couleur hexadécimale de la guilde. - * - * @param actor Le joueur initiateur de la commande. - * @param guild La guilde concernée. - * @param hexColor Le code couleur hexadécimal (ex: "#FF5555"). - * @return GuildResult. - */ public GuildResult setGuildColor(Player actor, Guild guild, String hexColor) { if (actor == null || guild == null || hexColor == null) { return GuildResult.failure("Paramètres invalides."); @@ -448,18 +555,11 @@ public class GuildManager { guild.setColor(hexColor); repository.saveGuildAsync(guild); + logAction(guild, actor, "COLOR", "Couleur modifiée vers " + hexColor); return GuildResult.success("La couleur de la guilde a été changée pour " + hexColor + " !"); } - /** - * Modifie le tag court de la guilde. - * - * @param actor Le joueur initiateur. - * @param guild La guilde concernée. - * @param newTag Le nouveau tag (2 à 6 caractères alphanumériques). - * @return GuildResult. - */ public GuildResult setGuildTag(Player actor, Guild guild, String newTag) { if (actor == null || guild == null || newTag == null) { return GuildResult.failure("Paramètres invalides."); @@ -475,6 +575,7 @@ public class GuildManager { guild.setTag(newTag.toUpperCase()); repository.saveGuildAsync(guild); + logAction(guild, actor, "TAG", "Tag modifié vers [" + newTag.toUpperCase() + "]"); return GuildResult.success("Le tag de la guilde a été changé pour " + guild.getFormattedTag() + " !"); } @@ -483,16 +584,6 @@ public class GuildManager { // 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."); @@ -500,52 +591,38 @@ public class GuildManager { 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); - // 7. Créer la région WorldGuard if (worldGuardManager != null) { worldGuardManager.createOrUpdateRegion(guild, world, chunkX, chunkZ); } + logAction(guild, player, "CLAIM", "Claim (" + chunkX + ", " + chunkZ + ") pour " + economyManager.format(price)); + broadcastToGuild(guild, "guild.broadcast.chunk_claimed", "{player}", player.getName(), "{chunk_x}", String.valueOf(chunkX), "{chunk_z}", String.valueOf(chunkZ), "{cost}", economyManager.format(price)); + return GuildResult.success("Chunk revendiqué avec succès pour " + economyManager.format(price) + " ! (Total: " + guild.getClaimCount() + " chunks)"); } - /** - * Abandonne un chunk possédé par la guilde. - * - * @param player Le joueur initiateur. - * @param guild La guilde propriétaire. - * @param world Le monde Minecraft. - * @param chunkX La coordonnée X du chunk. - * @param chunkZ La coordonnée Z du chunk. - * @return GuildResult. - */ public GuildResult unclaimChunk(Player player, Guild guild, World world, int chunkX, int chunkZ) { if (player == null || guild == null || world == null) { return GuildResult.failure("Paramètres invalides."); @@ -553,31 +630,29 @@ public class GuildManager { 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 la région WorldGuard if (worldGuardManager != null) { worldGuardManager.removeRegion(guild, world, chunkX, chunkZ); } - // 5. Supprimer le claim guild.removeClaim(worldName, chunkX, chunkZ); chunkGuildMap.remove(getChunkKey(worldName, chunkX, chunkZ)); repository.deleteClaimAsync(worldName, chunkX, chunkZ); + logAction(guild, player, "UNCLAIM", "Abandon du chunk (" + chunkX + ", " + chunkZ + ")"); + broadcastToGuild(guild, "guild.broadcast.chunk_unclaimed", "{player}", player.getName(), "{chunk_x}", String.valueOf(chunkX), "{chunk_z}", String.valueOf(chunkZ)); + return GuildResult.success("Le chunk a été libéré."); } @@ -585,14 +660,6 @@ public class GuildManager { // 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."); @@ -619,17 +686,12 @@ public class GuildManager { } repository.saveGuildAsync(guild); + logAction(guild, player, "DEPOSIT", player.getName() + " a déposé " + economyManager.format(amount)); + broadcastToGuild(guild, "guild.broadcast.bank_deposit", "{player}", player.getName(), "{amount}", economyManager.format(amount), "{balance}", economyManager.format(guild.getBalance())); + return GuildResult.success("Vous avez déposé " + economyManager.format(amount) + " dans la banque de guilde ! (Nouveau solde: " + economyManager.format(guild.getBalance()) + ")"); } - /** - * Retire de l'argent de la banque de guilde vers le compte d'un joueur. - * - * @param player Le joueur bénéficiaire. - * @param guild La guilde débitée. - * @param amount Le montant à retirer. - * @return GuildResult. - */ public GuildResult withdrawMoney(Player player, Guild guild, double amount) { if (player == null || guild == null || amount <= 0) { return GuildResult.failure("Montant invalide."); @@ -648,14 +710,21 @@ public class GuildManager { return GuildResult.failure("Impossible de débiter la banque de guilde."); } - economyManager.deposit(player, amount); + boolean depositedToPlayer = economyManager.deposit(player, amount); + if (!depositedToPlayer) { + guild.deposit(amount); + return GuildResult.failure("Échec du crédit sur votre compte personnel."); + } + repository.saveGuildAsync(guild); + logAction(guild, player, "WITHDRAW", player.getName() + " a retiré " + economyManager.format(amount)); + broadcastToGuild(guild, "guild.broadcast.bank_withdraw", "{player}", player.getName(), "{amount}", economyManager.format(amount), "{balance}", economyManager.format(guild.getBalance())); return GuildResult.success("Vous avez retiré " + economyManager.format(amount) + " de la banque de guilde."); } // ========================================== - // GETTERS & LOOKUPS RAPIDES + // GETTERS & INDEXATION // ========================================== public Guild getGuildById(UUID guildId) { @@ -704,14 +773,10 @@ public class GuildManager { return inviteManager; } - public fr.luc.gamingcore.worldguard.WorldGuardManager getWorldGuardManager() { + public WorldGuardManager getWorldGuardManager() { return worldGuardManager; } - // ========================================== - // MÉTHODES INTERNES D'INDEXATION - // ========================================== - private void indexGuild(Guild guild) { guildsById.put(guild.getId(), guild); guildsByName.put(guild.getName().toLowerCase(), guild.getId()); diff --git a/src/main/java/fr/luc/gamingcore/model/GuildLog.java b/src/main/java/fr/luc/gamingcore/model/GuildLog.java new file mode 100644 index 0000000..cb6a3d1 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/model/GuildLog.java @@ -0,0 +1,80 @@ +package fr.luc.gamingcore.model; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * Entité représentant une entrée d'historique (log) au sein d'une guilde. + */ +public class GuildLog { + + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM HH:mm").withZone(ZoneId.systemDefault()); + + private final long id; + private final UUID guildId; + private final UUID actorUuid; + private final String actorName; + private final String actionType; + private final String details; + private final long timestamp; + + /** + * Constructeur complet d'une entrée de log de guilde. + * + * @param id Identifiant unique en BDD. + * @param guildId UUID de la guilde associée. + * @param actorUuid UUID du joueur ayant déclenché l'action (peut être null). + * @param actorName Nom du joueur ou "Système". + * @param actionType Type d'action (ex: JOIN, LEAVE, CLAIM, BANK_DEPOSIT, etc.). + * @param details Description textuelle de l'événement. + * @param timestamp Timestamp Unix en millisecondes. + */ + public GuildLog(long id, UUID guildId, UUID actorUuid, String actorName, String actionType, String details, long timestamp) { + this.id = id; + this.guildId = guildId; + this.actorUuid = actorUuid; + this.actorName = actorName != null ? actorName : "Système"; + this.actionType = actionType != null ? actionType : "INFO"; + this.details = details != null ? details : ""; + this.timestamp = timestamp; + } + + public long getId() { + return id; + } + + public UUID getGuildId() { + return guildId; + } + + public UUID getActorUuid() { + return actorUuid; + } + + public String getActorName() { + return actorName; + } + + public String getActionType() { + return actionType; + } + + public String getDetails() { + return details; + } + + public long getTimestamp() { + return timestamp; + } + + /** + * Retourne la date et l'heure formatée de l'entrée de log. + * + * @return Ex: "20/08 20:45" + */ + public String getFormattedDate() { + return TIME_FORMATTER.format(Instant.ofEpochMilli(timestamp)); + } +} diff --git a/src/main/java/fr/luc/gamingcore/worldguard/WorldGuardManager.java b/src/main/java/fr/luc/gamingcore/worldguard/WorldGuardManager.java index b142450..38c5fca 100644 --- a/src/main/java/fr/luc/gamingcore/worldguard/WorldGuardManager.java +++ b/src/main/java/fr/luc/gamingcore/worldguard/WorldGuardManager.java @@ -123,6 +123,10 @@ public class WorldGuardManager { region.setFlag(Flags.TNT, StateFlag.State.DENY); region.setFlag(Flags.CREEPER_EXPLOSION, StateFlag.State.DENY); region.setFlag(Flags.PVP, StateFlag.State.DENY); + region.setFlag(Flags.PISTONS, StateFlag.State.ALLOW); + region.setFlag(Flags.FIRE_SPREAD, StateFlag.State.DENY); + region.setFlag(Flags.LAVA_FIRE, StateFlag.State.DENY); + region.setFlag(Flags.LIGHTER, StateFlag.State.DENY); regionManager.addRegion(region); } diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index bc8611a..18da778 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -53,7 +53,7 @@ guild: sent: "Invitation envoyée à {player} (Expire dans {seconds}s)." received_interactive: "[Guilde] Vous avez été invité à rejoindre la guilde {guild} {tag} par {sender} !\nCliquez pour accepter l''invitation et rejoindre la guilde !'>[ ✅ ACCEPTER ] Cliquez pour refuser l''invitation.'>[ ❌ REFUSER ]" no_pending_invite: "Vous n'avez aucune invitation valide en attente pour la guilde {guild}." - accepted: "Le joueur {player} a rejoint la guilde !" + accepted_self: "Vous avez rejoint la guilde {guild} {tag} !" declined_sender: "{player} a refusé votre invitation." declined_target: "Vous avez refusé l'invitation de la guilde {guild}." @@ -65,6 +65,7 @@ guild: unclaim_no_permission: "Votre rang ne vous autorise pas à abandonner des territoires." unclaim_not_owned: "Ce chunk n'appartient pas à votre guilde." unclaim_last_chunk: "Vous ne pouvez pas abandonner le dernier territoire de votre guilde." + unclaim_confirm_prompt: "⚠ ATTENTION : Vous allez libérer le chunk {world} ({chunk_x}, {chunk_z}).\nExécutez à nouveau /guild unclaim pour confirmer l'abandon du territoire." unclaim_success: "Le chunk a été libéré." bank: @@ -89,6 +90,38 @@ guild: empty: "Aucune guilde n'a été créée pour le moment." pagination: "Page précédente'>[◀ Précédent] --- Page suivante'>[Suivant ▶]" + chat: + format: "[Chat Guilde] {rank} {player} » {message}" + + broadcast: + member_joined: "{player} a rejoint la guilde !" + member_left: "{player} a quitté la guilde." + member_kicked: "{player} a été expulsé de la guilde par {actor}." + chunk_claimed: "{player} a revendiqué un nouveau chunk ({chunk_x}, {chunk_z}) pour {cost} !" + chunk_unclaimed: "{player} a libéré le chunk ({chunk_x}, {chunk_z})." + bank_deposit: "{player} a déposé {amount} dans la banque (Solde: {balance})." + bank_withdraw: "{player} a retiré {amount} de la banque (Solde: {balance})." + rank_changed: "{player} a été promu/assigné au rang {rank} par {actor}." + + history: + header: "=== HISTORIQUE DE GUILDE ({page}/{max_page}) ===" + item: "[{date}] {actor} » {details}" + empty: "Aucun événement enregistré dans l'historique." + pagination: "Page précédente'>[◀ Précédent] --- Page suivante'>[Suivant ▶]" + + ranks: + header: "=== RANGS DE LA GUILDE ===" + item: "• {name} (ID: {id}, Priorité: {priority})\n Permissions : {permissions}" + created: "Le rang {name} (ID: {id}, Priorité: {priority}) a été créé avec succès !" + deleted: "Le rang {name} a été supprimé." + perm_updated: "Permission {perm} mise à {state} pour le rang {rank}." + no_permission: "Votre rang ne vous autorise pas à gérer les rangs de la guilde." + cannot_modify_leader: "Vous ne pouvez pas modifier ou supprimer le rang de Chef." + cannot_delete_default: "Vous ne pouvez pas supprimer le rang par défaut des recrues." + rank_already_exists: "Un rang portant l'identifiant {id} existe déjà." + rank_not_found: "Rang introuvable avec l'ID {id}." + insufficient_priority: "Vous ne pouvez pas créer/modifier un rang ayant une priorité supérieure ou égale à la vôtre." + protection: territory_enter_own: "Territoire de guilde : {guild} {tag}" territory_enter_other: "Territoire étranger : {guild} {tag}" @@ -104,3 +137,4 @@ protection: piston_denied: "⛔ Les pistons ne peuvent pas déplacer de blocs à travers les frontières d'un territoire de guilde." fluid_denied: "⛔ L'écoulement de fluides vers un territoire protégé est bloqué." hanging_denied: "⛔ Vous ne pouvez pas modifier ou casser les entités de décoration dans ce territoire." + fire_denied: "🔥 La propagation du feu et la combustion des blocs sont désactivées sur les territoires protégés."