feat: Add guild history, guild chat (#), rank management system, fire spread protection, piston fixes and broadcasts

This commit is contained in:
2026-08-20 20:50:52 +02:00
parent db0ee45b45
commit dd09dc751f
10 changed files with 873 additions and 209 deletions
@@ -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 !");
}
@@ -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 <message>
// -------------------------------------------------------------
.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 <id> <nom> <priorite>
.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 <id>
.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 <id> <permission> <true/false>
.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 <joueur> <rang>
// -------------------------------------------------------------
.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<HelpEntry> entries = new ArrayList<>();
entries.add(new HelpEntry("/guild create <nom> [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 <message>", "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 <joueur> <rang>", "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 "));
@@ -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" +
");");
}
}
@@ -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<java.util.List<fr.luc.gamingcore.model.GuildLog>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
java.util.List<fr.luc.gamingcore.model.GuildLog> 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));
});
}
}
@@ -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);
}
}
}
}
}
@@ -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,16 +267,40 @@ 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)) {
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,10 +317,17 @@ 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)) {
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);
}
}
@@ -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<UUID, Guild> guildsById = new ConcurrentHashMap<>();
private final Map<String, UUID> guildsByName = new ConcurrentHashMap<>();
private final Map<UUID, UUID> playerGuildMap = new ConcurrentHashMap<>();
private final Map<String, UUID> 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<UUID, Guild> loaded = repository.loadAllGuilds();
for (Guild guild : loaded.values()) {
Collection<Guild> 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);
}
// 1. Vérifier si le joueur est déjà dans une guilde
if (isInGuild(creator.getUniqueId())) {
// ==========================================
// 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.");
}
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 <white><bold>" + name + "</bold></white> 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 <white>" + guild.getName() + "</white> 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 <white>" + player.getName() + "</white> 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 <white>" + newRank.getName() + "</white>.");
}
/**
* 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 <yellow>" + name + "</yellow> (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 <white>" + rank.getName() + "</white> 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 <yellow>" + permission.name() + "</yellow> mise à <aqua>" + state + "</aqua> pour le rang <yellow>" + rank.getName() + "</yellow>.");
}
// ==========================================
// 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 <color:" + hexColor + ">" + hexColor + "</color> !");
}
/**
* 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 <yellow>" + economyManager.format(price) + "</yellow> ! (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é <green>" + economyManager.format(amount) + "</green> 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é <green>" + economyManager.format(amount) + "</green> 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());
@@ -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));
}
}
@@ -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);
}
+35 -1
View File
@@ -53,7 +53,7 @@ guild:
sent: "<green>Invitation envoyée à <white>{player}</white> (Expire dans <yellow>{seconds}s</yellow>).</green>"
received_interactive: "<gradient:#00C9FF:#92FE9D>[Guilde]</gradient> Vous avez été invité à rejoindre la guilde {guild} {tag} par <yellow>{sender}</yellow> !\n<click:run_command:'/guild join {raw_guild}'><hover:show_text:'<green>Cliquez pour accepter l''invitation et rejoindre la guilde !</green>'><green><bold>[ ✅ ACCEPTER ]</bold></green></hover></click> <click:run_command:'/guild decline {raw_guild}'><hover:show_text:'<red>Cliquez pour refuser l''invitation.</red>'><red><bold>[ ❌ REFUSER ]</bold></red></hover></click>"
no_pending_invite: "<red>Vous n'avez aucune invitation valide en attente pour la guilde {guild}.</red>"
accepted: "<green>Le joueur <white>{player}</white> a rejoint la guilde !</green>"
accepted_self: "<green>Vous avez rejoint la guilde {guild} {tag} !</green>"
declined_sender: "<yellow><white>{player}</white> a refusé votre invitation.</yellow>"
declined_target: "<gray>Vous avez refusé l'invitation de la guilde {guild}.</gray>"
@@ -65,6 +65,7 @@ guild:
unclaim_no_permission: "<red>Votre rang ne vous autorise pas à abandonner des territoires.</red>"
unclaim_not_owned: "<red>Ce chunk n'appartient pas à 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_success: "<yellow>Le chunk a été libéré.</yellow>"
bank:
@@ -89,6 +90,38 @@ guild:
empty: "<gray>Aucune guilde n'a été créée pour le moment.</gray>"
pagination: "<click:run_command:'/guild list {prev_page}'><hover:show_text:'<yellow>Page précédente</yellow>'><aqua>[◀ Précédent]</aqua></hover></click> <dark_gray>---</dark_gray> <click:run_command:'/guild list {next_page}'><hover:show_text:'<yellow>Page suivante</yellow>'><aqua>[Suivant ▶]</aqua></hover></click>"
chat:
format: "<color:{color}>[Chat Guilde] <gold>{rank}</gold> <white>{player}</white> <dark_gray>»</dark_gray> <white>{message}</white></color>"
broadcast:
member_joined: "<green><white>{player}</white> a rejoint la guilde !</green>"
member_left: "<yellow><white>{player}</white> a quitté la guilde.</yellow>"
member_kicked: "<red><white>{player}</white> a été expulsé de la guilde par <yellow>{actor}</yellow>.</red>"
chunk_claimed: "<aqua><white>{player}</white> a revendiqué un nouveau chunk ({chunk_x}, {chunk_z}) pour <yellow>{cost}</yellow> !</aqua>"
chunk_unclaimed: "<gray><white>{player}</white> a libéré le chunk ({chunk_x}, {chunk_z}).</gray>"
bank_deposit: "<green><white>{player}</white> a déposé <yellow>{amount}</yellow> dans la banque (Solde: <green>{balance}</green>).</green>"
bank_withdraw: "<yellow><white>{player}</white> a retiré <yellow>{amount}</yellow> de la banque (Solde: <green>{balance}</green>).</yellow>"
rank_changed: "<gold><white>{player}</white> a été promu/assigné au rang <yellow>{rank}</yellow> par <white>{actor}</white>.</gold>"
history:
header: "<gradient:#00C9FF:#92FE9D><bold>=== HISTORIQUE DE GUILDE ({page}/{max_page}) ===</bold></gradient>"
item: "<dark_gray>[{date}]</dark_gray> <yellow>{actor}</yellow> <dark_gray>»</dark_gray> <gray>{details}</gray>"
empty: "<gray>Aucun événement enregistré dans l'historique.</gray>"
pagination: "<click:run_command:'/guild history {prev_page}'><hover:show_text:'<yellow>Page précédente</yellow>'><aqua>[◀ Précédent]</aqua></hover></click> <dark_gray>---</dark_gray> <click:run_command:'/guild history {next_page}'><hover:show_text:'<yellow>Page suivante</yellow>'><aqua>[Suivant ▶]</aqua></hover></click>"
ranks:
header: "<gradient:#00C9FF:#92FE9D><bold>=== RANGS DE LA GUILDE ===</bold></gradient>"
item: "<gold>• {name} <dark_gray>(ID: {id}, Priorité: {priority})</dark_gray></gold>\n <gray>Permissions : <aqua>{permissions}</aqua></gray>"
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>"
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>"
rank_already_exists: "<red>Un rang portant l'identifiant <yellow>{id}</yellow> existe déjà.</red>"
rank_not_found: "<red>Rang introuvable avec l'ID <yellow>{id}</yellow>.</red>"
insufficient_priority: "<red>Vous ne pouvez pas créer/modifier un rang ayant une priorité supérieure ou égale à la vôtre.</red>"
protection:
territory_enter_own: "<gray>Territoire de guilde : </gray>{guild} {tag}"
territory_enter_other: "<gray>Territoire étranger : </gray>{guild} {tag}"
@@ -104,3 +137,4 @@ protection:
piston_denied: "<red>⛔ Les pistons ne peuvent pas déplacer de blocs à travers les frontières d'un territoire de guilde.</red>"
fluid_denied: "<red>⛔ L'écoulement de fluides vers un territoire protégé est bloqué.</red>"
hanging_denied: "<red>⛔ Vous ne pouvez pas modifier ou casser les entités de décoration dans ce territoire.</red>"
fire_denied: "<red>🔥 La propagation du feu et la combustion des blocs sont désactivées sur les territoires protégés.</red>"