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")
.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<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);
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;
@@ -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<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
// ==========================================
+6
View File
@@ -60,6 +60,7 @@ guild:
claim:
no_permission: "<red>Votre rang ne vous autorise pas à revendiquer des territoires.</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>"
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>"
@@ -67,6 +68,7 @@ guild:
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_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:
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>"
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_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>"
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>"