feat: Interactive chat UI for rank permissions, contiguous claims rule, and cascade orphaned unclaim
This commit is contained in:
@@ -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
|
||||
// ==========================================
|
||||
|
||||
@@ -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>"
|
||||
|
||||
Reference in New Issue
Block a user