feat: Interactive chat UI for rank permissions, contiguous claims rule, and cascade orphaned unclaim

This commit is contained in:
2026-08-20 21:24:08 +02:00
parent dd09dc751f
commit cbe9089699
3 changed files with 178 additions and 8 deletions
@@ -1005,23 +1005,41 @@ public class GuildCommands {
} }
}) })
) )
// /guild rank setperm <id> <permission> <true/false> // /guild rank setperm <id> [permission] [valeur]
.subcommand(BetterMcCommands.subBuilder("setperm") .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() .playerOnly()
.argument(Arguments.string("id").description("ID du rang")) .argument(Arguments.string("id").description("ID du rang"))
.argument(Arguments.enumOf("permission", GuildPermission.class).description("Permission")) .argument(Arguments.enumOf("permission", GuildPermission.class).optional().description("Permission"))
.argument(Arguments.bool("valeur").description("true ou false")) .argument(Arguments.bool("valeur").optional().description("true ou false"))
.executes(context -> { .executes(context -> {
Player player = context.getPlayer(); Player player = context.getPlayer();
Guild guild = guildManager.getGuildByPlayer(player.getUniqueId()); Guild guild = guildManager.getGuildByPlayer(player.getUniqueId());
String id = context.getString("id"); 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<GuildPermission> permOpt = context.getOptional("permission", GuildPermission.class);
Optional<Boolean> 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); GuildResult res = guildManager.setRankPermission(player, guild, id, perm, state);
if (res.isSuccess()) { if (res.isSuccess()) {
context.replySuccess(res.getMessage()); context.reply(res.getMessage());
sendRankPermissionMenu(context, guild, rank);
} else { } else {
context.replyError(res.getMessage()); context.replyError(res.getMessage());
} }
@@ -1132,6 +1150,34 @@ public class GuildCommands {
context.reply(footer); 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 { private static class HelpEntry {
final String usage; final String usage;
final String description; final String description;
@@ -595,10 +595,16 @@ public class GuildManager {
return GuildResult.failure("Votre rang ne vous autorise pas à revendiquer de nouveaux territoires."); 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)) { if (isChunkClaimed(worldName, chunkX, chunkZ)) {
return GuildResult.failure("Ce chunk est déjà revendiqué."); 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); double price = priceCalculator.calculateNextChunkPrice(guild);
if (!guild.hasBalance(price)) { if (!guild.hasBalance(price)) {
@@ -650,12 +656,124 @@ public class GuildManager {
chunkGuildMap.remove(getChunkKey(worldName, chunkX, chunkZ)); chunkGuildMap.remove(getChunkKey(worldName, chunkX, chunkZ));
repository.deleteClaimAsync(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)); 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é."); 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<GuildClaim> 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<GuildClaim> connected = new HashSet<>();
Queue<GuildClaim> 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<GuildClaim> 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 // GESTION DE LA BANQUE
// ========================================== // ==========================================
+6
View File
@@ -60,6 +60,7 @@ guild:
claim: claim:
no_permission: "<red>Votre rang ne vous autorise pas à revendiquer des territoires.</red>" no_permission: "<red>Votre rang ne vous autorise pas à revendiquer des territoires.</red>"
already_claimed: "<red>Ce chunk est déjà revendiqué.</red>" already_claimed: "<red>Ce chunk est déjà revendiqué.</red>"
must_be_adjacent: "<red>Votre nouveau territoire doit être directement collé (adjacent) à un territoire déjà possédé par votre guilde.</red>"
insufficient_bank: "<red>La banque de guilde n'a pas assez d'argent (Requis : <yellow>{cost}</yellow>, Solde : <green>{balance}</green>).</red>" insufficient_bank: "<red>La banque de guilde n'a pas assez d'argent (Requis : <yellow>{cost}</yellow>, Solde : <green>{balance}</green>).</red>"
success: "<green>Chunk revendiqué avec succès pour <yellow>{cost}</yellow> ! (Total : <aqua>{count}</aqua> chunks)</green>" success: "<green>Chunk revendiqué avec succès pour <yellow>{cost}</yellow> ! (Total : <aqua>{count}</aqua> chunks)</green>"
unclaim_no_permission: "<red>Votre rang ne vous autorise pas à abandonner des territoires.</red>" unclaim_no_permission: "<red>Votre rang ne vous autorise pas à abandonner des territoires.</red>"
@@ -67,6 +68,7 @@ guild:
unclaim_last_chunk: "<red>Vous ne pouvez pas abandonner le dernier territoire de votre guilde.</red>" unclaim_last_chunk: "<red>Vous ne pouvez pas abandonner le dernier territoire de votre guilde.</red>"
unclaim_confirm_prompt: "<red><bold>⚠ ATTENTION :</bold> Vous allez libérer le chunk <aqua>{world} ({chunk_x}, {chunk_z})</aqua>.\n<gold>Exécutez à nouveau <yellow>/guild unclaim</yellow> pour confirmer l'abandon du territoire.</gold></red>" unclaim_confirm_prompt: "<red><bold>⚠ ATTENTION :</bold> Vous allez libérer le chunk <aqua>{world} ({chunk_x}, {chunk_z})</aqua>.\n<gold>Exécutez à nouveau <yellow>/guild unclaim</yellow> pour confirmer l'abandon du territoire.</gold></red>"
unclaim_success: "<yellow>Le chunk a été libéré.</yellow>" unclaim_success: "<yellow>Le chunk a été libéré.</yellow>"
unclaim_cascade: "<yellow>Le chunk a été libéré. <gold>{count}</gold> territoire(s) orphelin(s) ont également été libérés automatiquement.</yellow>"
bank: bank:
deposit_no_permission: "<red>Votre rang ne vous autorise pas à déposer dans la banque.</red>" deposit_no_permission: "<red>Votre rang ne vous autorise pas à déposer dans la banque.</red>"
@@ -115,6 +117,10 @@ guild:
created: "<green>Le rang <yellow>{name}</yellow> (ID: {id}, Priorité: {priority}) a été créé avec succès !</green>" created: "<green>Le rang <yellow>{name}</yellow> (ID: {id}, Priorité: {priority}) a été créé avec succès !</green>"
deleted: "<yellow>Le rang <white>{name}</white> a été supprimé.</yellow>" deleted: "<yellow>Le rang <white>{name}</white> a été supprimé.</yellow>"
perm_updated: "<green>Permission <yellow>{perm}</yellow> mise à <aqua>{state}</aqua> pour le rang <yellow>{rank}</yellow>.</green>" perm_updated: "<green>Permission <yellow>{perm}</yellow> mise à <aqua>{state}</aqua> pour le rang <yellow>{rank}</yellow>.</green>"
perm_menu_header: "<gradient:#00C9FF:#92FE9D><bold>=== PERMISSIONS : {rank} (Priorité: {priority}) ===</bold></gradient>"
perm_menu_hint: "<gray>Cliquez sur <green>[ OUI ]</green> ou <red>[ NON ]</red> pour basculer instantanément chaque droit.</gray>"
perm_entry_active: "<gray>• {desc} <dark_gray>({perm})</dark_gray> :</gray>\n <green><bold>[ ✅ OUI ]</bold></green> <click:run_command:'/guild rank setperm {id} {perm} false'><hover:show_text:'<red>Cliquez pour DÉSACTIVER la permission</red>'><gray>[ NON ]</gray></hover></click>"
perm_entry_inactive: "<gray>• {desc} <dark_gray>({perm})</dark_gray> :</gray>\n <click:run_command:'/guild rank setperm {id} {perm} true'><hover:show_text:'<green>Cliquez pour ACTIVER la permission</green>'><gray>[ OUI ]</gray></hover></click> <red><bold>[ ❌ NON ]</bold></red>"
no_permission: "<red>Votre rang ne vous autorise pas à gérer les rangs de la guilde.</red>" no_permission: "<red>Votre rang ne vous autorise pas à gérer les rangs de la guilde.</red>"
cannot_modify_leader: "<red>Vous ne pouvez pas modifier ou supprimer le rang de Chef.</red>" cannot_modify_leader: "<red>Vous ne pouvez pas modifier ou supprimer le rang de Chef.</red>"
cannot_delete_default: "<red>Vous ne pouvez pas supprimer le rang par défaut des recrues.</red>" cannot_delete_default: "<red>Vous ne pouvez pas supprimer le rang par défaut des recrues.</red>"