From cbe90896991626154b2fc6c2289a98dc3f7ab883 Mon Sep 17 00:00:00 2001 From: GeminiAntigravityCLI Date: Thu, 20 Aug 2026 21:24:08 +0200 Subject: [PATCH] feat: Interactive chat UI for rank permissions, contiguous claims rule, and cascade orphaned unclaim --- .../luc/gamingcore/command/GuildCommands.java | 60 ++++++++- .../luc/gamingcore/manager/GuildManager.java | 120 +++++++++++++++++- src/main/resources/messages.yml | 6 + 3 files changed, 178 insertions(+), 8 deletions(-) diff --git a/src/main/java/fr/luc/gamingcore/command/GuildCommands.java b/src/main/java/fr/luc/gamingcore/command/GuildCommands.java index bbc7dc0..28d5955 100644 --- a/src/main/java/fr/luc/gamingcore/command/GuildCommands.java +++ b/src/main/java/fr/luc/gamingcore/command/GuildCommands.java @@ -1005,23 +1005,41 @@ public class GuildCommands { } }) ) - // /guild rank setperm + // /guild rank setperm [permission] [valeur] .subcommand(BetterMcCommands.subBuilder("setperm") - .description("Modifier une permission pour un rang") + .description("Afficher ou modifier les permissions d'un rang avec boutons cliquables") + .aliases("perm", "perms", "permission", "permissions") .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")) + .argument(Arguments.enumOf("permission", GuildPermission.class).optional().description("Permission")) + .argument(Arguments.bool("valeur").optional().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"); + + GuildRank rank = guild.getRank(id); + if (rank == null) { + context.reply(msg.get("guild.ranks.rank_not_found", "{id}", id)); + return; + } + + Optional permOpt = context.getOptional("permission", GuildPermission.class); + Optional valOpt = context.getOptional("valeur", Boolean.class); + + if (permOpt.isEmpty() || valOpt.isEmpty()) { + // Afficher le menu interactif avec boutons cliquables + sendRankPermissionMenu(context, guild, rank); + return; + } + + GuildPermission perm = permOpt.get(); + boolean state = valOpt.get(); GuildResult res = guildManager.setRankPermission(player, guild, id, perm, state); if (res.isSuccess()) { - context.replySuccess(res.getMessage()); + context.reply(res.getMessage()); + sendRankPermissionMenu(context, guild, rank); } else { context.replyError(res.getMessage()); } @@ -1132,6 +1150,34 @@ public class GuildCommands { context.reply(footer); } + /** + * Envoie le menu interactif de gestion des permissions d'un rang avec boutons cliquables OUI / NON. + */ + private void sendRankPermissionMenu(fr.luc.bettermccommands.api.CommandContext context, Guild guild, GuildRank rank) { + context.reply(msg.get("guild.ranks.perm_menu_header", + "{rank}", rank.getName(), + "{priority}", String.valueOf(rank.getPriority()) + )); + context.reply(msg.get("guild.ranks.perm_menu_hint")); + + for (GuildPermission perm : GuildPermission.values()) { + boolean has = rank.hasPermission(perm); + if (has) { + context.reply(msg.get("guild.ranks.perm_entry_active", + "{desc}", perm.getDescription(), + "{perm}", perm.name(), + "{id}", rank.getId() + )); + } else { + context.reply(msg.get("guild.ranks.perm_entry_inactive", + "{desc}", perm.getDescription(), + "{perm}", perm.name(), + "{id}", rank.getId() + )); + } + } + } + private static class HelpEntry { final String usage; final String description; diff --git a/src/main/java/fr/luc/gamingcore/manager/GuildManager.java b/src/main/java/fr/luc/gamingcore/manager/GuildManager.java index c42323c..1fd46e3 100644 --- a/src/main/java/fr/luc/gamingcore/manager/GuildManager.java +++ b/src/main/java/fr/luc/gamingcore/manager/GuildManager.java @@ -595,10 +595,16 @@ public class GuildManager { 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é."); } + // 2. bis Vérifier l'adjacence obligatoire si la guilde possède déjà des claims + if (guild.getClaimCount() > 0 && !isAdjacentToGuildClaims(guild, worldName, chunkX, chunkZ)) { + return GuildResult.failure(messageManager.get("guild.claim.must_be_adjacent")); + } + double price = priceCalculator.calculateNextChunkPrice(guild); if (!guild.hasBalance(price)) { @@ -650,12 +656,124 @@ public class GuildManager { chunkGuildMap.remove(getChunkKey(worldName, chunkX, chunkZ)); repository.deleteClaimAsync(worldName, chunkX, chunkZ); - logAction(guild, player, "UNCLAIM", "Abandon du chunk (" + chunkX + ", " + chunkZ + ")"); + // Nettoyage automatique en cascade des chunks orphelins + int orphansCount = removeOrphanedClaims(guild, worldName); + + logAction(guild, player, "UNCLAIM", "Abandon du chunk (" + chunkX + ", " + chunkZ + ")" + (orphansCount > 0 ? " + " + orphansCount + " orphelin(s)" : "")); broadcastToGuild(guild, "guild.broadcast.chunk_unclaimed", "{player}", player.getName(), "{chunk_x}", String.valueOf(chunkX), "{chunk_z}", String.valueOf(chunkZ)); + if (orphansCount > 0) { + return GuildResult.success(messageManager.get("guild.claim.unclaim_cascade", "{count}", String.valueOf(orphansCount))); + } + return GuildResult.success("Le chunk a été libéré."); } + /** + * Vérifie si les coordonnées de chunk données sont directement adjacentes (Nord, Sud, Est, Ouest) + * à au moins un chunk possédé par la guilde dans le même monde. + * + * @param guild La guilde concernée. + * @param worldName Le monde du claim. + * @param chunkX Coordonnée X du chunk. + * @param chunkZ Coordonnée Z du chunk. + * @return true si au moins un territoire adjacent appartient à la guilde. + */ + public boolean isAdjacentToGuildClaims(Guild guild, String worldName, int chunkX, int chunkZ) { + if (guild == null || guild.getClaimCount() == 0) return true; + + for (GuildClaim c : guild.getClaims()) { + if (c.getWorldName().equalsIgnoreCase(worldName)) { + int dx = Math.abs(c.getChunkX() - chunkX); + int dz = Math.abs(c.getChunkZ() - chunkZ); + if (dx + dz == 1) { + return true; + } + } + } + return false; + } + + /** + * Détecte et libère automatiquement tous les territoires orphelins (qui ne sont plus rattachés + * au bloc principal / Home de la guilde suite à un unclaim). + * + * @param guild La guilde concernée. + * @param worldName Le monde où effectuer la vérification de contiguïté. + * @return Le nombre de territoires orphelins libérés en cascade. + */ + public int removeOrphanedClaims(Guild guild, String worldName) { + if (guild == null || guild.getClaimCount() == 0) return 0; + + List worldClaims = guild.getClaims().stream() + .filter(c -> c.getWorldName().equalsIgnoreCase(worldName)) + .collect(Collectors.toList()); + + if (worldClaims.size() <= 1) { + return 0; + } + + GuildClaim anchor = null; + if (guild.getHomeWorld() != null && guild.getHomeWorld().equalsIgnoreCase(worldName)) { + int homeChunkX = ((int) Math.floor(guild.getHomeX())) >> 4; + int homeChunkZ = ((int) Math.floor(guild.getHomeZ())) >> 4; + for (GuildClaim c : worldClaims) { + if (c.getChunkX() == homeChunkX && c.getChunkZ() == homeChunkZ) { + anchor = c; + break; + } + } + } + + if (anchor == null) { + anchor = worldClaims.stream() + .min(Comparator.comparingLong(GuildClaim::getClaimedAt)) + .orElse(worldClaims.get(0)); + } + + Set connected = new HashSet<>(); + Queue queue = new ArrayDeque<>(); + queue.add(anchor); + connected.add(anchor); + + while (!queue.isEmpty()) { + GuildClaim curr = queue.poll(); + for (GuildClaim candidate : worldClaims) { + if (!connected.contains(candidate)) { + int dx = Math.abs(candidate.getChunkX() - curr.getChunkX()); + int dz = Math.abs(candidate.getChunkZ() - curr.getChunkZ()); + if (dx + dz == 1) { + connected.add(candidate); + queue.add(candidate); + } + } + } + } + + List orphans = new ArrayList<>(); + for (GuildClaim c : worldClaims) { + if (!connected.contains(c)) { + orphans.add(c); + } + } + + if (orphans.isEmpty()) { + return 0; + } + + org.bukkit.World world = org.bukkit.Bukkit.getWorld(worldName); + for (GuildClaim orphan : orphans) { + if (worldGuardManager != null && world != null) { + worldGuardManager.removeRegion(guild, world, orphan.getChunkX(), orphan.getChunkZ()); + } + guild.removeClaim(worldName, orphan.getChunkX(), orphan.getChunkZ()); + chunkGuildMap.remove(getChunkKey(worldName, orphan.getChunkX(), orphan.getChunkZ())); + repository.deleteClaimAsync(worldName, orphan.getChunkX(), orphan.getChunkZ()); + } + + return orphans.size(); + } + // ========================================== // GESTION DE LA BANQUE // ========================================== diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 18da778..2966a79 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -60,6 +60,7 @@ guild: claim: no_permission: "Votre rang ne vous autorise pas à revendiquer des territoires." already_claimed: "Ce chunk est déjà revendiqué." + must_be_adjacent: "Votre nouveau territoire doit être directement collé (adjacent) à un territoire déjà possédé par votre guilde." insufficient_bank: "La banque de guilde n'a pas assez d'argent (Requis : {cost}, Solde : {balance})." success: "Chunk revendiqué avec succès pour {cost} ! (Total : {count} chunks)" unclaim_no_permission: "Votre rang ne vous autorise pas à abandonner des territoires." @@ -67,6 +68,7 @@ guild: 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é." + unclaim_cascade: "Le chunk a été libéré. {count} territoire(s) orphelin(s) ont également été libérés automatiquement." bank: deposit_no_permission: "Votre rang ne vous autorise pas à déposer dans la banque." @@ -115,6 +117,10 @@ guild: 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}." + perm_menu_header: "=== PERMISSIONS : {rank} (Priorité: {priority}) ===" + perm_menu_hint: "Cliquez sur [ OUI ] ou [ NON ] pour basculer instantanément chaque droit." + perm_entry_active: "• {desc} ({perm}) :\n [ ✅ OUI ] Cliquez pour DÉSACTIVER la permission'>[ NON ]" + perm_entry_inactive: "• {desc} ({perm}) :\n Cliquez pour ACTIVER la permission'>[ OUI ] [ ❌ NON ]" 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."