Instanciée une fois dans {@code onEnable()}, branche en cascade : + *
{@code
+ * public class MyGamePlugin extends JavaPlugin {
+ *
+ * private CRCore core;
+ *
+ * @Override
+ * public void onEnable() {
+ * this.core = new CRCore(this).enable();
+ * // /core team create/delete/add/remove/join/leave/... est prêt
+ * }
+ *
+ * @Override
+ * public void onDisable() {
+ * if (core != null) core.disable();
+ * }
+ * }
+ * }
+ *
+ * Le plugin de jeu doit avoir déclaré la commande dans son {@code plugin.yml} : + *
{@code
+ * commands:
+ * core:
+ * description: Commandes CR-Core
+ * }
+ *
+ * {@code
+ * new CRCore(this, new CRCoreConfig()
+ * .withSqliteFile("mydata.db")
+ * .withCommandName("game"))
+ * .enable();
+ * }
+ *
+ * Valeurs par défaut : + *
Une commande peut être : + *
L'override par les plugins de jeu se fait via {@link #replaceSubCommand}
+ * (remplacer une sous-commande par nom) ou en sous-classant et en redéfinissant
+ * {@link #execute}.
+ */
public abstract class AbstractCommand implements Command {
private final String name;
private final List Comportement par défaut :
+ * {@code BaseCommand} se contente de relayer {@code onCommand} et
+ * {@code onTabComplete} vers {@link AbstractCommand#dispatch} et
+ * {@link AbstractCommand#tabComplete}. Toute la logique (routage récursif,
+ * permissions, player-only, parsing d'arguments) vit dans
+ * {@link AbstractCommand}.
+ */
public abstract class BaseCommand extends AbstractCommand
implements CommandExecutor, TabCompleter {
- private final Map Toute la machinerie de routage / parsing / tab-complete est héritée de
+ * {@link AbstractCommand}.
+ */
public abstract class SubCommand extends AbstractCommand {
protected SubCommand(String name, String... aliases) {
diff --git a/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java b/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java
new file mode 100644
index 0000000..a4d5466
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java
@@ -0,0 +1,48 @@
+package fr.luc.crcore.command.builtin;
+
+import fr.luc.crcore.command.BaseCommand;
+import fr.luc.crcore.command.builtin.team.TeamGroupSubCommand;
+import fr.luc.crcore.player.PlayerProfileService;
+import fr.luc.crcore.team.TeamService;
+
+import java.util.Objects;
+
+/**
+ * Commande racine {@code /core}. Container des groupes par défaut.
+ *
+ * Branchée par {@code CRCore.enable()} sur la {@code PluginCommand "core"}
+ * du plugin de jeu (qui doit l'avoir déclarée dans son {@code plugin.yml}).
+ *
+ * Sans arguments, affiche l'aide des groupes disponibles. Avec {@code team
+ * Le chef ajoute un joueur à son équipe. Marche que la team soit PUBLIC ou
+ * PRIVATE — c'est une action chef, pas un auto-join.
+ */
+public class TeamAddSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamAddSubCommand(TeamService service) {
+ super("add", "invite");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Ajouter un joueur à son équipe (chef uniquement)");
+ playerOnly();
+ argument("player", ArgumentTypes.ONLINE_PLAYER);
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Player executor = ctx.requirePlayer();
+ Player target = ctx.get("player");
+
+ Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
+ if (team == null) {
+ return CommandResult.failure("Vous n'appartenez à aucune équipe.");
+ }
+ if (!team.getLeaderId().equals(executor.getUniqueId())) {
+ return CommandResult.failure("Seul le chef peut ajouter des membres.");
+ }
+ if (service.getTeamOfPlayer(target.getUniqueId()).isPresent()) {
+ return CommandResult.failure(target.getName() + " est déjà dans une équipe.");
+ }
+ service.addMember(team.getId(), target.getUniqueId());
+ return CommandResult.success(target.getName() + " ajouté à l'équipe " + team.getName() + ".");
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamArgumentTypes.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamArgumentTypes.java
new file mode 100644
index 0000000..3d53753
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamArgumentTypes.java
@@ -0,0 +1,47 @@
+package fr.luc.crcore.command.builtin.team;
+
+import fr.luc.crcore.command.ArgumentType;
+import fr.luc.crcore.team.Team;
+import fr.luc.crcore.team.TeamService;
+import org.bukkit.command.CommandSender;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Types d'arguments spécifiques aux commandes team. Fournit un
+ * {@link ArgumentType} qui résout un nom d'équipe en {@link Team} et propose
+ * la tab-complétion des équipes existantes.
+ */
+public final class TeamArgumentTypes {
+
+ private TeamArgumentTypes() {
+ }
+
+ /**
+ * Résout un nom d'équipe en {@link Team} (case-insensitive) en interrogeant
+ * le {@link TeamService}. Suggère les noms d'équipes existantes en
+ * tab-completion.
+ */
+ public static ArgumentType Crée une équipe dont l'exécutant devient le chef. Visibilité par défaut :
+ * {@link TeamVisibility#PRIVATE}.
+ */
+public class TeamCreateSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamCreateSubCommand(TeamService service) {
+ super("create", "c", "new");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Créer une équipe");
+ permission("crcore.team.create");
+ playerOnly();
+ argument("name", ArgumentTypes.STRING);
+ argument("tag", ArgumentTypes.STRING);
+ argument("color", ArgumentTypes.enumOf(TeamColor.class));
+ optionalArgument("visibility", ArgumentTypes.enumOf(TeamVisibility.class));
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Player player = ctx.requirePlayer();
+ String name = ctx.get("name");
+ String tag = ctx.get("tag");
+ TeamColor color = ctx.get("color");
+ TeamVisibility visibility = ctx. Dissout l'équipe de l'exécutant. Réservé au chef. Pas d'argument :
+ * l'équipe ciblée est déduite du joueur.
+ *
+ * Aliases : {@code disband}, {@code dissolve}.
+ */
+public class TeamDeleteSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamDeleteSubCommand(TeamService service) {
+ super("delete", "disband", "dissolve");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Dissoudre son équipe (chef uniquement)");
+ playerOnly();
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Player player = ctx.requirePlayer();
+ Team team = service.getTeamOfPlayer(player.getUniqueId())
+ .orElse(null);
+ if (team == null) {
+ return CommandResult.failure("Vous n'appartenez à aucune équipe.");
+ }
+ if (!team.getLeaderId().equals(player.getUniqueId())) {
+ return CommandResult.failure("Seul le chef peut dissoudre l'équipe.");
+ }
+ service.dissolveTeam(team.getId());
+ return CommandResult.success("Équipe " + team.getName() + " dissoute.");
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java
new file mode 100644
index 0000000..dfb3d22
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java
@@ -0,0 +1,51 @@
+package fr.luc.crcore.command.builtin.team;
+
+import fr.luc.crcore.command.SubCommand;
+import fr.luc.crcore.team.TeamService;
+
+import java.util.Objects;
+
+/**
+ * Groupe {@code /core team ...} : container de toutes les sous-commandes
+ * d'équipe par défaut.
+ *
+ * Pour overrider une sous-commande, un plugin de jeu fait :
+ * Ou sous-classe {@code TeamGroupSubCommand} et redéfinit son constructeur
+ * pour swap ce qu'il faut.
+ */
+public class TeamGroupSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamGroupSubCommand(TeamService service) {
+ super("team", "t");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Gestion des équipes");
+ registerDefaults();
+ }
+
+ /**
+ * Enregistre toutes les sous-commandes par défaut. Override pour exclure
+ * ou ajouter des sous-commandes au lieu du jeu standard.
+ */
+ protected void registerDefaults() {
+ addSubCommand(new TeamCreateSubCommand(service));
+ addSubCommand(new TeamDeleteSubCommand(service));
+ addSubCommand(new TeamAddSubCommand(service));
+ addSubCommand(new TeamRemoveSubCommand(service));
+ addSubCommand(new TeamJoinSubCommand(service));
+ addSubCommand(new TeamLeaveSubCommand(service));
+ addSubCommand(new TeamInfoSubCommand(service));
+ addSubCommand(new TeamListSubCommand(service));
+ addSubCommand(new TeamTransferSubCommand(service));
+ addSubCommand(new TeamVisibilitySubCommand(service));
+ addSubCommand(new TeamScoreSubCommand(service));
+ addSubCommand(new TeamTopSubCommand(service));
+ addSubCommand(new TeamSetSpawnSubCommand(service));
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamInfoSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamInfoSubCommand.java
new file mode 100644
index 0000000..019186f
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamInfoSubCommand.java
@@ -0,0 +1,72 @@
+package fr.luc.crcore.command.builtin.team;
+
+import fr.luc.crcore.command.CommandContext;
+import fr.luc.crcore.command.CommandResult;
+import fr.luc.crcore.command.SubCommand;
+import fr.luc.crcore.team.Team;
+import fr.luc.crcore.team.TeamMember;
+import fr.luc.crcore.team.TeamService;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.entity.Player;
+
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * {@code /core team info [name]}
+ *
+ * Affiche les infos d'une équipe. Si aucun nom n'est donné, affiche celle
+ * de l'exécutant.
+ */
+public class TeamInfoSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamInfoSubCommand(TeamService service) {
+ super("info", "i");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Afficher les infos d'une équipe");
+ optionalArgument("name", TeamArgumentTypes.teamByName(service));
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Team team = ctx. Auto-join sur une équipe PUBLIC. Lève une {@link TeamException} si la
+ * team est privée ou si le joueur est déjà dans une équipe (rendu en
+ * message d'erreur).
+ */
+public class TeamJoinSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamJoinSubCommand(TeamService service) {
+ super("join", "j");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Rejoindre une équipe publique");
+ playerOnly();
+ argument("name", TeamArgumentTypes.teamByName(service));
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Player player = ctx.requirePlayer();
+ Team team = ctx.get("name");
+ try {
+ service.joinTeam(team.getId(), player.getUniqueId());
+ return CommandResult.success("Vous avez rejoint " + team.getName() + ".");
+ } catch (TeamException ex) {
+ return CommandResult.failure(ex.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamLeaveSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamLeaveSubCommand.java
new file mode 100644
index 0000000..f6cb6d9
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamLeaveSubCommand.java
@@ -0,0 +1,43 @@
+package fr.luc.crcore.command.builtin.team;
+
+import fr.luc.crcore.command.CommandContext;
+import fr.luc.crcore.command.CommandResult;
+import fr.luc.crcore.command.SubCommand;
+import fr.luc.crcore.team.Team;
+import fr.luc.crcore.team.TeamService;
+import org.bukkit.entity.Player;
+
+import java.util.Objects;
+
+/**
+ * {@code /core team leave}
+ *
+ * Le joueur quitte volontairement son équipe. Refusé pour le chef (il doit
+ * d'abord transférer le leadership ou dissoudre l'équipe).
+ */
+public class TeamLeaveSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamLeaveSubCommand(TeamService service) {
+ super("leave", "quit");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Quitter son équipe");
+ playerOnly();
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Player player = ctx.requirePlayer();
+ Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null);
+ if (team == null) {
+ return CommandResult.failure("Vous n'appartenez à aucune équipe.");
+ }
+ if (team.getLeaderId().equals(player.getUniqueId())) {
+ return CommandResult.failure(
+ "Vous êtes le chef. Transférez le leadership avec /core team transfer Affiche toutes les équipes existantes avec leur tag, nom, taille et
+ * visibilité.
+ */
+public class TeamListSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamListSubCommand(TeamService service) {
+ super("list", "ls");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Lister toutes les équipes");
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Collection Le chef retire un membre. Accepte les joueurs offline (utilise leur nom
+ * pour résoudre l'UUID via {@link Bukkit#getOfflinePlayer(String)}).
+ */
+public class TeamRemoveSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamRemoveSubCommand(TeamService service) {
+ super("remove", "kick", "expel");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Retirer un joueur de son équipe (chef uniquement)");
+ playerOnly();
+ argument("player", ArgumentTypes.STRING);
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Player executor = ctx.requirePlayer();
+ String targetName = ctx.get("player");
+
+ Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
+ if (team == null) {
+ return CommandResult.failure("Vous n'appartenez à aucune équipe.");
+ }
+ if (!team.getLeaderId().equals(executor.getUniqueId())) {
+ return CommandResult.failure("Seul le chef peut retirer des membres.");
+ }
+
+ @SuppressWarnings("deprecation")
+ OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
+ if (target.getUniqueId().equals(executor.getUniqueId())) {
+ return CommandResult.failure("Pour quitter l'équipe en tant que chef, transférez d'abord le leadership.");
+ }
+ if (!team.hasMember(target.getUniqueId())) {
+ return CommandResult.failure(targetName + " n'est pas dans votre équipe.");
+ }
+ service.removeMember(team.getId(), target.getUniqueId());
+ return CommandResult.success(targetName + " retiré de l'équipe.");
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamScoreSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamScoreSubCommand.java
new file mode 100644
index 0000000..7b5d956
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamScoreSubCommand.java
@@ -0,0 +1,50 @@
+package fr.luc.crcore.command.builtin.team;
+
+import fr.luc.crcore.command.ArgumentTypes;
+import fr.luc.crcore.command.CommandContext;
+import fr.luc.crcore.command.CommandResult;
+import fr.luc.crcore.command.SubCommand;
+import fr.luc.crcore.team.Team;
+import fr.luc.crcore.team.TeamService;
+
+import java.util.Objects;
+
+/**
+ * {@code /core team score Commande admin pour ajuster les scores d'une équipe à la main (debug,
+ * fix, init). Le gameplay normal pilote les scores via le service, pas via
+ * cette commande.
+ *
+ * Restreinte par défaut à la permission {@code crcore.team.score.modify}.
+ */
+public class TeamScoreSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamScoreSubCommand(TeamService service) {
+ super("score");
+ this.service = Objects.requireNonNull(service, "service");
+ description("[Admin] Modifier le score d'une équipe");
+ permission("crcore.team.score.modify");
+ argument("team", TeamArgumentTypes.teamByName(service));
+ argument("name", ArgumentTypes.STRING);
+ argument("op", ArgumentTypes.choice("add", "set"));
+ argument("value", ArgumentTypes.INTEGER);
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Team team = ctx.get("team");
+ String name = ctx.get("name");
+ String op = ctx.get("op");
+ int value = ctx.get("value");
+
+ int result = switch (op) {
+ case "add" -> service.addScore(team.getId(), name, value);
+ case "set" -> service.setScore(team.getId(), name, value);
+ default -> throw new IllegalStateException("unreachable: " + op);
+ };
+ return CommandResult.success("Score " + name + " de " + team.getName() + " = " + result);
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetSpawnSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetSpawnSubCommand.java
new file mode 100644
index 0000000..0f122a3
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetSpawnSubCommand.java
@@ -0,0 +1,41 @@
+package fr.luc.crcore.command.builtin.team;
+
+import fr.luc.crcore.command.CommandContext;
+import fr.luc.crcore.command.CommandResult;
+import fr.luc.crcore.command.SubCommand;
+import fr.luc.crcore.team.Team;
+import fr.luc.crcore.team.TeamService;
+import org.bukkit.entity.Player;
+
+import java.util.Objects;
+
+/**
+ * {@code /core team setspawn}
+ *
+ * Définit le point de spawn de l'équipe à la position courante du chef.
+ */
+public class TeamSetSpawnSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamSetSpawnSubCommand(TeamService service) {
+ super("setspawn", "spawn");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Définir le point de spawn de l'équipe (chef uniquement)");
+ playerOnly();
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Player player = ctx.requirePlayer();
+ Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null);
+ if (team == null) {
+ return CommandResult.failure("Vous n'appartenez à aucune équipe.");
+ }
+ if (!team.getLeaderId().equals(player.getUniqueId())) {
+ return CommandResult.failure("Seul le chef peut définir le spawn.");
+ }
+ service.setSpawnPoint(team.getId(), player.getLocation());
+ return CommandResult.success("Spawn de l'équipe défini à votre position.");
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamTopSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamTopSubCommand.java
new file mode 100644
index 0000000..5853030
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamTopSubCommand.java
@@ -0,0 +1,58 @@
+package fr.luc.crcore.command.builtin.team;
+
+import fr.luc.crcore.command.ArgumentTypes;
+import fr.luc.crcore.command.CommandContext;
+import fr.luc.crcore.command.CommandResult;
+import fr.luc.crcore.command.SubCommand;
+import fr.luc.crcore.team.TeamRanking;
+import fr.luc.crcore.team.TeamService;
+import org.bukkit.ChatColor;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * {@code /core team top [score]}
+ *
+ * Affiche le classement des équipes. Sans argument, classement global
+ * (somme de tous les scores). Avec un nom de score, classement sur ce score
+ * précis.
+ */
+public class TeamTopSubCommand extends SubCommand {
+
+ protected final TeamService service;
+ protected final int limit;
+
+ public TeamTopSubCommand(TeamService service) {
+ this(service, 10);
+ }
+
+ public TeamTopSubCommand(TeamService service, int limit) {
+ super("top", "ranking", "leaderboard");
+ this.service = Objects.requireNonNull(service, "service");
+ this.limit = limit;
+ description("Classement des équipes");
+ optionalArgument("score", ArgumentTypes.STRING);
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ String scoreName = ctx. Le chef transmet son rôle à un autre membre existant de son équipe.
+ */
+public class TeamTransferSubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamTransferSubCommand(TeamService service) {
+ super("transfer");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Transférer le rôle de chef à un autre membre");
+ playerOnly();
+ argument("player", ArgumentTypes.STRING);
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Player executor = ctx.requirePlayer();
+ String targetName = ctx.get("player");
+ Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
+ if (team == null) {
+ return CommandResult.failure("Vous n'appartenez à aucune équipe.");
+ }
+ if (!team.getLeaderId().equals(executor.getUniqueId())) {
+ return CommandResult.failure("Seul le chef peut transférer le leadership.");
+ }
+ @SuppressWarnings("deprecation")
+ OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
+ if (!team.hasMember(target.getUniqueId())) {
+ return CommandResult.failure(targetName + " n'est pas dans votre équipe.");
+ }
+ service.transferLeadership(team.getId(), target.getUniqueId());
+ return CommandResult.success("Leadership transféré à " + targetName + ".");
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamVisibilitySubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamVisibilitySubCommand.java
new file mode 100644
index 0000000..697e10e
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamVisibilitySubCommand.java
@@ -0,0 +1,46 @@
+package fr.luc.crcore.command.builtin.team;
+
+import fr.luc.crcore.command.ArgumentTypes;
+import fr.luc.crcore.command.CommandContext;
+import fr.luc.crcore.command.CommandResult;
+import fr.luc.crcore.command.SubCommand;
+import fr.luc.crcore.team.Team;
+import fr.luc.crcore.team.TeamService;
+import fr.luc.crcore.team.TeamVisibility;
+import org.bukkit.entity.Player;
+
+import java.util.Objects;
+
+/**
+ * {@code /core team visibility Le chef change la visibilité de son équipe. PUBLIC = les autres joueurs
+ * peuvent rejoindre avec {@code /core team join}.
+ */
+public class TeamVisibilitySubCommand extends SubCommand {
+
+ protected final TeamService service;
+
+ public TeamVisibilitySubCommand(TeamService service) {
+ super("visibility", "vis");
+ this.service = Objects.requireNonNull(service, "service");
+ description("Changer la visibilité de son équipe");
+ playerOnly();
+ argument("visibility", ArgumentTypes.enumOf(TeamVisibility.class));
+ }
+
+ @Override
+ public CommandResult execute(CommandContext ctx) {
+ Player player = ctx.requirePlayer();
+ TeamVisibility visibility = ctx.get("visibility");
+ Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null);
+ if (team == null) {
+ return CommandResult.failure("Vous n'appartenez à aucune équipe.");
+ }
+ if (!team.getLeaderId().equals(player.getUniqueId())) {
+ return CommandResult.failure("Seul le chef peut changer la visibilité.");
+ }
+ service.setVisibility(team.getId(), visibility);
+ return CommandResult.success("Visibilité réglée sur " + visibility + ".");
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/common/Repository.java b/src/main/java/fr/luc/crcore/common/Repository.java
index e860f1d..7f745b0 100644
--- a/src/main/java/fr/luc/crcore/common/Repository.java
+++ b/src/main/java/fr/luc/crcore/common/Repository.java
@@ -4,6 +4,17 @@ import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
+/**
+ * Contrat CRUD générique pour tout aggregate {@link Identifiable}. Permet de
+ * brancher différents backends (in-memory, SQLite, …) sans toucher au code
+ * service.
+ *
+ * Implémentations CR-Core par défaut :
+ * Les scores sont identifiés par un nom libre (ex. {@code "kills"},
+ * {@code "objectives"}, {@code "global"}) et stockés comme entiers. Un jeu
+ * mono-score peut conventionnellement utiliser {@code "global"}.
+ *
+ * {@link #getScore(String)} renvoie 0 pour un score jamais initialisé
+ * (utile pour {@code addScore("kills", 1)} sans set préalable). Pour
+ * distinguer "jamais set" et "set à 0", utiliser {@link #hasScore(String)}.
+ */
public interface ScoreHolder {
int getScore(String scoreName);
diff --git a/src/main/java/fr/luc/crcore/database/ColumnType.java b/src/main/java/fr/luc/crcore/database/ColumnType.java
new file mode 100644
index 0000000..66ee48f
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/database/ColumnType.java
@@ -0,0 +1,36 @@
+package fr.luc.crcore.database;
+
+/**
+ * Types de colonnes supportés par {@link TableBuilder}, chacun mappé sur un
+ * type natif SQLite. Volontairement réduit : SQLite est faiblement typé, ces
+ * 6 types couvrent largement les besoins d'un plugin Minecraft.
+ *
+ * Ouvre une connexion JDBC vers un fichier SQLite et expose 4 méthodes pour
+ * couvrir 95 % des besoins :
+ *
+ * Les paramètres ({@code Object...}) sont liés via PreparedStatement (donc
+ * pas d'injection SQL possible). Les {@link UUID} sont automatiquement
+ * convertis en {@code TEXT}.
+ *
+ * Pour créer une table de manière fluide, voir {@link #table(String)}.
+ *
+ * Cette classe n'est pas thread-safe. Un plugin Bukkit accède en général
+ * à la DB depuis le main thread ; pour de l'async, synchroniser explicitement
+ * ou ouvrir plusieurs instances.
+ */
+public class Database implements AutoCloseable {
+
+ private final Connection connection;
+
+ /**
+ * Ouvre (ou crée) un fichier SQLite. Le dossier parent doit exister.
+ *
+ * @throws DatabaseException si le driver JDBC SQLite est absent du
+ * classpath, ou si l'ouverture du fichier échoue.
+ */
+ public Database(File file) {
+ Objects.requireNonNull(file, "file");
+ try {
+ // Force le chargement du driver (utile sur certains classloaders Bukkit).
+ Class.forName("org.sqlite.JDBC");
+ String url = "jdbc:sqlite:" + file.getAbsolutePath();
+ this.connection = DriverManager.getConnection(url);
+ // Active les foreign keys (désactivées par défaut sur SQLite).
+ execute("PRAGMA foreign_keys = ON");
+ } catch (ClassNotFoundException ex) {
+ throw new DatabaseException(
+ "SQLite JDBC driver not found on classpath (org.sqlite.JDBC).", ex);
+ } catch (SQLException ex) {
+ throw new DatabaseException("Failed to open SQLite database: " + file, ex);
+ }
+ }
+
+ /** Connexion JDBC sous-jacente, pour les cas avancés (transactions custom, etc.). */
+ public Connection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Démarre la création d'une table de manière fluide.
+ *
+ * Utilisé par {@link Database#query} et {@link Database#queryOne}. Le mapper
+ * ne doit pas appeler {@code rs.next()} : c'est {@link Database} qui
+ * itère.
+ *
+ * Ne supporte pas (volontairement) les FOREIGN KEY ni les contraintes
+ * multi-colonnes pour rester simple. Pour ces cas, utiliser
+ * {@link Database#execute(String, Object...)} avec un {@code CREATE TABLE}
+ * brut.
+ */
+public class TableBuilder {
+
+ private final Database database;
+ private final String name;
+ private final List Indépendant du domaine team : un joueur peut entrer / quitter / changer
+ * d'équipe sans toucher à son profil. Géré par {@link PlayerProfileService}
+ * qui auto-crée le profil à la première écriture de score.
+ */
public class PlayerProfile extends AbstractEntity implements ScoreHolder {
private final Map Auto-création : {@link #addScore}, {@link #setScore} créent
+ * automatiquement le profil s'il n'existe pas (via {@link #getOrCreateProfile}).
+ * Pas besoin d'initialiser explicitement.
+ */
public interface PlayerProfileService {
PlayerProfile getOrCreateProfile(UUID playerId);
diff --git a/src/main/java/fr/luc/crcore/player/SqlitePlayerProfileRepository.java b/src/main/java/fr/luc/crcore/player/SqlitePlayerProfileRepository.java
new file mode 100644
index 0000000..6b6d1f2
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/player/SqlitePlayerProfileRepository.java
@@ -0,0 +1,96 @@
+package fr.luc.crcore.player;
+
+import fr.luc.crcore.database.ColumnType;
+import fr.luc.crcore.database.Database;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Implémentation {@link PlayerProfileRepository} adossée à SQLite.
+ *
+ * Mêmes principes que {@link fr.luc.crcore.team.SqliteTeamRepository} :
+ * cache mémoire en write-through, schéma créé à l'init, état rechargé depuis
+ * la DB au constructeur.
+ *
+ * Schéma (2 tables) :
+ * Chaque sous-classe concrète doit fournir sa propre {@code HandlerList}
+ * statique (contrainte Bukkit).
+ */
+public abstract class PlayerProfileEvent extends Event {
+
+ private final PlayerProfile profile;
+
+ protected PlayerProfileEvent(PlayerProfile profile) {
+ this.profile = Objects.requireNonNull(profile, "profile");
+ }
+
+ /** Le profil concerné. */
+ public PlayerProfile getProfile() {
+ return profile;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/player/event/PlayerScoreChangeEvent.java b/src/main/java/fr/luc/crcore/player/event/PlayerScoreChangeEvent.java
new file mode 100644
index 0000000..7ba7e7b
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/player/event/PlayerScoreChangeEvent.java
@@ -0,0 +1,40 @@
+package fr.luc.crcore.player.event;
+
+import fr.luc.crcore.player.PlayerProfile;
+import org.bukkit.event.HandlerList;
+
+import java.util.Objects;
+
+/**
+ * Déclenché après changement effectif d'un score joueur. {@link #getScoreName()}
+ * donne le nom du score touché.
+ */
+public class PlayerScoreChangeEvent extends PlayerProfileEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final String scoreName;
+ private final int oldValue;
+ private final int newValue;
+
+ public PlayerScoreChangeEvent(PlayerProfile profile, String scoreName, int oldValue, int newValue) {
+ super(profile);
+ this.scoreName = Objects.requireNonNull(scoreName, "scoreName");
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+ }
+
+ public String getScoreName() { return scoreName; }
+ public int getOldValue() { return oldValue; }
+ public int getNewValue() { return newValue; }
+ public int getDelta() { return newValue - oldValue; }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/BukkitEventFiringTeamServiceImpl.java b/src/main/java/fr/luc/crcore/team/BukkitEventFiringTeamServiceImpl.java
new file mode 100644
index 0000000..08b5234
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/BukkitEventFiringTeamServiceImpl.java
@@ -0,0 +1,89 @@
+package fr.luc.crcore.team;
+
+import fr.luc.crcore.team.event.PlayerJoinTeamEvent;
+import fr.luc.crcore.team.event.TeamCreateEvent;
+import fr.luc.crcore.team.event.TeamDissolveEvent;
+import fr.luc.crcore.team.event.TeamLeadershipTransferEvent;
+import fr.luc.crcore.team.event.TeamMemberAddEvent;
+import fr.luc.crcore.team.event.TeamMemberRemoveEvent;
+import fr.luc.crcore.team.event.TeamScoreChangeEvent;
+import fr.luc.crcore.team.event.TeamSpawnPointChangeEvent;
+import fr.luc.crcore.team.event.TeamVisibilityChangeEvent;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Variante de {@link TeamServiceImpl} qui, en plus de la logique métier,
+ * tire des évènements Bukkit via les hooks {@code on...} hérités.
+ *
+ * C'est cette implémentation qu'utilise {@code CRCore} par défaut. Les
+ * plugins de jeu peuvent toujours sous-classer pour ajouter d'autres effets
+ * (logs, scoreboard sync, etc.) en overridant les mêmes hooks et en appelant
+ * {@code super}.
+ */
+public class BukkitEventFiringTeamServiceImpl extends TeamServiceImpl {
+
+ private final JavaPlugin plugin;
+
+ public BukkitEventFiringTeamServiceImpl(JavaPlugin plugin, TeamRepository repository) {
+ super(repository);
+ this.plugin = Objects.requireNonNull(plugin, "plugin");
+ }
+
+ protected JavaPlugin getPlugin() {
+ return plugin;
+ }
+
+ @Override
+ protected void onAfterCreate(Team team) {
+ Bukkit.getPluginManager().callEvent(new TeamCreateEvent(team));
+ }
+
+ @Override
+ protected void onAfterDissolve(Team team) {
+ Bukkit.getPluginManager().callEvent(new TeamDissolveEvent(team));
+ }
+
+ @Override
+ protected void onMemberAdded(Team team, TeamMember member) {
+ Bukkit.getPluginManager().callEvent(new TeamMemberAddEvent(team, member));
+ }
+
+ @Override
+ protected void onMemberRemoved(Team team, UUID playerId) {
+ Bukkit.getPluginManager().callEvent(new TeamMemberRemoveEvent(team, playerId));
+ }
+
+ @Override
+ protected void onPlayerJoined(Team team, TeamMember member) {
+ Bukkit.getPluginManager().callEvent(new PlayerJoinTeamEvent(team, member));
+ }
+
+ @Override
+ protected void onLeadershipTransferred(Team team, UUID oldLeaderId, UUID newLeaderId) {
+ Bukkit.getPluginManager().callEvent(
+ new TeamLeadershipTransferEvent(team, oldLeaderId, newLeaderId));
+ }
+
+ @Override
+ protected void onVisibilityChanged(Team team, TeamVisibility oldValue, TeamVisibility newValue) {
+ Bukkit.getPluginManager().callEvent(
+ new TeamVisibilityChangeEvent(team, oldValue, newValue));
+ }
+
+ @Override
+ protected void onScoreChanged(Team team, String scoreName, int oldValue, int newValue) {
+ Bukkit.getPluginManager().callEvent(
+ new TeamScoreChangeEvent(team, scoreName, oldValue, newValue));
+ }
+
+ @Override
+ protected void onSpawnPointChanged(Team team, Location oldLocation, Location newLocation) {
+ Bukkit.getPluginManager().callEvent(
+ new TeamSpawnPointChangeEvent(team, oldLocation, newLocation));
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java b/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java
new file mode 100644
index 0000000..0f7093f
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java
@@ -0,0 +1,209 @@
+package fr.luc.crcore.team;
+
+import fr.luc.crcore.database.ColumnType;
+import fr.luc.crcore.database.Database;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Implémentation {@link TeamRepository} adossée à SQLite.
+ *
+ * Stratégie : write-through cache. On hérite de
+ * {@link InMemoryTeamRepository} pour conserver les requêtes rapides
+ * (findAll, findByName, etc. en mémoire), et on overrride {@link #save} et
+ * {@link #delete} pour persister synchroniquement vers SQLite. Le constructeur
+ * crée les tables si nécessaire et recharge l'état complet depuis la DB.
+ *
+ * Schéma (3 tables) :
+ * Sur {@link #save}, on supprime puis ré-insère membres et scores de
+ * l'équipe (approche simple et robuste pour des volumes faibles d'event).
+ */
+public class SqliteTeamRepository extends InMemoryTeamRepository {
+
+ private static final String TABLE_TEAMS = "crcore_teams";
+ private static final String TABLE_MEMBERS = "crcore_team_members";
+ private static final String TABLE_SCORES = "crcore_team_scores";
+
+ private final Database db;
+
+ public SqliteTeamRepository(Database db) {
+ this.db = Objects.requireNonNull(db, "db");
+ ensureSchema();
+ loadAll();
+ }
+
+ private void ensureSchema() {
+ db.table(TABLE_TEAMS).ifNotExists()
+ .column("id", ColumnType.UUID).primaryKey()
+ .column("name", ColumnType.TEXT).notNull().unique()
+ .column("tag", ColumnType.TEXT).notNull().unique()
+ .column("color", ColumnType.TEXT).notNull()
+ .column("leader_id", ColumnType.UUID).notNull()
+ .column("visibility", ColumnType.TEXT).notNull()
+ .column("spawn_world", ColumnType.TEXT)
+ .column("spawn_x", ColumnType.REAL)
+ .column("spawn_y", ColumnType.REAL)
+ .column("spawn_z", ColumnType.REAL)
+ .column("spawn_yaw", ColumnType.REAL)
+ .column("spawn_pitch", ColumnType.REAL)
+ .create();
+
+ db.table(TABLE_MEMBERS).ifNotExists()
+ .column("team_id", ColumnType.UUID).notNull()
+ .column("player_id", ColumnType.UUID).notNull()
+ .column("role", ColumnType.TEXT).notNull()
+ .column("joined_at", ColumnType.INTEGER).notNull()
+ .create();
+ // Index logique (team_id, player_id) — pas créé explicitement, SQLite gère via lookups.
+
+ db.table(TABLE_SCORES).ifNotExists()
+ .column("team_id", ColumnType.UUID).notNull()
+ .column("score_name", ColumnType.TEXT).notNull()
+ .column("value", ColumnType.INTEGER).notNull()
+ .create();
+ }
+
+ /** Recharge tous les Teams depuis la DB dans le cache mémoire hérité. */
+ private void loadAll() {
+ // On query toutes les équipes en flat puis on ré-hydrate.
+ var teamRows = db.query(
+ "SELECT id, name, tag, color, leader_id, visibility, " +
+ "spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch " +
+ "FROM " + TABLE_TEAMS,
+ rs -> new TeamRow(
+ UUID.fromString(rs.getString("id")),
+ rs.getString("name"),
+ rs.getString("tag"),
+ TeamColor.valueOf(rs.getString("color")),
+ UUID.fromString(rs.getString("leader_id")),
+ TeamVisibility.valueOf(rs.getString("visibility")),
+ rs.getString("spawn_world"),
+ (Double) rs.getObject("spawn_x"),
+ (Double) rs.getObject("spawn_y"),
+ (Double) rs.getObject("spawn_z"),
+ (Float) (rs.getObject("spawn_yaw") == null ? null : (float) rs.getDouble("spawn_yaw")),
+ (Float) (rs.getObject("spawn_pitch") == null ? null : (float) rs.getDouble("spawn_pitch"))
+ )
+ );
+
+ for (TeamRow row : teamRows) {
+ Team team = new Team(row.id, row.name, row.tag, row.color, row.leaderId, row.visibility);
+ // Membres
+ var members = db.query(
+ "SELECT player_id, role, joined_at FROM " + TABLE_MEMBERS + " WHERE team_id = ?",
+ rs -> new MemberRow(
+ UUID.fromString(rs.getString("player_id")),
+ TeamRole.valueOf(rs.getString("role")),
+ Instant.ofEpochMilli(rs.getLong("joined_at"))
+ ),
+ row.id
+ );
+ // Le leader est ajouté par le constructeur de Team avec role LEADER.
+ // On ajoute les autres membres manuellement via addMember (qui les marque MEMBER).
+ for (MemberRow m : members) {
+ if (!m.playerId.equals(row.leaderId)) {
+ team.addMember(m.playerId);
+ }
+ }
+ // Scores
+ db.query(
+ "SELECT score_name, value FROM " + TABLE_SCORES + " WHERE team_id = ?",
+ rs -> {
+ team.setScore(rs.getString("score_name"), rs.getInt("value"));
+ return null;
+ },
+ row.id
+ );
+ // Spawn point — différé : nécessite un World qui n'est pas forcément chargé
+ // au moment du load. Le serveur charge les worlds avant les plugins normalement,
+ // mais on est défensif.
+ if (row.spawnWorld != null && row.spawnX != null) {
+ var world = org.bukkit.Bukkit.getWorld(row.spawnWorld);
+ if (world != null) {
+ org.bukkit.Location loc = new org.bukkit.Location(
+ world, row.spawnX, row.spawnY, row.spawnZ);
+ if (row.spawnYaw != null) loc.setYaw(row.spawnYaw);
+ if (row.spawnPitch != null) loc.setPitch(row.spawnPitch);
+ team.setSpawnPoint(loc);
+ }
+ }
+ // Inject dans le cache mémoire hérité (super.save persiste à nouveau — on évite ça).
+ super.save(team);
+ }
+ }
+
+ @Override
+ public Team save(Team team) {
+ super.save(team); // met à jour le cache mémoire
+ persist(team);
+ return team;
+ }
+
+ @Override
+ public boolean delete(UUID id) {
+ boolean removed = super.delete(id);
+ if (removed) {
+ db.update("DELETE FROM " + TABLE_SCORES + " WHERE team_id = ?", id);
+ db.update("DELETE FROM " + TABLE_MEMBERS + " WHERE team_id = ?", id);
+ db.update("DELETE FROM " + TABLE_TEAMS + " WHERE id = ?", id);
+ }
+ return removed;
+ }
+
+ private void persist(Team team) {
+ var spawn = team.getSpawnPoint();
+ String spawnWorld = spawn.map(l -> l.getWorld().getName()).orElse(null);
+ Double spawnX = spawn.map(org.bukkit.Location::getX).orElse(null);
+ Double spawnY = spawn.map(org.bukkit.Location::getY).orElse(null);
+ Double spawnZ = spawn.map(org.bukkit.Location::getZ).orElse(null);
+ Float spawnYaw = spawn.map(org.bukkit.Location::getYaw).orElse(null);
+ Float spawnPitch = spawn.map(org.bukkit.Location::getPitch).orElse(null);
+
+ // INSERT OR REPLACE = upsert simple sur SQLite (la PK gère la collision).
+ db.update(
+ "INSERT OR REPLACE INTO " + TABLE_TEAMS +
+ " (id, name, tag, color, leader_id, visibility, " +
+ " spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch) " +
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ team.getId(), team.getName(), team.getTag(), team.getColor(),
+ team.getLeaderId(), team.getVisibility(),
+ spawnWorld, spawnX, spawnY, spawnZ, spawnYaw, spawnPitch
+ );
+
+ // Approche simple : on remplace en bloc les membres et les scores.
+ db.update("DELETE FROM " + TABLE_MEMBERS + " WHERE team_id = ?", team.getId());
+ for (TeamMember m : team.getMembers()) {
+ db.update(
+ "INSERT INTO " + TABLE_MEMBERS +
+ " (team_id, player_id, role, joined_at) VALUES (?, ?, ?, ?)",
+ team.getId(), m.getPlayerId(), m.getRole(), m.getJoinedAt().toEpochMilli()
+ );
+ }
+
+ db.update("DELETE FROM " + TABLE_SCORES + " WHERE team_id = ?", team.getId());
+ team.getScores().forEach((name, value) ->
+ db.update(
+ "INSERT INTO " + TABLE_SCORES + " (team_id, score_name, value) VALUES (?, ?, ?)",
+ team.getId(), name, value
+ )
+ );
+ }
+
+ // Tuples internes pour le load.
+ private record TeamRow(
+ UUID id, String name, String tag, TeamColor color,
+ UUID leaderId, TeamVisibility visibility,
+ String spawnWorld, Double spawnX, Double spawnY, Double spawnZ,
+ Float spawnYaw, Float spawnPitch
+ ) {}
+
+ private record MemberRow(UUID playerId, TeamRole role, Instant joinedAt) {}
+}
diff --git a/src/main/java/fr/luc/crcore/team/Team.java b/src/main/java/fr/luc/crcore/team/Team.java
index 09f5af1..43f596c 100644
--- a/src/main/java/fr/luc/crcore/team/Team.java
+++ b/src/main/java/fr/luc/crcore/team/Team.java
@@ -14,6 +14,21 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
+/**
+ * Représente une équipe de joueurs. Aggregate mutable : ajout / retrait de
+ * membres, transfert de leadership, mise à jour de visibilité, des scores
+ * nommés, et du point de spawn passent par les méthodes de cette classe.
+ *
+ * L'identité d'une équipe est son UUID ({@link #getId()}). Deux Team avec
+ * le même UUID sont égales, quel que soit le reste de leur état.
+ *
+ * Implémente {@link Named} (a un nom) et {@link ScoreHolder} (porte des
+ * scores nommés). Hérite de {@link AbstractEntity} pour l'identité.
+ *
+ * Toutes les modifications passent normalement par le {@link TeamService},
+ * qui orchestre persistance + hooks + évènements Bukkit. Modifier une instance
+ * directement (ex. {@code team.addMember(...)}) court-circuite la persistance.
+ */
public class Team extends AbstractEntity implements Named, ScoreHolder {
private final String name;
diff --git a/src/main/java/fr/luc/crcore/team/TeamService.java b/src/main/java/fr/luc/crcore/team/TeamService.java
index 368ee50..1d7fc44 100644
--- a/src/main/java/fr/luc/crcore/team/TeamService.java
+++ b/src/main/java/fr/luc/crcore/team/TeamService.java
@@ -8,6 +8,19 @@ import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
+/**
+ * Façade pour toutes les opérations sur les équipes : lifecycle (create /
+ * dissolve), membres, scores, classements, point de spawn, visibilité.
+ *
+ * Toute logique d'écriture passe par le service (jamais directement sur
+ * {@link Team}) — il garantit l'unicité nom/tag, déclenche les hooks
+ * d'override et tire les évènements Bukkit (via la sous-classe par défaut
+ * {@code BukkitEventFiringTeamServiceImpl}).
+ *
+ * L'implémentation par défaut est {@link TeamServiceImpl} avec ses ~12
+ * hooks {@code protected} surchargeables (factories {@code newTeam},
+ * {@code newRanking}, et hooks {@code on...} autour de chaque opération).
+ */
public interface TeamService {
// ---- Lifecycle ----
diff --git a/src/main/java/fr/luc/crcore/team/event/PlayerJoinTeamEvent.java b/src/main/java/fr/luc/crcore/team/event/PlayerJoinTeamEvent.java
new file mode 100644
index 0000000..0c6698d
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/event/PlayerJoinTeamEvent.java
@@ -0,0 +1,39 @@
+package fr.luc.crcore.team.event;
+
+import fr.luc.crcore.team.Team;
+import fr.luc.crcore.team.TeamMember;
+import org.bukkit.event.HandlerList;
+
+import java.util.Objects;
+
+/**
+ * Déclenché spécifiquement quand un joueur rejoint une équipe par sa propre
+ * action (auto-join sur une équipe PUBLIC via {@code TeamService.joinTeam}).
+ *
+ * Dans ce cas, {@link TeamMemberAddEvent} est aussi tiré juste avant.
+ * Pour réagir uniquement aux auto-joins, écouter ce {@code PlayerJoinTeamEvent}.
+ */
+public class PlayerJoinTeamEvent extends TeamEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final TeamMember member;
+
+ public PlayerJoinTeamEvent(Team team, TeamMember member) {
+ super(team);
+ this.member = Objects.requireNonNull(member, "member");
+ }
+
+ public TeamMember getMember() {
+ return member;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/event/TeamCreateEvent.java b/src/main/java/fr/luc/crcore/team/event/TeamCreateEvent.java
new file mode 100644
index 0000000..6b9306d
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/event/TeamCreateEvent.java
@@ -0,0 +1,26 @@
+package fr.luc.crcore.team.event;
+
+import fr.luc.crcore.team.Team;
+import org.bukkit.event.HandlerList;
+
+/**
+ * Déclenché juste après qu'une équipe a été créée et persistée. Non annulable
+ * (la validation est faite côté service avant création).
+ */
+public class TeamCreateEvent extends TeamEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public TeamCreateEvent(Team team) {
+ super(team);
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/event/TeamDissolveEvent.java b/src/main/java/fr/luc/crcore/team/event/TeamDissolveEvent.java
new file mode 100644
index 0000000..0bdd9e0
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/event/TeamDissolveEvent.java
@@ -0,0 +1,23 @@
+package fr.luc.crcore.team.event;
+
+import fr.luc.crcore.team.Team;
+import org.bukkit.event.HandlerList;
+
+/** Déclenché juste après la dissolution d'une équipe. */
+public class TeamDissolveEvent extends TeamEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public TeamDissolveEvent(Team team) {
+ super(team);
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/event/TeamEvent.java b/src/main/java/fr/luc/crcore/team/event/TeamEvent.java
new file mode 100644
index 0000000..2697a5d
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/event/TeamEvent.java
@@ -0,0 +1,27 @@
+package fr.luc.crcore.team.event;
+
+import fr.luc.crcore.team.Team;
+import org.bukkit.event.Event;
+
+import java.util.Objects;
+
+/**
+ * Base abstraite pour tous les évènements Bukkit liés à une équipe. Porte
+ * toujours la {@link Team} concernée.
+ *
+ * Chaque sous-classe concrète doit fournir sa propre {@code HandlerList}
+ * statique (contrainte Bukkit — pas de moyen de partager via héritage).
+ */
+public abstract class TeamEvent extends Event {
+
+ private final Team team;
+
+ protected TeamEvent(Team team) {
+ this.team = Objects.requireNonNull(team, "team");
+ }
+
+ /** L'équipe concernée par l'évènement. */
+ public Team getTeam() {
+ return team;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/event/TeamLeadershipTransferEvent.java b/src/main/java/fr/luc/crcore/team/event/TeamLeadershipTransferEvent.java
new file mode 100644
index 0000000..0714f60
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/event/TeamLeadershipTransferEvent.java
@@ -0,0 +1,34 @@
+package fr.luc.crcore.team.event;
+
+import fr.luc.crcore.team.Team;
+import org.bukkit.event.HandlerList;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/** Déclenché après un transfert de leadership. {@link #getOldLeaderId()} et {@link #getNewLeaderId()} renvoient les UUID des deux joueurs. */
+public class TeamLeadershipTransferEvent extends TeamEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final UUID oldLeaderId;
+ private final UUID newLeaderId;
+
+ public TeamLeadershipTransferEvent(Team team, UUID oldLeaderId, UUID newLeaderId) {
+ super(team);
+ this.oldLeaderId = Objects.requireNonNull(oldLeaderId, "oldLeaderId");
+ this.newLeaderId = Objects.requireNonNull(newLeaderId, "newLeaderId");
+ }
+
+ public UUID getOldLeaderId() { return oldLeaderId; }
+ public UUID getNewLeaderId() { return newLeaderId; }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/event/TeamMemberAddEvent.java b/src/main/java/fr/luc/crcore/team/event/TeamMemberAddEvent.java
new file mode 100644
index 0000000..38a9b5c
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/event/TeamMemberAddEvent.java
@@ -0,0 +1,40 @@
+package fr.luc.crcore.team.event;
+
+import fr.luc.crcore.team.Team;
+import fr.luc.crcore.team.TeamMember;
+import org.bukkit.event.HandlerList;
+
+import java.util.Objects;
+
+/**
+ * Déclenché quand un membre est ajouté à une équipe — par action du chef
+ * ({@code TeamService.addMember}) OU par auto-join du joueur
+ * ({@code TeamService.joinTeam}).
+ *
+ * Pour distinguer les deux cas, écouter aussi {@link PlayerJoinTeamEvent}
+ * qui n'est tiré que dans le second cas.
+ */
+public class TeamMemberAddEvent extends TeamEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final TeamMember member;
+
+ public TeamMemberAddEvent(Team team, TeamMember member) {
+ super(team);
+ this.member = Objects.requireNonNull(member, "member");
+ }
+
+ public TeamMember getMember() {
+ return member;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/event/TeamMemberRemoveEvent.java b/src/main/java/fr/luc/crcore/team/event/TeamMemberRemoveEvent.java
new file mode 100644
index 0000000..0265af6
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/event/TeamMemberRemoveEvent.java
@@ -0,0 +1,33 @@
+package fr.luc.crcore.team.event;
+
+import fr.luc.crcore.team.Team;
+import org.bukkit.event.HandlerList;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/** Déclenché après le retrait d'un membre d'une équipe (action chef ou départ volontaire). */
+public class TeamMemberRemoveEvent extends TeamEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final UUID playerId;
+
+ public TeamMemberRemoveEvent(Team team, UUID playerId) {
+ super(team);
+ this.playerId = Objects.requireNonNull(playerId, "playerId");
+ }
+
+ public UUID getPlayerId() {
+ return playerId;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/event/TeamScoreChangeEvent.java b/src/main/java/fr/luc/crcore/team/event/TeamScoreChangeEvent.java
new file mode 100644
index 0000000..a05aa43
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/event/TeamScoreChangeEvent.java
@@ -0,0 +1,40 @@
+package fr.luc.crcore.team.event;
+
+import fr.luc.crcore.team.Team;
+import org.bukkit.event.HandlerList;
+
+import java.util.Objects;
+
+/**
+ * Déclenché après changement effectif d'un score d'équipe (uniquement si la
+ * valeur change). {@link #getScoreName()} donne le nom du score touché.
+ */
+public class TeamScoreChangeEvent extends TeamEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final String scoreName;
+ private final int oldValue;
+ private final int newValue;
+
+ public TeamScoreChangeEvent(Team team, String scoreName, int oldValue, int newValue) {
+ super(team);
+ this.scoreName = Objects.requireNonNull(scoreName, "scoreName");
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+ }
+
+ public String getScoreName() { return scoreName; }
+ public int getOldValue() { return oldValue; }
+ public int getNewValue() { return newValue; }
+ public int getDelta() { return newValue - oldValue; }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/event/TeamSpawnPointChangeEvent.java b/src/main/java/fr/luc/crcore/team/event/TeamSpawnPointChangeEvent.java
new file mode 100644
index 0000000..77e7cae
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/event/TeamSpawnPointChangeEvent.java
@@ -0,0 +1,38 @@
+package fr.luc.crcore.team.event;
+
+import fr.luc.crcore.team.Team;
+import org.bukkit.Location;
+import org.bukkit.event.HandlerList;
+
+/**
+ * Déclenché après changement du point de spawn d'une équipe. {@code oldLocation}
+ * et {@code newLocation} peuvent être {@code null} (clear / setup initial).
+ */
+public class TeamSpawnPointChangeEvent extends TeamEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final Location oldLocation;
+ private final Location newLocation;
+
+ public TeamSpawnPointChangeEvent(Team team, Location oldLocation, Location newLocation) {
+ super(team);
+ this.oldLocation = oldLocation;
+ this.newLocation = newLocation;
+ }
+
+ /** L'ancien spawn, ou {@code null} s'il n'y en avait pas. */
+ public Location getOldLocation() { return oldLocation; }
+
+ /** Le nouveau spawn, ou {@code null} si on a fait un clear. */
+ public Location getNewLocation() { return newLocation; }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/team/event/TeamVisibilityChangeEvent.java b/src/main/java/fr/luc/crcore/team/event/TeamVisibilityChangeEvent.java
new file mode 100644
index 0000000..ffa993a
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/team/event/TeamVisibilityChangeEvent.java
@@ -0,0 +1,34 @@
+package fr.luc.crcore.team.event;
+
+import fr.luc.crcore.team.Team;
+import fr.luc.crcore.team.TeamVisibility;
+import org.bukkit.event.HandlerList;
+
+import java.util.Objects;
+
+/** Déclenché après changement effectif de visibilité PUBLIC ↔ PRIVATE. */
+public class TeamVisibilityChangeEvent extends TeamEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final TeamVisibility oldVisibility;
+ private final TeamVisibility newVisibility;
+
+ public TeamVisibilityChangeEvent(Team team, TeamVisibility oldVisibility, TeamVisibility newVisibility) {
+ super(team);
+ this.oldVisibility = Objects.requireNonNull(oldVisibility, "oldVisibility");
+ this.newVisibility = Objects.requireNonNull(newVisibility, "newVisibility");
+ }
+
+ public TeamVisibility getOldVisibility() { return oldVisibility; }
+ public TeamVisibility getNewVisibility() { return newVisibility; }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
{@code
+ * coreCommand.findSubCommand("team")
+ * .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc)));
+ * }
+ *
+ * @return l'ancienne sous-commande remplacée, ou {@link Optional#empty()}
+ * si aucune n'existait sous ce nom.
+ */
+ public final Optional
+ *
+ */
+ public CommandResult execute(CommandContext context) {
+ if (hasSubCommands()) {
+ return listSubCommands(context);
+ }
+ return CommandResult.invalidUsage("Usage: " + buildDefaultUsage());
+ }
+
+ // ---- Override helpers ----
+
+ /** Default {@code execute} pour les groupes : affiche la liste des sous-commandes accessibles. */
+ protected CommandResult listSubCommands(CommandContext context) {
+ StringBuilder sb = new StringBuilder(ChatColor.YELLOW.toString())
+ .append("Commandes disponibles pour ")
+ .append(ChatColor.WHITE).append('/').append(context.getLabel());
+ for (SubCommand sub : subCommandsByName.values()) {
+ if (sub.getPermission() != null && !context.getSender().hasPermission(sub.getPermission())) continue;
+ sb.append('\n').append(ChatColor.GRAY).append(" - ")
+ .append(ChatColor.WHITE).append(sub.getName());
+ if (!sub.getDescription().isEmpty()) {
+ sb.append(ChatColor.GRAY).append(" — ").append(sub.getDescription());
+ }
+ }
+ context.reply(sb.toString());
+ return CommandResult.success();
+ }
+
+ /** Construit un usage par défaut à partir du nom et des arguments déclarés. */
protected String buildDefaultUsage() {
StringBuilder sb = new StringBuilder("/").append(name);
for (ArgumentDef def : arguments) {
@@ -108,26 +322,26 @@ public abstract class AbstractCommand implements Command {
return sb.toString();
}
- @Override
- public List{@code
+ * PluginCommand cmd = plugin.getCommand("core");
+ * cmd.setExecutor(new CoreCommand(...));
+ * cmd.setTabCompleter((CoreCommand) cmd.getExecutor());
+ * }
+ *
+ *
+ *
+ *
+ * Override
+ * Pour remplacer un groupe entier :
+ * {@code
+ * core.getCoreCommand().replaceSubCommand("team", new MyTeamGroup(svc));
+ * }
+ * Pour remplacer une feuille :
+ * {@code
+ * core.getCoreCommand().findSubCommand("team")
+ * .ifPresent(t -> t.replaceSubCommand("create", new MyCreate(svc)));
+ * }
+ */
+public class CoreCommand extends BaseCommand {
+
+ protected final TeamService teamService;
+ protected final PlayerProfileService playerProfileService;
+
+ public CoreCommand(TeamService teamService, PlayerProfileService playerProfileService) {
+ super("core", "cr", "crcore");
+ this.teamService = Objects.requireNonNull(teamService, "teamService");
+ this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService");
+ description("Commandes du noyau CR-Core");
+ registerDefaults();
+ }
+
+ /** Enregistre les groupes par défaut. Override pour ajouter / retirer des groupes. */
+ protected void registerDefaults() {
+ addSubCommand(new TeamGroupSubCommand(teamService));
+ // Futur : addSubCommand(new PlayerGroupSubCommand(playerProfileService));
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamAddSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamAddSubCommand.java
new file mode 100644
index 0000000..85945bd
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamAddSubCommand.java
@@ -0,0 +1,49 @@
+package fr.luc.crcore.command.builtin.team;
+
+import fr.luc.crcore.command.ArgumentTypes;
+import fr.luc.crcore.command.CommandContext;
+import fr.luc.crcore.command.CommandResult;
+import fr.luc.crcore.command.SubCommand;
+import fr.luc.crcore.team.Team;
+import fr.luc.crcore.team.TeamService;
+import org.bukkit.entity.Player;
+
+import java.util.Objects;
+
+/**
+ * {@code /core team add {@code
+ * core.getCoreCommand().findSubCommand("team")
+ * .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc)));
+ * }
+ *
+ *
+ *
+ */
public interface Repository
+ *
+ */
+public enum ColumnType {
+
+ INTEGER("INTEGER"),
+ REAL("REAL"),
+ TEXT("TEXT"),
+ BLOB("BLOB"),
+ BOOLEAN("INTEGER"),
+ UUID("TEXT");
+
+ private final String sqlType;
+
+ ColumnType(String sqlType) {
+ this.sqlType = sqlType;
+ }
+
+ /** Nom SQL natif côté SQLite. */
+ public String getSqlType() {
+ return sqlType;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/database/Database.java b/src/main/java/fr/luc/crcore/database/Database.java
new file mode 100644
index 0000000..456f356
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/database/Database.java
@@ -0,0 +1,207 @@
+package fr.luc.crcore.database;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Façade SQLite minimaliste pour CR-Core et les plugins de jeu downstream.
+ *
+ *
+ *
+ *
+ * {@code
+ * db.table("foo")
+ * .ifNotExists()
+ * .column("id", ColumnType.UUID).primaryKey()
+ * .column("name", ColumnType.TEXT).notNull()
+ * .create();
+ * }
+ */
+ public TableBuilder table(String name) {
+ return new TableBuilder(this, name);
+ }
+
+ /** Vérifie l'existence d'une table dans la base. */
+ public boolean tableExists(String name) {
+ Objects.requireNonNull(name, "name");
+ return queryOne(
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
+ rs -> rs.getString(1),
+ name
+ ).isPresent();
+ }
+
+ /**
+ * Exécute un statement SQL (DDL ou autre) sans collecter de résultat. Pour
+ * les INSERT/UPDATE/DELETE préférer {@link #update}.
+ */
+ public void execute(String sql, Object... params) {
+ try (PreparedStatement stmt = prepare(sql, params)) {
+ stmt.execute();
+ } catch (SQLException ex) {
+ throw new DatabaseException("execute() failed: " + sql, ex);
+ }
+ }
+
+ /**
+ * Exécute un INSERT/UPDATE/DELETE et renvoie le nombre de lignes affectées.
+ */
+ public int update(String sql, Object... params) {
+ try (PreparedStatement stmt = prepare(sql, params)) {
+ return stmt.executeUpdate();
+ } catch (SQLException ex) {
+ throw new DatabaseException("update() failed: " + sql, ex);
+ }
+ }
+
+ /**
+ * Exécute un SELECT et renvoie au plus une ligne. {@link Optional#empty()}
+ * si aucune ligne. Lève {@link DatabaseException} si plusieurs lignes
+ * matchent — penser à mettre {@code LIMIT 1} ou un {@code WHERE} unique.
+ */
+ public {@code
+ * RowMapper
+ */
+@FunctionalInterface
+public interface RowMapper{@code
+ * db.table("my_scores")
+ * .ifNotExists()
+ * .column("player_id", ColumnType.UUID).primaryKey()
+ * .column("score", ColumnType.INTEGER).notNull().defaultValue("0")
+ * .column("updated_at", ColumnType.INTEGER).notNull()
+ * .create();
+ * }
+ *
+ *
+ *
+ */
+public class SqlitePlayerProfileRepository extends InMemoryPlayerProfileRepository {
+
+ private static final String TABLE_PROFILES = "crcore_player_profiles";
+ private static final String TABLE_SCORES = "crcore_player_scores";
+
+ private final Database db;
+
+ public SqlitePlayerProfileRepository(Database db) {
+ this.db = Objects.requireNonNull(db, "db");
+ ensureSchema();
+ loadAll();
+ }
+
+ private void ensureSchema() {
+ db.table(TABLE_PROFILES).ifNotExists()
+ .column("id", ColumnType.UUID).primaryKey()
+ .create();
+
+ db.table(TABLE_SCORES).ifNotExists()
+ .column("profile_id", ColumnType.UUID).notNull()
+ .column("score_name", ColumnType.TEXT).notNull()
+ .column("value", ColumnType.INTEGER).notNull()
+ .create();
+ }
+
+ private void loadAll() {
+ var ids = db.query(
+ "SELECT id FROM " + TABLE_PROFILES,
+ rs -> UUID.fromString(rs.getString("id"))
+ );
+ for (UUID id : ids) {
+ PlayerProfile profile = new PlayerProfile(id);
+ db.query(
+ "SELECT score_name, value FROM " + TABLE_SCORES + " WHERE profile_id = ?",
+ rs -> {
+ profile.setScore(rs.getString("score_name"), rs.getInt("value"));
+ return null;
+ },
+ id
+ );
+ super.save(profile); // injecte dans le cache hérité sans repasser par notre override
+ }
+ }
+
+ @Override
+ public PlayerProfile save(PlayerProfile profile) {
+ super.save(profile);
+ persist(profile);
+ return profile;
+ }
+
+ @Override
+ public boolean delete(UUID id) {
+ boolean removed = super.delete(id);
+ if (removed) {
+ db.update("DELETE FROM " + TABLE_SCORES + " WHERE profile_id = ?", id);
+ db.update("DELETE FROM " + TABLE_PROFILES + " WHERE id = ?", id);
+ }
+ return removed;
+ }
+
+ private void persist(PlayerProfile profile) {
+ db.update(
+ "INSERT OR REPLACE INTO " + TABLE_PROFILES + " (id) VALUES (?)",
+ profile.getId()
+ );
+ db.update("DELETE FROM " + TABLE_SCORES + " WHERE profile_id = ?", profile.getId());
+ profile.getScores().forEach((name, value) ->
+ db.update(
+ "INSERT INTO " + TABLE_SCORES + " (profile_id, score_name, value) VALUES (?, ?, ?)",
+ profile.getId(), name, value
+ )
+ );
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/player/event/PlayerProfileCreateEvent.java b/src/main/java/fr/luc/crcore/player/event/PlayerProfileCreateEvent.java
new file mode 100644
index 0000000..845803c
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/player/event/PlayerProfileCreateEvent.java
@@ -0,0 +1,23 @@
+package fr.luc.crcore.player.event;
+
+import fr.luc.crcore.player.PlayerProfile;
+import org.bukkit.event.HandlerList;
+
+/** Déclenché juste après la création d'un profil (lazy ou explicite). */
+public class PlayerProfileCreateEvent extends PlayerProfileEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public PlayerProfileCreateEvent(PlayerProfile profile) {
+ super(profile);
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/player/event/PlayerProfileDeleteEvent.java b/src/main/java/fr/luc/crcore/player/event/PlayerProfileDeleteEvent.java
new file mode 100644
index 0000000..460f377
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/player/event/PlayerProfileDeleteEvent.java
@@ -0,0 +1,23 @@
+package fr.luc.crcore.player.event;
+
+import fr.luc.crcore.player.PlayerProfile;
+import org.bukkit.event.HandlerList;
+
+/** Déclenché juste après la suppression d'un profil. */
+public class PlayerProfileDeleteEvent extends PlayerProfileEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public PlayerProfileDeleteEvent(PlayerProfile profile) {
+ super(profile);
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/fr/luc/crcore/player/event/PlayerProfileEvent.java b/src/main/java/fr/luc/crcore/player/event/PlayerProfileEvent.java
new file mode 100644
index 0000000..bfceec2
--- /dev/null
+++ b/src/main/java/fr/luc/crcore/player/event/PlayerProfileEvent.java
@@ -0,0 +1,26 @@
+package fr.luc.crcore.player.event;
+
+import fr.luc.crcore.player.PlayerProfile;
+import org.bukkit.event.Event;
+
+import java.util.Objects;
+
+/**
+ * Base abstraite pour tous les évènements liés à un {@link PlayerProfile}.
+ *
+ *
+ *
+ *
+ *